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

[HttpWebRequest] Add error.type for trace and metric #5034

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
environment variable is set to `http` or `http/dup`.

([#5005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5005))
([#5034](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5034))

## 1.6.0-beta.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ private static void AddResponseTags(HttpWebResponse response, Activity activity)
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string GetErrorType(Exception exception)
{
if (exception is WebException wexc)
{
// TODO: consider other Status values from
// https://learn.microsoft.com/dotnet/api/system.net.webexceptionstatus?view=netframework-4.6.2
return wexc.Status switch
{
WebExceptionStatus.NameResolutionFailure => "name_resolution_failure",
WebExceptionStatus.ConnectFailure => "connect_failure",
WebExceptionStatus.SendFailure => "send_failure",
WebExceptionStatus.RequestCanceled => "request_cancelled",
WebExceptionStatus.TrustFailure => "trust_failure",
WebExceptionStatus.SecureChannelFailure => "secure_channel_failure",
WebExceptionStatus.ServerProtocolViolation => "server_protocol_violation",
WebExceptionStatus.Timeout => "timeout",
WebExceptionStatus.MessageLengthLimitExceeded => "message_length_limit_exceeded",
_ => wexc.GetType().FullName,
};
}

return exception.GetType().FullName;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddExceptionTags(Exception exception, Activity activity, out HttpStatusCode? statusCode)
{
Expand Down Expand Up @@ -371,6 +396,7 @@ private static void HookOrProcessResult(HttpWebRequest request)
private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy, HttpWebRequest request, long startTimestamp)
{
HttpStatusCode? httpStatusCode = null;
string errorType = null;

// Activity may be null if we are not tracing in these cases:
// 1. No listeners
Expand All @@ -386,6 +412,7 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC
{
if (result is Exception ex)
{
errorType = GetErrorType(ex);
if (activity != null)
{
AddExceptionTags(ex, activity, out httpStatusCode);
Expand Down Expand Up @@ -430,13 +457,24 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC

httpStatusCode = response.StatusCode;
}

if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)httpStatusCode.Value) == ActivityStatusCode.Error)
{
// override the errorType to statusCode for failures.
errorType = TelemetryHelper.GetStatusCodeString(httpStatusCode.Value);
}
}
}
catch (Exception ex)
{
HttpInstrumentationEventSource.Log.FailedProcessResult(ex);
}

if (tracingEmitNewAttributes && errorType != null)
{
activity?.SetTag(SemanticConventions.AttributeErrorType, errorType);
}

activity?.Stop();

if (HttpClientDuration.Enabled || HttpClientRequestDuration.Enabled)
Expand Down Expand Up @@ -504,6 +542,11 @@ private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncC
tags.Add(SemanticConventions.AttributeHttpResponseStatusCode, (int)httpStatusCode.Value);
}

if (errorType != null)
{
tags.Add(SemanticConventions.AttributeErrorType, errorType);
}

HttpClientRequestDuration.Record(durationS, tags);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ namespace OpenTelemetry.Instrumentation.Http.Implementation;

internal static class TelemetryHelper
{
public static readonly object[] BoxedStatusCodes;
public static readonly (object, string)[] BoxedStatusCodes;

static TelemetryHelper()
{
BoxedStatusCodes = new object[500];
BoxedStatusCodes = new (object, string)[500];
for (int i = 0, c = 100; i < BoxedStatusCodes.Length; i++, c++)
{
BoxedStatusCodes[i] = c;
BoxedStatusCodes[i] = (c, c.ToString());
}
}

Expand All @@ -36,9 +36,20 @@ public static object GetBoxedStatusCode(HttpStatusCode statusCode)
int intStatusCode = (int)statusCode;
if (intStatusCode >= 100 && intStatusCode < 600)
{
return BoxedStatusCodes[intStatusCode - 100];
return BoxedStatusCodes[intStatusCode - 100].Item1;
}

return intStatusCode;
return statusCode;
}

public static string GetStatusCodeString(HttpStatusCode statusCode)
{
int intStatusCode = (int)statusCode;
if (intStatusCode >= 100 && intStatusCode < 600)
{
return BoxedStatusCodes[intStatusCode - 100].Item2;
}

return statusCode.ToString();
}
}
40 changes: 11 additions & 29 deletions test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(

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;

Expand All @@ -350,13 +349,6 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
: 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);

Expand Down Expand Up @@ -389,24 +381,23 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
{
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

#if NET8_0_OR_GREATER
// 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
#elif NETFRAMEWORK
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure");
#else
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
#endif
}
}
Expand Down Expand Up @@ -543,7 +534,6 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
attributes[tag.Key] = tag.Value;
}

#if !NETFRAMEWORK
#if !NET8_0_OR_GREATER
var numberOfTags = 6;
#else
Expand All @@ -558,10 +548,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
}

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]);
Expand All @@ -576,29 +563,24 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
{
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
#if NET8_0_OR_GREATER
// 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");
#elif NETFRAMEWORK
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_failure");

// 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
#else
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
#endif
}

Expand Down