< Summary

Information
Class: NGql.Core.Observability.NGqlTelemetry
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Observability/NGqlTelemetry.cs
Line coverage
100%
Covered lines: 90
Uncovered lines: 0
Coverable lines: 90
Total lines: 264
Line coverage: 100%
Branch coverage
100%
Covered branches: 12
Total branches: 12
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
StartQueryBuildingActivity(...)100%11100%
StartFieldActivity(...)100%11100%
StartPoolingActivity(...)100%11100%
TagQueryActivity(...)100%22100%
TagFieldActivity(...)100%22100%
TagPoolingActivity(...)100%22100%
RecordQueryBuilt(...)100%11100%
RecordFieldAdded(...)100%11100%
RecordSerialization(...)100%11100%
RecordPoolOperation(...)100%11100%
RecordActiveQueryChange(...)100%11100%
GetSizeCategory(...)100%66100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Observability/NGqlTelemetry.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Diagnostics.Metrics;
 3using System.Runtime.CompilerServices;
 4
 5namespace NGql.Core.Observability;
 6
 7/// <summary>
 8/// Centralized telemetry provider for NGql using .NET's built-in observability primitives.
 9/// Provides OpenTelemetry-compatible traces and metrics using only BCL components (zero dependencies).
 10/// </summary>
 11internal static class NGqlTelemetry
 12{
 13    // OpenTelemetry-standard naming convention: {company}.{product}
 14    private const string ActivitySourceName = "NGql.Core";
 15    private const string MeterName = "NGql.Core";
 16    private const string Version = "1.0.0"; // Should match assembly version
 17
 18    // Activity source for distributed tracing (OpenTelemetry-compatible)
 319    private static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version);
 20
 21    // Meter for metrics collection (OpenTelemetry-compatible)
 322    private static readonly Meter Meter = new(MeterName, Version);
 23
 24    #region Traces (Activities)
 25
 26    /// <summary>
 27    /// Starts a new activity for query building operations
 28    /// </summary>
 29    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 30    internal static Activity? StartQueryBuildingActivity(string operationName)
 31    {
 21332        return ActivitySource.StartActivity($"ngql.query.{operationName}");
 33    }
 34
 35    /// <summary>
 36    /// Starts a new activity for field operations
 37    /// </summary>
 38    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 39    internal static Activity? StartFieldActivity(string operationName)
 40    {
 3641        return ActivitySource.StartActivity($"ngql.field.{operationName}");
 42    }
 43
 44    /// <summary>
 45    /// Starts a new activity for pooling operations
 46    /// </summary>
 47    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 48    internal static Activity? StartPoolingActivity(string poolType, string operation)
 49    {
 1077050        return ActivitySource.StartActivity($"ngql.pool.{poolType}.{operation}");
 51    }
 52
 53    /// <summary>
 54    /// Adds standard tags to an activity for query operations
 55    /// </summary>
 56    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 57    internal static void TagQueryActivity(Activity? activity, string? queryName, int fieldCount)
 58    {
 8159        if (activity == null) return;
 60
 361        activity.SetTag("ngql.query.name", queryName);
 362        activity.SetTag("ngql.query.field_count", fieldCount);
 363        activity.SetTag("ngql.operation.type", "query_building");
 364    }
 65
 66    /// <summary>
 67    /// Adds standard tags to an activity for field operations
 68    /// </summary>
 69    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 70    internal static void TagFieldActivity(Activity? activity, string fieldPath, bool hasArguments, bool hasMetadata)
 71    {
 8172        if (activity == null) return;
 73
 374        activity.SetTag("ngql.field.path", fieldPath);
 375        activity.SetTag("ngql.field.has_arguments", hasArguments);
 376        activity.SetTag("ngql.field.has_metadata", hasMetadata);
 377        activity.SetTag("ngql.operation.type", "field_building");
 378    }
 79
 80    /// <summary>
 81    /// Adds standard tags to an activity for pooling operations
 82    /// </summary>
 83    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 84    internal static void TagPoolingActivity(Activity? activity, string poolType, string cacheHit, int poolSize)
 85    {
 2154386        if (activity == null) return;
 87
 388        activity.SetTag("ngql.pool.type", poolType);
 389        activity.SetTag("ngql.pool.cache_hit", cacheHit);
 390        activity.SetTag("ngql.pool.size", poolSize);
 391        activity.SetTag("ngql.operation.type", "pooling");
 392    }
 93
 94    #endregion
 95
 96    #region Metrics
 97
 98    // Counters (monotonic increasing values)
 399    private static readonly Counter<long> QueryBuiltCounter = Meter.CreateCounter<long>(
 3100        "ngql_queries_built_total",
 3101        description: "Total number of GraphQL queries built");
 102
 3103    private static readonly Counter<long> FieldsAddedCounter = Meter.CreateCounter<long>(
 3104        "ngql_fields_added_total",
 3105        description: "Total number of fields added to queries");
 106
 3107    private static readonly Counter<long> PoolOperationsCounter = Meter.CreateCounter<long>(
 3108        "ngql_pool_operations_total",
 3109        description: "Total number of pooling operations");
 110
 111    // Histograms (value distributions)
 3112    private static readonly Histogram<double> QueryBuildDuration = Meter.CreateHistogram<double>(
 3113        "ngql_query_build_duration_seconds",
 3114        unit: "s",
 3115        description: "Duration of query building operations");
 116
 3117    private static readonly Histogram<double> SerializationDuration = Meter.CreateHistogram<double>(
 3118        "ngql_serialization_duration_seconds",
 3119        unit: "s",
 3120        description: "Duration of query serialization to string");
 121
 3122    private static readonly Histogram<long> QueryFieldCount = Meter.CreateHistogram<long>(
 3123        "ngql_query_field_count",
 3124        description: "Number of fields in built queries");
 125
 126#if NET7_0_OR_GREATER
 127    // UpDownCounters (can increase or decrease) - Available in .NET 7+
 3128    private static readonly UpDownCounter<long> ActiveQueriesGauge = Meter.CreateUpDownCounter<long>(
 3129        "ngql_active_queries",
 3130        description: "Number of queries currently being built");
 131
 3132    private static readonly UpDownCounter<long> PoolSizeGauge = Meter.CreateUpDownCounter<long>(
 3133        "ngql_pool_size",
 3134        description: "Current size of object pools");
 135#else
 136    // For .NET 6, we'll use regular counters and manage state manually
 137    private static readonly Counter<long> ActiveQueriesChangeCounter = Meter.CreateCounter<long>(
 138        "ngql_active_queries_changes_total",
 139        description: "Total changes to active query count");
 140
 141    private static readonly Counter<long> PoolSizeChangeCounter = Meter.CreateCounter<long>(
 142        "ngql_pool_size_changes_total",
 143        description: "Total changes to pool sizes");
 144
 145    // Manual state tracking for .NET 6 compatibility
 146    private static long _currentActiveQueries = 0;
 147#endif
 148
 149    #region Metric Recording Methods
 150
 151    /// <summary>
 152    /// Records a query built event with contextual tags
 153    /// </summary>
 154    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 155    internal static void RecordQueryBuilt(string? queryName, int fieldCount, double durationSeconds)
 156    {
 36157        var tags = new TagList
 36158        {
 36159            { "query.name", queryName },
 36160            { "operation", "build" }
 36161        };
 162
 36163        QueryBuiltCounter.Add(1, tags);
 36164        QueryBuildDuration.Record(durationSeconds, tags);
 36165        QueryFieldCount.Record(fieldCount, tags);
 36166    }
 167
 168    /// <summary>
 169    /// Records a field added event
 170    /// </summary>
 171    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 172    internal static void RecordFieldAdded(string fieldPath, bool hasArguments, bool hasMetadata)
 173    {
 18174        var tags = new TagList
 18175        {
 18176            { "field.has_arguments", hasArguments },
 18177            { "field.has_metadata", hasMetadata },
 18178            { "field.is_nested", fieldPath.Contains('.') }
 18179        };
 180
 18181        FieldsAddedCounter.Add(1, tags);
 18182    }
 183
 184    /// <summary>
 185    /// Records query serialization metrics
 186    /// </summary>
 187    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 188    internal static void RecordSerialization(double durationSeconds, int outputLength)
 189    {
 42190        var tags = new TagList
 42191        {
 42192            { "operation", "serialize" },
 42193            { "output.size_category", GetSizeCategory(outputLength) }
 42194        };
 195
 42196        SerializationDuration.Record(durationSeconds, tags);
 42197    }
 198
 199    /// <summary>
 200    /// Records pool operation metrics
 201    /// </summary>
 202    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 203    internal static void RecordPoolOperation(string poolType, string operation, string cacheLevel, int poolSize)
 204    {
 10755205        var tags = new TagList
 10755206        {
 10755207            { "pool.type", poolType },
 10755208            { "pool.operation", operation },
 10755209            { "pool.cache_level", cacheLevel }
 10755210        };
 211
 10755212        PoolOperationsCounter.Add(1, tags);
 213
 214#if NET7_0_OR_GREATER
 10755215        PoolSizeGauge.Add(poolSize, new TagList { { "pool.type", poolType } });
 216#else
 217        // For .NET 6, record as counter change
 218        PoolSizeChangeCounter.Add(Math.Abs(poolSize), new TagList { { "pool.type", poolType } });
 219#endif
 10755220    }
 221
 222    /// <summary>
 223    /// Records active query count changes
 224    /// </summary>
 225    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 226    internal static void RecordActiveQueryChange(int delta)
 227    {
 228#if NET7_0_OR_GREATER
 45229        ActiveQueriesGauge.Add(delta);
 230#else
 231        // For .NET 6, track manually and record as counter changes
 232        var newCount = Interlocked.Add(ref _currentActiveQueries, delta);
 233        ActiveQueriesChangeCounter.Add(Math.Abs(delta), new TagList { { "operation", "active_query_change" } });
 234
 235        // Optionally record the current value in activities for observability
 236        using var activity = ActivitySource.StartActivity("ngql.metrics.active_queries");
 237        activity?.SetTag("current_count", newCount);
 238        activity?.SetTag("delta", delta);
 239#endif
 45240    }
 241
 242    #endregion
 243
 244    #endregion
 245
 246    #region Utility Methods
 247
 248    /// <summary>
 249    /// Categorizes output size for better metric cardinality control
 250    /// </summary>
 251    private static string GetSizeCategory(int size)
 252    {
 42253        return size switch
 42254        {
 15255            < 1024 => "small",      // < 1KB
 15256            < 10240 => "medium",    // < 10KB
 6257            < 102400 => "large",    // < 100KB
 6258            _ => "very_large"       // >= 100KB
 42259        };
 260    }
 261
 262    #endregion
 263
 264}