From be84f7dbdad8a34cf29a257e3b53a283735b5f50 Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:12:39 +0100 Subject: [PATCH 1/9] Add User-Agent customization to OtlpExporterOptions and tests --- .../OtlpExporterOptions.cs | 40 ++++++-- .../OtlpExporterOptionsTests.cs | 98 +++++++++++++++++++ 2 files changed, 132 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 91ebfdbd3e1..8b4191c42d5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -32,17 +32,16 @@ public class OtlpExporterOptions : IOtlpExporterOptions internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; #endif - internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] - { - new("User-Agent", GetUserAgentString()), - }; + internal static KeyValuePair[] StandardHeaders => standardHeaders; internal readonly Func DefaultHttpClientFactory; + private static KeyValuePair[]? standardHeaders; private OtlpExportProtocol? protocol; private Uri? endpoint; private int? timeoutMilliseconds; private Func? httpClientFactory; + private string? userAgentProductIdentifier; /// /// Initializes a new instance of the class. @@ -78,6 +77,11 @@ internal OtlpExporterOptions( }; }; + standardHeaders = + [ + new("User-Agent", this.GetUserAgentString()) + ]; + this.BatchExportProcessorOptions = defaultBatchOptions!; } @@ -124,6 +128,23 @@ public OtlpExportProtocol Protocol set => this.protocol = value; } + /// + /// Gets or sets the user agent identifier. + /// + public string UserAgentProductIdentifier + { + get => this.userAgentProductIdentifier ?? string.Empty; + set + { + this.userAgentProductIdentifier = string.IsNullOrWhiteSpace(value) ? string.Empty : value; + + standardHeaders = + [ + new("User-Agent", this.GetUserAgentString()) + ]; + } + } + /// /// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is . /// @@ -226,10 +247,17 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp return this; } - private static string GetUserAgentString() + private string GetUserAgentString() { var assembly = typeof(OtlpExporterOptions).Assembly; - return $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}"; + var baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}"; + + if (!string.IsNullOrEmpty(this.userAgentProductIdentifier)) + { + return $"{this.userAgentProductIdentifier} {baseUserAgent}"; + } + + return baseUserAgent; } private void ApplyConfiguration( diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 9536d283cf5..6167d871408 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -263,4 +263,102 @@ public void OtlpExporterOptions_ApplyDefaultsTest() Assert.NotEqual(defaultOptionsWithData.TimeoutMilliseconds, targetOptionsWithData.TimeoutMilliseconds); Assert.NotEqual(defaultOptionsWithData.HttpClientFactory, targetOptionsWithData.HttpClientFactory); } + + [Fact] + public void UserAgentProductIdentifier_Default_IsEmpty() + { + var options = new OtlpExporterOptions(); + + Assert.Equal(string.Empty, options.UserAgentProductIdentifier); + } + + [Fact] + public void UserAgentProductIdentifier_DefaultUserAgent_ContainsExporterInfo() + { + var options = new OtlpExporterOptions(); + + var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent"); + + Assert.NotNull(userAgentHeader.Key); + Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", userAgentHeader.Value, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void UserAgentProductIdentifier_WithProductIdentifier_IsPrepended() + { + var options = new OtlpExporterOptions + { + UserAgentProductIdentifier = "MyDistribution/1.2.3", + }; + + Assert.Equal("MyDistribution/1.2.3", options.UserAgentProductIdentifier); + + var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent"); + + Assert.NotNull(userAgentHeader.Key); + Assert.StartsWith("MyDistribution/1.2.3 OTel-OTLP-Exporter-Dotnet/", userAgentHeader.Value, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void UserAgentProductIdentifier_UpdatesStandardHeaders() + { + var options = new OtlpExporterOptions(); + + var initialUserAgent = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", initialUserAgent, StringComparison.OrdinalIgnoreCase); + + options.UserAgentProductIdentifier = "MyProduct/1.0.0"; + + var updatedUserAgent = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + Assert.StartsWith("MyProduct/1.0.0 OTel-OTLP-Exporter-Dotnet/", updatedUserAgent, StringComparison.OrdinalIgnoreCase); + Assert.NotEqual(initialUserAgent, updatedUserAgent); + } + + [Fact] + public void UserAgentProductIdentifier_Rfc7231Compliance_SpaceSeparatedTokens() + { + var options = new OtlpExporterOptions + { + UserAgentProductIdentifier = "MyProduct/1.0.0", + }; + + var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + + // Should have two product tokens separated by a space + var tokens = userAgentHeader.Split(' '); + Assert.Equal(2, tokens.Length); + Assert.Equal("MyProduct/1.0.0", tokens[0]); + Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", tokens[1],StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void UserAgentProductIdentifier_EmptyOrWhitespace_UsesDefaultUserAgent(string identifier) + { + var options = new OtlpExporterOptions + { + UserAgentProductIdentifier = identifier, + }; + + var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + + // Should only contain the default exporter identifier, no leading space + Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", userAgentHeader, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain(" ", userAgentHeader, StringComparison.OrdinalIgnoreCase); // No double spaces + } + + [Fact] + public void UserAgentProductIdentifier_MultipleProducts_CorrectFormat() + { + var options = new OtlpExporterOptions + { + UserAgentProductIdentifier = "MySDK/2.0.0 MyDistribution/1.0.0", + }; + + var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + + Assert.StartsWith("MySDK/2.0.0 MyDistribution/1.0.0 OTel-OTLP-Exporter-Dotnet/", userAgentHeader, StringComparison.OrdinalIgnoreCase); + } } From c1e219da0fcda7b7b708ecdc880e7d748563f5b9 Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:11:31 +0100 Subject: [PATCH 2/9] Refactor OtlpExporterOptions to use instance StandardHeaders and update related tests --- .../OtlpExporterOptions.cs | 32 +++---------------- .../OtlpExporterOptionsExtensions.cs | 2 +- .../OtlpHttpTraceExportClientTests.cs | 12 +++---- .../OtlpExporterOptionsExtensionsTests.cs | 10 +++--- .../OtlpExporterOptionsTests.cs | 14 ++++---- .../UseOtlpExporterExtensionTests.cs | 2 +- 6 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 23bd3fb8f56..48dc6c1ab7e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -1,4 +1,4 @@ -// Copyright The OpenTelemetry Authors +// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; @@ -32,11 +32,10 @@ public class OtlpExporterOptions : IOtlpExporterOptions internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; #endif - internal static KeyValuePair[] StandardHeaders => standardHeaders; + internal KeyValuePair[] StandardHeaders => [new("User-Agent", this.GetUserAgentString())]; internal readonly Func DefaultHttpClientFactory; - private static KeyValuePair[]? standardHeaders; private OtlpExportProtocol? protocol; private Uri? endpoint; private int? timeoutMilliseconds; @@ -71,17 +70,9 @@ internal OtlpExporterOptions( this.DefaultHttpClientFactory = () => { - return new HttpClient - { - Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), - }; + return new HttpClient { Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), }; }; - standardHeaders = - [ - new("User-Agent", this.GetUserAgentString()) - ]; - this.BatchExportProcessorOptions = defaultBatchOptions!; } @@ -134,15 +125,7 @@ public OtlpExportProtocol Protocol public string UserAgentProductIdentifier { get => this.userAgentProductIdentifier ?? string.Empty; - set - { - this.userAgentProductIdentifier = string.IsNullOrWhiteSpace(value) ? string.Empty : value; - - standardHeaders = - [ - new("User-Agent", this.GetUserAgentString()) - ]; - } + set => this.userAgentProductIdentifier = string.IsNullOrWhiteSpace(value) ? string.Empty : value; } /// @@ -252,12 +235,7 @@ private string GetUserAgentString() var assembly = typeof(OtlpExporterOptions).Assembly; var baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}"; - if (!string.IsNullOrEmpty(this.userAgentProductIdentifier)) - { - return $"{this.userAgentProductIdentifier} {baseUserAgent}"; - } - - return baseUserAgent; + return !string.IsNullOrEmpty(this.userAgentProductIdentifier) ? $"{this.userAgentProductIdentifier} {baseUserAgent}" : baseUserAgent; } private void ApplyConfiguration( diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index 218b4721caa..c00c7e14efb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -60,7 +60,7 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac } } - foreach (var header in OtlpExporterOptions.StandardHeaders) + foreach (var header in options.StandardHeaders) { addHeader(headers, header.Key, header.Value); } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs index 92262f35429..bc3cdcca101 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -57,13 +57,13 @@ public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectP Assert.NotNull(client.HttpClient); - Assert.Equal(2 + OtlpExporterOptions.StandardHeaders.Length, client.Headers.Count); + Assert.Equal(2 + options.StandardHeaders.Length, client.Headers.Count); Assert.Contains(client.Headers, kvp => kvp.Key == header1.Name && kvp.Value == header1.Value); Assert.Contains(client.Headers, kvp => kvp.Key == header2.Name && kvp.Value == header2.Value); - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + for (int i = 0; i < options.StandardHeaders.Length; i++) { - Assert.Contains(client.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); + Assert.Contains(client.Headers, entry => entry.Key == options.StandardHeaders[i].Key && entry.Value == options.StandardHeaders[i].Value); } } @@ -156,13 +156,13 @@ void RunTest(Batch batch) Assert.Equal(HttpMethod.Post, httpRequest.Method); Assert.NotNull(httpRequest.RequestUri); Assert.Equal("http://localhost:4317/", httpRequest.RequestUri.AbsoluteUri); - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + 2, httpRequest.Headers.Count()); + Assert.Equal(options.StandardHeaders.Length + 2, httpRequest.Headers.Count()); Assert.Contains(httpRequest.Headers, h => h.Key == header1.Name && h.Value.First() == header1.Value); Assert.Contains(httpRequest.Headers, h => h.Key == header2.Name && h.Value.First() == header2.Value); - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + for (int i = 0; i < options.StandardHeaders.Length; i++) { - Assert.Contains(httpRequest.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value.First() == OtlpExporterOptions.StandardHeaders[i].Value); + Assert.Contains(httpRequest.Headers, entry => entry.Key == options.StandardHeaders[i].Key && entry.Value.First() == options.StandardHeaders[i].Value); } Assert.NotNull(testHttpHandler.HttpRequestContent); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs index 11f2d9563e9..0c3457e36d5 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -25,11 +25,11 @@ public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string? optionHead var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length, headers.Count); + Assert.Equal(options.StandardHeaders.Length, headers.Count); - for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + for (int i = 0; i < options.StandardHeaders.Length; i++) { - Assert.Contains(headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); + Assert.Contains(headers, entry => entry.Key == options.StandardHeaders[i].Key && entry.Value == options.StandardHeaders[i].Value); } } @@ -185,14 +185,14 @@ private static void VerifyHeaders(string inputOptionHeaders, string expectedNorm } } - Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + expectedOptional.Count, headers.Count); + Assert.Equal(options.StandardHeaders.Length + expectedOptional.Count, headers.Count); foreach (var kvp in expectedOptional) { Assert.Contains(headers, h => h.Key == kvp.Key && h.Value == kvp.Value); } - foreach (var std in OtlpExporterOptions.StandardHeaders) + foreach (var std in options.StandardHeaders) { Assert.Contains(headers, h => h.Key == std.Key && h.Value == std.Value); } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 18ad90eca59..a7d9de14395 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -276,7 +276,7 @@ public void UserAgentProductIdentifier_DefaultUserAgent_ContainsExporterInfo() { var options = new OtlpExporterOptions(); - var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent"); + var userAgentHeader = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent"); Assert.NotNull(userAgentHeader.Key); Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", userAgentHeader.Value, StringComparison.OrdinalIgnoreCase); @@ -292,7 +292,7 @@ public void UserAgentProductIdentifier_WithProductIdentifier_IsPrepended() Assert.Equal("MyDistribution/1.2.3", options.UserAgentProductIdentifier); - var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent"); + var userAgentHeader = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent"); Assert.NotNull(userAgentHeader.Key); Assert.StartsWith("MyDistribution/1.2.3 OTel-OTLP-Exporter-Dotnet/", userAgentHeader.Value, StringComparison.OrdinalIgnoreCase); @@ -303,12 +303,12 @@ public void UserAgentProductIdentifier_UpdatesStandardHeaders() { var options = new OtlpExporterOptions(); - var initialUserAgent = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + var initialUserAgent = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", initialUserAgent, StringComparison.OrdinalIgnoreCase); options.UserAgentProductIdentifier = "MyProduct/1.0.0"; - var updatedUserAgent = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + var updatedUserAgent = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; Assert.StartsWith("MyProduct/1.0.0 OTel-OTLP-Exporter-Dotnet/", updatedUserAgent, StringComparison.OrdinalIgnoreCase); Assert.NotEqual(initialUserAgent, updatedUserAgent); } @@ -321,7 +321,7 @@ public void UserAgentProductIdentifier_Rfc7231Compliance_SpaceSeparatedTokens() UserAgentProductIdentifier = "MyProduct/1.0.0", }; - var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + var userAgentHeader = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; // Should have two product tokens separated by a space var tokens = userAgentHeader.Split(' '); @@ -341,7 +341,7 @@ public void UserAgentProductIdentifier_EmptyOrWhitespace_UsesDefaultUserAgent(st UserAgentProductIdentifier = identifier, }; - var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + var userAgentHeader = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; // Should only contain the default exporter identifier, no leading space Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", userAgentHeader, StringComparison.OrdinalIgnoreCase); @@ -356,7 +356,7 @@ public void UserAgentProductIdentifier_MultipleProducts_CorrectFormat() UserAgentProductIdentifier = "MySDK/2.0.0 MyDistribution/1.0.0", }; - var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; + var userAgentHeader = options.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value; Assert.StartsWith("MySDK/2.0.0 MyDistribution/1.0.0 OTel-OTLP-Exporter-Dotnet/", userAgentHeader, StringComparison.OrdinalIgnoreCase); } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs index 424a9922cba..acc7b786e66 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/UseOtlpExporterExtensionTests.cs @@ -38,7 +38,7 @@ public void UseOtlpExporterDefaultTest() var exporterOptions = sp.GetRequiredService>().CurrentValue; #if NETFRAMEWORK || NETSTANDARD2_0 - Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), exporterOptions.DefaultOptions.Endpoint); + Assert.Equal(new Uri(options.DefaultHttpEndpoint), exporterOptions.DefaultOptions.Endpoint); #else Assert.Equal(new Uri(OtlpExporterOptions.DefaultGrpcEndpoint), exporterOptions.DefaultOptions.Endpoint); #endif From 3d05c14ed742d605c89a731d48babfb14b251c37 Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:27:46 +0100 Subject: [PATCH 3/9] Add base User-Agent string to OtlpExporterOptions --- .../OtlpExporterOptions.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 48dc6c1ab7e..35478450997 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -41,6 +41,7 @@ public class OtlpExporterOptions : IOtlpExporterOptions private int? timeoutMilliseconds; private Func? httpClientFactory; private string? userAgentProductIdentifier; + private string baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetName().Version}"; /// /// Initializes a new instance of the class. @@ -70,7 +71,10 @@ internal OtlpExporterOptions( this.DefaultHttpClientFactory = () => { - return new HttpClient { Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), }; + return new HttpClient + { + Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), + }; }; this.BatchExportProcessorOptions = defaultBatchOptions!; @@ -232,9 +236,6 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp private string GetUserAgentString() { - var assembly = typeof(OtlpExporterOptions).Assembly; - var baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}"; - return !string.IsNullOrEmpty(this.userAgentProductIdentifier) ? $"{this.userAgentProductIdentifier} {baseUserAgent}" : baseUserAgent; } From a91b704be1d30ffc3b4cd86b3bb01b9babc1f5f4 Mon Sep 17 00:00:00 2001 From: Anne Meijer <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:38:46 +0100 Subject: [PATCH 4/9] Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs Co-authored-by: Martin Costello --- .../OtlpExporterOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 35478450997..7e01c4118f9 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -41,7 +41,7 @@ public class OtlpExporterOptions : IOtlpExporterOptions private int? timeoutMilliseconds; private Func? httpClientFactory; private string? userAgentProductIdentifier; - private string baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetName().Version}"; + private string baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetPackageVersion()}"; /// /// Initializes a new instance of the class. From 2490ff8185b97da8f461b65150fc69fab1db760d Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:51:30 +0100 Subject: [PATCH 5/9] Refactor StandardHeaders to use a static base User-Agent and handle customization --- .../OtlpExporterOptions.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 7e01c4118f9..557d75763fa 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -32,7 +32,16 @@ public class OtlpExporterOptions : IOtlpExporterOptions internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; #endif - internal KeyValuePair[] StandardHeaders => [new("User-Agent", this.GetUserAgentString())]; + private static readonly string baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetPackageVersion()}"; + private static readonly KeyValuePair[] DefaultHeader = + [ + new("User-Agent", baseUserAgent) + ]; + + internal KeyValuePair[] StandardHeaders => + string.IsNullOrEmpty(this.userAgentProductIdentifier) + ? DefaultHeader + : [new("User-Agent", this.GetUserAgentString())]; internal readonly Func DefaultHttpClientFactory; @@ -41,7 +50,6 @@ public class OtlpExporterOptions : IOtlpExporterOptions private int? timeoutMilliseconds; private Func? httpClientFactory; private string? userAgentProductIdentifier; - private string baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetPackageVersion()}"; /// /// Initializes a new instance of the class. From fe1837be39110a5e5a446a2347300baa3b8a83e9 Mon Sep 17 00:00:00 2001 From: Anne Meijer <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:42:16 +0100 Subject: [PATCH 6/9] Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs Co-authored-by: Martin Costello --- .../OtlpExporterOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 557d75763fa..cf7d048cb03 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -33,7 +33,7 @@ public class OtlpExporterOptions : IOtlpExporterOptions #endif private static readonly string baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetPackageVersion()}"; - private static readonly KeyValuePair[] DefaultHeader = + private static readonly KeyValuePair[] DefaultHeaders = [ new("User-Agent", baseUserAgent) ]; From 2c3a14a7db2199b96c42b35de3f28603029cba6c Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:03:10 +0100 Subject: [PATCH 7/9] Simplify GetUserAgentString method --- .../OtlpExporterOptions.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index cf7d048cb03..e0dfd1a02c1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -40,7 +40,7 @@ public class OtlpExporterOptions : IOtlpExporterOptions internal KeyValuePair[] StandardHeaders => string.IsNullOrEmpty(this.userAgentProductIdentifier) - ? DefaultHeader + ? DefaultHeaders : [new("User-Agent", this.GetUserAgentString())]; internal readonly Func DefaultHttpClientFactory; @@ -242,10 +242,7 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp return this; } - private string GetUserAgentString() - { - return !string.IsNullOrEmpty(this.userAgentProductIdentifier) ? $"{this.userAgentProductIdentifier} {baseUserAgent}" : baseUserAgent; - } + private string GetUserAgentString() => $"{this.userAgentProductIdentifier} {baseUserAgent}"; private void ApplyConfiguration( IConfiguration configuration, From b570bee7ce88a6508e939c0db9d7b867f0637ade Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:12:19 +0100 Subject: [PATCH 8/9] Simplified StandardHeaders --- .../OtlpExporterOptions.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index e0dfd1a02c1..a9dbecb56b5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -39,9 +39,9 @@ public class OtlpExporterOptions : IOtlpExporterOptions ]; internal KeyValuePair[] StandardHeaders => - string.IsNullOrEmpty(this.userAgentProductIdentifier) + string.IsNullOrEmpty(this.UserAgentProductIdentifier) ? DefaultHeaders - : [new("User-Agent", this.GetUserAgentString())]; + : [new("User-Agent", $"{this.UserAgentProductIdentifier} {baseUserAgent}")]; internal readonly Func DefaultHttpClientFactory; @@ -49,7 +49,6 @@ public class OtlpExporterOptions : IOtlpExporterOptions private Uri? endpoint; private int? timeoutMilliseconds; private Func? httpClientFactory; - private string? userAgentProductIdentifier; /// /// Initializes a new instance of the class. @@ -134,11 +133,7 @@ public OtlpExportProtocol Protocol /// /// Gets or sets the user agent identifier. /// - public string UserAgentProductIdentifier - { - get => this.userAgentProductIdentifier ?? string.Empty; - set => this.userAgentProductIdentifier = string.IsNullOrWhiteSpace(value) ? string.Empty : value; - } + public string UserAgentProductIdentifier { get; set; } = string.Empty; /// /// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is . @@ -242,8 +237,6 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp return this; } - private string GetUserAgentString() => $"{this.userAgentProductIdentifier} {baseUserAgent}"; - private void ApplyConfiguration( IConfiguration configuration, OtlpExporterOptionsConfigurationType configurationType) From 71bbc786c5d0584c7a47955479542f1251086c8d Mon Sep 17 00:00:00 2001 From: "A. Meijer" <13779871+meijeran@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:17:12 +0100 Subject: [PATCH 9/9] Add UserAgentProductIdentifier to OtlpExporterOptions for custom User-Agent support --- .../CHANGELOG.md | 6 ++++++ .../OtlpExporterOptions.cs | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index ad7456e2386..ed35ad22226 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -7,6 +7,12 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added `UserAgentProductIdentifier` property to `OtlpExporterOptions` to allow + custom product identifiers to be prepended to the User-Agent header. When set, + the custom identifier is prepended with a space separator to the default + User-Agent string (e.g., "MyApp/1.0 OTel-OTLP-Exporter-Dotnet/1.14.0"). + ([#6496]) + ## 1.14.0 Released 2025-Nov-12 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index a9dbecb56b5..8badc7a5ad1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -131,7 +131,8 @@ public OtlpExportProtocol Protocol } /// - /// Gets or sets the user agent identifier. + /// Gets or sets a custom user agent identifier. + /// This will be prepended to the default user agent string. /// public string UserAgentProductIdentifier { get; set; } = string.Empty;