// ----------------------------------------------------------------------------- // ResolutionTelemetry.cs // Sprint: SPRINT_1227_0001_0002_BE_resolution_api // Task: T11 - Telemetry for resolution API // ----------------------------------------------------------------------------- using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Diagnostics; using System.Diagnostics.Metrics; namespace StellaOps.BinaryIndex.WebService.Telemetry; /// /// OpenTelemetry instrumentation for binary resolution API. /// public sealed class ResolutionTelemetry : IDisposable { public const string ServiceName = "StellaOps.BinaryIndex.Resolution"; public const string MeterName = "StellaOps.BinaryIndex.Resolution"; public const string ActivitySourceName = "StellaOps.BinaryIndex.Resolution"; private readonly Meter _meter; // Counters private readonly Counter _requestsTotal; private readonly Counter _cacheHitsTotal; private readonly Counter _cacheMissesTotal; private readonly Counter _resolutionsTotal; private readonly Counter _errorsTotal; private readonly Counter _rateLimitedTotal; // Histograms private readonly Histogram _requestDurationMs; private readonly Histogram _cacheLatencyMs; private readonly Histogram _fingerprintMatchDurationMs; private readonly Histogram _batchSize; private readonly Histogram _confidenceScore; // Gauges private readonly UpDownCounter _requestsInProgress; public static readonly ActivitySource ActivitySource = new(ActivitySourceName); public ResolutionTelemetry(IMeterFactory? meterFactory = null) { _meter = meterFactory?.Create(MeterName) ?? new Meter(MeterName); _requestsTotal = _meter.CreateCounter( "binaryindex.resolution.requests.total", unit: "{request}", description: "Total resolution API requests"); _cacheHitsTotal = _meter.CreateCounter( "binaryindex.resolution.cache.hits.total", unit: "{hit}", description: "Total cache hits"); _cacheMissesTotal = _meter.CreateCounter( "binaryindex.resolution.cache.misses.total", unit: "{miss}", description: "Total cache misses"); _resolutionsTotal = _meter.CreateCounter( "binaryindex.resolution.resolutions.total", unit: "{resolution}", description: "Total successful resolutions"); _errorsTotal = _meter.CreateCounter( "binaryindex.resolution.errors.total", unit: "{error}", description: "Total resolution errors"); _rateLimitedTotal = _meter.CreateCounter( "binaryindex.resolution.rate_limited.total", unit: "{request}", description: "Total rate-limited requests"); _requestDurationMs = _meter.CreateHistogram( "binaryindex.resolution.request.duration.ms", unit: "ms", description: "Request duration in milliseconds"); _cacheLatencyMs = _meter.CreateHistogram( "binaryindex.resolution.cache.latency.ms", unit: "ms", description: "Cache lookup latency in milliseconds"); _fingerprintMatchDurationMs = _meter.CreateHistogram( "binaryindex.resolution.fingerprint_match.duration.ms", unit: "ms", description: "Fingerprint matching duration in milliseconds"); _batchSize = _meter.CreateHistogram( "binaryindex.resolution.batch.size", unit: "{item}", description: "Batch request size"); _confidenceScore = _meter.CreateHistogram( "binaryindex.resolution.confidence", unit: "1", description: "Resolution confidence score distribution"); _requestsInProgress = _meter.CreateUpDownCounter( "binaryindex.resolution.requests.in_progress", unit: "{request}", description: "Requests currently in progress"); } public void RecordRequest(string method, string status, TimeSpan duration, bool cacheHit) { var tags = new TagList { { ResolutionTelemetryTags.Method, method }, { ResolutionTelemetryTags.Status, status }, { ResolutionTelemetryTags.CacheHit, cacheHit.ToString().ToLowerInvariant() } }; _requestsTotal.Add(1, tags); _requestDurationMs.Record(duration.TotalMilliseconds, tags); if (cacheHit) _cacheHitsTotal.Add(1, tags); else _cacheMissesTotal.Add(1, tags); } public void RecordResolution(string matchType, string resolutionStatus, decimal confidence) { var tags = new TagList { { ResolutionTelemetryTags.MatchType, matchType }, { ResolutionTelemetryTags.ResolutionStatus, resolutionStatus } }; _resolutionsTotal.Add(1, tags); _confidenceScore.Record((double)confidence, tags); } public void RecordError(string errorCode, string method) { _errorsTotal.Add(1, new TagList { { ResolutionTelemetryTags.ErrorCode, errorCode }, { ResolutionTelemetryTags.Method, method } }); } public void RecordRateLimited(string tenantId) { _rateLimitedTotal.Add(1, new TagList { { ResolutionTelemetryTags.TenantId, tenantId } }); } public void RecordBatchRequest(int size) { _batchSize.Record(size); } public void RecordCacheLatency(TimeSpan latency, bool hit) { _cacheLatencyMs.Record(latency.TotalMilliseconds, new TagList { { ResolutionTelemetryTags.CacheHit, hit.ToString().ToLowerInvariant() } }); } public void RecordFingerprintMatchDuration(TimeSpan duration, string algorithm) { _fingerprintMatchDurationMs.Record(duration.TotalMilliseconds, new TagList { { ResolutionTelemetryTags.Algorithm, algorithm } }); } public void IncrementInProgress() => _requestsInProgress.Add(1); public void DecrementInProgress() => _requestsInProgress.Add(-1); public static Activity? StartResolveActivity(string package, string? cveId) { var activity = ActivitySource.StartActivity("Resolution.Resolve"); activity?.SetTag("package", package); if (cveId != null) activity?.SetTag("cve_id", cveId); return activity; } public static Activity? StartBatchResolveActivity(int count) { var activity = ActivitySource.StartActivity("Resolution.ResolveBatch"); activity?.SetTag("batch_size", count); return activity; } public void Dispose() => _meter.Dispose(); } public static class ResolutionTelemetryTags { public const string Method = "method"; public const string Status = "status"; public const string CacheHit = "cache_hit"; public const string MatchType = "match_type"; public const string ResolutionStatus = "resolution_status"; public const string ErrorCode = "error_code"; public const string TenantId = "tenant_id"; public const string Algorithm = "algorithm"; } public static class ResolutionTelemetryExtensions { public static IServiceCollection AddResolutionTelemetry(this IServiceCollection services) { services.TryAddSingleton(); return services; } }