diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md index ebe7093b1fa..37bebb10e77 100644 --- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md +++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md @@ -7,6 +7,10 @@ implementation (eg `LoggerProviderBuilder`) with dependency injection. ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index f8512a9b952..fcc99613615 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -19,6 +19,10 @@ implementation (`LoggerProviderBuilder`, `LoggerProvider`, `Logger`, etc.). ([#4433](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4433)) +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs index 8498b1417f7..170b7949837 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs @@ -32,6 +32,7 @@ public struct LogRecordAttributeList : IReadOnlyList>? OverflowAttributes; + private static readonly IReadOnlyList> Empty = Array.Empty>(); private KeyValuePair attribute1; private KeyValuePair attribute2; private KeyValuePair attribute3; @@ -207,12 +208,12 @@ public readonly Enumerator GetEnumerator() /// readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - internal readonly List>? Export(ref List>? attributeStorage) + internal readonly IReadOnlyList> Export(ref List>? attributeStorage) { int count = this.count; if (count <= 0) { - return null; + return Empty; } var overflowAttributes = this.OverflowAttributes; diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md index e3e30d98e5c..c13379b6420 100644 --- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md @@ -13,6 +13,10 @@ * Added `LoggerProviderBuilder.AddConsoleExporter` registration extension. ([#4583](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4583)) +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md index 100a1c60168..77d52dfbf3c 100644 --- a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md @@ -5,6 +5,10 @@ * Added `LoggerProviderBuilder.AddInMemoryExporter` registration extension. ([#4584](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4584)) +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md index 7da333f1efc..bdd4d000b78 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 5aeb54dd858..d3168653c1c 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -21,6 +21,10 @@ * Updated to support `Severity` and `SeverityText` when exporting `LogRecord`s. ([#4568](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4568)) +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md index f4a087974ec..f2e21b66c06 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md index df4fdb71df5..73c09094a4f 100644 --- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md index 96c5e0f1ebb..ae47a662498 100644 --- a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.5.1 + +Released 2023-Jun-26 + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index bdb8aeb04a8..39fd0dc1b6f 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -27,6 +27,26 @@ exists.` when using any of the following exporters: `ConsoleExporter`, `OtlpExporter`, `ZipkinExporter`, `JaegerExporter`. +## 1.5.1 + +Released 2023-Jun-26 + +* Fixed a breaking change causing `LogRecord.State` to be `null` where it was + previously set to a valid value when + `OpenTelemetryLoggerOptions.ParseStateValues` is `false` and states implement + `IReadOnlyList` or `IEnumerable` of `KeyValuePair`s. + ([#4609](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4609)) + +* **Breaking Change** Removed the support for parsing `TState` types passed to + the `ILogger.Log` API when `ParseStateValues` is true and `TState` + does not implement either `IReadOnlyList>` or + `IEnumerable>`. This feature was first introduced + in the `1.5.0` stable release with + [#4334](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4334) and + has been removed because it makes the OpenTelemetry .NET SDK incompatible with + native AOT. + ([#4614](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4614)) + ## 1.5.0 Released 2023-Jun-05 diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index a6663f34450..b7681339b6e 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -158,6 +158,17 @@ public void LoggerProviderException(string methodName, Exception ex) } } + [NonEvent] + public void LoggerProcessStateSkipped() + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.LoggerProcessStateSkipped( + typeof(TState).FullName!, + "because it does not implement a supported interface (either IReadOnlyList> or IEnumerable>)"); + } + } + [Event(4, Message = "Unknown error in SpanProcessor event '{0}': '{1}'.", Level = EventLevel.Error)] public void SpanProcessorException(string evnt, string ex) { @@ -319,6 +330,12 @@ public void LoggerProviderException(string methodName, string ex) this.WriteEvent(50, methodName, ex); } + [Event(51, Message = "Skipped processing log state of type '{0}' {1}.", Level = EventLevel.Warning)] + public void LoggerProcessStateSkipped(string type, string reason) + { + this.WriteEvent(51, type, reason); + } + #if DEBUG public class OpenTelemetryEventListener : EventListener { diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs index 732795729eb..8fb5af320f4 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs @@ -16,7 +16,6 @@ #nullable enable -using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -95,9 +94,8 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except LogRecordData.SetActivityContext(ref data, activity); - var attributes = record.Attributes = this.options.IncludeAttributes - ? ProcessState(record, ref iloggerData, in state, this.options.ParseStateValues) - : null; + var attributes = record.Attributes = + ProcessState(record, ref iloggerData, in state, this.options.IncludeAttributes, this.options.ParseStateValues); if (!TryGetOriginalFormatFromAttributes(attributes, out var originalFormat)) { @@ -153,9 +151,15 @@ internal static void SetLogRecordSeverityFields(ref LogRecordData logRecordData, LogRecord logRecord, ref LogRecord.LogRecordILoggerData iLoggerData, in TState state, + bool includeAttributes, bool parseStateValues) { - iLoggerData.State = null; + if (!includeAttributes + || (!typeof(TState).IsValueType && state is null)) + { + iLoggerData.State = null; + return null; + } if (typeof(TState) == typeof(LogRecordAttributeList)) { @@ -166,14 +170,31 @@ internal static void SetLogRecordSeverityFields(ref LogRecordData logRecordData, var logRecordAttributes = (LogRecordAttributeList)(object)state!; - return logRecordAttributes.Export(ref logRecord.AttributeStorage); + var exportedAttributes = logRecordAttributes.Export(ref logRecord.AttributeStorage); + + // Note: This is to preserve legacy behavior where State is exposed + // if we didn't parse state. We use exportedAttributes here to prevent a + // boxing of struct LogRecordAttributeList. + iLoggerData.State = !parseStateValues ? exportedAttributes : null; + + return exportedAttributes; } else if (state is IReadOnlyList> stateList) { + // Note: This is to preserve legacy behavior where State is exposed + // if we didn't parse state. We use stateList here to prevent a + // second boxing of struct TStates. + iLoggerData.State = !parseStateValues ? stateList : null; + return stateList; } else if (state is IEnumerable> stateValues) { + // Note: This is to preserve legacy behavior where State is exposed + // if we didn't parse state. We use stateValues here to prevent a + // second boxing of struct TStates. + iLoggerData.State = !parseStateValues ? stateValues : null; + var attributeStorage = logRecord.AttributeStorage; if (attributeStorage == null) { @@ -185,45 +206,23 @@ internal static void SetLogRecordSeverityFields(ref LogRecordData logRecordData, return attributeStorage; } } - else if (!parseStateValues || state is null) + else if (!parseStateValues) { // Note: This is to preserve legacy behavior where State is // exposed if we didn't parse state. iLoggerData.State = state; + return null; } else { - try - { - PropertyDescriptorCollection itemProperties = TypeDescriptor.GetProperties(state); - - var attributeStorage = logRecord.AttributeStorage ??= new List>(itemProperties.Count); - - foreach (PropertyDescriptor? itemProperty in itemProperties) - { - if (itemProperty == null) - { - continue; - } + // Note: We clear State because the LogRecord we are processing may + // have come from the pool and may have State from a prior log. + iLoggerData.State = null; - object? value = itemProperty.GetValue(state); - if (value == null) - { - continue; - } + OpenTelemetrySdkEventSource.Log.LoggerProcessStateSkipped(); - attributeStorage.Add(new KeyValuePair(itemProperty.Name, value)); - } - - return attributeStorage; - } - catch (Exception parseException) - { - OpenTelemetrySdkEventSource.Log.LoggerParseStateException(parseException); - - return Array.Empty>(); - } + return Array.Empty>(); } } diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs index f2a392e52e7..d3ff3681f2a 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLoggerOptions.cs @@ -58,13 +58,21 @@ public class OpenTelemetryLoggerOptions /// /// Notes: /// - /// Parsing is only executed when the state logged does NOT - /// implement or As of OpenTelemetry v1.5 state parsing is handled automatically if + /// the state logged implements or where T is KeyValuePair<string, - /// object>. + /// object> than will be set + /// regardless of the value of . /// When is set to will always be . + /// langword="null"/>. When is set to will always be set to + /// the logged state to support legacy exporters which access directly. Exporters should NOT access directly because is NOT safe and may lead to + /// exceptions or incorrect data especially when using batching. Exporters + /// should use to safely access any data + /// attached to log messages. /// /// public bool ParseStateValues { get; set; } diff --git a/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs b/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs index 4c44b738444..248494307bb 100644 --- a/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs +++ b/test/OpenTelemetry.AotCompatibility.Tests/AotCompatibilityTests.cs @@ -85,7 +85,7 @@ public void EnsureAotCompatibility() Assert.True(process.ExitCode == 0, "Publishing the AotCompatibility app failed. See test output for more details."); var warnings = expectedOutput.ToString().Split('\n', '\r').Where(line => line.Contains("warning IL")); - Assert.Equal(37, warnings.Count()); + Assert.Equal(36, warnings.Count()); } } } diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs index 21d856c9799..21a583810df 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs @@ -117,7 +117,7 @@ public void ExportTest(int numberOfItems) if (numberOfItems == 0) { - Assert.Null(exportedAttributes); + Assert.Empty(exportedAttributes); Assert.Null(storage); return; } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 455c41c69a9..2830d95b048 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -66,7 +66,7 @@ public void AddOtlpLogExporterReceivesAttributesWithParseStateValueSetToFalse() Assert.Single(logRecords); var logRecord = logRecords[0]; #pragma warning disable CS0618 // Type or member is obsolete - Assert.Null(logRecord.State); + Assert.NotNull(logRecord.State); #pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(logRecord.Attributes); } @@ -101,7 +101,11 @@ public void AddOtlpLogExporterParseStateValueCanBeTurnedOff(bool parseState) { Assert.Null(logRecord.State); Assert.NotNull(logRecord.Attributes); - Assert.Contains(logRecord.Attributes, kvp => kvp.Key == "propertyA" && (string)kvp.Value == "valueA"); + + // Note: We currently do not support parsing custom states which do + // not implement the standard interfaces. We return empty attributes + // for these. + Assert.Empty(logRecord.Attributes); } else { @@ -141,7 +145,11 @@ public void AddOtlpLogExporterParseStateValueCanBeTurnedOffHosting(bool parseSta { Assert.Null(logRecord.State); Assert.NotNull(logRecord.Attributes); - Assert.Contains(logRecord.Attributes, kvp => kvp.Key == "propertyA" && (string)kvp.Value == "valueA"); + + // Note: We currently do not support parsing custom states which do + // not implement the standard interfaces. We return empty attributes + // for these. + Assert.Empty(logRecord.Attributes); } else { diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs b/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs index f9f6e6cb038..a828e7af63e 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs @@ -82,7 +82,7 @@ public void CheckStateForUnstructuredLog(bool includeFormattedMessage) const string message = "Hello, World!"; logger.LogInformation(message); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -113,7 +113,7 @@ public void CheckStateForUnstructuredLogWithStringInterpolation(bool includeForm var message = $"Hello from potato {0.99}."; logger.LogInformation(message); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -143,7 +143,7 @@ public void CheckStateForStructuredLogWithTemplate(bool includeFormattedMessage) const string message = "Hello from {name} {price}."; logger.LogInformation(message, "tomato", 2.99); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -185,7 +185,7 @@ public void CheckStateForStructuredLogWithStrongType(bool includeFormattedMessag var food = new Food { Name = "artichoke", Price = 3.99 }; logger.LogInformation("{food}", food); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -226,7 +226,7 @@ public void CheckStateForStructuredLogWithAnonymousType(bool includeFormattedMes var anonymousType = new { Name = "pumpkin", Price = 5.99 }; logger.LogInformation("{food}", anonymousType); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -271,7 +271,7 @@ public void CheckStateForStructuredLogWithGeneralType(bool includeFormattedMessa }; logger.LogInformation("{food}", food); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -321,7 +321,13 @@ public void CheckStateForExceptionLogged() const string message = "Exception Occurred"; logger.LogInformation(exception, message); - Assert.Null(exportedItems[0].State); + Assert.NotNull(exportedItems[0].State); + + var state = exportedItems[0].State; + var itemCount = state.GetType().GetProperty("Count").GetValue(state); + + // state only has {OriginalFormat} + Assert.Equal(1, itemCount); var attributes = exportedItems[0].Attributes; Assert.NotNull(attributes); @@ -334,6 +340,7 @@ public void CheckStateForExceptionLogged() Assert.Equal(exceptionMessage, loggedException.Message); Assert.Equal(message, exportedItems[0].Body); + Assert.Equal(message, state.ToString()); Assert.Null(exportedItems[0].FormattedMessage); } @@ -711,7 +718,14 @@ public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues var logRecord = exportedItems[0]; Assert.NotNull(logRecord.StateValues); - Assert.Null(logRecord.State); + if (parseStateValues) + { + Assert.Null(logRecord.State); + } + else + { + Assert.NotNull(logRecord.State); + } Assert.NotNull(logRecord.StateValues); Assert.Equal(3, logRecord.StateValues.Count); @@ -725,7 +739,14 @@ public void VerifyParseStateValues_UsingStandardExtensions(bool parseStateValues logRecord = exportedItems[1]; Assert.NotNull(logRecord.StateValues); - Assert.Null(logRecord.State); + if (parseStateValues) + { + Assert.Null(logRecord.State); + } + else + { + Assert.NotNull(logRecord.State); + } Assert.NotNull(logRecord.StateValues); Assert.Equal(4, logRecord.StateValues.Count); @@ -805,7 +826,7 @@ public void ParseStateValuesUsingIEnumerableTest() } [Fact] - public void ParseStateValuesUsingCustomTest() + public void ParseStateValuesUsingNonconformingCustomTypeTest() { using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); var logger = loggerFactory.CreateLogger(); @@ -827,12 +848,11 @@ public void ParseStateValuesUsingCustomTest() Assert.Null(logRecord.State); Assert.NotNull(logRecord.StateValues); - Assert.Equal(1, logRecord.StateValues.Count); - - KeyValuePair actualState = logRecord.StateValues[0]; - Assert.Equal("Property", actualState.Key); - Assert.Equal("Value", actualState.Value); + // Note: We currently do not support parsing custom states which do + // not implement the standard interfaces. We return empty attributes + // for these. + Assert.Empty(logRecord.StateValues); } [Fact]