diff --git a/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md b/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md index 9c95cbdc73..bb331581ec 100644 --- a/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md @@ -5,6 +5,10 @@ * Updated OpenTelemetry core component version(s) to `1.11.1`. ([#2477](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2477)) +* Event full name validation can now be bypassed by supplying `{EventFullName}` + as the first attribute on logs. + ([#2529](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2529)) + ## 1.10.0 Released 2024-Nov-18 diff --git a/src/OpenTelemetry.Exporter.OneCollector/Internal/EventNameManager.cs b/src/OpenTelemetry.Exporter.OneCollector/Internal/EventNameManager.cs index 2bef0e3473..8f9158c0e3 100644 --- a/src/OpenTelemetry.Exporter.OneCollector/Internal/EventNameManager.cs +++ b/src/OpenTelemetry.Exporter.OneCollector/Internal/EventNameManager.cs @@ -12,6 +12,7 @@ internal sealed class EventNameManager // Note: OneCollector will silently drop events which have a name less than 4 characters. internal const int MinimumEventFullNameLength = 4; internal const int MaximumEventFullNameLength = 100; + private static readonly Regex EventNamespaceValidationRegex = new(@"^[A-Za-z](?:\.?[A-Za-z0-9]+?)*$", RegexOptions.Compiled); private static readonly Regex EventNameValidationRegex = new(@"^[A-Za-z][A-Za-z0-9]*$", RegexOptions.Compiled); @@ -19,6 +20,7 @@ internal sealed class EventNameManager private readonly string defaultEventName; private readonly IReadOnlyDictionary? eventFullNameMappings; private readonly ResolvedEventFullName defaultEventFullName; + private readonly Hashtable eventFullNameCache = new(StringComparer.OrdinalIgnoreCase); public EventNameManager( string defaultEventNamespace, @@ -42,15 +44,43 @@ public EventNameManager( #endif } - // Note: This is exposed for unit tests. + // Note: These caches are exposed for unit tests. internal Hashtable EventNamespaceCache { get; } = new(StringComparer.OrdinalIgnoreCase); + internal Hashtable EventFullNameCache => this.eventFullNameCache; + public static bool IsEventNamespaceValid(string eventNamespace) => EventNamespaceValidationRegex.IsMatch(eventNamespace); public static bool IsEventNameValid(string eventName) => EventNameValidationRegex.IsMatch(eventName); + public ResolvedEventFullName ResolveEventFullName( + string eventFullName) + { + if (this.eventFullNameCache[eventFullName] is ResolvedEventFullName cachedEventFullName) + { + return cachedEventFullName; + } + + byte[] eventFullNameBlob = BuildEventFullName(string.Empty, eventFullName); + + var resolvedEventFullName = new ResolvedEventFullName( + eventFullNameBlob, + originalEventNamespace: null, + originalEventName: null); + + lock (this.eventFullNameCache) + { + if (this.eventFullNameCache[eventFullName] is null) + { + this.eventFullNameCache[eventFullName] = resolvedEventFullName; + } + } + + return resolvedEventFullName; + } + public ResolvedEventFullName ResolveEventFullName( string? eventNamespace, string? eventName) diff --git a/src/OpenTelemetry.Exporter.OneCollector/Internal/Serialization/LogRecordCommonSchemaJsonSerializer.cs b/src/OpenTelemetry.Exporter.OneCollector/Internal/Serialization/LogRecordCommonSchemaJsonSerializer.cs index 2398c20743..4aa9a566b3 100644 --- a/src/OpenTelemetry.Exporter.OneCollector/Internal/Serialization/LogRecordCommonSchemaJsonSerializer.cs +++ b/src/OpenTelemetry.Exporter.OneCollector/Internal/Serialization/LogRecordCommonSchemaJsonSerializer.cs @@ -89,9 +89,23 @@ protected override void SerializeItemToJson(Resource resource, LogRecord item, C Debug.Assert(writer != null, "writer was null"); - var resolvedEventFullName = this.eventNameManager.ResolveEventFullName( - item.CategoryName, - item.EventId.Name); + int attributeStartIndex = 0; + EventNameManager.ResolvedEventFullName resolvedEventFullName; + if (item.Attributes != null + && item.Attributes.Count > 0 + && item.Attributes[0].Key == "{EventFullName}" + && item.Attributes[0].Value is string eventFullName + && !string.IsNullOrEmpty(eventFullName)) + { + attributeStartIndex++; + resolvedEventFullName = this.eventNameManager.ResolveEventFullName(eventFullName); + } + else + { + resolvedEventFullName = this.eventNameManager.ResolveEventFullName( + item.CategoryName, + item.EventId.Name); + } writer!.WriteStartObject(); @@ -150,7 +164,7 @@ protected override void SerializeItemToJson(Resource resource, LogRecord item, C if (item.Attributes != null) { - for (var i = 0; i < item.Attributes.Count; i++) + for (int i = attributeStartIndex; i < item.Attributes.Count; i++) { var attribute = item.Attributes[i]; diff --git a/test/OpenTelemetry.Exporter.OneCollector.Tests/EventNameManagerTests.cs b/test/OpenTelemetry.Exporter.OneCollector.Tests/EventNameManagerTests.cs index 53c8723f27..c3e98fd27f 100644 --- a/test/OpenTelemetry.Exporter.OneCollector.Tests/EventNameManagerTests.cs +++ b/test/OpenTelemetry.Exporter.OneCollector.Tests/EventNameManagerTests.cs @@ -103,6 +103,22 @@ public void EventNameCacheTest() Assert.Single((eventNameManager.EventNamespaceCache["Test"] as Hashtable)!); } + [Fact] + public void EventFullNameCacheTest() + { + var eventNameManager = BuildEventNameManagerWithDefaultOptions(); + + Assert.Empty(eventNameManager.EventFullNameCache); + + eventNameManager.ResolveEventFullName("Company_Product_EventName"); + + Assert.Single(eventNameManager.EventFullNameCache); + + eventNameManager.ResolveEventFullName("company_product_eventName"); + + Assert.Single(eventNameManager.EventFullNameCache); + } + [Fact] public void EventFullNameMappedWhenEventNamespaceMatchesTest() { diff --git a/test/OpenTelemetry.Exporter.OneCollector.Tests/LogRecordCommonSchemaJsonSerializerTests.cs b/test/OpenTelemetry.Exporter.OneCollector.Tests/LogRecordCommonSchemaJsonSerializerTests.cs index 8df736a680..aa6b6258eb 100644 --- a/test/OpenTelemetry.Exporter.OneCollector.Tests/LogRecordCommonSchemaJsonSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.OneCollector.Tests/LogRecordCommonSchemaJsonSerializerTests.cs @@ -207,7 +207,7 @@ public void LogRecordScopesJsonTest() } [Fact] - public void LogRecordStateValuesJsonTest() + public void LogRecordAttributesJsonTest() { var json = GetLogRecordJson(1, (index, logRecord) => { @@ -223,6 +223,24 @@ public void LogRecordStateValuesJsonTest() json); } + [Fact] + public void LogRecordAttributesWithEventFullNameJsonTest() + { + string json = GetLogRecordJson(1, (index, logRecord) => + { + logRecord.Attributes = new List> + { + new KeyValuePair("{EventFullName}", "company_Product_EventName"), + new KeyValuePair("stateKey1", "stateValue1"), + new KeyValuePair("stateKey2", "stateValue2"), + }; + }); + + Assert.Equal( + """{"ver":"4.0","name":"Company_Product_EventName","time":"2032-01-18T10:11:12Z","iKey":"o:tenant-token","data":{"severityText":"Trace","severityNumber":1,"stateKey1":"stateValue1","stateKey2":"stateValue2"}}""" + "\n", + json); + } + [Fact] public void LogRecordTraceContextJsonTest() {