< Summary

Information
Class: IceRpc.Telemetry.TelemetryMiddleware
Assembly: IceRpc.Telemetry
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryMiddleware.cs
Tag: 1321_24790053727
Line coverage
95%
Covered lines: 42
Uncovered lines: 2
Coverable lines: 44
Total lines: 97
Line coverage: 95.4%
Branch coverage
75%
Covered branches: 6
Total branches: 8
Branch coverage: 75%
Method coverage
100%
Covered methods: 3
Fully covered methods: 2
Total methods: 3
Method coverage: 100%
Full method coverage: 66.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
DispatchAsync()66.66%6688.23%
RestoreActivityContext(...)100%22100%

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 System.Buffers;
 5using System.Diagnostics;
 6using ZeroC.Slice.Codec;
 7
 8namespace IceRpc.Telemetry;
 9
 10/// <summary>A middleware that starts an <see cref="Activity" /> per request, following
 11/// <see href="https://opentelemetry.io/">OpenTelemetry</see> conventions. The middleware restores the parent invocation
 12/// activity from the request <see cref="RequestFieldKey.TraceContext" /> field before starting the dispatch activity.
 13/// </summary>
 14/// <remarks>The activities are only created for requests using the icerpc protocol.</remarks>
 15/// <seealso cref="TelemetryRouterExtensions" />
 16/// <seealso cref="TelemetryDispatcherBuilderExtensions"/>
 17public class TelemetryMiddleware : IDispatcher
 18{
 19    private readonly IDispatcher _next;
 20    private readonly ActivitySource _activitySource;
 21
 22    /// <summary>Constructs a telemetry middleware.</summary>
 23    /// <param name="next">The next dispatcher in the dispatch pipeline.</param>
 24    /// <param name="activitySource">The <see cref="ActivitySource" /> is used to start the request activity.</param>
 825    public TelemetryMiddleware(IDispatcher next, ActivitySource activitySource)
 826    {
 827        _next = next;
 828        _activitySource = activitySource;
 829    }
 30
 31    /// <inheritdoc/>
 32    public async ValueTask<OutgoingResponse> DispatchAsync(IncomingRequest request, CancellationToken cancellationToken)
 833    {
 834        if (request.Protocol.HasFields)
 835        {
 836            string name = $"{request.Path}/{request.Operation}";
 837            using Activity activity = _activitySource.CreateActivity(name, ActivityKind.Server) ?? new Activity(name);
 838            activity.AddTag("rpc.system", "icerpc");
 839            activity.AddTag("rpc.service", request.Path);
 840            activity.AddTag("rpc.method", request.Operation);
 841            if (request.Fields.TryGetValue(RequestFieldKey.TraceContext, out ReadOnlySequence<byte> buffer))
 742            {
 743                RestoreActivityContext(buffer, activity);
 644            }
 745            activity.Start();
 746            return await _next.DispatchAsync(request, cancellationToken).ConfigureAwait(false);
 47        }
 48        else
 049        {
 050            return await _next.DispatchAsync(request, cancellationToken).ConfigureAwait(false);
 51        }
 752    }
 53
 54    internal static void RestoreActivityContext(ReadOnlySequence<byte> buffer, Activity activity)
 1055    {
 1056        var decoder = new SliceDecoder(buffer);
 57
 58        // Read W3C traceparent binary encoding (1 byte version, 16 bytes trace-ID, 8 bytes span-ID,
 59        // 1 byte flags) https://www.w3.org/TR/trace-context/#traceparent-header-field-values
 60
 1061        byte traceIdVersion = decoder.DecodeUInt8();
 62
 963        using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(16);
 964        Span<byte> traceIdSpan = memoryOwner.Memory.Span[0..16];
 965        decoder.CopyTo(traceIdSpan);
 966        var traceId = ActivityTraceId.CreateFromBytes(traceIdSpan);
 67
 968        Span<byte> spanIdSpan = memoryOwner.Memory.Span[0..8];
 969        decoder.CopyTo(spanIdSpan);
 970        var spanId = ActivitySpanId.CreateFromBytes(spanIdSpan);
 71
 972        var traceFlags = (ActivityTraceFlags)decoder.DecodeUInt8();
 73
 974        activity.SetParentId(traceId, spanId, traceFlags);
 75
 76        // Read TraceState encoded as a string
 977        activity.TraceStateString = decoder.DecodeString();
 78
 79        // Decode the baggage sequence, silently clipping to MaxBaggageEntries. OpenTelemetry SDKs
 80        // follow a strict no-throw policy for observability operations: losing a piece of contextual
 81        // metadata is less damaging than failing the RPC, and silent clipping matches the behavior of
 82        // OpenTelemetry .NET's and Python's BaggagePropagator on incoming headers. Only the first
 83        // MaxBaggageEntries entries are read from the buffer; the remainder is left unconsumed.
 84        //
 85        // Activity.Baggage's enumeration order is undocumented, so duplicate-key resolution across the
 86        // wire is inherently undefined on both ends — we don't attempt to preserve it.
 987        int count = decoder.DecodeSize();
 988        int kept = Math.Min(count, TelemetryInterceptor.MaxBaggageEntries);
 89
 59890        for (int i = 0; i < kept; i++)
 29091        {
 29092            string key = decoder.DecodeString();
 29093            string value = decoder.DecodeString();
 29094            activity.AddBaggage(key, value);
 29095        }
 1896    }
 97}