diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogFieldNumberConstants.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogFieldNumberConstants.cs new file mode 100644 index 0000000000..3361a8551c --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogFieldNumberConstants.cs @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpLogFieldNumberConstants +{ + // Resource Logs +#pragma warning disable SA1310 // Field names should not contain underscore + internal const int ResourceLogs_Resource = 1; + internal const int ResourceLogs_Scope_Logs = 2; + internal const int ResourceLogs_Schema_Url = 3; + + // Resource + internal const int Resource_Attributes = 1; + + // ScopeLogs + internal const int ScopeLogs_Scope = 1; + internal const int ScopeLogs_Log_Records = 2; + internal const int ScopeLogs_Schema_Url = 3; + + // LogRecord + internal const int LogRecord_Time_Unix_Nano = 1; + internal const int LogRecord_Observed_Time_Unix_Nano = 11; + internal const int LogRecord_Severity_Number = 2; + internal const int LogRecord_Severity_Text = 3; + internal const int LogRecord_Body = 5; + internal const int LogRecord_Attributes = 6; + internal const int LogRecord_Dropped_Attributes_Count = 7; + internal const int LogRecord_Flags = 8; + internal const int LogRecord_Trace_Id = 9; + internal const int LogRecord_Span_Id = 10; + + // SeverityNumber + internal const int Severity_Number_Unspecified = 0; + internal const int Severity_Number_Trace = 1; + internal const int Severity_Number_Trace2 = 2; + internal const int Severity_Number_Trace3 = 3; + internal const int Severity_Number_Trace4 = 4; + internal const int Severity_Number_Debug = 5; + internal const int Severity_Number_Debug2 = 6; + internal const int Severity_Number_Debug3 = 7; + internal const int Severity_Number_Debug4 = 8; + internal const int Severity_Number_Info = 9; + internal const int Severity_Number_Info2 = 10; + internal const int Severity_Number_Info3 = 11; + internal const int Severity_Number_Info4 = 12; + internal const int Severity_Number_Warn = 13; + internal const int Severity_Number_Warn2 = 14; + internal const int Severity_Number_Warn3 = 15; + internal const int Severity_Number_Warn4 = 16; + internal const int Severity_Number_Error = 17; + internal const int Severity_Number_Error2 = 18; + internal const int Severity_Number_Error3 = 19; + internal const int Severity_Number_Error4 = 20; + internal const int Severity_Number_Fatal = 21; + internal const int Severity_Number_Fatal2 = 22; + internal const int Severity_Number_Fatal3 = 23; + internal const int Severity_Number_Fatal4 = 24; + + // LogRecordFlags + + internal const int LogRecord_Flags_Do_Not_Use = 0; + internal const int LogRecord_Flags_Trace_Flags_Mask = 0x000000FF; + + // InstrumentationScope + internal const int InstrumentationScope_Name = 1; + internal const int InstrumentationScope_Version = 2; + internal const int InstrumentationScope_Attributes = 3; + internal const int InstrumentationScope_Dropped_Attributes_Count = 4; + + // KeyValue + internal const int KeyValue_Key = 1; + internal const int KeyValue_Value = 2; + + // AnyValue + internal const int AnyValue_String_Value = 1; + internal const int AnyValue_Bool_Value = 2; + internal const int AnyValue_Int_Value = 3; + internal const int AnyValue_Double_Value = 4; + internal const int AnyValue_Array_Value = 5; + internal const int AnyValue_Kvlist_Value = 6; + internal const int AnyValue_Bytes_Value = 7; + + internal const int ArrayValue_Value = 1; +#pragma warning restore SA1310 // Field names should not contain underscore +} + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs new file mode 100644 index 0000000000..5016d167e2 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs @@ -0,0 +1,282 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; + +internal static class ProtobufOtlpLogSerializer +{ + private const int ReserveSizeForLength = 4; + private const int TraceIdSize = 16; + private const int SpanIdSize = 8; + + private static readonly Stack> LogsListPool = []; + private static readonly Dictionary> ScopeLogsList = []; + + internal static int WriteLogsData(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Resources.Resource? resource, in Batch logRecordBatch) + { + foreach (var logRecord in logRecordBatch) + { + var scopeName = logRecord.Logger.Name; + if (!ScopeLogsList.TryGetValue(scopeName, out var logRecords)) + { + logRecords = LogsListPool.Count > 0 ? LogsListPool.Pop() : []; + ScopeLogsList[scopeName] = logRecords; + } + + logRecords.Add(logRecord); + } + + writePosition = WriteResourceLogs(buffer, writePosition, sdkLimitOptions, experimentalOptions, resource, ScopeLogsList); + ReturnLogRecordListToPool(); + + return writePosition; + } + + internal static void ReturnLogRecordListToPool() + { + if (ScopeLogsList.Count != 0) + { + foreach (var entry in ScopeLogsList) + { + entry.Value.Clear(); + LogsListPool.Push(entry.Value); + } + + ScopeLogsList.Clear(); + } + } + + internal static int WriteResourceLogs(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Resources.Resource? resource, Dictionary> scopeLogs) + { + writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource); + writePosition = WriteScopeLogs(buffer, writePosition, sdkLimitOptions, experimentalOptions, scopeLogs); + return writePosition; + } + + internal static int WriteScopeLogs(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, Dictionary> scopeLogs) + { + if (scopeLogs != null) + { + foreach (KeyValuePair> entry in scopeLogs) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpLogFieldNumberConstants.ResourceLogs_Scope_Logs, ProtobufWireType.LEN); + int resourceLogsScopeLogsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = WriteScopeLog(buffer, writePosition, sdkLimitOptions, experimentalOptions, entry.Value[0].Logger.Name, entry.Value); + ProtobufSerializer.WriteReservedLength(buffer, resourceLogsScopeLogsLengthPosition, writePosition - (resourceLogsScopeLogsLengthPosition + ReserveSizeForLength)); + } + } + + return writePosition; + } + + internal static int WriteScopeLog(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, string loggerName, List logRecords) + { + var value = loggerName.AsSpan(); + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(value); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // numberOfUtf8CharsInString + tagSize + length field size. + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, numberOfUtf8CharsInString + 1 + serializedLengthSize, ProtobufOtlpLogFieldNumberConstants.ScopeLogs_Scope, ProtobufWireType.LEN); + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpLogFieldNumberConstants.InstrumentationScope_Name, numberOfUtf8CharsInString, value); + + for (int i = 0; i < logRecords.Count; i++) + { + writePosition = WriteLogRecord(buffer, writePosition, sdkLimitOptions, experimentalOptions, logRecords[i]); + } + + return writePosition; + } + + internal static int WriteLogRecord(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, ExperimentalOptions experimentalOptions, LogRecord logRecord) + { + var attributeValueLengthLimit = sdkLimitOptions.LogRecordAttributeValueLengthLimit; + var attributeCountLimit = sdkLimitOptions.LogRecordAttributeCountLimit ?? int.MaxValue; + + ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState + { + Buffer = buffer, + WritePosition = writePosition, + TagCount = 0, + DroppedTagCount = 0, + }; + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.ScopeLogs_Log_Records, ProtobufWireType.LEN); + int logRecordLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + var timestamp = (ulong)logRecord.Timestamp.ToUnixTimeNanoseconds(); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Time_Unix_Nano, timestamp); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Observed_Time_Unix_Nano, timestamp); + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteEnumWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Number, logRecord.Severity.HasValue ? (int)logRecord.Severity : 0); + + if (!string.IsNullOrWhiteSpace(logRecord.SeverityText)) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteStringWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Text, logRecord.SeverityText!); + } + else if (logRecord.Severity.HasValue) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteStringWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Text, logRecord.Severity.Value.ToShortName()); + } + + if (experimentalOptions.EmitLogEventAttributes) + { + if (logRecord.EventId.Id != default) + { + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, ExperimentalOptions.LogRecordEventIdAttribute, logRecord.EventId.Id, attributeCountLimit, attributeValueLengthLimit); + } + + if (!string.IsNullOrEmpty(logRecord.EventId.Name)) + { + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, ExperimentalOptions.LogRecordEventNameAttribute, logRecord.EventId.Name!, attributeCountLimit, attributeValueLengthLimit); + } + } + + if (logRecord.Exception != null) + { + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name, attributeCountLimit, attributeValueLengthLimit); + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message, attributeCountLimit, attributeValueLengthLimit); + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString(), attributeCountLimit, attributeValueLengthLimit); + } + + bool bodyPopulatedFromFormattedMessage = false; + bool isLogRecordBodySet = false; + + if (logRecord.FormattedMessage != null) + { + otlpTagWriterState.WritePosition = WriteLogRecordBody(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.FormattedMessage.AsSpan()); + bodyPopulatedFromFormattedMessage = true; + isLogRecordBodySet = true; + } + + if (logRecord.Attributes != null) + { + foreach (var attribute in logRecord.Attributes) + { + // Special casing {OriginalFormat} + // See https://github.com/open-telemetry/opentelemetry-dotnet/pull/3182 + // for explanation. + if (attribute.Key.Equals("{OriginalFormat}") && !bodyPopulatedFromFormattedMessage) + { + otlpTagWriterState.WritePosition = WriteLogRecordBody(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, (attribute.Value as string).AsSpan()); + isLogRecordBodySet = true; + } + else + { + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, attribute, attributeCountLimit, attributeValueLengthLimit); + } + } + + // Supports setting Body directly on LogRecord for the Logs Bridge API. + if (!isLogRecordBodySet && logRecord.Body != null) + { + // If {OriginalFormat} is not present in the attributes, + // use logRecord.Body if it is set. + otlpTagWriterState.WritePosition = WriteLogRecordBody(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.Body.AsSpan()); + } + } + + if (logRecord.TraceId != default && logRecord.SpanId != default) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTagAndLength(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, TraceIdSize, ProtobufOtlpLogFieldNumberConstants.LogRecord_Trace_Id, ProtobufWireType.LEN); + otlpTagWriterState.WritePosition = ProtobufOtlpTraceSerializer.WriteTraceId(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.TraceId); + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTagAndLength(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, SpanIdSize, ProtobufOtlpLogFieldNumberConstants.LogRecord_Span_Id, ProtobufWireType.LEN); + otlpTagWriterState.WritePosition = ProtobufOtlpTraceSerializer.WriteSpanId(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, logRecord.SpanId); + + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed32WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Flags, (uint)logRecord.TraceFlags); + } + + /* + * TODO: Handle scopes, otlpTagWriterState needs to be passed as ref. + logRecord.ForEachScope(ProcessScope, otlpTagWriterState); + + void ProcessScope(LogRecordScope scope, ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState) + { + foreach (var scopeItem in scope) + { + if (scopeItem.Key.Equals("{OriginalFormat}") || string.IsNullOrEmpty(scopeItem.Key)) + { + // Ignore if the scope key is empty. + // Ignore if the scope key is {OriginalFormat} + // Attributes should not contain duplicates, + // and it is expensive to de-dup, so this + // exporter is going to pass the scope items as is. + // {OriginalFormat} is going to be the key + // if one uses formatted string for scopes + // and if there are nested scopes, this is + // guaranteed to create duplicate keys. + // Similar for empty keys, which is what the + // key is going to be if user simply + // passes a string as scope. + // To summarize this exporter only allows + // IReadOnlyList> + // or IEnumerable>. + // and expect users to provide unique keys. + // Note: It is possible that we allow users + // to override this exporter feature. So not blocking + // empty/{OriginalFormat} in the SDK itself. + } + else + { + otlpTagWriterState = AddLogAttribute(ref otlpTagWriterState, scopeItem, attributeCountLimit, attributeValueLengthLimit); + } + } + } + */ + + if (otlpTagWriterState.DroppedTagCount > 0) + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Dropped_Attributes_Count, ProtobufWireType.VARINT); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteVarInt32(buffer, otlpTagWriterState.WritePosition, (uint)otlpTagWriterState.DroppedTagCount); + } + + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, logRecordLengthPosition, otlpTagWriterState.WritePosition - (logRecordLengthPosition + ReserveSizeForLength)); + + return otlpTagWriterState.WritePosition; + } + + private static int WriteLogRecordBody(byte[] buffer, int writePosition, ReadOnlySpan value) + { + var numberOfUtf8CharsInString = ProtobufSerializer.GetNumberOfUtf8CharsInString(value); + var serializedLengthSize = ProtobufSerializer.ComputeVarInt64Size((ulong)numberOfUtf8CharsInString); + + // length = numberOfUtf8CharsInString + tagSize + length field size. + writePosition = ProtobufSerializer.WriteTagAndLength(buffer, writePosition, numberOfUtf8CharsInString + 1 + serializedLengthSize, ProtobufOtlpLogFieldNumberConstants.LogRecord_Body, ProtobufWireType.LEN); + writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.AnyValue_String_Value, numberOfUtf8CharsInString, value); + return writePosition; + } + + private static ProtobufOtlpTagWriter.OtlpTagWriterState AddLogAttribute(ref ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState, KeyValuePair attribute, int maxAttributeCount, int? maxValueLength) + { + return AddLogAttribute(ref otlpTagWriterState, attribute.Key, attribute.Value, maxAttributeCount, maxValueLength); + } + + private static ProtobufOtlpTagWriter.OtlpTagWriterState AddLogAttribute(ref ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState, string key, object? value, int maxAttributeCount, int? maxValueLength) + { + if (otlpTagWriterState.TagCount == maxAttributeCount) + { + otlpTagWriterState.DroppedTagCount++; + } + else + { + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Attributes, ProtobufWireType.LEN); + int logAttributesLengthPosition = otlpTagWriterState.WritePosition; + otlpTagWriterState.WritePosition += ReserveSizeForLength; + + ProtobufOtlpTagWriter.Instance.TryWriteTag(ref otlpTagWriterState, key, value, maxValueLength); + + var logAttributesLength = otlpTagWriterState.WritePosition - (logAttributesLengthPosition + ReserveSizeForLength); + ProtobufSerializer.WriteReservedLength(otlpTagWriterState.Buffer, logAttributesLengthPosition, logAttributesLength); + otlpTagWriterState.TagCount++; + } + + return otlpTagWriterState; + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs index fdfbcf1af8..79b3213a1c 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTagWriter.cs @@ -84,6 +84,8 @@ protected override void OnUnsupportedTagDropped( internal struct OtlpTagWriterState { public byte[] Buffer; + public int DroppedTagCount; + public int TagCount; public int WritePosition; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs index a250d85096..a1ea0ccefa 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpTraceSerializer.cs @@ -18,7 +18,7 @@ internal static class ProtobufOtlpTraceSerializer private static readonly Stack> ActivityListPool = []; private static readonly Dictionary> ScopeTracesList = []; - internal static int WriteTraceData(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Resources.Resource? resource, Batch batch) + internal static int WriteTraceData(byte[] buffer, int writePosition, SdkLimitOptions sdkLimitOptions, Resources.Resource? resource, in Batch batch) { foreach (var activity in batch) { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index c47fa7b5a5..981006aeb4 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission; using OpenTelemetry.Internal; using OpenTelemetry.Logs; @@ -243,8 +244,10 @@ public void AddOtlpLogExporterParseStateValueCanBeTurnedOffHosting(bool parseSta #pragma warning restore CS0618 // Type or member is obsolete } - [Fact] - public void OtlpLogRecordTestWhenStateValuesArePopulated() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void OtlpLogRecordTestWhenStateValuesArePopulated(bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -266,7 +269,16 @@ public void OtlpLogRecordTestWhenStateValuesArePopulated() var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); @@ -287,10 +299,13 @@ public void OtlpLogRecordTestWhenStateValuesArePopulated() } [Theory] - [InlineData("true")] - [InlineData("false")] - [InlineData(null)] - public void CheckToOtlpLogRecordEventId(string? emitLogEventAttributes) + [InlineData("true", true)] + [InlineData("false", true)] + [InlineData(null, true)] + [InlineData("true", false)] + [InlineData("false", false)] + [InlineData(null, false)] + public void CheckToOtlpLogRecordEventId(string? emitLogEventAttributes, bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -316,7 +331,16 @@ public void CheckToOtlpLogRecordEventId(string? emitLogEventAttributes) var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new(configuration), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); @@ -339,7 +363,16 @@ public void CheckToOtlpLogRecordEventId(string? emitLogEventAttributes) Assert.Single(logRecords); logRecord = logRecords[0]; - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new(configuration), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } + Assert.NotNull(otlpLogRecord); Assert.Equal("Hello from tomato 2.99.", otlpLogRecord.Body.StringValue); @@ -359,8 +392,10 @@ public void CheckToOtlpLogRecordEventId(string? emitLogEventAttributes) } } - [Fact] - public void CheckToOtlpLogRecordTimestamps() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckToOtlpLogRecordTimestamps(bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -373,15 +408,26 @@ public void CheckToOtlpLogRecordTimestamps() var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); Assert.True(otlpLogRecord.TimeUnixNano > 0); Assert.True(otlpLogRecord.ObservedTimeUnixNano > 0); } - [Fact] - public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity(bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -395,7 +441,16 @@ public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity() var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.Null(Activity.Current); Assert.NotNull(otlpLogRecord); @@ -404,8 +459,10 @@ public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity() Assert.Equal(0u, otlpLogRecord.Flags); } - [Fact] - public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag(bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -428,7 +485,17 @@ public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag() var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); Assert.Equal(expectedTraceId.ToString(), ActivityTraceId.CreateFromBytes(otlpLogRecord.TraceId.ToByteArray()).ToString()); @@ -437,13 +504,19 @@ public void CheckToOtlpLogRecordSpanIdTraceIdAndFlag() } [Theory] - [InlineData(LogLevel.Trace)] - [InlineData(LogLevel.Debug)] - [InlineData(LogLevel.Information)] - [InlineData(LogLevel.Warning)] - [InlineData(LogLevel.Error)] - [InlineData(LogLevel.Critical)] - public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel) + [InlineData(LogLevel.Trace, true)] + [InlineData(LogLevel.Debug, true)] + [InlineData(LogLevel.Information, true)] + [InlineData(LogLevel.Warning, true)] + [InlineData(LogLevel.Error, true)] + [InlineData(LogLevel.Critical, true)] + [InlineData(LogLevel.Trace, false)] + [InlineData(LogLevel.Debug, false)] + [InlineData(LogLevel.Information, false)] + [InlineData(LogLevel.Warning, false)] + [InlineData(LogLevel.Error, false)] + [InlineData(LogLevel.Critical, false)] + public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel, bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -462,7 +535,16 @@ public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel) var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); #pragma warning disable CS0618 // Type or member is obsolete @@ -494,9 +576,11 @@ public void CheckToOtlpLogRecordSeverityLevelAndText(LogLevel logLevel) } [Theory] - [InlineData(true)] - [InlineData(false)] - public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage, bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -519,7 +603,16 @@ public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); if (includeFormattedMessage) @@ -538,7 +631,15 @@ public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) Assert.Single(logRecords); logRecord = logRecords[0]; - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); @@ -554,7 +655,15 @@ public void CheckToOtlpLogRecordBodyIsPopulated(bool includeFormattedMessage) Assert.Single(logRecords); logRecord = logRecords[0]; - otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); @@ -621,8 +730,10 @@ public void LogRecordBodyIsExportedWhenUsingBridgeApi(bool isBodySet) Assert.Equal("Hello from {name} {price}.", otlpLogRecord.Body.StringValue); } - [Fact] - public void CheckToOtlpLogRecordExceptionAttributes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckToOtlpLogRecordExceptionAttributes(bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -638,7 +749,16 @@ public void CheckToOtlpLogRecordExceptionAttributes() var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); var otlpLogRecordAttributes = otlpLogRecord.Attributes.ToString(); @@ -654,8 +774,10 @@ public void CheckToOtlpLogRecordExceptionAttributes() Assert.Contains(logRecord.Exception.ToInvariantString(), otlpLogRecordAttributes); } - [Fact] - public void CheckToOtlpLogRecordRespectsAttributeLimits() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CheckToOtlpLogRecordRespectsAttributeLimits(bool useCustomSerializer) { var sdkLimitOptions = new SdkLimitOptions { @@ -677,7 +799,16 @@ public void CheckToOtlpLogRecordRespectsAttributeLimits() var otlpLogRecordTransformer = new OtlpLogRecordTransformer(sdkLimitOptions, new()); var logRecord = logRecords[0]; - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(sdkLimitOptions, new(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } Assert.NotNull(otlpLogRecord); Assert.Equal(1u, otlpLogRecord.DroppedAttributesCount); @@ -763,8 +894,10 @@ public void Export_WhenExportIsSuccessful_ReturnsExportResultSuccess() Assert.Equal(ExportResult.Success, result); } - [Fact] - public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribute() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribute(bool useCustomSerializer) { // Arrange. var logRecords = new List(1); @@ -791,7 +924,17 @@ public void ToOtlpLog_WhenOptionsIncludeScopesIsFalse_DoesNotContainScopeAttribu // Assert. var logRecord = logRecords.Single(); var otlpLogRecordTransformer = new OtlpLogRecordTransformer(DefaultSdkLimitOptions, new()); - var otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + OtlpLogs.LogRecord? otlpLogRecord; + + if (useCustomSerializer) + { + otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecord); + } + else + { + otlpLogRecord = otlpLogRecordTransformer.ToOtlpLog(logRecord); + } + Assert.NotNull(otlpLogRecord); var actualScope = TryGetAttribute(otlpLogRecord, expectedScopeKey); Assert.Null(actualScope); @@ -1340,8 +1483,10 @@ public void AddOtlpLogExporterLogRecordProcessorOptionsTest(ExportProcessorType } } - [Fact] - public void ValidateInstrumentationScope() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ValidateInstrumentationScope(bool useCustomSerializer) { var logRecords = new List(); using var loggerFactory = LoggerFactory.Create(builder => @@ -1363,7 +1508,15 @@ public void ValidateInstrumentationScope() var resourceBuilder = ResourceBuilder.CreateEmpty(); var processResource = resourceBuilder.Build().ToOtlpResource(); - var request = logRecordTransformer.BuildExportRequest(processResource, batch); + OtlpCollector.ExportLogsServiceRequest request; + if (useCustomSerializer) + { + request = CreateLogsExportRequest(DefaultSdkLimitOptions, new ExperimentalOptions(), batch, resourceBuilder.Build()); + } + else + { + request = logRecordTransformer.BuildExportRequest(processResource, batch); + } Assert.Single(request.ResourceLogs); @@ -1388,12 +1541,19 @@ public void ValidateInstrumentationScope() logRecordTransformer.Return(request); Assert.Equal(2, OtlpLogRecordTransformer.LogListPool.Count); - request = logRecordTransformer.BuildExportRequest(processResource, batch); + if (useCustomSerializer) + { + request = CreateLogsExportRequest(DefaultSdkLimitOptions, new ExperimentalOptions(), batch, resourceBuilder.Build()); + } + else + { + request = logRecordTransformer.BuildExportRequest(processResource, batch); - Assert.Single(request.ResourceLogs); + // ScopeLogs will be reused. + Assert.Empty(OtlpLogRecordTransformer.LogListPool); + } - // ScopeLogs will be reused. - Assert.Empty(OtlpLogRecordTransformer.LogListPool); + Assert.Single(request.ResourceLogs); } [Theory] @@ -1451,9 +1611,11 @@ public void VerifyEnvironmentVariablesTakenFromIConfigurationWhenUsingLoggingBui } [Theory] - [InlineData("my_instrumentation_scope_name", "my_instrumentation_scope_name")] - [InlineData(null, "")] - public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string? loggerName, string expectedScopeName) + [InlineData("my_instrumentation_scope_name", "my_instrumentation_scope_name", true)] + [InlineData(null, "", true)] + [InlineData("my_instrumentation_scope_name", "my_instrumentation_scope_name", false)] + [InlineData(null, "", false)] + public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string? loggerName, string expectedScopeName, bool useCustomSerializer) { LogRecordAttributeList attributes = default; attributes.Add("name", "tomato"); @@ -1477,9 +1639,17 @@ public void LogRecordLoggerNameIsExportedWhenUsingBridgeApi(string? loggerName, var batch = new Batch(new[] { logRecords[0] }, 1); - var request = otlpLogRecordTransformer.BuildExportRequest( + OtlpCollector.ExportLogsServiceRequest request; + if (useCustomSerializer) + { + request = CreateLogsExportRequest(DefaultSdkLimitOptions, new ExperimentalOptions(), batch, ResourceBuilder.CreateEmpty().Build()); + } + else + { + request = otlpLogRecordTransformer.BuildExportRequest( new Proto.Resource.V1.Resource(), batch); + } Assert.NotNull(request); Assert.Single(request.ResourceLogs); @@ -1624,4 +1794,24 @@ private static void ConfigureOtlpExporter( #pragma warning restore CS0618 // Type or member is obsolete } } + + private static OtlpCollector.ExportLogsServiceRequest CreateLogsExportRequest(SdkLimitOptions sdkOptions, ExperimentalOptions experimentalOptions, in Batch batch, Resource resource) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpLogSerializer.WriteLogsData(buffer, 0, sdkOptions, experimentalOptions, resource, batch); + using var stream = new MemoryStream(buffer, 0, writePosition); + var logsData = OtlpLogs.ResourceLogs.Parser.ParseFrom(stream); + var request = new OtlpCollector.ExportLogsServiceRequest(); + request.ResourceLogs.Add(logsData); + return request; + } + + private static OtlpLogs.LogRecord? ToOtlpLogs(SdkLimitOptions sdkOptions, ExperimentalOptions experimentalOptions, LogRecord logRecord) + { + var buffer = new byte[4096]; + var writePosition = ProtobufOtlpLogSerializer.WriteLogRecord(buffer, 0, sdkOptions, experimentalOptions, logRecord); + using var stream = new MemoryStream(buffer, 0, writePosition); + var scopeLogs = OtlpLogs.ScopeLogs.Parser.ParseFrom(stream); + return scopeLogs.LogRecords.FirstOrDefault(); + } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index 5b149bb029..9d43558d3a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -1049,7 +1049,7 @@ public void SpanLinkFlagsTest(bool isRecorded, bool isRemote, bool useCustomSeri return scopeSpans.Spans.FirstOrDefault(); } - private static OtlpCollector.ExportTraceServiceRequest CreateTraceExportRequest(SdkLimitOptions sdkOptions, Batch batch, Resource resource) + private static OtlpCollector.ExportTraceServiceRequest CreateTraceExportRequest(SdkLimitOptions sdkOptions, in Batch batch, Resource resource) { var buffer = new byte[4096]; var writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(buffer, 0, sdkOptions, resource, batch);