diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 3b84183b3ae..2c18840f57d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Include User-Agent header + [per the specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#user-agent). + ([#4120](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4120)) + ## 1.4.0-rc.2 Released 2023-Jan-09 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 289c0fec8b3..18efd9de04b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -39,11 +39,19 @@ public class OtlpExporterOptions internal const string TimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; internal const string ProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] + { + new KeyValuePair("User-Agent", UserAgentProductVersion != null ? $"{UserAgentProduct}/{UserAgentProductVersion}" : UserAgentProduct), + }; + internal readonly Func DefaultHttpClientFactory; private const string DefaultGrpcEndpoint = "http://localhost:4317"; private const string DefaultHttpEndpoint = "http://localhost:4318"; private const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; + private const string UserAgentProduct = "OTel-OTLP-Exporter-Dotnet"; + + private static readonly Version UserAgentProductVersion = GetAssemblyVersion(); private Uri endpoint; @@ -195,5 +203,18 @@ internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection servi configuration, sp.GetRequiredService>().Get(name))); } + + private static Version GetAssemblyVersion() + { + try + { + var assemblyName = typeof(OtlpExporterOptions).Assembly.GetName(); + return assemblyName.Version; + } + catch (Exception) + { + return null; + } + } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index 2275bdcca93..c4aa08f23d5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -89,6 +89,11 @@ public static THeaders GetHeaders(this OtlpExporterOptions options, Ac }); } + foreach (var header in OtlpExporterOptions.StandardHeaders) + { + addHeader(headers, header.Key, header.Value); + } + return headers; } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs index bd21628cd03..76998e52340 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -63,9 +63,14 @@ public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectP Assert.NotNull(client.HttpClient); - Assert.Equal(2, client.Headers.Count); + Assert.Equal(2 + OtlpExporterOptions.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++) + { + Assert.Contains(client.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); + } } [Theory] @@ -179,10 +184,15 @@ void RunTest(Batch batch) Assert.NotNull(httpRequest); Assert.Equal(HttpMethod.Post, httpRequest.Method); Assert.Equal("http://localhost:4317/", httpRequest.RequestUri.AbsoluteUri); - Assert.Equal(2, httpRequest.Headers.Count()); + Assert.Equal(OtlpExporterOptions.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++) + { + Assert.Contains(httpRequest.Headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value.First() == OtlpExporterOptions.StandardHeaders[i].Value); + } + Assert.NotNull(httpRequest.Content); Assert.IsType(httpRequest.Content); Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs index acb49cf7185..74bae7c6ddb 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using Grpc.Core; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using Xunit; using Xunit.Sdk; @@ -37,12 +38,19 @@ public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] ke }; var metadata = options.GetMetadataFromHeaders(); - Assert.Equal(keys.Length, metadata.Count); + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length + keys.Length, metadata.Count); for (int i = 0; i < keys.Length; i++) { Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]); } + + for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + { + // Metadata key is always converted to lowercase. + // See: https://cloud.google.com/dotnet/docs/reference/Grpc.Core/latest/Grpc.Core.Metadata.Entry#Grpc_Core_Metadata_Entry__ctor_System_String_System_String_ + Assert.Contains(metadata, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key.ToLower() && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); + } } [Theory] @@ -71,7 +79,7 @@ public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers) [Theory] [InlineData("")] [InlineData(null)] - public void GetHeaders_NoOptionHeaders_ReturnsEmptyHeaders(string optionHeaders) + public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string optionHeaders) { var options = new OtlpExporterOptions { @@ -80,7 +88,12 @@ public void GetHeaders_NoOptionHeaders_ReturnsEmptyHeaders(string optionHeaders) var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); - Assert.Empty(headers); + Assert.Equal(OtlpExporterOptions.StandardHeaders.Length, headers.Count); + + for (int i = 0; i < OtlpExporterOptions.StandardHeaders.Length; i++) + { + Assert.Contains(headers, entry => entry.Key == OtlpExporterOptions.StandardHeaders[i].Key && entry.Value == OtlpExporterOptions.StandardHeaders[i].Value); + } } [Theory]