< Summary

Information
Class: IceRpc.Telemetry.TelemetryInterceptor
Assembly: IceRpc.Telemetry
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Telemetry/TelemetryInterceptor.cs
Tag: 275_13775359185
Line coverage
84%
Covered lines: 37
Uncovered lines: 7
Coverable lines: 44
Total lines: 113
Line coverage: 84%
Branch coverage
57%
Covered branches: 8
Total branches: 14
Branch coverage: 57.1%
Method coverage
100%
Covered methods: 3
Total methods: 3
Method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InvokeAsync()50%6.11685.71%
WriteActivityContext(...)62.5%8.51880%

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;
 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    private readonly IInvoker _next;
 20    private readonly ActivitySource _activitySource;
 21
 22    /// <summary>Constructs a telemetry interceptor.</summary>
 23    /// <param name="next">The next invoker in the invocation pipeline.</param>
 24    /// <param name="activitySource">The <see cref="ActivitySource" /> used to start the request activity.</param>
 225    public TelemetryInterceptor(IInvoker next, ActivitySource activitySource)
 226    {
 227        _next = next;
 228        _activitySource = activitySource;
 229    }
 30
 31    /// <inheritdoc/>
 32    public async Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
 233    {
 234        if (request.Protocol.HasFields)
 235        {
 236            string name = $"{request.ServiceAddress.Path}/{request.Operation}";
 237            using Activity activity = _activitySource?.CreateActivity(name, ActivityKind.Client) ?? new Activity(name);
 238            activity.AddTag("rpc.system", "icerpc");
 239            activity.AddTag("rpc.service", request.ServiceAddress.Path);
 240            activity.AddTag("rpc.method", request.Operation);
 241            activity.Start();
 242            request.Fields = request.Fields.With(RequestFieldKey.TraceContext, activity, WriteActivityContext);
 243            return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 44        }
 45        else
 046        {
 047            return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 48        }
 249    }
 50
 51    internal static void WriteActivityContext(ref SliceEncoder encoder, Activity activity)
 252    {
 253        if (activity.IdFormat != ActivityIdFormat.W3C)
 054        {
 055            throw new NotSupportedException(
 056                $"The activity ID format '{activity.IdFormat}' is not supported, the only supported activity ID format i
 57        }
 58
 259        if (activity.Id is null)
 060        {
 061            throw new ArgumentException("The activity ID property cannot be null.", nameof(activity.Id));
 62        }
 63
 64        // The activity context is written to the field value, as if it has the following Slice definition
 65        //
 66        // struct BaggageEntry
 67        // {
 68        //    string key;
 69        //    string value;
 70        // }
 71        // Sequence<BaggageEntry> Baggage;
 72        //
 73        // struct ActivityContext
 74        // {
 75        //    // ActivityID version 1 byte
 76        //    uint8 version;
 77        //    // ActivityTraceId 16 bytes
 78        //    uint64 activityTraceId0;
 79        //    uint64 activityTraceId1;
 80        //    // ActivitySpanId 8 bytes
 81        //    uint64 activitySpanId
 82        //    // ActivityTraceFlags 1 byte
 83        //    uint8 ActivityTraceFlags;
 84        //    string traceStateString;
 85        //    Baggage baggage;
 86        // }
 87
 88        // W3C traceparent binary encoding (1 byte version, 16 bytes trace-ID, 8 bytes span-ID,
 89        // 1 byte flags) https://www.w3.org/TR/trace-context/#traceparent-header-field-values
 290        encoder.EncodeUInt8(0);
 91
 92        // Unfortunately we can't use stackalloc.
 293        using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(16);
 294        Span<byte> buffer = memoryOwner.Memory.Span[0..16];
 295        activity.TraceId.CopyTo(buffer);
 296        encoder.WriteByteSpan(buffer);
 297        activity.SpanId.CopyTo(buffer[0..8]);
 298        encoder.WriteByteSpan(buffer[0..8]);
 299        encoder.EncodeUInt8((byte)activity.ActivityTraceFlags);
 100
 101        // TraceState encoded as an string
 2102        encoder.EncodeString(activity.TraceStateString ?? "");
 103
 104        // Baggage encoded as a Sequence<BaggageEntry>
 2105        encoder.EncodeSequence(
 2106            activity.Baggage,
 2107            (ref SliceEncoder encoder, KeyValuePair<string, string?> entry) =>
 2108            {
 2109                encoder.EncodeString(entry.Key);
 2110                encoder.EncodeString(entry.Value ?? "");
 4111            });
 4112    }
 113}