Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
Expand Up @@ -32,17 +32,16 @@ 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 static KeyValuePair<string, string>[] StandardHeaders => standardHeaders;

internal readonly Func<HttpClient> DefaultHttpClientFactory;

private static KeyValuePair<string, string>[]? standardHeaders;
private OtlpExportProtocol? protocol;
private Uri? endpoint;
private int? timeoutMilliseconds;
private Func<HttpClient>? httpClientFactory;
private string? userAgentProductIdentifier;

/// <summary>
/// Initializes a new instance of the <see cref="OtlpExporterOptions"/> class.
Expand Down Expand Up @@ -78,6 +77,11 @@ internal OtlpExporterOptions(
};
};

standardHeaders =
[
new("User-Agent", this.GetUserAgentString())
];

this.BatchExportProcessorOptions = defaultBatchOptions!;
}

Expand Down Expand Up @@ -124,6 +128,23 @@ public OtlpExportProtocol Protocol
set => this.protocol = value;
}

/// <summary>
/// Gets or sets the user agent identifier.
/// </summary>
public string UserAgentProductIdentifier
{
get => this.userAgentProductIdentifier ?? string.Empty;
set
{
this.userAgentProductIdentifier = string.IsNullOrWhiteSpace(value) ? string.Empty : value;

standardHeaders =
[
new("User-Agent", this.GetUserAgentString())
];
}
}

/// <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 Down Expand Up @@ -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(
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 = 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);
}
}