opentelemetry-dotnet icon indicating copy to clipboard operation
opentelemetry-dotnet copied to clipboard

Add Additional Standard Tags in AddAspNetCoreInstrumentation

Open RehanSaeed opened this issue 3 years ago • 8 comments

Feature Request

Is your feature request related to a problem?

There are several tags specified by the open telemetry specification that are not logged by default in AddAspNetCoreInstrumentation. It would be nice if we could add an option to log them too.

Describe the solution you'd like:

I currently use the following code to log them:

options.Enrich = (activity, eventName, obj) =>
{
    if (obj is HttpRequest request)
    {
        var context = request.HttpContext;
        activity.AddTag(OpenTelemetryAttributeName.Http.Flavor, GetHttpFlavour(request.Protocol));
        activity.AddTag(OpenTelemetryAttributeName.Http.Scheme, request.Scheme);
        activity.AddTag(OpenTelemetryAttributeName.Http.ClientIP, context.Connection.RemoteIpAddress);
        activity.AddTag(OpenTelemetryAttributeName.Http.RequestContentLength, request.ContentLength);
        activity.AddTag(OpenTelemetryAttributeName.Http.RequestContentType, request.ContentType);

        var user = context.User;
        if (user.Identity?.Name is not null)
        {
            activity.AddTag(OpenTelemetryAttributeName.EndUser.Id, user.Identity.Name);
            activity.AddTag(OpenTelemetryAttributeName.EndUser.Scope, string.Join(',', user.Claims.Select(x => x.Value)));
        }
    }
    else if (obj is HttpResponse response)
    {
        activity.AddTag(OpenTelemetryAttributeName.Http.ResponseContentLength, response.ContentLength);
        activity.AddTag(OpenTelemetryAttributeName.Http.ResponseContentType, response.ContentType);
    }

    static string GetHttpFlavour(string protocol)
    {
        if (HttpProtocol.IsHttp10(protocol))
        {
            return HttpFlavour.Http10;
        }
        else if (HttpProtocol.IsHttp11(protocol))
        {
            return HttpFlavour.Http11;
        }
        else if (HttpProtocol.IsHttp2(protocol))
        {
            return HttpFlavour.Http20;
        }
        else if (HttpProtocol.IsHttp3(protocol))
        {
            return HttpFlavour.Http30;
        }

        throw new InvalidOperationException($"Protocol {protocol} not recognised.");
    }
};

/// <summary>
/// Constants for semantic attribute names outlined by the OpenTelemetry specifications.
/// <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/README.md"/>.
/// </summary>
public static class OpenTelemetryAttributeName
{
    /// <summary>
    /// Constants for deployment semantic attribute names outlined by the OpenTelemetry specifications.
    /// <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/11cc73939a32e3a2e6f11bdeab843c61cf8594e9/specification/resource/semantic_conventions/deployment_environment.md"/>.
    /// </summary>
    public static class Deployment
    {
        /// <summary>
        /// The name of the deployment environment (aka deployment tier).
        /// </summary>
        /// <example>staging; production.</example>
        public const string Environment = "deployment.environment";
    }

    /// <summary>
    /// Constants for end user semantic attribute names outlined by the OpenTelemetry specifications.
    /// <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/span-general.md"/>.
    /// </summary>
    public static class EndUser
    {
        /// <summary>
        /// Username or client_id extracted from the access token or Authorization header in the inbound request from outside the system.
        /// </summary>
        /// <example>E.g. username.</example>
        public const string Id = "enduser.id";

        /// <summary>
        /// Actual/assumed role the client is making the request under extracted from token or application security context.
        /// </summary>
        /// <example>E.g. admin.</example>
        public const string Role = "enduser.role";

        /// <summary>
        /// Scopes or granted authorities the client currently possesses extracted from token or application security context.
        /// The value would come from the scope associated with an OAuth 2.0 Access Token or an attribute value in a SAML 2.0 Assertion.
        /// </summary>
        /// <example>E.g. read:message,write:files.</example>
        public const string Scope = "enduser.scope";
    }

    /// <summary>
    /// Constants for HTTP semantic attribute names outlined by the OpenTelemetry specifications.
    /// <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md"/>.
    /// </summary>
    public static class Http
    {
        /// <summary>
        /// The URI scheme identifying the used protocol.
        /// </summary>
        /// <example>E.g. http or https.</example>
        public const string Scheme = "http.scheme";

        /// <summary>
        /// Kind of HTTP protocol used.
        /// </summary>
        /// <example>E.g. 1.0, 1.1, 2.0, SPDY or QUIC.</example>
        public const string Flavor = "http.flavor";

        /// <summary>
        /// The IP address of the original client behind all proxies, if known (e.g. from X-Forwarded-For).
        /// </summary>
        /// <example>E.g. 83.164.160.102.</example>
        public const string ClientIP = "http.client_ip";

        /// <summary>
        /// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often,
        /// but not always, present as the Content-Length header. For requests using transport encoding, this should be the
        /// compressed size.
        /// </summary>
        /// <example>E.g. 3495.</example>
        public const string RequestContentLength = "http.request_content_length";

        /// <summary>
        /// The content type of the request body.
        /// </summary>
        /// <example>E.g. application/json.</example>
        public const string RequestContentType = "http.request_content_type";

        /// <summary>
        /// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often,
        /// but not always, present as the Content-Length header. For requests using transport encoding, this should be the
        /// compressed size.
        /// </summary>
        /// <example>E.g. 3495.</example>
        public const string ResponseContentLength = "http.response_content_length";

