< Summary

Information
Class: IceRpc.Telemetry.TelemetryInterceptor
Assembly: IceRpc.Telemetry
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryInterceptor.cs
Tag: 1321_24790053727
Line coverage
90%
Covered lines: 39
Uncovered lines: 4
Coverable lines: 43
Total lines: 124
Line coverage: 90.6%
Branch coverage
60%
Covered branches: 6
Total branches: 10
Branch coverage: 60%
Method coverage
100%
Covered methods: 3
Fully covered methods: 1
Total methods: 3
Method coverage: 100%
Full method coverage: 33.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InvokeAsync()50%4486.66%
WriteActivityContext(...)66.66%6691.3%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryInterceptor.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>An interceptor that starts an <see cref="Activity" /> per request, following
 11/// <see href="https://opentelemetry.io/">OpenTelemetry</see> conventions. The activity context is written in the
 12/// request <see cref="RequestFieldKey.TraceContext" /> field and can be restored on the server-side by installing the
 13/// <see cref="TelemetryMiddleware" />.</summary>
 14/// <remarks>The activities are only created for requests using the icerpc protocol.</remarks>
 15/// <seealso cref="TelemetryPipelineExtensions"/>
 16/// <seealso cref="TelemetryDispatcherBuilderExtensions"/>
 17public class TelemetryInterceptor : IInvoker
 18{
 19    // The W3C Baggage spec guarantees propagation of all entries only when the baggage has at most 64
 20    // list-members and fits in 8192 bytes. We clip at 64 entries — the spec-mandated floor — so a
 21    // strictly spec-conforming peer in any language can always round-trip the baggage we send. Clipping
 22    // here also prevents amplification of entry count across forwarded hops.
 23    internal const int MaxBaggageEntries = 64;
 24
 25    private readonly IInvoker _next;
 26    private readonly ActivitySource _activitySource;
 27
 28    /// <summary>Constructs a telemetry interceptor.</summary>
 29    /// <param name="next">The next invoker in the invocation pipeline.</param>
 30    /// <param name="activitySource">The <see cref="ActivitySource" /> used to start the request activity.</param>
 331    public TelemetryInterceptor(IInvoker next, ActivitySource activitySource)
 332    {
 333        _next = next;
 334        _activitySource = activitySource;
 335    }
 36
 37    /// <inheritdoc/>
 38    public async Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
 339    {
 340        if (request.Protocol.HasFields)
 341        {
 342            string name = $"{request.ServiceAddress.Path}/{request.Operation}";
 343            using Activity activity = _activitySource.CreateActivity(name, ActivityKind.Client) ?? new Activity(name);
 344            activity.SetIdFormat(ActivityIdFormat.W3C);
 345            activity.AddTag("rpc.system", "icerpc");
 346            activity.AddTag("rpc.service", request.ServiceAddress.Path);
 347            activity.AddTag("rpc.method", request.Operation);
 348            activity.Start();
 349            request.Fields = request.Fields.With(RequestFieldKey.TraceContext, activity, WriteActivityContext);
 350            return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 51        }
 52        else
 053        {
 054            return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 55        }
 356    }
 57
 58    internal static void WriteActivityContext(ref SliceEncoder encoder, Activity activity)
 459    {
 460        Debug.Assert(activity.IdFormat == ActivityIdFormat.W3C);
 61
 462        if (activity.Id is null)
 063        {
 064            throw new ArgumentException("The activity ID property cannot be null.", nameof(activity.Id));
 65        }
 66
 67        // The activity context is written to the field value, as if it has the following Slice definition
 68        //
 69        // compact struct BaggageEntry
 70        // {
 71        //    string key;
 72        //    string value;
 73        // }
 74        //
 75        // Sequence<BaggageEntry> Baggage;
 76        //
 77        // compact struct ActivityContext
 78        // {
 79        //    // ActivityID version 1 byte
 80        //    uint8 version;
 81        //    // ActivityTraceId 16 bytes
 82        //    uint64 activityTraceId0;
 83        //    uint64 activityTraceId1;
 84        //    // ActivitySpanId 8 bytes
 85        //    uint64 activitySpanId
 86        //    // ActivityTraceFlags 1 byte
 87        //    uint8 ActivityTraceFlags;
 88        //    string traceStateString;
 89        //    Baggage baggage;
 90        // }
 91        //
 92        // Baggage is modeled as a sequence rather than a dictionary because Activity.Baggage allows
 93        // duplicate keys: encoding as a Slice dictionary could produce bytes that a strict dictionary
 94        // decoder in another language would reject (see #4518).
 95
 96        // 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
 498        encoder.EncodeUInt8(0);
 99
 100        // Unfortunately we can't use stackalloc.
 4101        using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(16);
 4102        Span<byte> buffer = memoryOwner.Memory.Span[0..16];
 4103        activity.TraceId.CopyTo(buffer);
 4104        encoder.WriteByteSpan(buffer);
 4105        activity.SpanId.CopyTo(buffer[0..8]);
 4106        encoder.WriteByteSpan(buffer[0..8]);
 4107        encoder.EncodeUInt8((byte)activity.ActivityTraceFlags);
 108
 109        // TraceState encoded as a string
 4110        encoder.EncodeString(activity.TraceStateString ?? "");
 111
 112        // Baggage encoded as a Sequence<BaggageEntry>, clipped to MaxBaggageEntries. Activity.Baggage has
 113        // no documented iteration order, so which entries we retain when clipping is unspecified; the W3C
 114        // Baggage spec permits dropping list-members in any order.
 4115        KeyValuePair<string, string?>[] baggage = activity.Baggage.Take(MaxBaggageEntries).ToArray();
 4116        encoder.EncodeSequence(
 4117            baggage,
 4118            (ref SliceEncoder encoder, KeyValuePair<string, string?> entry) =>
 66119            {
 66120                encoder.EncodeString(entry.Key);
 66121                encoder.EncodeString(entry.Value ?? "");
 70122            });
 8123    }
 124}