< Summary

Information
Class: IceRpc.Telemetry.TelemetryInterceptor
Assembly: IceRpc.Telemetry
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryInterceptor.cs
Tag: 2300_35243572715
Line coverage
93%
Covered lines: 57
Uncovered lines: 4
Coverable lines: 61
Total lines: 152
Line coverage: 93.4%
Branch coverage
66%
Covered branches: 8
Total branches: 12
Branch coverage: 66.6%
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()66.66%6693.93%
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 IceRpc.Telemetry.Internal;
 5using System.Buffers;
 6using System.Diagnostics;
 7using ZeroC.Slice.Codec;
 8
 9namespace IceRpc.Telemetry;
 10
 11/// <summary>An interceptor that starts an <see cref="Activity" /> per request, following
 12/// <see href="https://opentelemetry.io/">OpenTelemetry</see> conventions. The activity context is written in the
 13/// request <see cref="RequestFieldKey.TraceContext" /> field and can be restored on the server-side by installing the
 14/// <see cref="TelemetryMiddleware" />.</summary>
 15/// <remarks>The activities are only created for requests using the icerpc protocol. The activity records the outcome
 16/// of the invocation. When the invocation returns a response, the <c>rpc.status_code</c> tag holds its status code.
 17/// When the status code is not <see cref="StatusCode.Ok" /> or the invocation throws an exception, the activity
 18/// status is <see cref="ActivityStatusCode.Error" /> and the <c>error.type</c> tag identifies the failure. A
 19/// cancellation by the token passed to <see cref="InvokeAsync" /> is not a failure: the <c>icerpc.canceled</c> tag is
 20/// set to <see langword="true" /> and the activity status stays unset.</remarks>
 21/// <seealso cref="TelemetryPipelineExtensions"/>
 22/// <seealso cref="TelemetryDispatcherBuilderExtensions"/>
 23public class TelemetryInterceptor : IInvoker
 24{
 25    // The W3C Baggage spec guarantees propagation of all entries only when the baggage has at most 64
 26    // list-members and fits in 8192 bytes. We clip at 64 entries — the spec-mandated floor — so a
 27    // strictly spec-conforming peer in any language can always round-trip the baggage we send. Clipping
 28    // here also prevents amplification of entry count across forwarded hops.
 29    internal const int MaxBaggageEntries = 64;
 30
 31    private readonly IInvoker _next;
 32    private readonly ActivitySource _activitySource;
 33
 34    /// <summary>Constructs a telemetry interceptor.</summary>
 35    /// <param name="next">The next invoker in the invocation pipeline.</param>
 36    /// <param name="activitySource">The <see cref="ActivitySource" /> used to start the request activity.</param>
 1037    public TelemetryInterceptor(IInvoker next, ActivitySource activitySource)
 1038    {
 1039        _next = next;
 1040        _activitySource = activitySource;
 1041    }
 42
 43    /// <inheritdoc/>
 44    public async Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
 1045    {
 1046        if (request.Protocol.HasFields)
 1047        {
 1048            string name = $"{request.ServiceAddress.Path}/{request.Operation}";
 1049            using Activity activity = _activitySource.CreateActivity(name, ActivityKind.Client) ?? new Activity(name);
 1050            activity.SetIdFormat(ActivityIdFormat.W3C);
 1051            activity.AddTag("rpc.system", "icerpc");
 1052            activity.AddTag("rpc.service", request.ServiceAddress.Path);
 1053            activity.AddTag("rpc.method", request.Operation);
 1054            activity.Start();
 1055            request.Fields = request.Fields.With(RequestFieldKey.TraceContext, activity, WriteActivityContext);
 56            try
 1057            {
 1058                IncomingResponse response = await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 659                activity.SetTag("rpc.status_code", response.StatusCode.ToString());
 660                if (response.StatusCode != StatusCode.Ok)
 261                {
 262                    activity.SetTag("error.type", response.StatusCode.ToErrorType());
 263                    activity.SetStatus(ActivityStatusCode.Error, response.ErrorMessage);
 264                }
 665                return response;
 66            }
 367            catch (OperationCanceledException exception) when (
 368                cancellationToken.IsCancellationRequested && exception.CancellationToken == cancellationToken)
 169            {
 170                activity.SetTag("icerpc.canceled", true);
 171                throw;
 72            }
 373            catch (Exception exception)
 374            {
 375                activity.SetTag("error.type", exception.GetType().FullName);
 376                activity.SetStatus(ActivityStatusCode.Error, exception.Message);
 377                throw;
 78            }
 79        }
 80        else
 081        {
 082            return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 83        }
 684    }
 85
 86    internal static void WriteActivityContext(ref SliceEncoder encoder, Activity activity)
 487    {
 488        Debug.Assert(activity.IdFormat == ActivityIdFormat.W3C);
 89
 490        if (activity.Id is null)
 091        {
 092            throw new ArgumentException("The activity ID property cannot be null.", nameof(activity.Id));
 93        }
 94
 95        // The activity context is written to the field value, as if it has the following Slice definition
 96        //
 97        // compact struct BaggageEntry
 98        // {
 99        //    string key;
 100        //    string value;
 101        // }
 102        //
 103        // Sequence<BaggageEntry> Baggage;
 104        //
 105        // compact struct ActivityContext
 106        // {
 107        //    // ActivityID version 1 byte
 108        //    uint8 version;
 109        //    // ActivityTraceId 16 bytes
 110        //    uint64 activityTraceId0;
 111        //    uint64 activityTraceId1;
 112        //    // ActivitySpanId 8 bytes
 113        //    uint64 activitySpanId
 114        //    // ActivityTraceFlags 1 byte
 115        //    uint8 ActivityTraceFlags;
 116        //    string traceStateString;
 117        //    Baggage baggage;
 118        // }
 119        //
 120        // Baggage is modeled as a sequence rather than a dictionary because Activity.Baggage allows
 121        // duplicate keys: encoding as a Slice dictionary could produce bytes that a strict dictionary
 122        // decoder in another language would reject (see #4518).
 123
 124        // W3C traceparent binary encoding (1 byte version, 16 bytes trace-ID, 8 bytes span-ID,
 125        // 1 byte flags) https://www.w3.org/TR/trace-context/#traceparent-header-field-values
 4126        encoder.EncodeUInt8(0);
 127
 128        // Unfortunately we can't use stackalloc.
 4129        using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(16);
 4130        Span<byte> buffer = memoryOwner.Memory.Span[0..16];
 4131        activity.TraceId.CopyTo(buffer);
 4132        encoder.WriteByteSpan(buffer);
 4133        activity.SpanId.CopyTo(buffer[0..8]);
 4134        encoder.WriteByteSpan(buffer[0..8]);
 4135        encoder.EncodeUInt8((byte)activity.ActivityTraceFlags);
 136
 137        // TraceState encoded as a string
 4138        encoder.EncodeString(activity.TraceStateString ?? "");
 139
 140        // Baggage encoded as a Sequence<BaggageEntry>, clipped to MaxBaggageEntries. Activity.Baggage has
 141        // no documented iteration order, so which entries we retain when clipping is unspecified; the W3C
 142        // Baggage spec permits dropping list-members in any order.
 4143        KeyValuePair<string, string?>[] baggage = activity.Baggage.Take(MaxBaggageEntries).ToArray();
 4144        encoder.EncodeSequence(
 4145            baggage,
 4146            (ref SliceEncoder encoder, KeyValuePair<string, string?> entry) =>
 66147            {
 66148                encoder.EncodeString(entry.Key);
 66149                encoder.EncodeString(entry.Value ?? "");
 70150            });
 8151    }
 152}