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

Add standard tags to HttpClient native trace instrumentation #104251

Merged
merged 17 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<Compile Include="System\Net\Http\ClientCertificateOption.cs" />
<Compile Include="System\Net\Http\DelegatingHandler.cs" />
<Compile Include="System\Net\Http\DiagnosticsHandler.cs" />
<Compile Include="System\Net\Http\DiagnosticsHelper.cs" />
<Compile Include="System\Net\Http\DiagnosticsHandlerLoggingStrings.cs" />
<Compile Include="System\Net\Http\EmptyContent.cs" />
<Compile Include="System\Net\Http\EmptyReadStream.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage re
{
activity.Start();

// Set tags known at activity start
if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri)
{
activity.SetTag("server.address", requestUri.Host);
activity.SetTag("server.port", requestUri.Port);
activity.SetTag("uri.full", DiagnosticsHelper.GetRedactedUriString(requestUri));
antonfirsov marked this conversation as resolved.
Show resolved Hide resolved
}

KeyValuePair<string, object?> methodTag = DiagnosticsHelper.GetMethodTag(request.Method);
activity.SetTag(methodTag.Key, methodTag.Value);

// Only send start event to users who subscribed for it.
if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ActivityStartName))
{
Expand All @@ -141,6 +152,7 @@ private async ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage re
}

HttpResponseMessage? response = null;
Exception? exception = null;
TaskStatus taskStatus = TaskStatus.RanToCompletion;
try
{
Expand All @@ -159,6 +171,7 @@ await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false)
catch (Exception ex)
{
taskStatus = TaskStatus.Faulted;
exception = ex;

if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ExceptionEventName))
{
Expand All @@ -176,6 +189,18 @@ await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false)
{
activity.SetEndTime(DateTime.UtcNow);

// Set tags known at activity Stop.
if (response is not null)
{
activity.SetTag("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
activity.SetTag("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
}

if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType))
{
activity.SetTag("error.type", errorType);
}

// Only send stop event to users who subscribed for it.
if (diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ActivityStopName))
{
Expand Down
134 changes: 134 additions & 0 deletions src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace System.Net.Http
{
internal static class DiagnosticsHelper
{
internal static string GetRedactedUriString(Uri uri)
{
Debug.Assert(uri.IsAbsoluteUri);
string uriString = uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped);

if (GlobalHttpSettings.DiagnosticsHandler.DisableUriQueryRedaction)
{
return uriString;
}

int fragmentOffset = uriString.IndexOf('#');
int queryOffset = uriString.IndexOf('?');
if (fragmentOffset >= 0 && queryOffset > fragmentOffset)
{
// Not a query delimiter, but a '?' in the fragment.
queryOffset = -1;
}

if (queryOffset < 0 || queryOffset == uriString.Length - 1 || queryOffset == fragmentOffset - 1)
{
// No query or empty query.
return uriString;
}

if (fragmentOffset < 0)
{
return $"{uriString.AsSpan(0, queryOffset + 1)}*";
}
else
{
ReadOnlySpan<char> uriSpan = uriString.AsSpan();
return $"{uriSpan[..(queryOffset + 1)]}*{uriSpan[fragmentOffset..]}";
}
}

internal static KeyValuePair<string, object?> GetMethodTag(HttpMethod method)
{
// Return canonical names for known methods and "_OTHER" for unknown ones.
antonfirsov marked this conversation as resolved.
Show resolved Hide resolved
HttpMethod? known = HttpMethod.GetKnownMethod(method.Method);
return new KeyValuePair<string, object?>("http.request.method", known?.Method ?? "_OTHER");
}

internal static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch
{
(1, 0) => "1.0",
(1, 1) => "1.1",
(2, 0) => "2",
(3, 0) => "3",
_ => httpVersion.ToString()
};

public static bool TryGetErrorType(HttpResponseMessage? response, Exception? exception, out string? errorType)
{
if (response is not null)
{
int statusCode = (int)response.StatusCode;

// In case the status code indicates a client or a server error, return the string representation of the status code.
// See the paragraph Status and the definition of 'error.type' in
// https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md
if (statusCode >= 400 && statusCode <= 599)
{
errorType = GetErrorStatusCodeString(statusCode);
return true;
}
}

if (exception is null)
{
errorType = null;
return false;
}

Debug.Assert(Enum.GetValues<HttpRequestError>().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError.");
errorType = (exception as 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 or when exception is not an HttpRequestException.
_ => exception.GetType().FullName!
};
return true;
}

private static object[]? s_boxedStatusCodes;
private static string[]? s_statusCodeStrings;

#pragma warning disable CA1859 // we explictly box here
public static object GetBoxedStatusCode(int statusCode)
{
object[] boxes = LazyInitializer.EnsureInitialized(ref s_boxedStatusCodes, static () => new object[512]);
antonfirsov marked this conversation as resolved.
Show resolved Hide resolved

return (uint)statusCode < (uint)boxes.Length
? boxes[statusCode] ??= statusCode
: statusCode;
}
#pragma warning restore

private static string GetErrorStatusCodeString(int statusCode)
{
Debug.Assert(statusCode >= 400 && statusCode <= 599);

string[] strings = LazyInitializer.EnsureInitialized(ref s_statusCodeStrings, static () => new string[200]);
int index = statusCode - 400;
return (uint)index < (uint)strings.Length
? strings[index] ??= statusCode.ToString()
: statusCode.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ internal static class DiagnosticsHandler
"System.Net.Http.EnableActivityPropagation",
"DOTNET_SYSTEM_NET_HTTP_ENABLEACTIVITYPROPAGATION",
true);

public static bool DisableUriQueryRedaction { get; } = RuntimeSettingParser.QueryRuntimeSettingSwitch(
"System.Net.Http.DisableUriQueryRedaction",
"DOTNET_SYSTEM_NET_HTTP_DISABLEURIQUERYREDACTION",
false);
}

internal static class SocketsHttpHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon

if (response is not null)
{
tags.Add("http.response.status_code", GetBoxedStatusCode((int)response.StatusCode));
tags.Add("network.protocol.version", GetProtocolVersionString(response.Version));
tags.Add("http.response.status_code", DiagnosticsHelper.GetBoxedStatusCode((int)response.StatusCode));
tags.Add("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
}

if (TryGetErrorType(response, exception, out string? errorType))
if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType))
{
tags.Add("error.type", errorType);
}
Expand All @@ -131,58 +131,6 @@ private void RequestStop(HttpRequestMessage request, HttpResponseMessage? respon
}
}

