Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OTLP exporter add User-Agent header #4120

Merged
merged 9 commits into from
Feb 1, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>[] StandardHeaders = new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("User-Agent", UserAgentProductVersion != null ? $"{UserAgentProduct}/{UserAgentProductVersion}" : UserAgentProduct),
};

internal readonly Func<HttpClient> 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";
cijothomas marked this conversation as resolved.
Show resolved Hide resolved

private static readonly Version UserAgentProductVersion = GetAssemblyVersion();

private Uri endpoint;

Expand Down Expand Up @@ -195,5 +203,18 @@ internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection servi
configuration,
sp.GetRequiredService<IOptionsMonitor<BatchExportActivityProcessorOptions>>().Get(name)));
}

private static Version GetAssemblyVersion()
{
try
{
var assemblyName = typeof(OtlpExporterOptions).Assembly.GetName();
return assemblyName.Version;
Comment on lines +211 to +212
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it would be better to use either the assembly file version (blue) or even better the assembly informational version (green) instead of the assembly version (red) as it's much more granular:

image

I'd be happy to submit a PR to do that if people agree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4143.

}
catch (Exception)
{
return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ public static THeaders GetHeaders<THeaders>(this OtlpExporterOptions options, Ac
});
}

foreach (var header in OtlpExporterOptions.StandardHeaders)
{
addHeader(headers, header.Key, header.Value);
}

return headers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -179,10 +184,15 @@ void RunTest(Batch<Activity> 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<OtlpHttpTraceExportClient.ExportRequestContent>(httpRequest.Content);
Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>

using Grpc.Core;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using Xunit;
using Xunit.Sdk;
Expand All @@ -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]
Expand Down Expand Up @@ -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
{
Expand All @@ -80,7 +88,12 @@ public void GetHeaders_NoOptionHeaders_ReturnsEmptyHeaders(string optionHeaders)

var headers = options.GetHeaders<Dictionary<string, string>>((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]
Expand Down