diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/README.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/README.md index 507590ee40f64..70c2b0c3fa57b 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/README.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/README.md @@ -301,6 +301,39 @@ The Azure Monitor Distro is a distribution package that facilitates users in sen Refer to [`Program.cs`](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/monitor/Azure.Monitor.OpenTelemetry.AspNetCore/tests/Azure.Monitor.OpenTelemetry.AspNetCore.Demo/Program.cs) for a complete demo. +### Log Scopes + +Log [scopes](https://learn.microsoft.com/dotnet/core/extensions/logging#log-scopes) allow you to add additional properties to the logs generated by your application. +Although the Azure Monitor Distro does support scopes, this feature is off by default in OpenTelemetry. +To leverage log scopes, you must explicitly enable them. + +To include the scope with your logs, set `OpenTelemetryLoggerOptions.IncludeScopes` to `true` in your application's configuration: +```csharp +builder.Services.Configure((loggingOptions) => +{ + loggingOptions.IncludeScopes = true; +}); +``` + +When using `ILogger` scopes, use a `List>` or `IReadOnlyList>` as the state for best performance. +All logs written within the context of the scope will include the specified information. +Azure Monitor will add these scope values to the Log's CustomProperties. +```csharp +List> scope = +[ + new("scopeKey", "scopeValue") +]; + +using (logger.BeginScope(scope)) +{ + logger.LogInformation("Example message."); +} +``` + +In scenarios involving multiple scopes or a single scope with multiple key-value pairs, if duplicate keys are present, +only the first occurrence of the key-value pair from the outermost scope will be recorded. +However, when the same key is utilized both within a logging scope and directly in the log statement, the value specified in the log message template will take precedence. + ## Troubleshooting The Azure Monitor Distro uses EventSource for its own internal logging. The logs are available to any EventListener by opting into the source named "OpenTelemetry-AzureMonitor-Exporter". diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/README.md b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/README.md index be13c36ce0f71..5e2cfb962c06c 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/README.md +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/README.md @@ -141,6 +141,43 @@ For more information on the OpenTelemetry project, please review the [OpenTeleme Refer to [`Program.cs`](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Demo/Program.cs) for a complete demo. +### Log Scopes + +Log [scopes](https://learn.microsoft.com/dotnet/core/extensions/logging#log-scopes) allow you to add additional properties to the logs generated by your application. +Although the Azure Monitor Exporter does support scopes, this feature is off by default in OpenTelemetry. +To leverage log scopes, you must explicitly enable them. + +To include the scope with your logs, set `OpenTelemetryLoggerOptions.IncludeScopes` to `true` in your application's configuration: +```csharp +var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddOpenTelemetry(options => + { + options.AddAzureMonitorLogExporter(o => o.ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000"); + options.IncludeScopes = true; + }); +}); +``` + +When using `ILogger` scopes, use a `List>` or `IReadOnlyList>` as the state for best performance. +All logs written within the context of the scope will include the specified information. +Azure Monitor will add these scope values to the Log's CustomProperties. +```csharp +List> scope = +[ + new("scopeKey", "scopeValue") +]; + +using (logger.BeginScope(scope)) +{ + logger.LogInformation("Example message."); +} +``` + +In scenarios involving multiple scopes or a single scope with multiple key-value pairs, if duplicate keys are present, +only the first occurrence of the key-value pair from the outermost scope will be recorded. +However, when the same key is utilized both within a logging scope and directly in the log statement, the value specified in the log message template will take precedence. + ## Troubleshooting The Azure Monitor exporter uses EventSource for its own internal logging. The exporter logs are available to any EventListener by opting into the source named "OpenTelemetry-AzureMonitor-Exporter". diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/LogsTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/LogsTests.cs index 6e9ac78a225c5..d80dd9064fbfd 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/LogsTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/E2ETelemetryItemValidation/LogsTests.cs @@ -57,6 +57,7 @@ public void VerifyLog(LogLevel logLevel, string expectedSeverityLevel) .AddFilter(logCategoryName, logLevel) .AddOpenTelemetry(options => { + options.IncludeScopes = true; options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddAttributes(testResourceAttributes)); options.AddAzureMonitorLogExporterForTest(out telemetryItems); }); @@ -64,12 +65,28 @@ public void VerifyLog(LogLevel logLevel, string expectedSeverityLevel) // ACT var logger = loggerFactory.CreateLogger(logCategoryName); - logger.Log( - logLevel: logLevel, - eventId: 1, - exception: null, - message: "Hello {name}.", - args: new object[] { "World" }); + + List> scope1 = new() + { + new("scopeKey1", "scopeValue1"), + new("scopeKey1", "scopeValue2") + }; + + List> scope2 = new() + { + new("scopeKey1", "scopeValue3") + }; + + using (logger.BeginScope(scope1)) + using (logger.BeginScope(scope2)) + { + logger.Log( + logLevel: logLevel, + eventId: 1, + exception: null, + message: "Hello {name}.", + args: new object[] { "World" }); + } // CLEANUP loggerFactory.Dispose(); @@ -83,7 +100,7 @@ public void VerifyLog(LogLevel logLevel, string expectedSeverityLevel) telemetryItem: telemetryItem!, expectedSeverityLevel: expectedSeverityLevel, expectedMessage: "Hello {name}.", - expectedMessageProperties: new Dictionary { { "EventId", "1" }, { "name", "World" }, { "CategoryName", logCategoryName } }, + expectedMessageProperties: new Dictionary { { "EventId", "1" }, { "name", "World" }, { "CategoryName", logCategoryName }, { "scopeKey1", "scopeValue1" } }, expectedSpanId: null, expectedTraceId: null); } diff --git a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogsHelperTests.cs b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogsHelperTests.cs index 969dbf976737e..87bbbc96535ef 100644 --- a/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogsHelperTests.cs +++ b/sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/LogsHelperTests.cs @@ -503,6 +503,47 @@ public void DuplicateKeysInLogRecordAttributesAndLogScope() Assert.Equal(expectedAttributeValue, actualAttributeValue); } + [Fact] + public void DuplicateKeysInLogRecordAttributesAndLogScope2() + { + // Arrange. + var logRecords = new List(1); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.IncludeScopes = true; + options.AddInMemoryExporter(logRecords); + }); + }); + + var logger = loggerFactory.CreateLogger("Some category"); + + const string expectedScopeKey = "Some scope key"; + const string expectedScopeValue = "Some scope value"; + const string duplicateScopeValue = "Some duplicate scope value"; + const string duplicateScopeValue2 = "Another duplicate scope value"; + + // Act. + using (logger.BeginScope(new List> + { + new KeyValuePair(expectedScopeKey, expectedScopeValue), + new KeyValuePair(expectedScopeKey, duplicateScopeValue), + })) + { + logger.LogInformation($"Some log information message. {{{expectedScopeKey}}}.", duplicateScopeValue2); + } + + // Assert. + var logRecord = logRecords.Single(); + var properties = new ChangeTrackingDictionary(); + LogsHelper.GetMessageAndSetProperties(logRecords[0], properties); + + Assert.Equal(2, properties.Count); + Assert.True(properties.TryGetValue(expectedScopeKey, out string actualScopeValue)); + Assert.Equal(duplicateScopeValue2, actualScopeValue); + } + private class CustomObject { public override string ToString()