[HttpClient] Add `error.type` for traces and metrics (#5005)
Co-authored-by: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> Co-authored-by: Timothy Mothra <tilee@microsoft.com> Co-authored-by: Mikel Blanchard <mblanchard@macrosssoftware.com>
This commit is contained in:
parent
91ed41fcd9
commit
4a3c8d36b3
|
|
@ -18,6 +18,22 @@
|
|||
`http` or `http/dup`.
|
||||
([#5003](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5003))
|
||||
|
||||
* An additional attribute `error.type` will be added to activity and
|
||||
`http.client.request.duration` metric in case of failed requests as per the
|
||||
[specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes).
|
||||
|
||||
Users moving to `net8.0` or newer frameworks from lower versions will see
|
||||
difference in values in case of an exception. `net8.0` or newer frameworks add
|
||||
the ability to further drill down the exceptions to a specific type through
|
||||
[HttpRequestError](https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0)
|
||||
enum. For lower versions, the individual types will be rolled in to a single
|
||||
type. This could be a **breaking change** if alerts are set based on the values.
|
||||
|
||||
The attribute will only be added when `OTEL_SEMCONV_STABILITY_OPT_IN`
|
||||
environment variable is set to `http` or `http/dup`.
|
||||
|
||||
([#5005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5005))
|
||||
|
||||
## 1.6.0-beta.2
|
||||
|
||||
Released 2023-Oct-26
|
||||
|
|
|
|||
|
|
@ -271,6 +271,11 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler
|
|||
|
||||
if (TryFetchResponse(payload, out HttpResponseMessage response))
|
||||
{
|
||||
if (currentStatusCode == ActivityStatusCode.Unset)
|
||||
{
|
||||
activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode));
|
||||
}
|
||||
|
||||
if (this.emitOldAttributes)
|
||||
{
|
||||
activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
|
||||
|
|
@ -279,11 +284,10 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler
|
|||
if (this.emitNewAttributes)
|
||||
{
|
||||
activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
|
||||
}
|
||||
|
||||
if (currentStatusCode == ActivityStatusCode.Unset)
|
||||
{
|
||||
activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode));
|
||||
if (activity.Status == ActivityStatusCode.Error)
|
||||
{
|
||||
activity.SetTag(SemanticConventions.AttributeErrorType, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
|
|
@ -337,6 +341,11 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.emitNewAttributes)
|
||||
{
|
||||
activity.SetTag(SemanticConventions.AttributeErrorType, GetErrorType(exc));
|
||||
}
|
||||
|
||||
if (this.options.RecordException)
|
||||
{
|
||||
activity.RecordException(exc);
|
||||
|
|
@ -372,4 +381,33 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetErrorType(Exception exc)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
// For net8.0 and above exception type can be found using HttpRequestError.
|
||||
// https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0
|
||||
if (exc is HttpRequestException httpRequestException)
|
||||
{
|
||||
return httpRequestException.HttpRequestError switch
|
||||
{
|
||||
HttpRequestError.NameResolutionError => "name_resolution_error",
|
||||
HttpRequestError.ConnectionError => "connection_error",
|
||||
HttpRequestError.SecureConnectionError => "secure_connection_error",
|
||||
HttpRequestError.HttpProtocolError => "http_protocol_error",
|
||||
HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported",
|
||||
HttpRequestError.VersionNegotiationError => "version_negotiation_error",
|
||||
HttpRequestError.UserAuthenticationError => "user_authentication_error",
|
||||
HttpRequestError.ProxyTunnelError => "proxy_tunnel_error",
|
||||
HttpRequestError.InvalidResponse => "invalid_response",
|
||||
HttpRequestError.ResponseEnded => "response_ended",
|
||||
HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded",
|
||||
|
||||
// Fall back to the exception type name in case of HttpRequestError.Unknown
|
||||
_ => exc.GetType().FullName,
|
||||
};
|
||||
}
|
||||
#endif
|
||||
return exc.GetType().FullName;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,18 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler
|
|||
internal static readonly string MeterName = AssemblyName.Name;
|
||||
internal static readonly string MeterVersion = AssemblyName.Version.ToString();
|
||||
internal static readonly Meter Meter = new(MeterName, MeterVersion);
|
||||
private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception";
|
||||
private static readonly Histogram<double> HttpClientDuration = Meter.CreateHistogram<double>("http.client.duration", "ms", "Measures the duration of outbound HTTP requests.");
|
||||
private static readonly Histogram<double> HttpClientRequestDuration = Meter.CreateHistogram<double>("http.client.request.duration", "s", "Duration of HTTP client requests.");
|
||||
|
||||
private static readonly PropertyFetcher<HttpRequestMessage> StopRequestFetcher = new("Request");
|
||||
private static readonly PropertyFetcher<HttpResponseMessage> StopResponseFetcher = new("Response");
|
||||
private static readonly PropertyFetcher<Exception> StopExceptionFetcher = new("Exception");
|
||||
private static readonly PropertyFetcher<HttpRequestMessage> RequestFetcher = new("Request");
|
||||
#if NET6_0_OR_GREATER
|
||||
private static readonly HttpRequestOptionsKey<string> HttpRequestOptionsErrorKey = new HttpRequestOptionsKey<string>(SemanticConventions.AttributeErrorType);
|
||||
#endif
|
||||
|
||||
private readonly HttpClientMetricInstrumentationOptions options;
|
||||
private readonly bool emitOldAttributes;
|
||||
private readonly bool emitNewAttributes;
|
||||
|
|
@ -57,84 +64,118 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler
|
|||
|
||||
public override void OnEventWritten(string name, object payload)
|
||||
{
|
||||
if (name == OnStopEvent)
|
||||
if (name == OnUnhandledExceptionEvent)
|
||||
{
|
||||
if (Sdk.SuppressInstrumentation)
|
||||
if (this.emitNewAttributes)
|
||||
{
|
||||
return;
|
||||
this.OnExceptionEventWritten(Activity.Current, payload);
|
||||
}
|
||||
}
|
||||
else if (name == OnStopEvent)
|
||||
{
|
||||
this.OnStopEventWritten(Activity.Current, payload);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStopEventWritten(Activity activity, object payload)
|
||||
{
|
||||
if (Sdk.SuppressInstrumentation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryFetchRequest(payload, out HttpRequestMessage request))
|
||||
{
|
||||
// see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md
|
||||
if (this.emitOldAttributes)
|
||||
{
|
||||
TagList tags = default;
|
||||
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host));
|
||||
|
||||
if (!request.RequestUri.IsDefaultPort)
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port));
|
||||
}
|
||||
|
||||
if (TryFetchResponse(payload, out HttpResponseMessage response))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
|
||||
}
|
||||
|
||||
// We are relying here on HttpClient library to set duration before writing the stop event.
|
||||
// https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
|
||||
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
|
||||
HttpClientDuration.Record(activity.Duration.TotalMilliseconds, tags);
|
||||
}
|
||||
|
||||
var activity = Activity.Current;
|
||||
if (TryFetchRequest(payload, out HttpRequestMessage request))
|
||||
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
|
||||
if (this.emitNewAttributes)
|
||||
{
|
||||
// see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md
|
||||
if (this.emitOldAttributes)
|
||||
TagList tags = default;
|
||||
|
||||
if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod))
|
||||
{
|
||||
TagList tags = default;
|
||||
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host));
|
||||
|
||||
if (!request.RequestUri.IsDefaultPort)
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port));
|
||||
}
|
||||
|
||||
if (TryFetchResponse(payload, out HttpResponseMessage response))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
|
||||
}
|
||||
|
||||
// We are relying here on HttpClient library to set duration before writing the stop event.
|
||||
// https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
|
||||
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
|
||||
HttpClientDuration.Record(activity.Duration.TotalMilliseconds, tags);
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, httpMethod));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set to default "_OTHER" as per spec.
|
||||
// https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"));
|
||||
}
|
||||
|
||||
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
|
||||
if (this.emitNewAttributes)
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerAddress, request.RequestUri.Host));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
|
||||
|
||||
if (!request.RequestUri.IsDefaultPort)
|
||||
{
|
||||
TagList tags = default;
|
||||
|
||||
if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, httpMethod));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set to default "_OTHER" as per spec.
|
||||
// https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"));
|
||||
}
|
||||
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerAddress, request.RequestUri.Host));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme));
|
||||
|
||||
if (!request.RequestUri.IsDefaultPort)
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerPort, request.RequestUri.Port));
|
||||
}
|
||||
|
||||
if (TryFetchResponse(payload, out HttpResponseMessage response))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
|
||||
}
|
||||
|
||||
// We are relying here on HttpClient library to set duration before writing the stop event.
|
||||
// https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
|
||||
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
|
||||
HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags);
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeServerPort, request.RequestUri.Port));
|
||||
}
|
||||
|
||||
if (TryFetchResponse(payload, out HttpResponseMessage response))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
|
||||
|
||||
// Set error.type to status code for failed requests
|
||||
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
|
||||
if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)response.StatusCode) == ActivityStatusCode.Error)
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeErrorType, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
#if !NET6_0_OR_GREATER
|
||||
request.Properties.TryGetValue(SemanticConventions.AttributeErrorType, out var errorType);
|
||||
#else
|
||||
request.Options.TryGetValue(HttpRequestOptionsErrorKey, out var errorType);
|
||||
#endif
|
||||
|
||||
// Set error.type to exception type if response was not received.
|
||||
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
|
||||
if (errorType != null)
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeErrorType, errorType));
|
||||
}
|
||||
}
|
||||
|
||||
// We are relying here on HttpClient library to set duration before writing the stop event.
|
||||
// https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
|
||||
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
|
||||
HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags);
|
||||
}
|
||||
}
|
||||
|
||||
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
|
||||
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
|
||||
#if NET6_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
|
||||
#endif
|
||||
static bool TryFetchRequest(object payload, out HttpRequestMessage request) =>
|
||||
StopRequestFetcher.TryFetch(payload, out request) && request != null;
|
||||
|
|
@ -142,9 +183,54 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler
|
|||
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
|
||||
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
|
||||
#if NET6_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
|
||||
#endif
|
||||
static bool TryFetchResponse(object payload, out HttpResponseMessage response) =>
|
||||
StopResponseFetcher.TryFetch(payload, out response) && response != null;
|
||||
}
|
||||
|
||||
public void OnExceptionEventWritten(Activity activity, object payload)
|
||||
{
|
||||
if (!TryFetchException(payload, out Exception exc) || !TryFetchRequest(payload, out HttpRequestMessage request))
|
||||
{
|
||||
HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerMetricsDiagnosticListener), nameof(this.OnExceptionEventWritten));
|
||||
return;
|
||||
}
|
||||
|
||||
#if !NET6_0_OR_GREATER
|
||||
request.Properties.Add(SemanticConventions.AttributeErrorType, exc.GetType().FullName);
|
||||
#else
|
||||
request.Options.Set(HttpRequestOptionsErrorKey, exc.GetType().FullName);
|
||||
#endif
|
||||
|
||||
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
|
||||
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
|
||||
#if NET6_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
|
||||
#endif
|
||||
static bool TryFetchException(object payload, out Exception exc)
|
||||
{
|
||||
if (!StopExceptionFetcher.TryFetch(payload, out exc) || exc == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
|
||||
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
|
||||
#if NET6_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
|
||||
#endif
|
||||
static bool TryFetchRequest(object payload, out HttpRequestMessage request)
|
||||
{
|
||||
if (!RequestFetcher.TryFetch(payload, out request) || request == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,19 +49,6 @@ public partial class HttpClientTests
|
|||
semanticConvention: HttpSemanticConvention.Old).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestData))]
|
||||
public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsNewSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
|
||||
{
|
||||
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
|
||||
this.host,
|
||||
this.port,
|
||||
tc,
|
||||
enableTracing: true,
|
||||
enableMetrics: true,
|
||||
semanticConvention: HttpSemanticConvention.New).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestData))]
|
||||
public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsDuplicateSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
|
||||
|
|
@ -76,6 +63,19 @@ public partial class HttpClientTests
|
|||
}
|
||||
#endif
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestData))]
|
||||
public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsNewSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
|
||||
{
|
||||
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
|
||||
this.host,
|
||||
this.port,
|
||||
tc,
|
||||
enableTracing: true,
|
||||
enableMetrics: true,
|
||||
semanticConvention: HttpSemanticConvention.New).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestData))]
|
||||
public async Task HttpOutCallsAreCollectedSuccessfullyMetricsOnlyAsync(HttpTestData.HttpOutTestCase tc)
|
||||
|
|
@ -346,11 +346,22 @@ public partial class HttpClientTests
|
|||
|
||||
var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString());
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
int numberOfNewTags = activity.Status == ActivityStatusCode.Error ? 6 : 5;
|
||||
int numberOfDupeTags = activity.Status == ActivityStatusCode.Error ? 12 : 11;
|
||||
|
||||
var expectedAttributeCount = semanticConvention == HttpSemanticConvention.Dupe
|
||||
? numberOfDupeTags + (tc.ResponseExpected ? 2 : 0)
|
||||
: semanticConvention == HttpSemanticConvention.New
|
||||
? numberOfNewTags + (tc.ResponseExpected ? 1 : 0)
|
||||
: 6 + (tc.ResponseExpected ? 1 : 0);
|
||||
#else
|
||||
var expectedAttributeCount = semanticConvention == HttpSemanticConvention.Dupe
|
||||
? 11 + (tc.ResponseExpected ? 2 : 0)
|
||||
: semanticConvention == HttpSemanticConvention.New
|
||||
? 5 + (tc.ResponseExpected ? 1 : 0)
|
||||
: 6 + (tc.ResponseExpected ? 1 : 0);
|
||||
#endif
|
||||
|
||||
Assert.Equal(expectedAttributeCount, normalizedAttributes.Count);
|
||||
|
||||
|
|
@ -382,10 +393,26 @@ public partial class HttpClientTests
|
|||
if (tc.ResponseExpected)
|
||||
{
|
||||
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
if (tc.ResponseCode >= 400)
|
||||
{
|
||||
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode);
|
||||
#if !NETFRAMEWORK
|
||||
#if !NET8_0_OR_GREATER
|
||||
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
|
||||
#else
|
||||
// we are using fake address so it will be "name_resolution_error"
|
||||
// TODO: test other error types.
|
||||
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error");
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +532,9 @@ public partial class HttpClientTests
|
|||
if (enableTracing)
|
||||
{
|
||||
var activity = Assert.Single(activities);
|
||||
#if !NET8_0_OR_GREATER
|
||||
Assert.Equal(activity.Duration.TotalSeconds, sum);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -519,22 +548,63 @@ public partial class HttpClientTests
|
|||
attributes[tag.Key] = tag.Value;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
#if !NET8_0_OR_GREATER
|
||||
var numberOfTags = 6;
|
||||
#else
|
||||
// network.protocol.version is not emitted when response if not received.
|
||||
// https://github.com/open-telemetry/opentelemetry-dotnet/issues/4928
|
||||
var numberOfTags = 5;
|
||||
#endif
|
||||
if (tc.ResponseExpected)
|
||||
{
|
||||
var expectedStatusCode = int.Parse(normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
|
||||
numberOfTags = (expectedStatusCode >= 400) ? 6 : 5;
|
||||
}
|
||||
|
||||
var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 1 : 0);
|
||||
#else
|
||||
var expectedAttributeCount = 5 + (tc.ResponseExpected ? 1 : 0);
|
||||
|
||||
#endif
|
||||
Assert.Equal(expectedAttributeCount, attributes.Count);
|
||||
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpMethod]);
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerName]);
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerPort]);
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]);
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeUrlScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpScheme]);
|
||||
#if !NET8_0_OR_GREATER
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]);
|
||||
#endif
|
||||
|
||||
if (tc.ResponseExpected)
|
||||
{
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
if (tc.ResponseCode >= 400)
|
||||
{
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode);
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
#if !NET8_0_OR_GREATER
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
|
||||
#else
|
||||
// we are using fake address so it will be "name_resolution_error"
|
||||
// TODO: test other error types.
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error");
|
||||
|
||||
// network.protocol.version is not emitted when response if not received.
|
||||
// https://github.com/open-telemetry/opentelemetry-dotnet/issues/4928
|
||||
Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// Inspect Histogram Bounds
|
||||
|
|
|
|||
|
|
@ -321,24 +321,6 @@
|
|||
"http.url": "http://{host}:{port}/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Response code: 600",
|
||||
"method": "GET",
|
||||
"url": "http://{host}:{port}/",
|
||||
"responseCode": 600,
|
||||
"spanName": "HTTP GET",
|
||||
"spanStatus": "Error",
|
||||
"responseExpected": true,
|
||||
"spanAttributes": {
|
||||
"http.scheme": "http",
|
||||
"http.method": "GET",
|
||||
"net.peer.name": "{host}",
|
||||
"net.peer.port": "{port}",
|
||||
"http.flavor": "{flavor}",
|
||||
"http.status_code": "600",
|
||||
"http.url": "http://{host}:{port}/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Http version attribute populated",
|
||||
"method": "GET",
|
||||
|
|
|
|||
Loading…
Reference in New Issue