Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
be84f7d
Add User-Agent customization to OtlpExporterOptions and tests
meijeran Nov 12, 2025
0908333
Merge branch 'open-telemetry:main' into add-user-agent-customization
meijeran Nov 12, 2025
c1e219d
Refactor OtlpExporterOptions to use instance StandardHeaders and upda…
meijeran Nov 12, 2025
3d05c14
Add base User-Agent string to OtlpExporterOptions
meijeran Nov 12, 2025
a91b704
Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterO…
meijeran Nov 12, 2025
2490ff8
Refactor StandardHeaders to use a static base User-Agent and handle c…
meijeran Nov 12, 2025
fe1837b
Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterO…
meijeran Nov 12, 2025
2c3a14a
Simplify GetUserAgentString method
meijeran Nov 12, 2025
b570bee
Simplified StandardHeaders
meijeran Nov 12, 2025
3fe1b37
Merge branch 'main' into add-user-agent-customization
meijeran Nov 12, 2025
c65392a
Merge branch 'main' into add-user-agent-customization
meijeran Nov 12, 2025
71bbc78
Add UserAgentProductIdentifier to OtlpExporterOptions for custom User…
meijeran Nov 19, 2025
3563b4e
Update CHANGELOG.md
meijeran Nov 21, 2025
38b0cbf
Fixed build errors
meijeran Nov 25, 2025
f6ce17e
Update changelog and PublicAPI files for UserAgentProductIdentifier c…
meijeran Nov 25, 2025
fc9b51c
Use string.IsNullOrWhiteSpace for UserAgentProductIdentifier check in…
meijeran Nov 25, 2025
33056dd
Fix endpoint assertion in UseOtlpExporterExtensionTests
meijeran Nov 25, 2025
81bd2d3
Merge branch 'main' into add-user-agent-customization
meijeran Nov 27, 2025
8b7d46a
Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md
meijeran Nov 27, 2025
c89b979
Fix formatting OtlpExporterOptions.cs
meijeran Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OpenTelemetry.Exporter.OtlpExporterOptions.UserAgentProductIdentifier.get -> string?
OpenTelemetry.Exporter.OtlpExporterOptions.UserAgentProductIdentifier.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ 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`).
([#6686](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6686))
* Added support for `ActivitySource.TelemetrySchemaUrl` property.
([#6730](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6730))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright The OpenTelemetry Authors
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
Expand Down Expand Up @@ -32,12 +32,12 @@ public class OtlpExporterOptions : IOtlpExporterOptions
internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc;
#endif

internal static readonly KeyValuePair<string, string>[] StandardHeaders = new KeyValuePair<string, string>[]
{
new("User-Agent", GetUserAgentString()),
};

internal readonly Func<HttpClient> DefaultHttpClientFactory;
private static readonly string BaseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{typeof(OtlpExporterOptions).Assembly.GetPackageVersion()}";
private static readonly KeyValuePair<string, string>[] DefaultHeaders =
[
new("User-Agent", BaseUserAgent)
];

private OtlpExportProtocol? protocol;
private Uri? endpoint;
Expand Down Expand Up @@ -124,6 +124,12 @@ public OtlpExportProtocol Protocol
set => this.protocol = value;
}

/// <summary>
/// Gets or sets a custom user agent identifier.
/// This will be prepended to the default user agent string.
/// </summary>
public string? UserAgentProductIdentifier { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is <see cref="ExportProcessorType.Batch"/>.
/// </summary>
Expand All @@ -148,6 +154,11 @@ public Func<HttpClient> HttpClientFactory
}
}

internal KeyValuePair<string, string>[] StandardHeaders =>
string.IsNullOrWhiteSpace(this.UserAgentProductIdentifier)
? DefaultHeaders
: [new("User-Agent", $"{this.UserAgentProductIdentifier} {BaseUserAgent}")];

/// <summary>
/// Gets a value indicating whether or not the signal-specific path should
/// be appended to <see cref="Endpoint"/>.
Expand Down Expand Up @@ -226,12 +237,6 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp
return this;
}

private static string GetUserAgentString()
{
var assembly = typeof(OtlpExporterOptions).Assembly;
return $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}";
}

private void ApplyConfiguration(
IConfiguration configuration,
OtlpExporterOptionsConfigurationType configurationType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public static THeaders GetHeaders<THeaders>(this OtlpExporterOptions options, Ac
}
}

foreach (var header in OtlpExporterOptions.StandardHeaders)
foreach (var header in options.StandardHeaders)
{
addHeader(headers, header.Key, header.Value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -156,13 +156,13 @@ void RunTest(Batch<Activity> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string? optionHead

var headers = options.GetHeaders<Dictionary<string, string>>((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);
}
}

Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,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 = options.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 = 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);
}

[Fact]
public void UserAgentProductIdentifier_UpdatesStandardHeaders()
{
var options = new OtlpExporterOptions();

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 = 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);
}

[Fact]
public void UserAgentProductIdentifier_Rfc7231Compliance_SpaceSeparatedTokens()
{
var options = new OtlpExporterOptions
{
UserAgentProductIdentifier = "MyProduct/1.0.0",
};

var userAgentHeader = options.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 = 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);
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 = 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);
}
}
Loading