private static bool TryGetErrorType(HttpResponseMessage? response, Exception? exception, out string? errorType)
{
if (response is not null)
{
int statusCode = (int)response.StatusCode;

// In case the status code indicates a client or a server error, return the string representation of the status code.
// See the paragraph Status and the definition of 'error.type' in
// https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md
if (statusCode >= 400 && statusCode <= 599)
{
errorType = GetErrorStatusCodeString(statusCode);
return true;
}
}

if (exception is null)
{
errorType = null;
return false;
}

Debug.Assert(Enum.GetValues<HttpRequestError>().Length == 12, "We need to extend the mapping in case new values are added to HttpRequestError.");
errorType = (exception as 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 or when exception is not an HttpRequestException.
_ => exception.GetType().FullName!
};
return true;
}

private static string GetProtocolVersionString(Version httpVersion) => (httpVersion.Major, httpVersion.Minor) switch
{
(1, 0) => "1.0",
(1, 1) => "1.1",
(2, 0) => "2",
(3, 0) => "3",
_ => httpVersion.ToString()
};

private static TagList InitializeCommonTags(HttpRequestMessage request)
{
TagList tags = default;
Expand All @@ -197,43 +145,11 @@ private static TagList InitializeCommonTags(HttpRequestMessage request)
tags.Add("server.port", requestUri.Port);
}
}
tags.Add(GetMethodTag(request.Method));
tags.Add(DiagnosticsHelper.GetMethodTag(request.Method));

return tags;
}

internal static KeyValuePair<string, object?> GetMethodTag(HttpMethod method)
{
// Return canonical names for known methods and "_OTHER" for unknown ones.
HttpMethod? known = HttpMethod.GetKnownMethod(method.Method);
return new KeyValuePair<string, object?>("http.request.method", known?.Method ?? "_OTHER");
}

private static object[]? s_boxedStatusCodes;
private static string[]? s_statusCodeStrings;

#pragma warning disable CA1859 // we explictly box here
private static object GetBoxedStatusCode(int statusCode)
{
object[] boxes = LazyInitializer.EnsureInitialized(ref s_boxedStatusCodes, static () => new object[512]);

return (uint)statusCode < (uint)boxes.Length
? boxes[statusCode] ??= statusCode
: statusCode;
}
#pragma warning restore

private static string GetErrorStatusCodeString(int statusCode)
{
Debug.Assert(statusCode >= 400 && statusCode <= 599);

string[] strings = LazyInitializer.EnsureInitialized(ref s_statusCodeStrings, static () => new string[200]);
int index = statusCode - 400;
return (uint)index < (uint)strings.Length
? strings[index] ??= statusCode.ToString()
: statusCode.ToString();
}

private sealed class SharedMeter : Meter
{
public static Meter Instance { get; } = new SharedMeter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void RequestLeftQueue(HttpRequestMessage request, HttpConnectionPool pool
tags.Add("server.port", pool.OriginAuthority.Port);
}

tags.Add(MetricsHandler.GetMethodTag(request.Method));
tags.Add(DiagnosticsHelper.GetMethodTag(request.Method));

RequestsQueueDuration.Record(duration.TotalSeconds, tags);
}
Expand Down
Loading
Loading