diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index 5cda05213a..091eb1da70 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -3,7 +3,8 @@ ## Unreleased * Add ResourceFieldNames to filter resource attributes to send to Geneva - ([#3552](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3552)) + ([#3552](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3552), + [#3646](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3646)) ## 1.14.0 diff --git a/src/OpenTelemetry.Exporter.Geneva/GenevaExporterOptions.cs b/src/OpenTelemetry.Exporter.Geneva/GenevaExporterOptions.cs index 9e83063502..8730a6b716 100644 --- a/src/OpenTelemetry.Exporter.Geneva/GenevaExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Geneva/GenevaExporterOptions.cs @@ -107,6 +107,8 @@ public IReadOnlyDictionary? TableNameMappings /// /// Gets or sets prepopulated fields. + /// + /// Pre-populated fields are fields that are added as dedicated fields to every record, unless it would conflict with a log or trace field that is marked as a custom field. /// public IReadOnlyDictionary PrepopulatedFields { diff --git a/src/OpenTelemetry.Exporter.Geneva/GenevaLogExporter.cs b/src/OpenTelemetry.Exporter.Geneva/GenevaLogExporter.cs index 2faab4661a..cc10468e1f 100644 --- a/src/OpenTelemetry.Exporter.Geneva/GenevaLogExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/GenevaLogExporter.cs @@ -9,6 +9,7 @@ using OpenTelemetry.Exporter.Geneva.Tld; using OpenTelemetry.Internal; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter.Geneva; @@ -17,10 +18,10 @@ namespace OpenTelemetry.Exporter.Geneva; /// public class GenevaLogExporter : GenevaBaseExporter { + internal readonly IDisposable Exporter; internal bool IsUsingUnixDomainSocket; private readonly ExportLogRecordFunc exportLogRecord; - private readonly IDisposable exporter; private bool isDisposed; @@ -46,7 +47,7 @@ public GenevaLogExporter(GenevaExporterOptions options) var eventHeaderLogExporter = new EventHeaderLogExporter(options); this.IsUsingUnixDomainSocket = false; this.exportLogRecord = eventHeaderLogExporter.Export; - this.exporter = eventHeaderLogExporter; + this.Exporter = eventHeaderLogExporter; return; #else throw new ArgumentException("Exporting data in user_events is only supported for .NET 8 or later on Linux."); @@ -90,17 +91,21 @@ public GenevaLogExporter(GenevaExporterOptions options) if (useMsgPackExporter) { - var msgPackLogExporter = new MsgPackLogExporter(options); + var msgPackLogExporter = new MsgPackLogExporter(options, () => + { + // this is not equivalent to passing a method reference, because the ParentProvider could change after the constructor. + return this.ParentProvider?.GetResource() ?? Resource.Empty; + }); this.IsUsingUnixDomainSocket = msgPackLogExporter.IsUsingUnixDomainSocket; this.exportLogRecord = msgPackLogExporter.Export; - this.exporter = msgPackLogExporter; + this.Exporter = msgPackLogExporter; } else { var tldLogExporter = new TldLogExporter(options); this.IsUsingUnixDomainSocket = false; this.exportLogRecord = tldLogExporter.Export; - this.exporter = tldLogExporter; + this.Exporter = tldLogExporter; } } @@ -124,7 +129,7 @@ protected override void Dispose(bool disposing) { try { - this.exporter.Dispose(); + this.Exporter.Dispose(); } catch (Exception ex) { diff --git a/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs b/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs index 17319575db..7e40829b34 100644 --- a/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs @@ -6,6 +6,7 @@ using OpenTelemetry.Exporter.Geneva.MsgPack; using OpenTelemetry.Exporter.Geneva.Tld; using OpenTelemetry.Internal; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter.Geneva; @@ -16,8 +17,9 @@ public class GenevaTraceExporter : GenevaBaseExporter { internal readonly bool IsUsingUnixDomainSocket; + internal readonly IDisposable Exporter; + private readonly ExportActivityFunc exportActivity; - private readonly IDisposable exporter; private bool isDisposed; @@ -68,17 +70,21 @@ public GenevaTraceExporter(GenevaExporterOptions options) if (useMsgPackExporter) { - var msgPackTraceExporter = new MsgPackTraceExporter(options, this.ParentProvider.GetResource); + var msgPackTraceExporter = new MsgPackTraceExporter(options, () => + { + // this is not equivalent to passing a method reference, because the ParentProvider could change after the constructor. + return this.ParentProvider?.GetResource() ?? Resource.Empty; + }); this.IsUsingUnixDomainSocket = msgPackTraceExporter.IsUsingUnixDomainSocket; this.exportActivity = msgPackTraceExporter.Export; - this.exporter = msgPackTraceExporter; + this.Exporter = msgPackTraceExporter; } else { var tldTraceExporter = new TldTraceExporter(options); this.IsUsingUnixDomainSocket = false; this.exportActivity = tldTraceExporter.Export; - this.exporter = tldTraceExporter; + this.Exporter = tldTraceExporter; } } @@ -102,7 +108,7 @@ protected override void Dispose(bool disposing) { try { - this.exporter.Dispose(); + this.Exporter.Dispose(); } catch (Exception ex) { diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackLogExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackLogExporter.cs index 7a616628f8..04b3539c49 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackLogExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackLogExporter.cs @@ -12,6 +12,7 @@ using OpenTelemetry.Exporter.Geneva.Transports; using OpenTelemetry.Internal; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter.Geneva.MsgPack; @@ -19,7 +20,8 @@ internal sealed class MsgPackLogExporter : MsgPackExporter, IDisposable { public const int BUFFER_SIZE = 65360; // the maximum ETW payload (inclusive) - internal static readonly ThreadLocal Buffer = new(); + // This helps tests subscribe to the output of this class + internal Action>? DataTransportListener; private static readonly Action ProcessScopeForIndividualColumnsAction = OnProcessScopeForIndividualColumns; private static readonly Action ProcessScopeForEnvPropertiesAction = OnProcessScopeForEnvProperties; @@ -28,31 +30,40 @@ internal sealed class MsgPackLogExporter : MsgPackExporter, IDisposable "Trace", "Debug", "Information", "Warning", "Error", "Critical", "None" ]; + private readonly ThreadLocal buffer = new(); private readonly bool shouldExportEventName; private readonly TableNameSerializer tableNameSerializer; #if NET private readonly FrozenSet? customFields; - private readonly FrozenDictionary? prepopulatedFields; #else private readonly HashSet? customFields; - private readonly Dictionary? prepopulatedFields; #endif private readonly ExceptionStackExportMode exportExceptionStack; - private readonly List? prepopulatedFieldKeys; + private readonly bool userProvidedPrepopulatedFields; + private readonly IEnumerable? resourceFieldNames; private readonly byte[] bufferEpilogue; private readonly IDataTransport dataTransport; + private readonly Func resourceProvider; private readonly int stringFieldSizeLimitCharCount; // the maximum string size limit for MsgPack strings // This is used for Scopes private readonly ThreadLocal serializationData = new(); +#if NET + private FrozenDictionary? prepopulatedFields; +#else + private Dictionary? prepopulatedFields; +#endif private bool isDisposed; - public MsgPackLogExporter(GenevaExporterOptions options) + public MsgPackLogExporter(GenevaExporterOptions options, Func resourceProvider) { Guard.ThrowIfNull(options); + Guard.ThrowIfNull(resourceProvider); + + this.resourceProvider = resourceProvider; this.tableNameSerializer = new(options, defaultTableName: "Log"); this.exportExceptionStack = options.ExceptionStackExportMode; @@ -88,14 +99,32 @@ public MsgPackLogExporter(GenevaExporterOptions options) } this.stringFieldSizeLimitCharCount = connectionStringBuilder.PrivatePreviewLogMessagePackStringSizeLimit; + + if (options.PrepopulatedFields != null && options.PrepopulatedFields.Count > 0 && options.ResourceFieldNames != null) + { + throw new ArgumentException("PrepopulatedFields and ResourceFieldNames are mutually exclusive options"); + } + + if (options.ResourceFieldNames != null) + { + foreach (var wantedResourceAttribute in options.ResourceFieldNames) + { + if (PART_A_MAPPING_DICTIONARY.Values.Contains(wantedResourceAttribute)) + { + throw new ArgumentException($"'{wantedResourceAttribute}' cannot be specified through a resource attribute. Remove it from ResourceFieldNames"); + } + } + + this.resourceFieldNames = options.ResourceFieldNames; + } + + this.userProvidedPrepopulatedFields = options.PrepopulatedFields != null && options.PrepopulatedFields.Count > 0; if (options.PrepopulatedFields != null) { - this.prepopulatedFieldKeys = []; var tempPrepopulatedFields = new Dictionary(options.PrepopulatedFields.Count, StringComparer.Ordinal); foreach (var kv in options.PrepopulatedFields) { tempPrepopulatedFields[kv.Key] = kv.Value; - this.prepopulatedFieldKeys.Add(kv.Key); } #if NET @@ -124,7 +153,7 @@ public MsgPackLogExporter(GenevaExporterOptions options) var buffer = new byte[BUFFER_SIZE]; var cursor = MessagePackSerializer.Serialize(buffer, 0, new Dictionary { { "TimeFormat", "DateTime" } }); this.bufferEpilogue = new byte[cursor - 0]; - System.Buffer.BlockCopy(buffer, 0, this.bufferEpilogue, 0, cursor - 0); + Buffer.BlockCopy(buffer, 0, this.bufferEpilogue, 0, cursor - 0); } internal bool IsUsingUnixDomainSocket => this.dataTransport is UnixDomainSocketDataTransport; @@ -139,6 +168,8 @@ public ExportResult Export(in Batch batch) { var data = this.SerializeLogRecord(logRecord); + this.DataTransportListener?.Invoke(data); + this.dataTransport.Send(data.Array!, data.Count); } catch (Exception ex) @@ -160,11 +191,11 @@ public void Dispose() return; } - // DO NOT Dispose m_buffer as it is a static type try { (this.dataTransport as IDisposable)?.Dispose(); this.serializationData.Dispose(); + this.buffer.Dispose(); } catch (Exception ex) { @@ -174,27 +205,127 @@ public void Dispose() this.isDisposed = true; } + /// + /// Updates the prepopulatedFields field to include resource attributes only available at runtime. + /// This function needs to be idempotent in case it's accidentally called twice. + /// + internal void AddResourceAttributesToPrepopulated() + { + var resourceAttributes = this.resourceProvider().Attributes; + + var tempPrepopulatedFields = this.prepopulatedFields != null + ? new Dictionary(this.prepopulatedFields, StringComparer.Ordinal) + : new Dictionary(StringComparer.Ordinal); + foreach (var resourceAttribute in resourceAttributes) + { + var key = resourceAttribute.Key; + var value = resourceAttribute.Value; + + var isWantedAttribute = false; + if (this.resourceFieldNames != null) + { + // this might seem inefficient, but it's only run once and I don't expect there to be many resource attributes + foreach (var wantedAttribute in this.resourceFieldNames!) + { + if (wantedAttribute == key) + { + switch (value) + { + case bool: + case byte: + case sbyte: + case short: + case ushort: + case int: + case uint: + case long: + case ulong: + case float: + case double: + case string: + break; + case null: + // This should be impossible because Resource attributes cannot have null values. + // But just in case, turn it into something serializable to avoid crashing. + value = ""; + break; + default: + // Try to construct a value that communicates that the type is not supported. + try + { + var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture); + value = stringValue == null ? "" : $""; + } + catch + { + value = ""; + } + + break; + } + + isWantedAttribute = true; + break; + } + } + } + + if (!this.userProvidedPrepopulatedFields) + { + // it's only safe to add these special resource fields if we are sure the user didn't provide them as a PrepopulatedField already + if (key == "service.name") + { + key = Schema.V40.PartA.Extensions.Cloud.Role; + isWantedAttribute = true; + } + + if (key == "service.instanceId") + { + key = Schema.V40.PartA.Extensions.Cloud.RoleInstance; + isWantedAttribute = true; + } + } + + if (isWantedAttribute) + { + tempPrepopulatedFields[key] = value; + } + } + +#if NET + this.prepopulatedFields = tempPrepopulatedFields.ToFrozenDictionary(StringComparer.Ordinal); +#else + this.prepopulatedFields = tempPrepopulatedFields; +#endif + } + internal ArraySegment SerializeLogRecord(LogRecord logRecord) { // `LogRecord.State` and `LogRecord.StateValues` were marked Obsolete in https://github.com/open-telemetry/opentelemetry-dotnet/pull/4334 #pragma warning disable 0618 - IReadOnlyList>? listKvp; + IReadOnlyList>? logFields; if (logRecord.StateValues != null) { - listKvp = logRecord.StateValues; + logFields = logRecord.StateValues; } else { // Attempt to see if State could be ROL_KVP. - listKvp = logRecord.State as IReadOnlyList>; + logFields = logRecord.State as IReadOnlyList>; } #pragma warning restore 0618 - var buffer = Buffer.Value ??= new byte[BUFFER_SIZE]; // TODO: handle OOM + var buffer = this.buffer.Value; + if (buffer == null) + { + this.AddResourceAttributesToPrepopulated(); + buffer = new byte[BUFFER_SIZE]; // TODO: handle OOM + this.buffer.Value = buffer; + } /* Fluentd Forward Mode: [ - "Log", + "Log", // (or category name) [ [ , { "env_ver": "4.0", ... } ] ], @@ -227,13 +358,11 @@ internal ArraySegment SerializeLogRecord(LogRecord logRecord) ushort cntFields = 0; var idxMapSizePatch = cursor - 2; - if (this.prepopulatedFieldKeys != null) + if (this.prepopulatedFields != null) { - for (var i = 0; i < this.prepopulatedFieldKeys.Count; i++) + foreach (var field in this.prepopulatedFields) { - var key = this.prepopulatedFieldKeys[i]; - var value = this.prepopulatedFields![key]; - cursor = AddPartAField(buffer, cursor, key, value); + cursor = AddPartAField(buffer, cursor, field.Key, field.Value); cntFields += 1; } } @@ -298,9 +427,9 @@ internal ArraySegment SerializeLogRecord(LogRecord logRecord) var hasEnvProperties = false; var bodyPopulated = false; var namePopulated = false; - for (var i = 0; i < listKvp?.Count; i++) + for (var i = 0; i < logFields?.Count; i++) { - var entry = listKvp[i]; + var entry = logFields[i]; // Iteration #1 - Get those fields which become dedicated columns // i.e all Part B fields and opt-in Part C fields. @@ -377,9 +506,9 @@ internal ArraySegment SerializeLogRecord(LogRecord logRecord) cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "env_properties"); cursor = MessagePackSerializer.WriteMapHeader(buffer, cursor, ushort.MaxValue); var idxMapSizeEnvPropertiesPatch = cursor - 2; - for (var i = 0; i < listKvp!.Count; i++) + for (var i = 0; i < logFields!.Count; i++) { - var entry = listKvp[i]; + var entry = logFields[i]; if (entry.Key == "{OriginalFormat}" || this.customFields!.Contains(entry.Key)) { continue; @@ -451,7 +580,7 @@ internal ArraySegment SerializeLogRecord(LogRecord logRecord) } MessagePackSerializer.WriteUInt16(buffer, idxMapSizePatch, cntFields); - System.Buffer.BlockCopy(this.bufferEpilogue, 0, buffer, cursor, this.bufferEpilogue.Length); + Buffer.BlockCopy(this.bufferEpilogue, 0, buffer, cursor, this.bufferEpilogue.Length); cursor += this.bufferEpilogue.Length; return new(buffer, 0, cursor); } diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs index 13628d5a7e..c7d310454b 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs @@ -75,6 +75,7 @@ internal sealed class MsgPackTraceExporter : MsgPackExporter, IDisposable // so constructing a whole new data structure for it is overkill. private readonly IEnumerable? resourceFieldNames; private readonly bool shouldIncludeTraceState; + private readonly bool userProvidedPrepopulatedFields; private readonly string partAName; private readonly Func resourceProvider; @@ -133,6 +134,11 @@ public MsgPackTraceExporter(GenevaExporterOptions options, Func resour throw new NotSupportedException($"Protocol '{connectionStringBuilder.Protocol}' is not supported"); } + if (options.PrepopulatedFields != null && options.PrepopulatedFields.Count > 0 && options.ResourceFieldNames != null) + { + throw new ArgumentException("PrepopulatedFields and ResourceFieldNames are mutually exclusive options"); + } + if (options.ResourceFieldNames != null) { foreach (var wantedResourceAttribute in options.ResourceFieldNames) @@ -144,6 +150,17 @@ public MsgPackTraceExporter(GenevaExporterOptions options, Func resour } } + this.userProvidedPrepopulatedFields = options.PrepopulatedFields != null && options.PrepopulatedFields.Count > 0; + + this.prepopulatedFields = new Dictionary(0, StringComparer.Ordinal); + if (options.PrepopulatedFields != null) + { + foreach (var entry in options.PrepopulatedFields) + { + this.prepopulatedFields.Add(entry.Key, entry.Value); + } + } + // TODO: Validate custom fields (reserved name? etc). if (options.CustomFields != null) { @@ -187,12 +204,6 @@ public MsgPackTraceExporter(GenevaExporterOptions options, Func resour #endif } - this.prepopulatedFields = []; - foreach (var entry in options.PrepopulatedFields) - { - this.prepopulatedFields.Add(entry.Key, entry.Value); - } - this.resourceFieldNames = options.ResourceFieldNames; this.shouldIncludeTraceState = options.IncludeTraceStateForSpan; } @@ -338,8 +349,8 @@ internal void CreateFraming() if (this.resourceFieldNames != null) { - // if ResourceFieldNames is set, it overrides the existing prepopulated fields setting. - this.prepopulatedFields = []; + // if ResourceFieldNames is set, we use resource attributes rather than PrepopulatedFields + this.prepopulatedFields = new Dictionary(0, StringComparer.Ordinal); } // this is guaranteed to not be null because it's set in the constructor @@ -399,21 +410,25 @@ internal void CreateFraming() } } - if (key == "service.name") + if (!this.userProvidedPrepopulatedFields) { - key = Schema.V40.PartA.Extensions.Cloud.Role; - isWantedAttribute = true; - } + // it's only safe to add these special resource fields if we are sure the user didn't provide them as a PrepopulatedField already + if (key == "service.name") + { + key = Schema.V40.PartA.Extensions.Cloud.Role; + isWantedAttribute = true; + } - if (key == "service.instanceId") - { - key = Schema.V40.PartA.Extensions.Cloud.RoleInstance; - isWantedAttribute = true; + if (key == "service.instanceId") + { + key = Schema.V40.PartA.Extensions.Cloud.RoleInstance; + isWantedAttribute = true; + } } if (isWantedAttribute) { - this.prepopulatedFields.Add(key, value); + this.prepopulatedFields[key] = value; } } diff --git a/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/LogExporterBenchmarks.cs b/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/LogExporterBenchmarks.cs index 1c1bef453d..7c539c56ed 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/LogExporterBenchmarks.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/LogExporterBenchmarks.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter.Geneva.MsgPack; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; /* BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) @@ -74,16 +75,18 @@ public LogExporterBenchmarks() // For msgpack serialization + export this.logRecord = GenerateTestLogRecord(); this.batch = GenerateTestLogRecordBatch(); - this.exporter = new MsgPackLogExporter(new GenevaExporterOptions - { - ConnectionString = "EtwSession=OpenTelemetry", - PrepopulatedFields = new Dictionary + this.exporter = new MsgPackLogExporter( + new GenevaExporterOptions { - ["cloud.role"] = "BusyWorker", - ["cloud.roleInstance"] = "CY1SCH030021417", - ["cloud.roleVer"] = "9.0.15289.2", + ConnectionString = "EtwSession=OpenTelemetry", + PrepopulatedFields = new Dictionary + { + ["cloud.role"] = "BusyWorker", + ["cloud.roleInstance"] = "CY1SCH030021417", + ["cloud.roleVer"] = "9.0.15289.2", + }, }, - }); + () => Resource.Empty); } [Benchmark] diff --git a/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/TLDLogExporterBenchmarks.cs b/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/TLDLogExporterBenchmarks.cs index 4256741e44..8808c1d016 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/TLDLogExporterBenchmarks.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Benchmarks/Exporter/TLDLogExporterBenchmarks.cs @@ -6,6 +6,7 @@ using OpenTelemetry.Exporter.Geneva.MsgPack; using OpenTelemetry.Exporter.Geneva.Tld; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; /* @@ -37,16 +38,18 @@ public class TLDLogExporterBenchmarks public TLDLogExporterBenchmarks() { - this.msgPackExporter = new MsgPackLogExporter(new GenevaExporterOptions - { - ConnectionString = "EtwSession=OpenTelemetry", - PrepopulatedFields = new Dictionary + this.msgPackExporter = new MsgPackLogExporter( + new GenevaExporterOptions { - ["cloud.role"] = "BusyWorker", - ["cloud.roleInstance"] = "CY1SCH030021417", - ["cloud.roleVer"] = "9.0.15289.2", + ConnectionString = "EtwSession=OpenTelemetry", + PrepopulatedFields = new Dictionary + { + ["cloud.role"] = "BusyWorker", + ["cloud.roleInstance"] = "CY1SCH030021417", + ["cloud.roleVer"] = "9.0.15289.2", + }, }, - }); + () => Resource.Empty); this.tldExporter = new TldLogExporter(new GenevaExporterOptions() { diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterAFDCorrelationTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterAFDCorrelationTests.cs index 4202aff167..cecf3adaf8 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterAFDCorrelationTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterAFDCorrelationTests.cs @@ -65,11 +65,31 @@ public void AFDCorrelationIdLogProcessor_MultithreadedAccess_HandlesGracefully() // Create a test exporter using var exporter = new GenevaLogExporter(exporterOptions); + List exportedCorrelationIds = []; + int foundWithoutCorrelationIds = 0; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => + { + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(data, MessagePack.Resolvers.ContractlessStandardResolver.Options); + var signal = (fluentdData as object[])[0] as string; + var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; + var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; + + if (mapping.ContainsKey("AFDCorrelationId")) + { + exportedCorrelationIds.Add(mapping["AFDCorrelationId"] as string); + } + else + { + foundWithoutCorrelationIds++; + } + }; + // Now create multiple threads to simulate concurrent access var logger = loggerFactory.CreateLogger(); const int threadCount = 10; var threads = new Thread[threadCount]; var countWithCorrelationId = 0; + List expectedCorrelationIds = []; var countWithoutCorrelationId = 0; for (int i = 0; i < threadCount; i++) @@ -80,35 +100,16 @@ public void AFDCorrelationIdLogProcessor_MultithreadedAccess_HandlesGracefully() if (threadIndex % 2 == 0) { // This thread sets AFDCorrelationId before logging - var actualCorrelationId = $"CorrelationId-{threadIndex}"; - OpenTelemetryContext.SetAFDCorrelationId(actualCorrelationId); + var expectedCorrelationId = $"CorrelationId-{threadIndex}"; + OpenTelemetryContext.SetAFDCorrelationId(expectedCorrelationId); #pragma warning disable CA2254 // Template should be a static expression logger.LogInformation($"Thread {threadIndex} with correlation ID"); #pragma warning restore CA2254 // Template should be a static expression lock (syncObj) { countWithCorrelationId++; + expectedCorrelationIds.Add(expectedCorrelationId); } - - byte[] serializedData; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - serializedData = MsgPackLogExporter.Buffer.Value; - } - else - { - // Read the data sent via socket. - serializedData = new byte[65360]; - _ = receiverSocket.Receive(serializedData); - } - - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(serializedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); - var signal = (fluentdData as object[])[0] as string; - var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; - var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; - - var expectedCorrelationId = mapping["AFDCorrelationId"] as string; - Assert.Equal(actualCorrelationId, expectedCorrelationId); } else { @@ -119,26 +120,6 @@ public void AFDCorrelationIdLogProcessor_MultithreadedAccess_HandlesGracefully() { countWithoutCorrelationId++; } - - byte[] serializedData; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - serializedData = MsgPackLogExporter.Buffer.Value; - } - else - { - // Read the data sent via socket - serializedData = new byte[65360]; - _ = receiverSocket.Receive(serializedData); - } - - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(serializedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); - var signal = (fluentdData as object[])[0] as string; - var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; - var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; - - // Verify that AFDCorrelationId is not present in the serialized data - Assert.False(mapping.ContainsKey("AFDCorrelationId")); } }); } @@ -160,6 +141,9 @@ public void AFDCorrelationIdLogProcessor_MultithreadedAccess_HandlesGracefully() Assert.Equal(threadCount / 2, countWithCorrelationId); Assert.Equal(threadCount / 2, countWithoutCorrelationId); + Assert.Equal(expectedCorrelationIds, exportedCorrelationIds); + Assert.Equal(countWithoutCorrelationId, foundWithoutCorrelationIds); + // Check that no exceptions were thrown // If our implementation is correct, logs from threads without correlation ID // should have been processed without exceptions @@ -205,15 +189,19 @@ public void AFDCorrelationIdLogProcessor_WithoutCorrelationId_HandlesGracefully( var exportedItems = new List(); + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { options.IncludeScopes = true; options.AddInMemoryExporter(exportedItems); - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - }); + options.AddProcessor(sp => + new CompositeProcessor( + [ + new AFDCorrelationIdLogProcessor(), + new ReentrantExportProcessor(exporter), + ])); })); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -222,26 +210,17 @@ public void AFDCorrelationIdLogProcessor_WithoutCorrelationId_HandlesGracefully( receiverSocket.ReceiveTimeout = 10000; } - // Create a test exporter to get MessagePack byte data for validation - using var exporter = new GenevaLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // In this test, AFDCorrelationId is not set in RuntimeContext var logger = loggerFactory.CreateLogger(); logger.LogInformation("No correlation ID should be present"); + loggerFactory.Dispose(); - byte[] serializedData; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - serializedData = MsgPackLogExporter.Buffer.Value; - } - else - { - // Read the data sent via socket - serializedData = new byte[65360]; - _ = receiverSocket.Receive(serializedData); - } + Assert.Single(exportedData); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(serializedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var signal = (fluentdData as object[])[0] as string; var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; @@ -296,15 +275,19 @@ public void GenevaExporter_WithAFDCorrelationId_IncludesCorrelationId() var exportedItems = new List(); + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { options.IncludeScopes = true; options.AddInMemoryExporter(exportedItems); - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - }); + options.AddProcessor(sp => + new CompositeProcessor( + [ + new AFDCorrelationIdLogProcessor(), + new ReentrantExportProcessor(exporter), + ])); })); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -313,26 +296,17 @@ public void GenevaExporter_WithAFDCorrelationId_IncludesCorrelationId() receiverSocket.ReceiveTimeout = 10000; } - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new GenevaLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of internal buffer for validation. var logger = loggerFactory.CreateLogger(); logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); + loggerFactory.Dispose(); - byte[] serializedData; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - serializedData = MsgPackLogExporter.Buffer.Value; - } - else - { - // Read the data sent via socket. - serializedData = new byte[65360]; - _ = receiverSocket.Receive(serializedData); - } + Assert.Single(exportedData); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(serializedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var signal = (fluentdData as object[])[0] as string; var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterTests.cs index f1e07b7138..72ddb6c420 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterTests.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter.Geneva.MsgPack; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; using OpenTelemetry.Tests; using Xunit; @@ -23,10 +24,45 @@ public class GenevaLogExporterTests [Fact] public void BadArgs() { - GenevaExporterOptions exporterOptions = null; + string connectionString; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + connectionString = "EtwSession=OpenTelemetry"; + } + else + { + var path = GenerateTempFilePath(); + connectionString = "Endpoint=unix:" + path; + } + + // should reject null exporter options Assert.Throws(() => { - using var exporter = new GenevaLogExporter(exporterOptions); + using var exporter = new GenevaLogExporter(null); + }); + + // reserved field in ResourceFieldNames + Assert.Throws(() => + { + using var exporter = new GenevaTraceExporter(new GenevaExporterOptions + { + ConnectionString = connectionString, + ResourceFieldNames = ["env_cloud_role"], + }); + }); + + // should reject mutually exclusive ResourceFieldNames and PrepopulatedFields + Assert.Throws(() => + { + using var exporter = new GenevaTraceExporter(new GenevaExporterOptions + { + ConnectionString = connectionString, + ResourceFieldNames = ["resource"], + PrepopulatedFields = new Dictionary + { + ["prepopulated"] = "hello", + }, + }); }); } @@ -140,8 +176,6 @@ public void TableNameMappingTest(params string[] category) { // ARRANGE var path = string.Empty; - Socket server = null; - var logRecordList = new List(); Dictionary mappingsDict = null; try { @@ -166,20 +200,19 @@ public void TableNameMappingTest(params string[] category) path = GenerateTempFilePath(); exporterOptions.ConnectionString = "Endpoint=unix:" + path; var endpoint = new UnixDomainSocketEndPoint(path); - server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - server.Bind(endpoint); - server.Listen(1); } + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddInMemoryExporter(logRecordList); + options.AddProcessor(new ReentrantExportProcessor(exporter)); }) .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); ILogger logger; object fluentdData; @@ -194,12 +227,11 @@ public void TableNameMappingTest(params string[] category) logger = loggerFactory.CreateLogger(mapping.Key); logger.LogError("this does not matter"); - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); actualTableName = (fluentdData as object[])[0] as string; Assert.Equal(mapping.Value, actualTableName); - logRecordList.Clear(); + exportedData.Clear(); } else { @@ -211,17 +243,15 @@ public void TableNameMappingTest(params string[] category) logger = loggerFactory.CreateLogger("random category"); logger.LogError("this does not matter"); - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); actualTableName = (fluentdData as object[])[0] as string; Assert.Equal(defaultLogTable, actualTableName); - logRecordList.Clear(); + exportedData.Clear(); } } finally { - server?.Dispose(); try { File.Delete(path); @@ -270,7 +300,6 @@ public void PassThruTableMappingsWhenTheRuleIsEnabled() new("1.2", null), }; - var logRecordList = new List(); var exporterOptions = new GenevaExporterOptions { TableNameMappings = userInitializedCategoryToTableNameMappings, @@ -290,18 +319,19 @@ public void PassThruTableMappingsWhenTheRuleIsEnabled() server.Listen(1); } + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddInMemoryExporter(logRecordList); + options.AddProcessor(new ReentrantExportProcessor(exporter)); }) .AddFilter("*", LogLevel.Trace)); // Enable all LogLevels - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); ILogger passThruTableMappingsLogger, userInitializedTableMappingsLogger; - var m_buffer = MsgPackLogExporter.Buffer; object fluentdData; string actualTableName; @@ -312,16 +342,16 @@ public void PassThruTableMappingsWhenTheRuleIsEnabled() { userInitializedTableMappingsLogger = loggerFactory.CreateLogger(mapping.Key); userInitializedTableMappingsLogger.LogInformation("This information does not matter."); - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(m_buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); actualTableName = (fluentdData as object[])[0] as string; userInitializedCategoryToTableNameMappings.TryGetValue(mapping.Key, out var expectedTableNme); Assert.Equal(expectedTableNme, actualTableName); - - logRecordList.Clear(); } + + exportedData.Clear(); } // Verify that when the "*" = "*" were enabled, the correct table names were being deduced following the set of rules. @@ -329,16 +359,15 @@ public void PassThruTableMappingsWhenTheRuleIsEnabled() { passThruTableMappingsLogger = loggerFactory.CreateLogger(mapping.Key); passThruTableMappingsLogger.LogInformation("This information does not matter."); - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(m_buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); actualTableName = (fluentdData as object[])[0] as string; var expectedTableName = string.Empty; expectedTableName = mapping.Value; Assert.Equal(expectedTableName, actualTableName); - logRecordList.Clear(); + exportedData.Clear(); } } finally @@ -360,8 +389,6 @@ public void PassThruTableMappingsWhenTheRuleIsEnabled() public void SerializeILoggerScopes(bool hasCustomFields) { var path = string.Empty; - Socket senderSocket = null; - Socket receiverSocket = null; try { var exporterOptions = new GenevaExporterOptions(); @@ -374,10 +401,6 @@ public void SerializeILoggerScopes(bool hasCustomFields) { path = GenerateTempFilePath(); exporterOptions.ConnectionString = "Endpoint=unix:" + path; - var endpoint = new UnixDomainSocketEndPoint(path); - senderSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - senderSocket.Bind(endpoint); - senderSocket.Listen(1); } if (hasCustomFields) @@ -385,28 +408,28 @@ public void SerializeILoggerScopes(bool hasCustomFields) exporterOptions.CustomFields = ["Food", "Name", "Key1"]; } - var exportedItems = new List(); + var exportedLogs = new List(); + + var resourceBuilder = ResourceBuilder.CreateEmpty().AddAttributes([ + new KeyValuePair("resourceAttr", "from resource"), + new KeyValuePair("service.name", "BusyWorker"), + new KeyValuePair("service.instanceId", "CY1SCH030021417") + ]); + + using var exporter = new GenevaLogExporter(exporterOptions); using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { options.IncludeScopes = true; - options.AddInMemoryExporter(exportedItems); - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - options.CustomFields = exporterOptions.CustomFields; - }); + options.SetResourceBuilder(resourceBuilder); + options.AddInMemoryExporter(exportedLogs); + options.AddProcessor(new ReentrantExportProcessor(exporter)); })); - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - receiverSocket = senderSocket.Accept(); - receiverSocket.ReceiveTimeout = 10000; - } + List> exportedData = []; - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new GenevaLogExporter(exporterOptions); + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of internal buffer for validation. var logger = loggerFactory.CreateLogger(); @@ -419,19 +442,11 @@ public void SerializeILoggerScopes(bool hasCustomFields) logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); } - byte[] serializedData; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - serializedData = MsgPackLogExporter.Buffer.Value; - } - else - { - // Read the data sent via socket. - serializedData = new byte[65360]; - _ = receiverSocket.Receive(serializedData); - } + Assert.Single(exportedLogs); + Assert.Single(exportedData); + var logRecord = exportedLogs[0]; - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(serializedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var signal = (fluentdData as object[])[0] as string; var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; @@ -470,15 +485,10 @@ public void SerializeILoggerScopes(bool hasCustomFields) } // Check other fields - Assert.Single(exportedItems); - var logRecord = exportedItems[0]; - - this.AssertFluentdForwardModeForLogRecord(exporterOptions, fluentdData, logRecord); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, Resource.Empty, fluentdData, logRecord); } finally { - senderSocket?.Dispose(); - receiverSocket?.Dispose(); try { File.Delete(path); @@ -490,21 +500,26 @@ public void SerializeILoggerScopes(bool hasCustomFields) } [Theory] - [InlineData(true)] - [InlineData(false)] - public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage, bool includeResourceAttributes) { // Dedicated test for the raw ILogger.Log method // https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger.log // ARRANGE var path = string.Empty; - Socket server = null; - var logRecordList = new List(); try { var exporterOptions = new GenevaExporterOptions(); + if (includeResourceAttributes) + { + exporterOptions.ResourceFieldNames = ["resourceAttr"]; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; @@ -514,25 +529,29 @@ public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage) path = GenerateTempFilePath(); exporterOptions.ConnectionString = "Endpoint=unix:" + path; var endpoint = new UnixDomainSocketEndPoint(path); - server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - server.Bind(endpoint); - server.Listen(1); } + var resourceBuilder = ResourceBuilder.CreateEmpty().AddAttributes([ + new KeyValuePair("resourceAttr", "from resource"), + new KeyValuePair("service.name", "BusyWorker"), + new KeyValuePair("service.instanceId", "CY1SCH030021417") + ]); + + using var exporter = new GenevaLogExporter(exporterOptions); + List exportedLogs = []; + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - }); - options.AddInMemoryExporter(logRecordList); + options.SetResourceBuilder(resourceBuilder); + options.AddProcessor(new ReentrantExportProcessor(exporter)); + options.AddInMemoryExporter(exportedLogs); options.IncludeFormattedMessage = includeFormattedMessage; }) .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter var logger = loggerFactory.CreateLogger(); @@ -551,19 +570,14 @@ public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage) (state, ex) => "Formatted Message"); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); - var body = GetField(fluentdData, "body"); - - // Body gets populated as "Formatted Message" regardless of the value of `IncludeFormattedMessage` - Assert.Equal("Formatted Message", body); - - Assert.Equal("Value1", GetField(fluentdData, "Key1")); - Assert.Equal("Value2", GetField(fluentdData, "Key2")); + Assert.Single(exportedLogs); + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resourceBuilder.Build(), fluentdData, exportedLogs[0]); // ARRANGE - logRecordList.Clear(); + exportedLogs.Clear(); + exportedData.Clear(); // ACT // This is treated as Un-structured logging as the state cannot be converted to IReadOnlyList> @@ -575,16 +589,14 @@ public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage) formatter: (state, ex) => "Formatted Message"); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); - body = GetField(fluentdData, "body"); - - // Body gets populated as "Formatted Message" regardless of the value of `IncludeFormattedMessage` - Assert.Equal("Formatted Message", body); + Assert.Single(exportedLogs); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resourceBuilder.Build(), fluentdData, exportedLogs[0]); // ARRANGE - logRecordList.Clear(); + exportedLogs.Clear(); + exportedData.Clear(); // ACT // This is treated as Un-structured logging as the state cannot be converted to IReadOnlyList> @@ -596,16 +608,14 @@ public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage) formatter: null); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); - body = GetField(fluentdData, "body"); - - // Even though Formatter is null, body is populated with the state - Assert.Equal("somestringasdata", body); + Assert.Single(exportedLogs); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resourceBuilder.Build(), fluentdData, exportedLogs[0]); // ARRANGE - logRecordList.Clear(); + exportedLogs.Clear(); + exportedData.Clear(); // ACT // This is treated as Structured logging as the state can be converted to IReadOnlyList> @@ -620,19 +630,13 @@ public void SerializationTestWithILoggerLogMethod(bool includeFormattedMessage) formatter: (state, ex) => "Example formatted message."); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); - Assert.Equal("Value1", GetField(fluentdData, "Key1")); - - body = GetField(fluentdData, "body"); - - // Body gets populated as "Formatted Message" regardless of the value of `IncludeFormattedMessage` - Assert.Equal("Example formatted message.", body); + Assert.Single(exportedLogs); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resourceBuilder.Build(), fluentdData, exportedLogs[0]); } finally { - server?.Dispose(); try { File.Delete(path); @@ -663,12 +667,17 @@ public void SerializationTestWithILoggerLogWithTemplates(bool hasTableNameMappin { PrepopulatedFields = new Dictionary { - ["cloud.role"] = "BusyWorker", - ["cloud.roleInstance"] = "CY1SCH030021417", ["cloud.roleVer"] = "9.0.15289.2", + ["prepopulated"] = "prepopulated field", }, }; + var resource = ResourceBuilder.CreateEmpty().AddAttributes([ + new KeyValuePair("resourceAttr", "from resource"), + new KeyValuePair("service.name", "BusyWorker"), + new KeyValuePair("service.instanceId", "CY1SCH030021417")]) + .Build(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; @@ -702,18 +711,13 @@ public void SerializationTestWithILoggerLogWithTemplates(bool hasTableNameMappin using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - options.PrepopulatedFields = exporterOptions.PrepopulatedFields; - }); options.AddInMemoryExporter(logRecordList); options.ParseStateValues = parseStateValues; }) .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + using var exporter = new MsgPackLogExporter(exporterOptions, () => resource); // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter var logger = loggerFactory.CreateLogger(); @@ -758,13 +762,11 @@ public void SerializationTestWithILoggerLogWithTemplates(bool hasTableNameMappin // logRecordList should have 14 logRecord entries as there were 14 Log calls Assert.Equal(14, logRecordList.Count); - var m_buffer = MsgPackLogExporter.Buffer; - foreach (var logRecord in logRecordList) { - _ = exporter.SerializeLogRecord(logRecord); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(m_buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); - this.AssertFluentdForwardModeForLogRecord(exporterOptions, fluentdData, logRecord); + var serializedLog = exporter.SerializeLogRecord(logRecord); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(serializedLog, MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resource, fluentdData, logRecord); } } finally @@ -813,11 +815,131 @@ public void SuccessfulExport_Windows() logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); } + [Fact] + public void AutoMappedResourceAttrDoesNotOverridePrepopulated() + { + var path = GenerateTempFilePath(); + try + { + var endpoint = new UnixDomainSocketEndPoint(path); + + var exporterOptions = new GenevaExporterOptions + { + PrepopulatedFields = new Dictionary + { + ["cloud.role"] = "cloud.role from prepopulated", + }, + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + } + + using var exporter = new GenevaLogExporter(exporterOptions); + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + var resourceBuilder = ResourceBuilder.CreateDefault().AddService("cloud.role from resource"); + + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.SetResourceBuilder(resourceBuilder); + options.AddProcessor(new ReentrantExportProcessor(exporter)); + })); + + var logger = loggerFactory.CreateLogger(); + + logger.LogInformation("Hello"); + + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + + Assert.Equal("cloud.role from prepopulated", GetField(fluentdData, "env_cloud_role")); + } + finally + { + try + { + File.Delete(path); + } + catch + { + } + } + } + + [Fact] + public void AutoMappedResourceAttr() + { + var path = GenerateTempFilePath(); + try + { + var endpoint = new UnixDomainSocketEndPoint(path); + + var exporterOptions = new GenevaExporterOptions + { + PrepopulatedFields = new Dictionary + { + // no prepopulated fields + }, + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + } + + using var exporter = new GenevaLogExporter(exporterOptions); + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + var resourceBuilder = ResourceBuilder.CreateDefault().AddService("cloud.role from resource"); + + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.SetResourceBuilder(resourceBuilder); + options.AddProcessor(new ReentrantExportProcessor(exporter)); + })); + + var logger = loggerFactory.CreateLogger(); + + logger.LogInformation("Hello"); + + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + + Assert.Equal("cloud.role from resource", GetField(fluentdData, "env_cloud_role")); + } + finally + { + try + { + File.Delete(path); + } + catch + { + } + } + } + [SkipUnlessPlatformMatchesFact(TestPlatform.Linux)] public void SuccessfulExport_Linux() { var path = GenerateTempFilePath(); - var logRecordList = new List(); + var exportedLogs = new List(); try { var endpoint = new UnixDomainSocketEndPoint(path); @@ -825,54 +947,51 @@ public void SuccessfulExport_Linux() server.Bind(endpoint); server.Listen(1); + // Create a test exporter to get MessagePack byte data for validation of the data received via Socket. + var exporterOptions = new GenevaExporterOptions + { + ConnectionString = "Endpoint=unix:" + path, + }; + + using var exporter = new GenevaLogExporter(exporterOptions); + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + var resourceBuilder = ResourceBuilder.CreateDefault(); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = "Endpoint=unix:" + path; - options.PrepopulatedFields = new Dictionary - { - ["cloud.role"] = "BusyWorker", - ["cloud.roleInstance"] = "CY1SCH030021417", - ["cloud.roleVer"] = "9.0.15289.2", - }; - }); - options.AddInMemoryExporter(logRecordList); + options.SetResourceBuilder(resourceBuilder); + options.AddInMemoryExporter(exportedLogs); + options.AddProcessor(new ReentrantExportProcessor(exporter)); })); + using var serverSocket = server.Accept(); serverSocket.ReceiveTimeout = 10000; - // Create a test exporter to get MessagePack byte data for validation of the data received via Socket. - using var exporter = new MsgPackLogExporter(new GenevaExporterOptions - { - ConnectionString = "Endpoint=unix:" + path, - PrepopulatedFields = new Dictionary - { - ["cloud.role"] = "BusyWorker", - ["cloud.roleInstance"] = "CY1SCH030021417", - ["cloud.roleVer"] = "9.0.15289.2", - }, - }); - // Emit a LogRecord and grab a copy of internal buffer for validation. var logger = loggerFactory.CreateLogger(); logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); // logRecordList should have a singleLogRecord entry after the logger.LogInformation call - Assert.Single(logRecordList); - - var messagePackDataSize = exporter.SerializeLogRecord(logRecordList[0]).Count; + Assert.Single(exportedLogs); + Assert.Single(exportedData); // Read the data sent via socket. var receivedData = new byte[1024]; var receivedDataSize = serverSocket.Receive(receivedData); - // Validation - Assert.Equal(messagePackDataSize, receivedDataSize); + // the number of bytes received over the socket should match the number of bytes of data exported + Assert.Equal(exportedData[0].Count, receivedDataSize); + + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resourceBuilder.Build(), fluentdData, exportedLogs[0]); - logRecordList.Clear(); + exportedLogs.Clear(); + exportedData.Clear(); // Emit log on a different thread to test for multithreading scenarios var thread = new Thread(() => @@ -883,11 +1002,17 @@ public void SuccessfulExport_Linux() thread.Join(); // logRecordList should have a singleLogRecord entry after the logger.LogInformation call - Assert.Single(logRecordList); + Assert.Single(exportedLogs); + Assert.Single(exportedData); - messagePackDataSize = exporter.SerializeLogRecord(logRecordList[0]).Count; + // Read the data sent via socket. receivedDataSize = serverSocket.Receive(receivedData); - Assert.Equal(messagePackDataSize, receivedDataSize); + + // the number of bytes received over the socket should match the number of bytes of data exported + Assert.Equal(exportedData[0].Count, receivedDataSize); + + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + this.AssertFluentdForwardModeForLogRecord(exporterOptions, resourceBuilder.Build(), fluentdData, exportedLogs[0]); } finally { @@ -907,7 +1032,6 @@ public void SerializationTestForException() // ARRANGE var path = string.Empty; Socket server = null; - var logRecordList = new List(); try { var exporterOptions = new GenevaExporterOptions(); @@ -926,19 +1050,17 @@ public void SerializationTestForException() server.Listen(1); } + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - }); - options.AddInMemoryExporter(logRecordList); + options.AddProcessor(sp => new ReentrantExportProcessor(exporter)); }) .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter var logger = loggerFactory.CreateLogger(); @@ -953,9 +1075,8 @@ public void SerializationTestForException() formatter: null); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var exceptionType = GetField(fluentdData, "env_ex_type"); var exceptionMessage = GetField(fluentdData, "env_ex_msg"); Assert.Equal("System.Exception", exceptionType); @@ -984,7 +1105,6 @@ public void SerializationTestForEventName(EventNameExportMode eventNameExportMod // ARRANGE var path = string.Empty; Socket server = null; - var logRecordList = new List(); try { var exporterOptions = new GenevaExporterOptions @@ -1014,24 +1134,16 @@ public void SerializationTestForEventName(EventNameExportMode eventNameExportMod server.Listen(1); } + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - options.EventNameExportMode = exporterOptions.EventNameExportMode; - - if (hasTableNameMapping) - { - options.TableNameMappings = exporterOptions.TableNameMappings; - } - }); - options.AddInMemoryExporter(logRecordList); + options.AddProcessor(sp => new ReentrantExportProcessor(exporter)); })); - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter var logger = loggerFactory.CreateLogger(); @@ -1048,9 +1160,8 @@ public void SerializationTestForEventName(EventNameExportMode eventNameExportMod formatter: null); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var eventName = GetField(fluentdData, "env_name"); if (eventNameExportMode.HasFlag(EventNameExportMode.ExportAsPartAName)) @@ -1064,13 +1175,13 @@ public void SerializationTestForEventName(EventNameExportMode eventNameExportMod #endregion - logRecordList.Clear(); + exportedData.Clear(); #region Test for extension method logger.LogInformation(eventId: new EventId(1, "TestEventNameWithLogExtensionMethod"), "Hello from {Name} {Price}.", "tomato", 2.99); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); eventName = GetField(fluentdData, "env_name"); if (eventNameExportMode.HasFlag(EventNameExportMode.ExportAsPartAName)) @@ -1083,13 +1194,13 @@ public void SerializationTestForEventName(EventNameExportMode eventNameExportMod } #endregion - logRecordList.Clear(); + exportedData.Clear(); #region Test with eventName as null logger.LogInformation(eventId: 1, "Hello from {Name} {Price}.", "tomato", 2.99); - _ = exporter.SerializeLogRecord(logRecordList[0]); - fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); eventName = GetField(fluentdData, "env_name"); Assert.Equal(hasTableNameMapping ? "CustomTableName" : "Log", eventName); #endregion @@ -1121,8 +1232,6 @@ public void SerializationTestForPartBName(bool hasCustomFields, bool hasNameInCu { // ARRANGE var path = string.Empty; - Socket server = null; - var logRecordList = new List(); try { var exporterOptions = new GenevaExporterOptions(); @@ -1136,9 +1245,6 @@ public void SerializationTestForPartBName(bool hasCustomFields, bool hasNameInCu path = GenerateTempFilePath(); exporterOptions.ConnectionString = "Endpoint=unix:" + path; var endpoint = new UnixDomainSocketEndPoint(path); - server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - server.Bind(endpoint); - server.Listen(1); } if (hasCustomFields) @@ -1146,19 +1252,16 @@ public void SerializationTestForPartBName(bool hasCustomFields, bool hasNameInCu exporterOptions.CustomFields = hasNameInCustomFields ? ["name", "Key1"] : ["Key1"]; } + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - options.CustomFields = exporterOptions.CustomFields; - }); - options.AddInMemoryExporter(logRecordList); + options.AddProcessor(new ReentrantExportProcessor(exporter)); })); - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter var logger = loggerFactory.CreateLogger(); @@ -1185,9 +1288,8 @@ public void SerializationTestForPartBName(bool hasCustomFields, bool hasNameInCu null); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var signal = (fluentdData as object[])[0] as string; var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; @@ -1220,12 +1322,9 @@ public void SerializationTestForPartBName(bool hasCustomFields, bool hasNameInCu } } } - - logRecordList.Clear(); } finally { - server?.Dispose(); try { File.Delete(path); @@ -1242,7 +1341,6 @@ public void SerializationTestForEventId() // ARRANGE var path = string.Empty; Socket server = null; - var logRecordList = new List(); try { var exporterOptions = new GenevaExporterOptions(); @@ -1261,19 +1359,17 @@ public void SerializationTestForEventId() server.Listen(1); } + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder .AddOpenTelemetry(options => { - options.AddGenevaLogExporter(options => - { - options.ConnectionString = exporterOptions.ConnectionString; - }); - options.AddInMemoryExporter(logRecordList); + options.AddProcessor(sp => new ReentrantExportProcessor(exporter)); }) .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels - // Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly. - using var exporter = new MsgPackLogExporter(exporterOptions); + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter var logger = loggerFactory.CreateLogger(); @@ -1288,9 +1384,8 @@ public void SerializationTestForEventId() formatter: null); // VALIDATE - Assert.Single(logRecordList); - _ = exporter.SerializeLogRecord(logRecordList[0]); - var fluentdData = MessagePack.MessagePackSerializer.Deserialize(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; @@ -1432,6 +1527,271 @@ public void AddGenevaBatchExportProcessorOptions() } } + [Fact] + public void InvalidResourceAttrType_PlaceholderMessage() + { + var path = string.Empty; + try + { + var exporterOptions = new GenevaExporterOptions + { + ResourceFieldNames = ["badresource"], + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + path = GenerateTempFilePath(); + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + var endpoint = new UnixDomainSocketEndPoint(path); + } + + var resourceAttributes = new Dictionary + { + { "badresource", new int[1] }, // the exporter does not accept complex types like an array + }; + var resourceBuilder = ResourceBuilder.CreateEmpty().AddAttributes(resourceAttributes); + var resource = resourceBuilder.Build(); + + using var exporter = new GenevaLogExporter(exporterOptions); + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddProcessor(new ReentrantExportProcessor(exporter)); + options.SetResourceBuilder(resourceBuilder); + }) + .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter + var logger = loggerFactory.CreateLogger(); + + // Set the ActivitySourceName to the unique value of the test method name to avoid interference with + // the ActivitySource used by other unit tests. + var sourceName = GetTestMethodName(); + + using var listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => activitySource.Name == sourceName; + listener.Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded; + ActivitySource.AddActivityListener(listener); + + using var source = new ActivitySource(sourceName); + + using (var activity = source.StartActivity("Activity")) + { + // Log inside an activity to set LogRecord.TraceId and LogRecord.SpanId + logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); // structured logging + } + + Assert.Single(exportedData); + + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; + var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; + + Assert.Contains("badresource", mapping.Keys); + Assert.Equal("", mapping["badresource"]); + } + finally + { + try + { + File.Delete(path); + } + catch + { + } + } + } + + [Fact] + public void WithoutResourceAttributes() + { + var path = string.Empty; + try + { + var exporterOptions = new GenevaExporterOptions + { + PrepopulatedFields = new Dictionary + { + ["unaffected prepopulated"] = "should be present", + }, + + // ResourceFieldNames not set + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + path = GenerateTempFilePath(); + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + var endpoint = new UnixDomainSocketEndPoint(path); + } + + var resourceAttributes = new Dictionary + { + { "resourceAttributes", "should not be present" }, + }; + var resourceBuilder = ResourceBuilder.CreateEmpty().AddAttributes(resourceAttributes); + var resource = resourceBuilder.Build(); + + using var exporter = new GenevaLogExporter(exporterOptions); + + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.SetResourceBuilder(resourceBuilder); + options.AddProcessor(new ReentrantExportProcessor(exporter)); + }) + .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter + var logger = loggerFactory.CreateLogger(); + + // Set the ActivitySourceName to the unique value of the test method name to avoid interference with + // the ActivitySource used by other unit tests. + var sourceName = GetTestMethodName(); + + using var listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => activitySource.Name == sourceName; + listener.Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded; + ActivitySource.AddActivityListener(listener); + + using var source = new ActivitySource(sourceName); + + using (var activity = source.StartActivity("Activity")) + { + // Log inside an activity to set LogRecord.TraceId and LogRecord.SpanId + logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); // structured logging + } + + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; + var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; + + Assert.DoesNotContain("resourceAttributes", mapping.Keys); + + if (mapping.ContainsKey("env_properties")) + { + var env_properties = mapping["env_properties"] as Dictionary ?? []; + Assert.DoesNotContain("resourceAttributes", env_properties); + } + + Assert.Contains("unaffected prepopulated", mapping.Keys); + } + finally + { + try + { + File.Delete(path); + } + catch + { + } + } + } + + /// + /// The purpose of this test is to make sure that when ResourceFieldNames is set to empty, + /// that no resource attributes make it to Geneva. + /// + [Fact] + public void WithEmptyResourceAttributes() + { + var path = string.Empty; + try + { + var exporterOptions = new GenevaExporterOptions + { + ResourceFieldNames = [], // ResourceFieldNames empty + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + path = GenerateTempFilePath(); + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + var endpoint = new UnixDomainSocketEndPoint(path); + } + + var resourceAttributes = new Dictionary + { + { "resourceAttributes", "should not be present" }, + }; + var resource = new Resource(resourceAttributes); + + using var exporter = new GenevaLogExporter(exporterOptions); + + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddProcessor(new ReentrantExportProcessor(exporter)); + }) + .AddFilter(typeof(GenevaLogExporterTests).FullName, LogLevel.Trace)); // Enable all LogLevels + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + // Emit a LogRecord and grab a copy of the LogRecord from the collection passed to InMemoryExporter + var logger = loggerFactory.CreateLogger(); + + // Set the ActivitySourceName to the unique value of the test method name to avoid interference with + // the ActivitySource used by other unit tests. + var sourceName = GetTestMethodName(); + + using var listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => activitySource.Name == sourceName; + listener.Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded; + ActivitySource.AddActivityListener(listener); + + using var source = new ActivitySource(sourceName); + + using (var activity = source.StartActivity("Activity")) + { + // Log inside an activity to set LogRecord.TraceId and LogRecord.SpanId + logger.LogInformation("Hello from {Food} {Price}.", "artichoke", 3.99); // structured logging + } + + Assert.Single(exportedData); + + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); + var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; + var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; + + Assert.DoesNotContain("resourceAttributes", mapping.Keys); + + if (mapping.ContainsKey("env_properties")) + { + var env_properties = mapping["env_properties"] as Dictionary ?? []; + Assert.DoesNotContain("resourceAttributes", env_properties); + } + } + finally + { + try + { + File.Delete(path); + } + catch + { + } + } + } + [SkipUnlessPlatformMatchesFact(TestPlatform.Linux)] public void SuccessfulUserEventsExport_Linux() { @@ -1508,7 +1868,7 @@ private static object GetField(object fluentdData, string key) return mapping.TryGetValue(key, out var value) ? value : null; } - private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporterOptions, object fluentdData, LogRecord logRecord) + private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporterOptions, Resource resource, object fluentdData, LogRecord logRecord) { /* Fluentd Forward Mode: [ @@ -1559,7 +1919,12 @@ private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporter foreach (var item in exporterOptions.PrepopulatedFields) { var partAValue = item.Value as string; - var partAKey = MsgPackExporter.V40_PART_A_MAPPING[item.Key]; + var partAKey = item.Key; + if (MsgPackExporter.V40_PART_A_MAPPING.ContainsKey(item.Key)) + { + partAKey = MsgPackExporter.V40_PART_A_MAPPING[item.Key]; + } + Assert.Equal(partAValue, mapping[partAKey]); } @@ -1584,6 +1949,26 @@ private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporter Assert.Equal(logRecord.Exception.Message, mapping["env_ex_msg"]); } + // Part A cloud extensions + if (exporterOptions.PrepopulatedFields != null && exporterOptions.PrepopulatedFields.Count == 0) + { + // this mapping only happens when there are no prepopulated fields, to avoid conflicts + + var serviceNameField = resource.Attributes.FirstOrDefault(attr => attr.Key == "service.name"); + if (serviceNameField.Key == "service.name") + { + Assert.Contains("env_cloud_role", mapping); + Assert.Equal(serviceNameField.Value, mapping["env_cloud_role"]); + } + + var serviceInstanceField = resource.Attributes.FirstOrDefault(attr => attr.Key == "service.instanceId"); + if (serviceInstanceField.Key == "service.instanceId") + { + Assert.Contains("env_cloud_roleInstance", mapping); + Assert.Equal(serviceInstanceField.Value, mapping["env_cloud_roleInstance"]); + } + } + // Part B fields // `LogRecord.LogLevel` was marked Obsolete in https://github.com/open-telemetry/opentelemetry-dotnet/pull/4568 @@ -1613,7 +1998,11 @@ private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporter { // `LogRecord.State` and `LogRecord.StateValues` were marked Obsolete in https://github.com/open-telemetry/opentelemetry-dotnet/pull/4334 #pragma warning disable 0618 - if (logRecord.State != null) + if (logRecord.FormattedMessage != null) + { + Assert.Equal(logRecord.FormattedMessage!, mapping["body"]); + } + else if (logRecord.State != null) { Assert.Equal(logRecord.State.ToString(), mapping["body"]); } @@ -1636,14 +2025,45 @@ private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporter } else if (exporterOptions.CustomFields == null || exporterOptions.CustomFields.Contains(item.Key)) { + // It should be found as a custom field if (item.Value != null) { Assert.Equal(item.Value, mapping[item.Key]); } + + if (envPropertiesMapping != null) + { + Assert.DoesNotContain(item.Key, envPropertiesMapping.Keys); + } } else { + // It should be found in env_properties Assert.Equal(item.Value, envPropertiesMapping[item.Key]); + Assert.DoesNotContain(item.Key, mapping); + } + } + + foreach (var item in resource.Attributes) + { + if (item.Key == "service.name" || item.Key == "service.instanceId") + { + // these ones are already checked. + continue; + } + + if (exporterOptions.ResourceFieldNames != null && exporterOptions.ResourceFieldNames.Contains(item.Key)) + { + // It should always be found as a custom field + if (item.Value != null) + { + Assert.Equal(item.Value, mapping[item.Key]); + } + + if (envPropertiesMapping != null) + { + Assert.DoesNotContain(item.Key, envPropertiesMapping.Keys); + } } } } @@ -1653,7 +2073,7 @@ private void AssertFluentdForwardModeForLogRecord(GenevaExporterOptions exporter Assert.Equal(logRecord.EventId.Id, int.Parse(mapping["eventId"].ToString(), CultureInfo.InvariantCulture)); } - // Epilouge + // Epilogue Assert.Equal("DateTime", timeFormat["TimeFormat"]); } diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs index e780fc8a30..043d9d1fd3 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaTraceExporterTests.cs @@ -77,6 +77,20 @@ public void GenevaTraceExporter_constructor_Invalid_Input() }); }); + // mutually exclusive ResourceFieldNames and PrepopulatedFields + Assert.Throws(() => + { + using var exporter = new GenevaTraceExporter(new GenevaExporterOptions + { + ConnectionString = connectionString, + ResourceFieldNames = ["resource"], + PrepopulatedFields = new Dictionary + { + ["prepopulated"] = "hello", + }, + }); + }); + // unsupported types(char) for PrepopulatedFields Assert.Throws(() => { @@ -203,31 +217,54 @@ public void GenevaTraceExporter_Success_Windows() } } + // hasResourceAttributes and hasPrepopulatedFields are mutually exclusive [Theory] - [InlineData(false, false, false)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, false)] - [InlineData(false, false, true)] - [InlineData(false, true, true)] - [InlineData(true, false, true)] - [InlineData(true, true, true)] - public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, bool hasCustomFields, bool includeTraceState) + [InlineData(false, false, false, false, true)] + [InlineData(false, true, false, false, true)] + [InlineData(true, false, false, false, true)] + [InlineData(true, true, false, false, true)] + [InlineData(false, false, true, false, true)] + [InlineData(false, true, true, false, true)] + [InlineData(true, false, true, false, true)] + [InlineData(true, true, true, false, true)] + [InlineData(false, false, false, true, false)] + [InlineData(false, true, false, true, false)] + [InlineData(true, false, false, true, false)] + [InlineData(true, true, false, true, false)] + [InlineData(false, false, true, true, false)] + [InlineData(false, true, true, true, false)] + [InlineData(true, false, true, true, false)] + [InlineData(true, true, true, true, false)] + [InlineData(false, false, false, false, false)] + [InlineData(false, true, false, false, false)] + [InlineData(true, false, false, false, false)] + [InlineData(true, true, false, false, false)] + [InlineData(false, false, true, false, false)] + [InlineData(false, true, true, false, false)] + [InlineData(true, false, true, false, false)] + [InlineData(true, true, true, false, false)] + public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, bool hasCustomFields, bool includeTraceState, bool hasPrepopulatedFields, bool hasResourceAttributes) { var path = string.Empty; Socket server = null; try { var invocationCount = 0; - var exporterOptions = new GenevaExporterOptions + var exporterOptions = new GenevaExporterOptions(); + if (hasPrepopulatedFields) { - PrepopulatedFields = new Dictionary + exporterOptions.PrepopulatedFields = new Dictionary { ["cloud.roleVer"] = "9.0.15289.2", ["resourceAndPrepopulated"] = "comes from prepopulated", - }, - ResourceFieldNames = ["resourceAttribute", "resourceAndPrepopulated"], - }; + }; + } + + if (hasResourceAttributes) + { + exporterOptions.ResourceFieldNames = ["resourceAttribute", "resourceAndPrepopulated"]; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; @@ -249,8 +286,8 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, if (hasCustomFields) { - // The tag "clientRequestId" should be present in the mapping as a separate key. Other tags which are not present - // in the m_dedicatedFields should be added in the mapping under "env_properties" + // The tag "clientRequestId" should be present in the exported data as a separate key. Other tags which are not present + // in DedicatedFields should be added in the mapping under "env_properties" exporterOptions.CustomFields = ["clientRequestId"]; } @@ -347,8 +384,16 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, this.AssertMappingEntry(userFieldsLocation, "foo", 1); this.AssertMappingEntry(userFieldsLocation, "bar", 2); - this.AssertMappingEntry(mapping, "resourceAttribute", "resourceValue"); - this.AssertMappingEntry(mapping, "resourceAndPrepopulated", "comes from resource"); + + if (hasResourceAttributes) + { + this.AssertMappingEntry(mapping, "resourceAttribute", "resourceValue"); + this.AssertMappingEntry(mapping, "resourceAndPrepopulated", "comes from resource"); + } + else if (hasPrepopulatedFields) + { + this.AssertMappingEntry(mapping, "resourceAndPrepopulated", "comes from prepopulated"); + } // Linked spans are checked in CheckSpanForActivity, so no need to do a custom check here }); @@ -394,7 +439,6 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping, public void GenevaTraceExporter_Resource_Overwrites_Prepopulated() { var path = string.Empty; - Socket server = null; try { var exporterOptions = new GenevaExporterOptions @@ -413,9 +457,6 @@ public void GenevaTraceExporter_Resource_Overwrites_Prepopulated() path = GetRandomFilePath(); exporterOptions.ConnectionString = "Endpoint=unix:" + path; var endpoint = new UnixDomainSocketEndPoint(path); - server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - server.Bind(endpoint); - server.Listen(1); } Dictionary resourceAttributes = new Dictionary @@ -452,7 +493,136 @@ public void GenevaTraceExporter_Resource_Overwrites_Prepopulated() } finally { - server?.Dispose(); + try + { + File.Delete(path); + } + catch + { + } + } + } + + [Fact] + public void AutoMappedResourceAttrDoesNotOverridePrepopulated() + { + var path = string.Empty; + try + { + var exporterOptions = new GenevaExporterOptions + { + PrepopulatedFields = new Dictionary + { + ["cloud.role"] = "cloud.role from prepopulated", + }, + }; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + path = GetRandomFilePath(); + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + var endpoint = new UnixDomainSocketEndPoint(path); + } + + using var exporter = new GenevaTraceExporter(exporterOptions); + + // Set the ActivitySourceName to the unique value of the test method name to avoid interference with + // the ActivitySource used by other unit tests. + var sourceName = GetTestMethodName(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .ConfigureResource(resourceBuilder => resourceBuilder.AddService("cloud.role from resource")) + .AddSource(sourceName) + .AddProcessor(new ReentrantExportProcessor(exporter)) + .Build(); + + var source = new ActivitySource(sourceName); + using (var activity = source.StartActivity("test")) + { + } + + var exportedData = (exporter.Exporter as MsgPackTraceExporter).Buffer.Value; + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); + + var signal = (fluentdData as object[])[0] as string; + var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; + + var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; + + Assert.Contains("env_cloud_role", mapping.Keys); + Assert.Equal("cloud.role from prepopulated", mapping["env_cloud_role"]); + } + finally + { + try + { + File.Delete(path); + } + catch + { + } + } + } + + [Fact] + public void AutoMappedResourceAttr() + { + var path = string.Empty; + try + { + var exporterOptions = new GenevaExporterOptions + { + PrepopulatedFields = new Dictionary + { + // no prepopulated fields + }, + }; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "EtwSession=OpenTelemetry"; + } + else + { + path = GetRandomFilePath(); + exporterOptions.ConnectionString = "Endpoint=unix:" + path; + var endpoint = new UnixDomainSocketEndPoint(path); + } + + using var exporter = new GenevaTraceExporter(exporterOptions); + + // Set the ActivitySourceName to the unique value of the test method name to avoid interference with + // the ActivitySource used by other unit tests. + var sourceName = GetTestMethodName(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .ConfigureResource(resourceBuilder => resourceBuilder.AddService("cloud.role from resource")) + .AddSource(sourceName) + .AddProcessor(new ReentrantExportProcessor(exporter)) + .Build(); + + var source = new ActivitySource(sourceName); + using (var activity = source.StartActivity("test")) + { + } + + var exportedData = (exporter.Exporter as MsgPackTraceExporter).Buffer.Value; + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData, MessagePack.Resolvers.ContractlessStandardResolver.Options); + + var signal = (fluentdData as object[])[0] as string; + var TimeStampAndMappings = ((fluentdData as object[])[1] as object[])[0]; + + var mapping = (TimeStampAndMappings as object[])[1] as Dictionary; + + Assert.Contains("env_cloud_role", mapping.Keys); + Assert.Equal("cloud.role from resource", mapping["env_cloud_role"]); + } + finally + { try { File.Delete(path); @@ -637,11 +807,6 @@ public void GenevaTraceExporter_WithEmptyResourceAttributes() { var exporterOptions = new GenevaExporterOptions { - PrepopulatedFields = new Dictionary - { - ["unaffected prepopulated"] = "should be present", - }, - ResourceFieldNames = [], // ResourceFieldNames empty }; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -724,10 +889,6 @@ public void GenevaTraceExporter_ResourceFieldNames() { var exporterOptions = new GenevaExporterOptions { - PrepopulatedFields = new Dictionary - { - ["overridden prepopulated"] = "should not be present", - }, ResourceFieldNames = new HashSet { "wanted", @@ -780,7 +941,6 @@ public void GenevaTraceExporter_ResourceFieldNames() this.ExpectSpanFromActivity(activity, (mapping) => { this.AssertMappingEntry(mapping, "wanted", "should be present"); - Assert.DoesNotContain("overridden prepopulated", mapping); Assert.DoesNotContain("unwanted", mapping); }); } @@ -1279,13 +1439,13 @@ private void CheckSpanForActivity(GenevaExporterOptions exporterOptions, object // Part A cloud extensions if (resourceAttributes.TryGetValue("service.name", out var expectedServiceName) - && !exporterOptions.PrepopulatedFields.ContainsKey("cloud.role")) + && exporterOptions.PrepopulatedFields != null && exporterOptions.PrepopulatedFields.Count == 0) { this.AssertMappingEntry(mapping, "env_cloud_role", expectedServiceName); } if (resourceAttributes.TryGetValue("service.instanceId", out var expectedInstanceId) - && !exporterOptions.PrepopulatedFields.ContainsKey("cloud.roleInstance")) + && exporterOptions.PrepopulatedFields != null && exporterOptions.PrepopulatedFields.Count == 0) { this.AssertMappingEntry(mapping, "env_cloud_roleInstance", expectedInstanceId); } diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/LogSerializationTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/LogSerializationTests.cs index 0c9758db87..4fdd00eab8 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/LogSerializationTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/LogSerializationTests.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Net.Sockets; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter.Geneva.MsgPack; @@ -77,22 +76,9 @@ public void SerializationTestForExceptionTrim() private static Dictionary GetExportedFieldsAfterLogging(Action doLog, Action configureGeneva = null) { - Socket server = null; var path = string.Empty; try { - var logRecordList = new List(); - using var loggerFactory = LoggerFactory.Create(builder => builder - .AddOpenTelemetry(options => - { - options.AddInMemoryExporter(logRecordList); - }) - .AddFilter(typeof(LogSerializationTests).FullName, LogLevel.Trace)); // Enable all LogLevels - - var logger = loggerFactory.CreateLogger(); - doLog(logger); - - Assert.Single(logRecordList); var exporterOptions = new GenevaExporterOptions(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -102,23 +88,32 @@ private static Dictionary GetExportedFieldsAfterLogging(Action(MsgPackLogExporter.Buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Options); + using var exporter = new GenevaLogExporter(exporterOptions); + + using var loggerFactory = LoggerFactory.Create(builder => builder + .AddOpenTelemetry(options => + { + options.AddProcessor(new ReentrantExportProcessor(exporter)); + }) + .AddFilter(typeof(LogSerializationTests).FullName, LogLevel.Trace)); // Enable all LogLevels + + List> exportedData = []; + (exporter.Exporter as MsgPackLogExporter).DataTransportListener = (data) => exportedData.Add(data); + + var logger = loggerFactory.CreateLogger(); + doLog(logger); + + Assert.Single(exportedData); + var fluentdData = MessagePack.MessagePackSerializer.Deserialize(exportedData[0], MessagePack.Resolvers.ContractlessStandardResolver.Options); return GetFields(fluentdData); } finally { - server?.Dispose(); try { File.Delete(path); diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/MsgPackLogExporterTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/MsgPackLogExporterTests.cs index e42e4427e3..3d2a63f95a 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/MsgPackLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/MsgPackLogExporterTests.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Runtime.InteropServices; using OpenTelemetry.Exporter.Geneva.MsgPack; +using OpenTelemetry.Resources; using Xunit; namespace OpenTelemetry.Exporter.Geneva.Tests; @@ -55,7 +56,7 @@ public void StringSizeLimit_Default_Success() { ConnectionString = this.connectionString, }; - using var exporter = new MsgPackLogExporter(exporterOptions); + using var exporter = new MsgPackLogExporter(exporterOptions, () => Resource.Empty); } [Fact] @@ -65,7 +66,7 @@ public void StringSizeLimit_Valid_Success() { ConnectionString = this.connectionString + ";PrivatePreviewLogMessagePackStringSizeLimit=65360", }; - using var exporter = new MsgPackLogExporter(exporterOptions); + using var exporter = new MsgPackLogExporter(exporterOptions, () => Resource.Empty); } private static string GenerateTempFilePath()