diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs index 2b0bc4eca00d5d..908efaf6afe325 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs @@ -381,34 +381,45 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation) GenParameters(lm); _builder.Append($@") - {nestedIndentation}{{ + {nestedIndentation}{{"); + + string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : " "; + if (!lm.SkipEnabledCheck) + { + _builder.Append($@" {nestedIndentation}if ({logger}.IsEnabled({level})) {nestedIndentation}{{"); + } if (UseLoggerMessageDefine(lm)) { _builder.Append($@" - {nestedIndentation}__{lm.Name}Callback({logger}, "); + {nestedIndentation}{enabledCheckIndentation}__{lm.Name}Callback({logger}, "); GenCallbackArguments(lm); - _builder.Append(@$"{exceptionArg});"); + _builder.Append($"{exceptionArg});"); } else { _builder.Append($@" - {nestedIndentation}{logger}.Log( - {level}, - new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}), - "); - GenHolder(lm); - _builder.Append($@", - {exceptionArg}, - __{lm.Name}Struct.Format);"); + {nestedIndentation}{enabledCheckIndentation}{logger}.Log( + {nestedIndentation}{enabledCheckIndentation}{level}, + {nestedIndentation}{enabledCheckIndentation}new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}), + {nestedIndentation}{enabledCheckIndentation}"); + GenHolder(lm); + _builder.Append($@", + {nestedIndentation}{enabledCheckIndentation}{exceptionArg}, + {nestedIndentation}{enabledCheckIndentation}__{lm.Name}Struct.Format);"); + } + + if (!lm.SkipEnabledCheck) + { + _builder.Append($@" + {nestedIndentation}}}"); } _builder.Append($@" - {nestedIndentation}}} {nestedIndentation}}}"); static string GetException(LoggerMethod lm) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs index 534249c232601d..4a995039104b9d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; namespace Microsoft.Extensions.Logging.Generators { @@ -28,7 +29,7 @@ public Parser(Compilation compilation, Action reportDiagnostic, Canc } /// - /// Gets the set of logging classes or structs containing methods to output. + /// Gets the set of logging classes containing methods to output. /// public IReadOnlyList GetLogClasses(IEnumerable classes) { @@ -83,7 +84,7 @@ public IReadOnlyList GetLogClasses(IEnumerable GetLogClasses(IEnumerable GetLogClasses(IEnumerable? boundAttrbutes = logMethodSymbol?.GetAttributes(); + + if (boundAttrbutes == null) + { + continue; + } + + foreach (AttributeData attributeData in boundAttrbutes) + { + // supports: [LoggerMessage(0, LogLevel.Warning, "custom message")] + // supports: [LoggerMessage(eventId: 0, level: LogLevel.Warning, message: "custom message")] + if (attributeData.ConstructorArguments.Any()) + { + foreach (TypedConstant typedConstant in attributeData.ConstructorArguments) + { + if (typedConstant.Kind == TypedConstantKind.Error) + { + hasMisconfiguredInput = true; + } + } + + ImmutableArray items = attributeData.ConstructorArguments; + Debug.Assert(items.Length == 3); + + eventId = items[0].IsNull ? -1 : (int)GetItem(items[0]); + level = items[1].IsNull ? null : (int?)GetItem(items[1]); + message = items[2].IsNull ? "" : (string)GetItem(items[2]); + } + + // argument syntax takes parameters. e.g. EventId = 0 + // supports: e.g. [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = "custom message")] + if (attributeData.NamedArguments.Any()) + { + foreach (KeyValuePair namedArgument in attributeData.NamedArguments) + { + TypedConstant typedConstant = namedArgument.Value; + if (typedConstant.Kind == TypedConstantKind.Error) + { + hasMisconfiguredInput = true; + } + else + { + TypedConstant value = namedArgument.Value; + switch (namedArgument.Key) + { + case "EventId": + eventId = (int)GetItem(value); + break; + case "Level": + level = value.IsNull ? null : (int?)GetItem(value); + break; + case "SkipEnabledCheck": + skipEnabledCheck = (bool)GetItem(value); + break; + case "EventName": + eventName = (string?)GetItem(value); + break; + case "Message": + message = value.IsNull ? "" : (string)GetItem(value); + break; + } + } + } + } + } + + if (hasMisconfiguredInput) + { + // skip further generator execution and let compiler generate the errors + break; + } IMethodSymbol? methodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken); if (methodSymbol != null) @@ -119,6 +194,7 @@ public IReadOnlyList GetLogClasses(IEnumerable return (loggerField, false); } - private (int eventId, int? level, string message, string? eventName) ExtractAttributeValues(AttributeArgumentListSyntax args, SemanticModel sm) - { - int eventId = 0; - int? level = null; - string? eventName = null; - string message = string.Empty; - foreach (AttributeArgumentSyntax a in args.Arguments) - { - // argument syntax takes parameters. e.g. EventId = 0 - Debug.Assert(a.NameEquals != null); - switch (a.NameEquals.Name.ToString()) - { - case "EventId": - eventId = (int)sm.GetConstantValue(a.Expression, _cancellationToken).Value!; - break; - case "EventName": - eventName = sm.GetConstantValue(a.Expression, _cancellationToken).ToString(); - break; - case "Level": - level = (int)sm.GetConstantValue(a.Expression, _cancellationToken).Value!; - break; - case "Message": - message = sm.GetConstantValue(a.Expression, _cancellationToken).ToString(); - break; - } - } - return (eventId, level, message, eventName); - } - private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) { _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); @@ -580,6 +627,8 @@ private string GetStringExpression(SemanticModel sm, SyntaxNode expr) return string.Empty; } + + private static object GetItem(TypedConstant arg) => arg.Kind == TypedConstantKind.Array ? arg.Values : arg.Value; } /// @@ -612,6 +661,7 @@ internal class LoggerMethod public bool IsExtensionMethod; public string Modifiers = string.Empty; public string LoggerField = string.Empty; + public bool SkipEnabledCheck; } /// diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index e7b9034ea81aff..ef9d46564ec61c 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -119,10 +119,12 @@ public static partial class LoggerMessage public sealed partial class LoggerMessageAttribute : System.Attribute { public LoggerMessageAttribute() { } + public LoggerMessageAttribute(int eventId, Microsoft.Extensions.Logging.LogLevel level, string message) { } public int EventId { get { throw null; } set { } } public string? EventName { get { throw null; } set { } } public Microsoft.Extensions.Logging.LogLevel Level { get { throw null; } set { } } public string Message { get { throw null; } set { } } + public bool SkipEnabledCheck { get { throw null; } set { } } } public partial class Logger : Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessageAttribute.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessageAttribute.cs index b103ef31a4e162..acb9af3d8601fe 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessageAttribute.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessageAttribute.cs @@ -37,6 +37,20 @@ public sealed class LoggerMessageAttribute : Attribute /// public LoggerMessageAttribute() { } + /// + /// Initializes a new instance of the class + /// which is used to guide the production of a strongly-typed logging method. + /// + /// The log event Id. + /// The log level. + /// Format string of the log message. + public LoggerMessageAttribute(int eventId, LogLevel level, string message) + { + EventId = eventId; + Level = level; + Message = message; + } + /// /// Gets the logging event id for the logging method. /// @@ -59,5 +73,10 @@ public LoggerMessageAttribute() { } /// Gets the message text for the logging method. /// public string Message { get; set; } = ""; + + /// + /// Gets the flag to skip IsEnabled check for the logging method. + /// + public bool SkipEnabledCheck { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithDefaultValues.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithDefaultValues.generated.txt new file mode 100644 index 00000000000000..c61da37d27bba4 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithDefaultValues.generated.txt @@ -0,0 +1,57 @@ +// +#nullable enable + +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + partial class TestWithDefaultValues + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")] + private readonly struct __M0Struct : global::System.Collections.Generic.IReadOnlyList> + { + + public override string ToString() + { + + return $""; + } + + public static string Format(__M0Struct state, global::System.Exception? ex) => state.ToString(); + + public int Count => 1; + + public global::System.Collections.Generic.KeyValuePair this[int index] + { + get => index switch + { + 0 => new global::System.Collections.Generic.KeyValuePair("{OriginalFormat}", ""), + + _ => throw new global::System.IndexOutOfRangeException(nameof(index)), // return the same exception LoggerMessage.Define returns in this case + }; + } + + public global::System.Collections.Generic.IEnumerator> GetEnumerator() + { + for (int i = 0; i < 1; i++) + { + yield return this[i]; + } + } + + global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")] + public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel level) + { + if (logger.IsEnabled(level)) + { + logger.Log( + level, + new global::Microsoft.Extensions.Logging.EventId(-1, nameof(M0)), + new __M0Struct(), + null, + __M0Struct.Format); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt new file mode 100644 index 00000000000000..c4b242a0a9e17d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt @@ -0,0 +1,18 @@ +// +#nullable enable + +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + partial class TestWithSkipEnabledCheck + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")] + private static readonly global::System.Action __M0Callback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", true); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")] + public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger) + { + __M0Callback(logger, null); + } + } +} \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratedCodeTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratedCodeTests.cs index c9180b70f23202..33d1ab7c901712 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratedCodeTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratedCodeTests.cs @@ -178,6 +178,22 @@ public void MessageTests() Assert.Equal(string.Empty, logger.LastFormattedString); Assert.Equal(LogLevel.Debug, logger.LastLogLevel); Assert.Equal(1, logger.CallCount); + + logger.Reset(); + MessageTestExtensions.M5(logger, LogLevel.Trace); + Assert.Null(logger.LastException); + Assert.Equal(string.Empty, logger.LastFormattedString); + Assert.Equal(LogLevel.Trace, logger.LastLogLevel); + Assert.Equal(-1, logger.LastEventId.Id); + Assert.Equal(1, logger.CallCount); + + logger.Reset(); + MessageTestExtensions.M6(logger, LogLevel.Trace); + Assert.Null(logger.LastException); + Assert.Equal(string.Empty, logger.LastFormattedString); + Assert.Equal(LogLevel.Trace, logger.LastLogLevel); + Assert.Equal(6, logger.LastEventId.Id); + Assert.Equal(1, logger.CallCount); } [Fact] @@ -309,6 +325,14 @@ public void LevelTests() Assert.Equal("M9", logger.LastFormattedString); Assert.Equal(LogLevel.Trace, logger.LastLogLevel); Assert.Equal(1, logger.CallCount); + + logger.Reset(); + LevelTestExtensions.M10vs11(logger); + Assert.Null(logger.LastException); + Assert.Equal("event ID 10 vs. 11", logger.LastFormattedString); + Assert.Equal(LogLevel.Warning, logger.LastLogLevel); + Assert.Equal(1, logger.CallCount); + Assert.Equal(11, logger.LastEventId.Id); } [Fact] @@ -343,6 +367,40 @@ public void EventNameTests() Assert.Equal(LogLevel.Trace, logger.LastLogLevel); Assert.Equal(1, logger.CallCount); Assert.Equal("CustomEventName", logger.LastEventId.Name); + + logger.Reset(); + EventNameTestExtensions.CustomEventName(logger); + Assert.Null(logger.LastException); + Assert.Equal("CustomEventName", logger.LastFormattedString); + Assert.Equal(LogLevel.Trace, logger.LastLogLevel); + Assert.Equal(1, logger.CallCount); + Assert.Equal("CustomEventName", logger.LastEventId.Name); + } + + [Fact] + public void SkipEnabledCheckTests() + { + var logger = new MockLogger(); + + logger.Reset(); + logger.Enabled = false; + Assert.False(logger.IsEnabled(LogLevel.Information)); + SkipEnabledCheckExtensions.LoggerMethodWithFalseSkipEnabledCheck(logger); + Assert.Null(logger.LastException); + Assert.Null(logger.LastFormattedString); + Assert.Equal((LogLevel)(-1), logger.LastLogLevel); + Assert.Equal(0, logger.CallCount); + Assert.Equal(default, logger.LastEventId); + + logger.Reset(); + logger.Enabled = false; + Assert.False(logger.IsEnabled(LogLevel.Debug)); + SkipEnabledCheckExtensions.LoggerMethodWithTrueSkipEnabledCheck(logger); + Assert.Null(logger.LastException); + Assert.Equal("Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", logger.LastFormattedString); + Assert.Equal(LogLevel.Debug, logger.LastLogLevel); + Assert.Equal(1, logger.CallCount); + Assert.Equal("LoggerMethodWithTrueSkipEnabledCheck", logger.LastEventId.Name); } [Fact] diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs index 433d601c3d940a..917d87cd127f1e 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs @@ -35,17 +35,51 @@ public async Task TestEmitter() } [Fact] - public async Task TestBaseline_TestWithTwoParams_Success() + public async Task TestBaseline_TestWithSkipEnabledCheck_Success() { string testSourceCode = @" namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses { - internal static partial class TestWithTwoParams + internal static partial class TestWithSkipEnabledCheck { - [LoggerMessage(EventId = 0, Level = LogLevel.Error, Message = ""M0 {a1} {a2}"")] - public static partial void M0(ILogger logger, int a1, System.Collections.Generic.IEnumerable a2); + [LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = ""Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check."", SkipEnabledCheck = true)] + public static partial void M0(ILogger logger); + } +}"; + await VerifyAgainstBaselineUsingFile("TestWithSkipEnabledCheck.generated.txt", testSourceCode); + } + + [Fact] + public async Task TestBaseline_TestWithDefaultValues_Success() + { + string testSourceCode = @" +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + internal static partial class TestWithDefaultValues + { + [LoggerMessage] + public static partial void M0(ILogger logger, LogLevel level); } }"; + await VerifyAgainstBaselineUsingFile("TestWithDefaultValues.generated.txt", testSourceCode); + } + + [Theory] + [InlineData("EventId = 0, Level = LogLevel.Error, Message = \"M0 {a1} {a2}\"")] + [InlineData("eventId: 0, level: LogLevel.Error, message: \"M0 {a1} {a2}\"")] + [InlineData("0, LogLevel.Error, \"M0 {a1} {a2}\"")] + [InlineData("0, LogLevel.Error, \"M0 {a1} {a2}\", SkipEnabledCheck = false")] + public async Task TestBaseline_TestWithTwoParams_Success(string argumentList) + { + string testSourceCode = $@" +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{{ + internal static partial class TestWithTwoParams + {{ + [LoggerMessage({argumentList})] + public static partial void M0(ILogger logger, int a1, System.Collections.Generic.IEnumerable a2); + }} +}}"; await VerifyAgainstBaselineUsingFile("TestWithTwoParams.generated.txt", testSourceCode); } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs index 4991e416cad519..8e5df4972cf9a1 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs @@ -64,6 +64,90 @@ static partial void M1(ILogger logger) Assert.Equal(DiagnosticDescriptors.LoggingMethodHasBody.Id, diagnostics[0].Id); } + [Theory] + [InlineData("EventId = 0, Level = null, Message = \"This is a message with {foo}\"")] + [InlineData("eventId: 0, level: null, message: \"This is a message with {foo}\"")] + [InlineData("0, null, \"This is a message with {foo}\"")] + public async Task WithNullLevel_GeneratorWontFail(string argumentList) + { + IReadOnlyList diagnostics = await RunGenerator($@" + partial class C + {{ + [LoggerMessage({argumentList})] + static partial void M1(ILogger logger, string foo); + + [LoggerMessage({argumentList})] + static partial void M2(ILogger logger, LogLevel level, string foo); + }} + "); + + Assert.Empty(diagnostics); + } + + [Theory] + [InlineData("EventId = null, Level = LogLevel.Debug, Message = \"This is a message with {foo}\"")] + [InlineData("eventId: null, level: LogLevel.Debug, message: \"This is a message with {foo}\"")] + [InlineData("null, LogLevel.Debug, \"This is a message with {foo}\"")] + public async Task WithNullEventId_GeneratorWontFail(string argumentList) + { + IReadOnlyList diagnostics = await RunGenerator($@" + partial class C + {{ + [LoggerMessage({argumentList})] + static partial void M1(ILogger logger, string foo); + }} + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WithNullMessage_GeneratorWontFail() + { + IReadOnlyList diagnostics = await RunGenerator(@" + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = null)] + static partial void M1(ILogger logger, string foo); + } + "); + + Assert.Single(diagnostics); + Assert.Equal(DiagnosticDescriptors.ArgumentHasNoCorrespondingTemplate.Id, diagnostics[0].Id); + Assert.Contains("Argument 'foo' is not referenced from the logging message", diagnostics[0].GetMessage(), StringComparison.InvariantCulture); + } + + [Fact] + public async Task WithNullSkipEnabledCheck_GeneratorWontFail() + { + IReadOnlyList diagnostics = await RunGenerator(@" + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""This is a message with {foo}"", SkipEnabledCheck = null)] + static partial void M1(ILogger logger, string foo); + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WithBadMisconfiguredInput_GeneratorWontFail() + { + IReadOnlyList diagnostics = await RunGenerator(@" + public static partial class C + { + [LoggerMessage(SkipEnabledCheck = 6)] + public static partial void M0(ILogger logger, LogLevel level); + + [LoggerMessage(eventId: true, level: LogLevel.Debug, message: ""misconfigured eventId as bool"")] + public static partial void M1(ILogger logger); + } + "); + + Assert.Empty(diagnostics); + } + [Fact] public async Task MissingTemplate() { @@ -266,6 +350,26 @@ public partial class Nested Assert.Empty(diagnostics); } + [Theory] + [InlineData("false")] + [InlineData("true")] + [InlineData("null")] + public async Task UsingSkipEnabledCheck(string skipEnabledCheckValue) + { + IReadOnlyList diagnostics = await RunGenerator($@" + partial class C + {{ + public partial class WithLoggerMethodUsingSkipEnabledCheck + {{ + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""M1"", SkipEnabledCheck = {skipEnabledCheckValue})] + static partial void M1(ILogger logger); + }} + }} + "); + + Assert.Empty(diagnostics); + } + [Fact] public async Task MissingExceptionType() { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/EventNameTestExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/EventNameTestExtensions.cs index 4c0ddf320aabf4..f41d615d04314a 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/EventNameTestExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/EventNameTestExtensions.cs @@ -7,5 +7,8 @@ internal static partial class EventNameTestExtensions { [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "M0", EventName = "CustomEventName")] public static partial void M0(ILogger logger); + + [LoggerMessage(EventId = 2, Level = LogLevel.Trace, Message = "CustomEventName")] // EventName inferred from method name + public static partial void CustomEventName(ILogger logger); } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/LevelTestExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/LevelTestExtensions.cs index 5726aa02a4c3fe..2251d198a2af29 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/LevelTestExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/LevelTestExtensions.cs @@ -34,5 +34,8 @@ internal static partial class LevelTestExtensions [LoggerMessage(EventId = 9, Message = "M9")] public static partial void M9(LogLevel level, ILogger logger); + + [LoggerMessage(eventId: 10, level: LogLevel.Warning, message: "event ID 10 vs. 11", EventId = 11)] + public static partial void M10vs11(ILogger logger); } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/MessageTestExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/MessageTestExtensions.cs index a30849288b53b4..8cad8db64cd9de 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/MessageTestExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/MessageTestExtensions.cs @@ -29,5 +29,11 @@ internal static partial class MessageTestExtensions [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "{p1}")] public static partial void M4(ILogger logger, string p1, int p2, int p3); #endif + + [LoggerMessage] + public static partial void M5(ILogger logger, LogLevel level); + + [LoggerMessage(EventId = 6, Message = "")] + public static partial void M6(ILogger logger, LogLevel level); } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/SkipEnabledCheckExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/SkipEnabledCheckExtensions.cs new file mode 100644 index 00000000000000..397acdf080ca4e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/SkipEnabledCheckExtensions.cs @@ -0,0 +1,15 @@ + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + internal static partial class SkipEnabledCheckExtensions + { + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", SkipEnabledCheck = true)] + internal static partial void LoggerMethodWithTrueSkipEnabledCheck(ILogger logger); + + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "M1", SkipEnabledCheck = false)] + internal static partial void LoggerMethodWithFalseSkipEnabledCheck(ILogger logger); + } +} \ No newline at end of file