        /// <summary>
        /// The content type of the response body.
        /// </summary>
        /// <example>E.g. application/json.</example>
        public const string ResponseContentType = "http.response_content_type";
    }

    /// <summary>
    /// Constants for host semantic attribute names outlined by the OpenTelemetry specifications.
    /// <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/11cc73939a32e3a2e6f11bdeab843c61cf8594e9/specification/resource/semantic_conventions/host.md"/>.
    /// </summary>
    public static class Host
    {
        /// <summary>
        /// Name of the host. On Unix systems, it may contain what the hostname command returns, or the fully qualified hostname,
        /// or another name specified by the user.
        /// </summary>
        /// <example>E.g. opentelemetry-test.</example>
        public const string Name = "host.name";
    }

    /// <summary>
    /// Constants for service semantic attribute names outlined by the OpenTelemetry specifications.
    /// <see href="https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/messaging.md"/>.
    /// </summary>
    public static class Service
    {
        /// <summary>
        /// The name of the service sending messages.
        /// </summary>
        public const string Name = "service.name";
    }
}

Describe alternatives you've considered.

Currently using the Enrich method to add additional tags but would be nice if these were built in as they are defined by the specification.

RehanSaeed avatar Mar 22 '21 08:03 RehanSaeed

This is something which requires some discussion/design. Current instrumentations populate only the required fields, and any other fields which are readily available (without much perf cost). We need to find a balance between adding all the fields vs adding just required fields, possibly by offering Options to control. One open PR in this direction: https://github.com/open-telemetry/opentelemetry-dotnet/pull/1546

cijothomas avatar Apr 13 '21 22:04 cijothomas

options.Enrich = (activity, eventName, obj) =>
{
    if (obj is HttpRequest request)
    {
        var user = request.HttpContext.User;
        if (user.Identity?.Name is not null)
        {
            activity.AddTag(OpenTelemetryAttributeName.EndUser.Id, user.Identity.Name);
            activity.AddTag(OpenTelemetryAttributeName.EndUser.Scope, string.Join(',', user.Claims.Select(x => x.Value)));
        }
    }
};

Is it even possible to obtain properties of the user this way? I'm just trying to do the same thing and the identity comes always empty, as if at the point where the Enrich callback is called it wasn't populated yet.

dgarciarubio avatar Jun 29 '21 15:06 dgarciarubio

Is it even possible to obtain properties of the user this way? I'm just trying to do the same thing and the identity comes always empty, as if at the point where the Enrich callback is called it wasn't populated yet.

I'm finding the same thing -- just commented over here about it: https://github.com/RehanSaeed/rehansaeed.github.io/issues/607#issuecomment-1129404665

TL;DR The "OnStartActivity" callback is way too early to successfully resolve an authenticated User principal. Authentication middleware hasn't executed yet.

Would love to have some guidance from the experts here about how to enrich telemetry data with "enduser" values. This seems like a common use-case -- something I'd expect to be a solved problem in the AspNetCore Instrumentation library.

The specification recommends propagating "enduser" values via the Baggage mechanism.

To accomplish that today, it seems I'd need to write some custom middleware that runs immediately after Authentication -- to copy the authenticated User ("enduser") data into Baggage -- and then add processors like this sample one to enrich telemetry data from Baggage. Is that the recommendation here?

bmcclory avatar May 17 '22 23:05 bmcclory

This is something which requires some discussion/design. Current instrumentations populate only the required fields, and any other fields which are readily available (without much perf cost). We need to find a balance between adding all the fields vs adding just required fields, possibly by offering Options to control. One open PR in this direction: #1546

Ideally all the "optional" but relevant conventions are available but maybe flagged by default? That way there isn't a bunch of people writing redundant code, but they can still control the perf aspects.

RichiCoder1 avatar Jul 08 '22 16:07 RichiCoder1

Is it even possible to obtain properties of the user this way? I'm just trying to do the same thing and the identity comes always empty, as if at the point where the Enrich callback is called it wasn't populated yet.

I'm finding the same thing -- just commented over here about it: RehanSaeed/rehansaeed.github.io#607 (comment)

TL;DR The "OnStartActivity" callback is way too early to successfully resolve an authenticated User principal. Authentication middleware hasn't executed yet.

Would love to have some guidance from the experts here about how to enrich telemetry data with "enduser" values. This seems like a common use-case -- something I'd expect to be a solved problem in the AspNetCore Instrumentation library.

The specification recommends propagating "enduser" values via the Baggage mechanism.

To accomplish that today, it seems I'd need to write some custom middleware that runs immediately after Authentication -- to copy the authenticated User ("enduser") data into Baggage -- and then add processors like this sample one to enrich telemetry data from Baggage. Is that the recommendation here?

A rather janky solution for this might be an EnrichOpenTelemetryIdentity that runs after UseAuthentication and looks for the IHttpAuthenticationFeature feature in context and hydrates from that. I don't see a way to automatically plug that info however.

RichiCoder1 avatar Jul 08 '22 16:07 RichiCoder1

If you use jwt in the authorization, you can directly read the claims which usually has the information you need, example: //get token and I remove the start word 'bearer ' sJwt = context.Request.Headers["Authorization"][0].Substring(7) //read token, user_name is the claim in my case if (sJwt != "" && jwtSecurityTokenHandler.CanReadToken(sJwt)) { var jwtSecurityToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(sJwt); var userName = jwtSecurityToken.Payload.Claims.FirstOrDefault(c => c.Type == "user_name")?.Value; if (userName != null) activity.AddTag("enduser.user_name", userName); }

heronmeza avatar Oct 16 '23 17:10 heronmeza