< Summary

Information
Class: IceRpc.Telemetry.TelemetryMiddleware
Assembly: IceRpc.Telemetry
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryMiddleware.cs
Tag: 2300_35243572715
Line coverage
97%
Covered lines: 75
Uncovered lines: 2
Coverable lines: 77
Total lines: 149
Line coverage: 97.4%
Branch coverage
92%
Covered branches: 23
Total branches: 25
Branch coverage: 92%
Method coverage
100%
Covered methods: 4
Fully covered methods: 3
Total methods: 4
Method coverage: 100%
Full method coverage: 75%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
DispatchAsync()80%101095%
RestoreActivityContext(...)100%22100%
IsServerError(...)100%1313100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryMiddleware.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Extensions.DependencyInjection;
 4using IceRpc.Telemetry.Internal;
 5using System.Buffers;
 6using System.Diagnostics;
 7using ZeroC.Slice.Codec;
 8
 9namespace IceRpc.Telemetry;
 10
 11/// <summary>A middleware that starts an <see cref="Activity" /> per request, following
 12/// <see href="https://opentelemetry.io/">OpenTelemetry</see> conventions. The middleware restores the parent invocation
 13/// activity from the request <see cref="RequestFieldKey.TraceContext" /> field before starting the dispatch activity.
 14/// </summary>
 15/// <remarks>The activities are only created for requests using the icerpc protocol. The activity records the outcome
 16/// of the dispatch. The <c>rpc.status_code</c> tag holds the status code of the response returned by the dispatch.
 17/// When the dispatch throws an exception, this tag holds the status code of the failure response the caller receives,
 18/// given by <see cref="DispatchException.FromException" />. A status code that reports a problem with the request
 19/// (<see cref="StatusCode.ApplicationError" />, <see cref="StatusCode.NotFound" />,
 20/// <see cref="StatusCode.InvalidData" />, <see cref="StatusCode.TruncatedPayload" /> and
 21/// <see cref="StatusCode.Unauthorized" />) leaves the activity status unset. Any other failure status code sets the
 22/// activity status to <see cref="ActivityStatusCode.Error" /> and the <c>error.type</c> tag identifies the failure. A
 23/// cancellation by the token passed to <see cref="DispatchAsync" /> is not a failure: the <c>icerpc.canceled</c> tag
 24/// is set to <see langword="true" />, the activity status stays unset, and the <c>rpc.status_code</c> tag is not set
 25/// since the caller receives no response.</remarks>
 26/// <seealso cref="TelemetryRouterExtensions" />
 27/// <seealso cref="TelemetryDispatcherBuilderExtensions"/>
 28public class TelemetryMiddleware : IDispatcher
 29{
 30    private readonly IDispatcher _next;
 31    private readonly ActivitySource _activitySource;
 32
 33    /// <summary>Constructs a telemetry middleware.</summary>
 34    /// <param name="next">The next dispatcher in the dispatch pipeline.</param>
 35    /// <param name="activitySource">The <see cref="ActivitySource" /> is used to start the request activity.</param>
 3136    public TelemetryMiddleware(IDispatcher next, ActivitySource activitySource)
 3137    {
 3138        _next = next;
 3139        _activitySource = activitySource;
 3140    }
 41
 42    /// <inheritdoc/>
 43    public async ValueTask<OutgoingResponse> DispatchAsync(IncomingRequest request, CancellationToken cancellationToken)
 3144    {
 3145        if (request.Protocol.HasFields)
 3146        {
 3147            string name = $"{request.Path}/{request.Operation}";
 3148            using Activity activity = _activitySource.CreateActivity(name, ActivityKind.Server) ?? new Activity(name);
 3149            activity.AddTag("rpc.system", "icerpc");
 3150            activity.AddTag("rpc.service", request.Path);
 3151            activity.AddTag("rpc.method", request.Operation);
 3152            if (request.Fields.TryGetValue(RequestFieldKey.TraceContext, out ReadOnlySequence<byte> buffer))
 753            {
 754                RestoreActivityContext(buffer, activity);
 655            }
 3056            activity.Start();
 57            try
 3058            {
 3059                OutgoingResponse response = await _next.DispatchAsync(request, cancellationToken).ConfigureAwait(false);
 1960                activity.SetTag("rpc.status_code", response.StatusCode.ToString());
 1961                if (IsServerError(response.StatusCode))
 662                {
 663                    activity.SetTag("error.type", response.StatusCode.ToErrorType());
 664                    activity.SetStatus(ActivityStatusCode.Error, response.ErrorMessage);
 665                }
 1966                return response;
 67            }
 368            catch (OperationCanceledException exception) when (
 369                cancellationToken.IsCancellationRequested && exception.CancellationToken == cancellationToken)
 170            {
 171                activity.SetTag("icerpc.canceled", true);
 172                throw;
 73            }
 1074            catch (Exception exception)
 1075            {
 1076                DispatchException dispatchException = DispatchException.FromException(exception);
 1077                activity.SetTag("rpc.status_code", dispatchException.StatusCode.ToString());
 1078                if (IsServerError(dispatchException.StatusCode))
 779                {
 780                    activity.SetTag("error.type", dispatchException.StatusCode.ToErrorType());
 781                    activity.SetStatus(ActivityStatusCode.Error, dispatchException.ErrorMessage);
 782                }
 1083                throw;
 84            }
 85        }
 86        else
 087        {
 088            return await _next.DispatchAsync(request, cancellationToken).ConfigureAwait(false);
 89        }
 1990    }
 91
 92    internal static void RestoreActivityContext(ReadOnlySequence<byte> buffer, Activity activity)
 1093    {
 1094        var decoder = new SliceDecoder(buffer);
 95
 96        // Read W3C traceparent binary encoding (1 byte version, 16 bytes trace-ID, 8 bytes span-ID,
 97        // 1 byte flags) https://www.w3.org/TR/trace-context/#traceparent-header-field-values
 98
 1099        byte traceIdVersion = decoder.DecodeUInt8();
 100
 9101        using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(16);
 9102        Span<byte> traceIdSpan = memoryOwner.Memory.Span[0..16];
 9103        decoder.CopyTo(traceIdSpan);
 9104        var traceId = ActivityTraceId.CreateFromBytes(traceIdSpan);
 105
 9106        Span<byte> spanIdSpan = memoryOwner.Memory.Span[0..8];
 9107        decoder.CopyTo(spanIdSpan);
 9108        var spanId = ActivitySpanId.CreateFromBytes(spanIdSpan);
 109
 9110        var traceFlags = (ActivityTraceFlags)decoder.DecodeUInt8();
 111
 9112        activity.SetParentId(traceId, spanId, traceFlags);
 113
 114        // Read TraceState encoded as a string
 9115        activity.TraceStateString = decoder.DecodeString();
 116
 117        // Decode the baggage sequence, silently clipping to MaxBaggageEntries. OpenTelemetry SDKs
 118        // follow a strict no-throw policy for observability operations: losing a piece of contextual
 119        // metadata is less damaging than failing the RPC, and silent clipping matches the behavior of
 120        // OpenTelemetry .NET's and Python's BaggagePropagator on incoming headers. Only the first
 121        // MaxBaggageEntries entries are read from the buffer; the remainder is left unconsumed.
 122        //
 123        // Activity.Baggage's enumeration order is undocumented, so duplicate-key resolution across the
 124        // wire is inherently undefined on both ends — we don't attempt to preserve it.
 9125        int count = decoder.DecodeSize();
 9126        int kept = Math.Min(count, TelemetryInterceptor.MaxBaggageEntries);
 127
 598128        for (int i = 0; i < kept; i++)
 290129        {
 290130            string key = decoder.DecodeString();
 290131            string value = decoder.DecodeString();
 290132            activity.AddBaggage(key, value);
 290133        }
 18134    }
 135
 136    /// <summary>Checks whether a status code reports a failure of the server or the target service, as opposed to a
 137    /// problem with the request.</summary>
 138    private static bool IsServerError(StatusCode statusCode) =>
 29139        statusCode switch
 29140        {
 29141            StatusCode.Ok or
 29142            StatusCode.ApplicationError or
 29143            StatusCode.NotFound or
 29144            StatusCode.InvalidData or
 29145            StatusCode.TruncatedPayload or
 16146            StatusCode.Unauthorized => false,
 13147            _ => true
 29148        };
 149}