Skip to content

Commit fd6cdd2

Browse files
committed
Add LogMethodMarkup source-generation support (Fixes #1)
1 parent 2910ff5 commit fd6cdd2

File tree

13 files changed

+614
-31
lines changed

13 files changed

+614
-31
lines changed

site/docs/aot.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Replace `win-x64` with your runtime identifier.
2222

2323
## Guidance
2424

25-
- Prefer source-generated APIs (`[LogMethod]`, `[LogFormatter]`) for predictable codegen.
25+
- Prefer source-generated APIs (`[LogMethod]`, `[LogMethodMarkup]`, `[LogFormatter]`) for predictable codegen.
2626
- Avoid reflection-based custom sinks on the hot path.
2727
- Keep custom formatters span-based (`TryFormat`) to avoid trimming surprises around serializer/runtime helpers.
2828
- If your sink depends on external libraries, validate their trimming/AOT compatibility separately.
@@ -33,4 +33,3 @@ Replace `win-x64` with your runtime identifier.
3333
- Run startup/shutdown log paths.
3434
- Run representative `Info`, `Warn`, `Error` logging with properties/scopes.
3535
- Validate file/JSON/terminal sink output in published binaries.
36-

site/docs/microsoft-extensions-logging.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Migration is API-level: update startup configuration and logging call sites.
2121
| `LogLevel` | `LogLevel` |
2222
| `EventId` | `LogEventId` |
2323
| `BeginScope(...)` | `BeginScope(LogProperties)` |
24-
| `LoggerMessageAttribute` | `[LogMethod]` |
24+
| `LoggerMessageAttribute` | `[LogMethod]` / `[LogMethodMarkup]` |
2525
| `appsettings` provider config | `LogManagerConfig` + code configuration |
2626

2727
## Minimal migration example
@@ -124,7 +124,7 @@ Use `Block` when correctness is more important than producer latency.
124124

125125
1. Introduce `LogManager` startup/shutdown lifecycle.
126126
2. Replace logger injection with `Logger` access (`LogManager.GetLogger(...)`) at app entry points/services.
127-
3. Migrate high-traffic logs first to `[LogMethod]` or interpolated APIs.
127+
3. Migrate high-traffic logs first to `[LogMethod]` / `[LogMethodMarkup]` or interpolated APIs.
128128
4. Move sinks to `FileLogWriter`/`JsonFileLogWriter`/`TerminalLogWriter` as needed.
129129
5. Validate throughput and behavior with [Benchmarks](benchmarks.md) and app-specific load tests.
130130

site/docs/packages.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Includes:
2020
- Sync/async processors
2121
- Built-in formatters
2222
- Stream/file/json writers
23-
- Embedded source generators/analyzers (`[LogMethod]`, `[LogFormatter]`)
23+
- Embedded source generators/analyzers (`[LogMethod]`, `[LogMethodMarkup]`, `[LogFormatter]`)
2424

2525
## Terminal sink
2626

@@ -47,6 +47,7 @@ Includes:
4747
After restore/build, generated members should appear in IDE for:
4848

4949
- `[LogMethod]` partial methods
50+
- `[LogMethodMarkup]` partial methods
5051
- `[LogFormatter]` partial records/properties
5152

5253
If not, verify analyzer wiring in your project file.

site/docs/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This section is organized as a practical map: start quickly, then go deeper by t
2626
## Formatting and generated APIs
2727

2828
- [Log Formatters](log-formatter.md): built-in formatters, template syntax, and custom formatter generation.
29-
- [Source-generated Logging](source-generator.md): `[LogMethod]` and diagnostics.
29+
- [Source-generated Logging](source-generator.md): `[LogMethod]`, `[LogMethodMarkup]`, and diagnostics.
3030

3131
## Terminal and UI output
3232

site/docs/source-generator.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ title: "Source-generated Logging"
99
This generator covers two features:
1010

1111
- `[LogMethod]`: generates strongly-typed logging methods from message templates.
12+
- `[LogMethodMarkup]`: same as `[LogMethod]`, but generated methods mark the message as markup.
1213
- `[LogFormatter]`: generates high-performance text formatters from declarative templates.
1314

1415
## Attribute-based API
@@ -28,13 +29,29 @@ public static partial class AppLogs
2829
}
2930
```
3031

32+
Markup-aware generated methods use `[LogMethodMarkup]`:
33+
34+
```csharp
35+
using XenoAtom.Logging;
36+
37+
public static partial class AppLogs
38+
{
39+
[LogMethodMarkup(LogLevel.Info, "[green]User {userId} connected[/]")]
40+
public static partial void UserConnectedMarkup(Logger logger, int userId);
41+
}
42+
```
43+
44+
`[LogMethodMarkup]` requires the `XenoAtom.Logging.Terminal` package, because generated code routes through the terminal markup APIs.
45+
3146
The generator emits method implementations that:
3247

3348
- perform a level check (`logger.IsEnabled(level)`)
3449
- construct optional `LogEventId`
35-
- call the matching generated `LoggerExtensions` level method
50+
- call the matching generated `LoggerExtensions` method (`[LogMethod]`) or `LoggerMarkupExtensions.LogMarkup` (`[LogMethodMarkup]`)
3651
- emit interpolation code derived from the compile-time template
3752

53+
`[LogMethodMarkup]` sets the message markup flag (`LogMessage.IsMarkup = true`), so terminal sinks can render markup while file/stream/json sinks keep stripping tags as documented.
54+
3855
## Message template rules
3956

4057
- Placeholder syntax: `{name}`, `{name,alignment}`, `{name:format}`, `{name,alignment:format}`
@@ -48,10 +65,11 @@ The generator emits method implementations that:
4865

4966
## Generator diagnostics
5067

51-
- `XLG0001`: invalid `[LogMethod]` signature
68+
- `XLG0001`: invalid `[LogMethod]` or `[LogMethodMarkup]` signature
5269
- `XLG0002`: unsupported log level
5370
- `XLG0003`: invalid message template
5471
- `XLG0004`: unknown template parameter
72+
- `XLG0005`: missing `XenoAtom.Logging.Terminal` reference for `[LogMethodMarkup]`
5573

5674
## Log formatters
5775

site/docs/terminal.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ logger.InfoMarkup("[green]ready[/] [gray]service started[/]");
120120
logger.ErrorMarkup($"[red]failed[/] request={requestId}");
121121
```
122122

