open-telemetry / opentelemetry-dotnet-contrib

This repository contains set of components extending functionality of the OpenTelemetry .NET SDK. Instrumentation libraries, exporters, and other components can find their home here.
https://opentelemetry.io
Apache License 2.0
447 stars 272 forks source link

Add Additional Standard Tags in AddAspNetCoreInstrumentation #1786

Open RehanSaeed opened 3 years ago

RehanSaeed commented 3 years ago

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.

cijothomas commented 3 years ago

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

dgarciarubio commented 3 years ago
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.

bmcclory commented 2 years ago

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?

RichiCoder1 commented 2 years ago

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: open-telemetry/opentelemetry-dotnet#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 commented 2 years ago

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.

heronmeza commented 11 months ago

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

danirzv commented 4 months ago

Any updates?!

cijothomas commented 4 months ago

Any updates?!

Asp.Net Core is considering adding native instrumentation, with capability to enrich/filter etc. There probably isn't much to do in this repo as instrumentation is getting native from asp.net core itself. (Though we may need to do something for older versions of asp.net core)

martinjt commented 3 months ago

Since we have the stable instrumentation for AspNetCore with the required attributes, I'm not sure whether there is anything that we should be doing with this ticket?

@cijothomas would you say this is safe to close in that case?

cijothomas commented 3 months ago

Since we have the stable instrumentation for AspNetCore with the required attributes, I'm not sure whether there is anything that we should be doing with this ticket?

@cijothomas would you say this is safe to close in that case?

Not really..

Instrumentation is probably only doing all the required attributes. What about the optional/opt-in ones? Most likely native instrumentation would offer some capability, but for pre .NET 9 versions, this repo is the only place to offer some ways to get the non-required attributes.