< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.AsyncResponseDiagnostics
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseDiagnostics.cs
Line coverage
98%
Covered lines: 73
Uncovered lines: 1
Coverable lines: 74
Total lines: 203
Line coverage: 98.6%
Branch coverage
92%
Covered branches: 48
Total branches: 52
Branch coverage: 92.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
StartActivity(...)100%44100%
SetCorrelationId(...)100%44100%
SetPayloadType(...)83.33%66100%
SetReplyTarget(...)100%66100%
SetWorker(...)100%66100%
SetLostSubscriberRoute(...)100%44100%
SetError(...)83.33%66100%
SetError(...)100%44100%
RecordLostSubscriber(...)75%44100%
RecordWaiterTimeout(...)100%22100%
RecordWorkerOutcome(...)100%22100%
RecordTypeResolutionFailure(...)100%22100%
RecordUnroutableResponse()50%2266.67%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseDiagnostics.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Diagnostics.Metrics;
 3using System.Runtime.CompilerServices;
 4
 5namespace AsyncResponse;
 6
 7/// <summary>
 8/// Provides diagnostic identifiers and sources for the AsyncResponse library.
 9/// </summary>
 10public static class AsyncResponseDiagnostics
 11{
 12    /// <summary>
 13    /// The name of the OpenTelemetry <see cref="ActivitySource"/> used by AsyncResponse.
 14    /// Consumers can subscribe to this name to capture distributed traces.
 15    /// </summary>
 16    public const string ActivitySourceName = "AsyncResponse";
 17
 18    /// <summary>
 19    /// The shared OpenTelemetry <see cref="ActivitySource"/> instance for the AsyncResponse library.
 20    /// </summary>
 321    internal static readonly ActivitySource ActivitySource = new(ActivitySourceName);
 22
 23    /// <summary>
 24    /// The name of the <see cref="System.Diagnostics.Metrics.Meter"/> used by AsyncResponse.
 25    /// Consumers subscribe to this name (e.g. via OpenTelemetry) to collect the library's metrics.
 26    /// </summary>
 27    public const string MeterName = "AsyncResponse";
 28
 29    /// <summary>The shared <see cref="System.Diagnostics.Metrics.Meter"/> for the AsyncResponse library.</summary>
 330    internal static readonly Meter Meter = new(MeterName);
 31
 32    // The core SLO instrument: every time a response or exception is published with no live waiter,
 33    // the recovery path is entered. Tags break it down by kind (response/exception), route
 34    // (resume/failure/unclassified), and whether a recovery callback actually ran — so an operator
 35    // can graph "how often does recovery fire" and the resume-vs-fail split that this library exists
 36    // to provide.
 337    private static readonly Counter<long> LostSubscriberDispatches =
 338        Meter.CreateCounter<long>("asyncresponse.lost_subscriber.dispatches", unit: "{dispatch}",
 339            description: "Responses/exceptions published with no live subscriber (the recovery path was entered).");
 40
 341    private static readonly Counter<long> WaiterTimeoutsCounter =
 342        Meter.CreateCounter<long>("asyncresponse.waiter.timeouts", unit: "{timeout}",
 343            description: "Waiters that faulted because no response arrived before the timeout.");
 44
 345    private static readonly Counter<long> WorkerJobsCounter =
 346        Meter.CreateCounter<long>("asyncresponse.worker.jobs", unit: "{job}",
 347            description: "Worker jobs processed, tagged by outcome (executed/failed/rejected).");
 48
 349    private static readonly Counter<long> TypeResolutionFailures =
 350        Meter.CreateCounter<long>("asyncresponse.type_resolution.unresolved", unit: "{failure}",
 351            description: "Persisted service/payload type names that could not be resolved; the callback may silently fai
 52
 353    private static readonly Counter<long> UnroutableResponsesCounter =
 354        Meter.CreateCounter<long>("asyncresponse.ingress.unroutable_responses", unit: "{message}",
 355            description: "Inbound response messages acknowledged without routing because they carry no correlation id (d
 56
 57    private static int _watchdogGaugesRegistered;
 58
 59    internal static Activity? StartActivity(
 60        string name,
 61        ActivityKind kind = ActivityKind.Internal,
 62        string? correlationId = null)
 63    {
 364        if (!ActivitySource.HasListeners())
 365            return null;
 66
 367        var activity = ActivitySource.StartActivity(name, kind);
 368        if (correlationId is not null)
 369            SetCorrelationId(activity, correlationId);
 70
 371        return activity;
 72    }
 73
 74    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 75    internal static void SetCorrelationId(Activity? activity, string? correlationId)
 76    {
 377        if (activity is null || string.IsNullOrWhiteSpace(correlationId))
 378            return;
 79
 380        activity.SetTag("asyncresponse.correlation_id", correlationId);
 381    }
 82
 83    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 84    internal static void SetPayloadType(Activity? activity, Type? payloadType)
 85    {
 386        if (activity is null || payloadType is null)
 287            return;
 88
 389        activity.SetTag("asyncresponse.payload_type", payloadType.FullName ?? payloadType.Name);
 390    }
 91
 92    internal static void SetReplyTarget(Activity? activity, AsyncResponseReplyTarget? replyTarget)
 93    {
 394        if (replyTarget is null)
 395            return;
 96
 397        activity?.SetTag("asyncresponse.reply_target.name", replyTarget.Name);
 398        activity?.SetTag("asyncresponse.reply_target.transport", replyTarget.Transport);
 299    }
 100
 101    internal static void SetWorker(Activity? activity, ReflectionCallDto? call)
 102    {
 3103        if (call is null)
 3104            return;
 105
 3106        activity?.SetTag("asyncresponse.worker.service", call.ServiceInterfaceFullName);
 3107        activity?.SetTag("asyncresponse.worker.method", call.MethodName);
 3108    }
 109
 110    internal static void SetLostSubscriberRoute(Activity? activity, bool? shouldResume)
 3111        => activity?.SetTag("asyncresponse.lost_subscriber_route", shouldResume switch
 3112        {
 3113            true => "resume",
 3114            false => "failure",
 2115            _ => "unclassified"
 3116        });
 117
 118    internal static void SetError(Activity? activity, Exception exception)
 119    {
 3120        activity?.SetTag("error.type", exception.GetType().FullName ?? exception.GetType().Name);
 3121        activity?.SetStatus(ActivityStatusCode.Error, exception.Message);
 3122    }
 123
 124    internal static void SetError(Activity? activity, string errorType, string? description = null)
 125    {
 3126        activity?.SetTag("error.type", errorType);
 3127        activity?.SetStatus(ActivityStatusCode.Error, description);
 3128    }
 129
 130    /// <summary>Records one lost-subscriber dispatch (the recovery path was entered for a publish).</summary>
 131    internal static void RecordLostSubscriber(string kind, bool? shouldResume, bool callbackInvoked)
 132    {
 3133        if (!LostSubscriberDispatches.Enabled)
 3134            return;
 135
 3136        var route = shouldResume switch { true => "resume", false => "failure", _ => "unclassified" };
 2137        LostSubscriberDispatches.Add(
 2138            1,
 2139            new KeyValuePair<string, object?>("kind", kind),
 2140            new KeyValuePair<string, object?>("route", route),
 2141            new KeyValuePair<string, object?>("invoked", callbackInvoked));
 2142    }
 143
 144    /// <summary>Records one waiter timeout on the given channel kind.</summary>
 145    internal static void RecordWaiterTimeout(string channel)
 146    {
 3147        if (WaiterTimeoutsCounter.Enabled)
 3148            WaiterTimeoutsCounter.Add(1, new KeyValuePair<string, object?>("channel", channel));
 3149    }
 150
 151    /// <summary>Records one worker-job outcome (executed/failed/rejected).</summary>
 152    internal static void RecordWorkerOutcome(string outcome)
 153    {
 3154        if (WorkerJobsCounter.Enabled)
 2155            WorkerJobsCounter.Add(1, new KeyValuePair<string, object?>("outcome", outcome));
 3156    }
 157
 158    /// <summary>
 159    /// Records that a persisted type name (kind = "service" or "payload") could not be resolved, so
 160    /// operators can correlate a silently-failing recovery callback with a missing/ALC-loaded type.
 161    /// </summary>
 162    internal static void RecordTypeResolutionFailure(string kind)
 163    {
 2164        if (TypeResolutionFailures.Enabled)
 2165            TypeResolutionFailures.Add(1, new KeyValuePair<string, object?>("kind", kind));
 2166    }
 167
 168    /// <summary>
 169    /// Records an inbound response acknowledged without routing because it carried no correlation
 170    /// id — every occurrence is a producer-side contract violation worth alerting on.
 171    /// </summary>
 172    internal static void RecordUnroutableResponse()
 173    {
 2174        if (UnroutableResponsesCounter.Enabled)
 0175            UnroutableResponsesCounter.Add(1);
 2176    }
 177
 178    /// <summary>
 179    /// Registers observable gauges reporting the latest watchdog scan: outstanding recovery
 180    /// registrations, those with a live waiter, and stale ones (no waiter, past the threshold).
 181    /// Registered once process-wide; the watchdog is a singleton, and the guard keeps repeated test
 182    /// constructions from stacking duplicate gauges on the shared meter.
 183    /// </summary>
 184    [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
 185    internal static void EnsureWatchdogGauges(AsyncResponseWatchdogState state)
 186    {
 187        if (Interlocked.Exchange(ref _watchdogGaugesRegistered, 1) != 0)
 188            return;
 189
 190        Meter.CreateObservableGauge("asyncresponse.recovery.outstanding",
 191            () => (long)(state.Latest?.Report?.TotalEntries ?? 0), unit: "{entry}",
 192            description: "Outstanding recovery registrations at the last watchdog scan.");
 193        Meter.CreateObservableGauge("asyncresponse.recovery.active_waiters",
 194            () => (long)(state.Latest?.Report?.EntriesWithActiveWaiter ?? 0), unit: "{entry}",
 195            description: "Recovery registrations with a live waiter at the last watchdog scan.");
 196        Meter.CreateObservableGauge("asyncresponse.recovery.stale",
 197            () => (long)(state.Latest?.Report?.StaleEntries.Count ?? 0), unit: "{entry}",
 198            description: "Stale recovery registrations (no live waiter, past the threshold) at the last watchdog scan.")
 199        Meter.CreateObservableGauge("asyncresponse.recovery.scan_truncated",
 200            () => state.Latest?.Report?.Truncated == true ? 1L : 0L, unit: "{scan}",
 201            description: "1 when the last watchdog scan stopped at the MaxScanEntries buffer cap — outstanding/stale the
 202    }
 203}