[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:
Vishwesh Bankwar 2023-11-08 15:50:42 -08:00 committed by GitHub
parent 91ed41fcd9
commit 4a3c8d36b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 292 additions and 100 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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",