123+
You can also generate markup-aware methods with `[LogMethodMarkup]`; see [Source-generated Logging](source-generator.md).
124+
123125
When using interpolated markup messages, formatted values are escaped:
124126

125127
```csharp

src/XenoAtom.Logging.Generators/LogMethodDiagnostics.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class LogMethodDiagnostics
1010
{
1111
public static readonly DiagnosticDescriptor InvalidMethodSignature = new(
1212
id: "XLG0001",
13-
title: "Invalid [LogMethod] signature",
13+
title: "Invalid [LogMethod] or [LogMethodMarkup] signature",
1414
messageFormat: "Method '{0}' cannot be source-generated: {1}",
1515
category: "XenoAtom.Logging.Generators",
1616
defaultSeverity: DiagnosticSeverity.Error,
@@ -40,6 +40,14 @@ internal static class LogMethodDiagnostics
4040
defaultSeverity: DiagnosticSeverity.Error,
4141
isEnabledByDefault: true);
4242

43+
public static readonly DiagnosticDescriptor MissingMarkupSupport = new(
44+
id: "XLG0005",
45+
title: "Missing markup logging support",
46+
messageFormat: "Method '{0}' uses [LogMethodMarkup], but the XenoAtom.Logging.Terminal package is not referenced.",
47+
category: "XenoAtom.Logging.Generators",
48+
defaultSeverity: DiagnosticSeverity.Error,
49+
isEnabledByDefault: true);
50+
4351
public static readonly DiagnosticDescriptor AllocationRiskParameter = new(
4452
id: "XLG0100",
4553
title: "Potential logging allocation",

src/XenoAtom.Logging.Generators/LogMethodGenerator.cs

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,41 @@
1212
namespace XenoAtom.Logging.Generators;
1313

1414
/// <summary>
15-
/// Generates implementations for methods annotated with <c>[LogMethod]</c>.
15+
/// Generates implementations for methods annotated with <c>[LogMethod]</c> and <c>[LogMethodMarkup]</c>.
1616
/// </summary>
1717
[Generator(LanguageNames.CSharp)]
1818
public sealed class LogMethodGenerator : IIncrementalGenerator
1919
{
2020
/// <inheritdoc />
2121
public void Initialize(IncrementalGeneratorInitializationContext context)
2222
{
23-
var generatedMethods = context.SyntaxProvider
23+
var logMethods = context.SyntaxProvider
2424
.ForAttributeWithMetadataName(
2525
LogMethodUtilities.LogMethodAttributeMetadataName,
2626
static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
27-
static (ctx, ct) => TryCreateGenerationResult(ctx, ct))
27+
static (ctx, ct) => TryCreateGenerationResult(ctx, isMarkupMethod: false, ct));
28+
29+
var logMarkupMethods = context.SyntaxProvider
30+
.ForAttributeWithMetadataName(
31+
LogMethodUtilities.LogMethodMarkupAttributeMetadataName,
32+
static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
33+
static (ctx, ct) => TryCreateGenerationResult(ctx, isMarkupMethod: true, ct));
34+
35+
var generatedLogMethods = logMethods
2836
.Where(static result => result is not null)
2937
.Select(static (result, _) => result!.Value)
3038
.WithComparer(LogMethodGenerationResultComparer.Instance);
3139

32-
context.RegisterSourceOutput(
33-
generatedMethods,
34-
static (spc, result) => Emit(spc, result));
40+
var generatedLogMarkupMethods = logMarkupMethods
41+
.Where(static result => result is not null)
42+
.Select(static (result, _) => result!.Value)
43+
.WithComparer(LogMethodGenerationResultComparer.Instance);
44+
45+
context.RegisterSourceOutput(generatedLogMethods, Emit);
46+
context.RegisterSourceOutput(generatedLogMarkupMethods, Emit);
3547
}
3648

37-
private static LogMethodGenerationResult? TryCreateGenerationResult(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
49+
private static LogMethodGenerationResult? TryCreateGenerationResult(GeneratorAttributeSyntaxContext context, bool isMarkupMethod, CancellationToken cancellationToken)
3850
{
3951
cancellationToken.ThrowIfCancellationRequested();
4052
if (context.TargetSymbol is not IMethodSymbol methodSymbol)
@@ -47,12 +59,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4759
return null;
4860
}
4961

50-
if (!LogMethodUtilities.TryGetLogMethodAttribute(methodSymbol, out var attribute) || attribute is null)
62+
if (context.Attributes is not { Length: > 0 })
5163
{
5264
return null;
5365
}
5466

55-
return GenerateMethod(context.SemanticModel.Compilation, methodSymbol, attribute);
67+
return GenerateMethod(context.SemanticModel.Compilation, methodSymbol, context.Attributes[0], isMarkupMethod);
5668
}
5769

5870
private static void Emit(SourceProductionContext context, LogMethodGenerationResult result)
@@ -68,10 +80,21 @@ private static void Emit(SourceProductionContext context, LogMethodGenerationRes
6880
}
6981
}
7082

71-
private static LogMethodGenerationResult GenerateMethod(Compilation compilation, IMethodSymbol methodSymbol, AttributeData attribute)
83+
private static LogMethodGenerationResult GenerateMethod(Compilation compilation, IMethodSymbol methodSymbol, AttributeData attribute, bool isMarkupMethod)
7284
{
7385
var diagnostics = ImmutableArray.CreateBuilder<Diagnostic>();
7486

87+
if (HasBothLogMethodAttributes(methodSymbol))
88+
{
89+
Report(
90+
diagnostics,
91+
LogMethodDiagnostics.InvalidMethodSignature,
92+
methodSymbol,
93+
methodSymbol.Name,
94+
"Only one of [LogMethod] or [LogMethodMarkup] can be applied.");
95+
return new LogMethodGenerationResult(null, null, diagnostics.ToImmutable());
96+
}
97+
7598
if (!TryValidateMethod(methodSymbol, diagnostics, out var loggerParameter, out var exceptionParameter, out var propertiesParameter))
7699
{
77100
return new LogMethodGenerationResult(null, null, diagnostics.ToImmutable());
@@ -82,6 +105,17 @@ private static LogMethodGenerationResult GenerateMethod(Compilation compilation,
82105
return new LogMethodGenerationResult(null, null, diagnostics.ToImmutable());
83106
}
84107

108+
if (isMarkupMethod &&
109+
compilation.GetTypeByMetadataName(LogMethodUtilities.LoggerMarkupExtensionsMetadataName) is null)
110+
{
111+
Report(
112+
diagnostics,
113+
LogMethodDiagnostics.MissingMarkupSupport,
114+
methodSymbol,
115+
methodSymbol.Name);
116+
return new LogMethodGenerationResult(null, null, diagnostics.ToImmutable());
117+
}
118+
85119
var messageTemplate = attribute.ConstructorArguments[1].Value as string;
86120
if (string.IsNullOrEmpty(messageTemplate))
87121
{
@@ -122,6 +156,7 @@ private static LogMethodGenerationResult GenerateMethod(Compilation compilation,
122156
propertiesParameter,
123157
logLevelValue,
124158
logMethodName!,
159+
isMarkupMethod,
125160
GetEventIdData(attribute, methodSymbol.Name));
126161

127162
var hintName = CreateHintName(methodSymbol);
@@ -351,6 +386,7 @@ private static string GenerateSource(
351386
IParameterSymbol? propertiesParameter,
352387
int logLevelValue,
353388
string logMethodName,
389+
bool isMarkupMethod,
354390
(bool HasEventId, int EventId, string EventName) eventIdData)
355391
{
356392
var source = new StringBuilder();
@@ -439,15 +475,27 @@ private static string GenerateSource(
439475
arguments.Add(BuildInterpolatedMessage(compilation, tokens, templateParameters));
440476

441477
AppendIndent(source, indent);
442-
source.Append("global::XenoAtom.Logging.LoggerExtensions.");
443-
source.Append(logMethodName);
444-
source.Append('(');
445-
source.Append(loggerIdentifier);
478+
if (isMarkupMethod)
479+
{
480+
source.Append("global::XenoAtom.Logging.LoggerMarkupExtensions.LogMarkup(");
481+
source.Append(loggerIdentifier);
482+
source.Append(", global::XenoAtom.Logging.LogLevel.");
483+
source.Append(LogMethodUtilities.GetLogLevelName(logLevelValue));
484+
}
485+
else
486+
{
487+
source.Append("global::XenoAtom.Logging.LoggerExtensions.");
488+
source.Append(logMethodName);
489+
source.Append('(');
490+
source.Append(loggerIdentifier);
491+
}
492+
446493
if (arguments.Count > 0)
447494
{
448495
source.Append(", ");
449496
source.Append(string.Join(", ", arguments));
450497
}
498+
451499
source.AppendLine(");");
452500

453501
indent -= 4;
@@ -598,6 +646,26 @@ private static bool HasGenericContainingType(INamedTypeSymbol? containingType)
598646
return false;
599647
}
600648

649+
private static bool HasBothLogMethodAttributes(IMethodSymbol methodSymbol)
650+
{
651+
var hasLogMethod = false;
652+
var hasLogMethodMarkup = false;
653+
foreach (var candidate in methodSymbol.GetAttributes())
654+
{
655+
var attributeName = candidate.AttributeClass?.ToDisplayString();
656+
if (attributeName == LogMethodUtilities.LogMethodAttributeMetadataName)
657+
{
658+
hasLogMethod = true;
659+
}
660+
else if (attributeName == LogMethodUtilities.LogMethodMarkupAttributeMetadataName)
661+
{
662+
hasLogMethodMarkup = true;
663+
}
664+
}
665+
666+
return hasLogMethod && hasLogMethodMarkup;
667+
}
668+
601669
private static string CreateHintName(IMethodSymbol methodSymbol)
602670
{
603671
var fullyQualifiedName = methodSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

src/XenoAtom.Logging.Generators/LogMethodUtilities.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ namespace XenoAtom.Logging.Generators;
1313
internal static class LogMethodUtilities
1414
{
1515
public const string LogMethodAttributeMetadataName = "XenoAtom.Logging.LogMethodAttribute";
16+
public const string LogMethodMarkupAttributeMetadataName = "XenoAtom.Logging.LogMethodMarkupAttribute";
1617
public const string LoggerMetadataName = "XenoAtom.Logging.Logger";
18+
public const string LoggerMarkupExtensionsMetadataName = "XenoAtom.Logging.LoggerMarkupExtensions";
1719
public const string LogPropertiesMetadataName = "XenoAtom.Logging.LogProperties";
1820

1921
private static readonly SymbolDisplayFormat TypeDisplayFormat =
@@ -22,17 +24,30 @@ internal static class LogMethodUtilities
2224
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);
2325

2426
public static bool TryGetLogMethodAttribute(IMethodSymbol methodSymbol, out AttributeData? attribute)
27+
=> TryGetLogMethodAttribute(methodSymbol, out attribute, out _);
28+
29+
public static bool TryGetLogMethodAttribute(IMethodSymbol methodSymbol, out AttributeData? attribute, out bool isMarkup)
2530
{
2631
foreach (var candidate in methodSymbol.GetAttributes())
2732
{
28-
if (candidate.AttributeClass?.ToDisplayString() == LogMethodAttributeMetadataName)
33+
var attributeName = candidate.AttributeClass?.ToDisplayString();
34+
if (attributeName == LogMethodAttributeMetadataName)
35+
{
36+
attribute = candidate;
37+
isMarkup = false;
38+
return true;
39+
}
40+
41+
if (attributeName == LogMethodMarkupAttributeMetadataName)
2942
{
3043
attribute = candidate;
44+
isMarkup = true;
3145
return true;
3246
}
3347
}
3448

3549
attribute = null;
50+
isMarkup = false;
3651
return false;
3752
}
3853

0 commit comments

Comments
 (0)