< 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: 101
Uncovered lines: 0
Coverable lines: 101
Total lines: 322
Line coverage: 100%
Branch coverage
100%
Covered branches: 14
Total branches: 14
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%
CreateTimedScope(...)100%11100%
.ctor(...)100%11100%
Dispose()100%22100%
GetElapsedSeconds()100%11100%

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    {
 27932        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    {
 1069550        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    {
 2136986        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    {
 96157        var tags = new TagList
 96158        {
 96159            { "query.name", queryName },
 96160            { "operation", "build" }
 96161        };
 162
 96163        QueryBuiltCounter.Add(1, tags);
 96164        QueryBuildDuration.Record(durationSeconds, tags);
 96165        QueryFieldCount.Record(fieldCount, tags);
 96166    }
 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    {
 78174        var tags = new TagList
 78175        {
 78176            { "field.has_arguments", hasArguments },
 78177            { "field.has_metadata", hasMetadata },
 78178            { "field.is_nested", fieldPath.Contains('.') }
 78179        };
 180
 78181        FieldsAddedCounter.Add(1, tags);
 78182    }
 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    {
 10668205        var tags = new TagList
 10668206        {
 10668207            { "pool.type", poolType },
 10668208            { "pool.operation", operation },
 10668209            { "pool.cache_level", cacheLevel }
 10668210        };
 211
 10668212        PoolOperationsCounter.Add(1, tags);
 213
 214#if NET7_0_OR_GREATER
 10668215        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
 10668220    }
 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    /// <summary>
 263    /// Creates a scoped measurement for timing operations
 264    /// </summary>
 265    internal static IDisposable CreateTimedScope(string operationName, TagList tags)
 266    {
 12267        return new TimedScope(operationName, tags);
 268    }
 269
 270    #endregion
 271
 272    #region Timed Scope Helper
 273
 274    /// <summary>
 275    /// RAII helper for automatic timing measurements
 276    /// </summary>
 277    private readonly struct TimedScope : IDisposable
 278    {
 279        private readonly string _operationName;
 280        private readonly TagList _tags;
 281        private readonly long _startTicks;
 282
 283        public TimedScope(string operationName, TagList tags)
 284        {
 12285            _operationName = operationName;
 12286            _tags = tags;
 12287            _startTicks = Stopwatch.GetTimestamp();
 12288        }
 289
 290        public void Dispose()
 291        {
 12292            var elapsed = GetElapsedSeconds(_startTicks);
 293
 294            // Record to appropriate histogram based on operation
 12295            if (_operationName.Contains("serialize"))
 296            {
 3297                SerializationDuration.Record(elapsed, _tags);
 298            }
 299            else
 300            {
 9301                QueryBuildDuration.Record(elapsed, _tags);
 302            }
 303
 304
 305            // <summary>
 306            // Gets elapsed seconds since the given timestamp (cross-.NET version compatible)
 307            // </summary>
 308            static double GetElapsedSeconds(long startTimestamp)
 309            {
 310#if NET7_0_OR_GREATER
 12311        return Stopwatch.GetElapsedTime(startTimestamp).TotalSeconds;
 312#else
 313                var elapsed = Stopwatch.GetTimestamp() - startTimestamp;
 314                return (double)elapsed / Stopwatch.Frequency;
 315#endif
 316            }
 9317        }
 318    }
 319
 320    #endregion
 321
 322}