Skip to content

Commit be84f7d

Browse files
committed
Add User-Agent customization to OtlpExporterOptions and tests
1 parent cd522d9 commit be84f7d

File tree

2 files changed

+132
-6
lines changed

2 files changed

+132
-6
lines changed

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,16 @@ public class OtlpExporterOptions : IOtlpExporterOptions
3232
internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc;
3333
#endif
3434

35-
internal static readonly KeyValuePair<string, string>[] StandardHeaders = new KeyValuePair<string, string>[]
36-
{
37-
new("User-Agent", GetUserAgentString()),
38-
};
35+
internal static KeyValuePair<string, string>[] StandardHeaders => standardHeaders;
3936

4037
internal readonly Func<HttpClient> DefaultHttpClientFactory;
4138

39+
private static KeyValuePair<string, string>[]? standardHeaders;
4240
private OtlpExportProtocol? protocol;
4341
private Uri? endpoint;
4442
private int? timeoutMilliseconds;
4543
private Func<HttpClient>? httpClientFactory;
44+
private string? userAgentProductIdentifier;
4645

4746
/// <summary>
4847
/// Initializes a new instance of the <see cref="OtlpExporterOptions"/> class.
@@ -78,6 +77,11 @@ internal OtlpExporterOptions(
7877
};
7978
};
8079

80+
standardHeaders =
81+
[
82+
new("User-Agent", this.GetUserAgentString())
83+
];
84+
8185
this.BatchExportProcessorOptions = defaultBatchOptions!;
8286
}
8387

@@ -124,6 +128,23 @@ public OtlpExportProtocol Protocol
124128
set => this.protocol = value;
125129
}
126130

131+
/// <summary>
132+
/// Gets or sets the user agent identifier.
133+
/// </summary>
134+
public string UserAgentProductIdentifier
135+
{
136+
get => this.userAgentProductIdentifier ?? string.Empty;
137+
set
138+
{
139+
this.userAgentProductIdentifier = string.IsNullOrWhiteSpace(value) ? string.Empty : value;
140+
141+
standardHeaders =
142+
[
143+
new("User-Agent", this.GetUserAgentString())
144+
];
145+
}
146+
}
147+
127148
/// <summary>
128149
/// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is <see cref="ExportProcessorType.Batch"/>.
129150
/// </summary>
@@ -226,10 +247,17 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp
226247
return this;
227248
}
228249

229-
private static string GetUserAgentString()
250+
private string GetUserAgentString()
230251
{
231252
var assembly = typeof(OtlpExporterOptions).Assembly;
232-
return $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}";
253+
var baseUserAgent = $"OTel-OTLP-Exporter-Dotnet/{assembly.GetPackageVersion()}";
254+
255+
if (!string.IsNullOrEmpty(this.userAgentProductIdentifier))
256+
{
257+
return $"{this.userAgentProductIdentifier} {baseUserAgent}";
258+
}
259+
260+
return baseUserAgent;
233261
}
234262

235263
private void ApplyConfiguration(

test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,102 @@ public void OtlpExporterOptions_ApplyDefaultsTest()
263263
Assert.NotEqual(defaultOptionsWithData.TimeoutMilliseconds, targetOptionsWithData.TimeoutMilliseconds);
264264
Assert.NotEqual(defaultOptionsWithData.HttpClientFactory, targetOptionsWithData.HttpClientFactory);
265265
}
266+
267+
[Fact]
268+
public void UserAgentProductIdentifier_Default_IsEmpty()
269+
{
270+
var options = new OtlpExporterOptions();
271+
272+
Assert.Equal(string.Empty, options.UserAgentProductIdentifier);
273+
}
274+
275+
[Fact]
276+
public void UserAgentProductIdentifier_DefaultUserAgent_ContainsExporterInfo()
277+
{
278+
var options = new OtlpExporterOptions();
279+
280+
var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent");
281+
282+
Assert.NotNull(userAgentHeader.Key);
283+
Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", userAgentHeader.Value, StringComparison.OrdinalIgnoreCase);
284+
}
285+
286+
[Fact]
287+
public void UserAgentProductIdentifier_WithProductIdentifier_IsPrepended()
288+
{
289+
var options = new OtlpExporterOptions
290+
{
291+
UserAgentProductIdentifier = "MyDistribution/1.2.3",
292+
};
293+
294+
Assert.Equal("MyDistribution/1.2.3", options.UserAgentProductIdentifier);
295+
296+
var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent");
297+
298+
Assert.NotNull(userAgentHeader.Key);
299+
Assert.StartsWith("MyDistribution/1.2.3 OTel-OTLP-Exporter-Dotnet/", userAgentHeader.Value, StringComparison.OrdinalIgnoreCase);
300+
}
301+
302+
[Fact]
303+
public void UserAgentProductIdentifier_UpdatesStandardHeaders()
304+
{
305+
var options = new OtlpExporterOptions();
306+
307+
var initialUserAgent = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value;
308+
Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", initialUserAgent, StringComparison.OrdinalIgnoreCase);
309+
310+
options.UserAgentProductIdentifier = "MyProduct/1.0.0";
311+
312+
var updatedUserAgent = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value;
313+
Assert.StartsWith("MyProduct/1.0.0 OTel-OTLP-Exporter-Dotnet/", updatedUserAgent, StringComparison.OrdinalIgnoreCase);
314+
Assert.NotEqual(initialUserAgent, updatedUserAgent);
315+
}
316+
317+
[Fact]
318+
public void UserAgentProductIdentifier_Rfc7231Compliance_SpaceSeparatedTokens()
319+
{
320+
var options = new OtlpExporterOptions
321+
{
322+
UserAgentProductIdentifier = "MyProduct/1.0.0",
323+
};
324+
325+
var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value;
326+
327+
// Should have two product tokens separated by a space
328+
var tokens = userAgentHeader.Split(' ');
329+
Assert.Equal(2, tokens.Length);
330+
Assert.Equal("MyProduct/1.0.0", tokens[0]);
331+
Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", tokens[1],StringComparison.OrdinalIgnoreCase);
332+
}
333+
334+
[Theory]
335+
[InlineData("")]
336+
[InlineData(" ")]
337+
[InlineData(" ")]
338+
public void UserAgentProductIdentifier_EmptyOrWhitespace_UsesDefaultUserAgent(string identifier)
339+
{
340+
var options = new OtlpExporterOptions
341+
{
342+
UserAgentProductIdentifier = identifier,
343+
};
344+
345+
var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value;
346+
347+
// Should only contain the default exporter identifier, no leading space
348+
Assert.StartsWith("OTel-OTLP-Exporter-Dotnet/", userAgentHeader, StringComparison.OrdinalIgnoreCase);
349+
Assert.DoesNotContain(" ", userAgentHeader, StringComparison.OrdinalIgnoreCase); // No double spaces
350+
}
351+
352+
[Fact]
353+
public void UserAgentProductIdentifier_MultipleProducts_CorrectFormat()
354+
{
355+
var options = new OtlpExporterOptions
356+
{
357+
UserAgentProductIdentifier = "MySDK/2.0.0 MyDistribution/1.0.0",
358+
};
359+
360+
var userAgentHeader = OtlpExporterOptions.StandardHeaders.FirstOrDefault(h => h.Key == "User-Agent").Value;
361+
362+
Assert.StartsWith("MySDK/2.0.0 MyDistribution/1.0.0 OTel-OTLP-Exporter-Dotnet/", userAgentHeader, StringComparison.OrdinalIgnoreCase);
363+
}
266364
}

0 commit comments

Comments
 (0)