Files
git.stella-ops.org/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Telemetry/ResolutionTelemetry.cs
2026-02-01 21:37:40 +02:00

220 lines
7.5 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// OpenTelemetry instrumentation for binary resolution API.
/// </summary>
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<long> _requestsTotal;
private readonly Counter<long> _cacheHitsTotal;
private readonly Counter<long> _cacheMissesTotal;
private readonly Counter<long> _resolutionsTotal;
private readonly Counter<long> _errorsTotal;
private readonly Counter<long> _rateLimitedTotal;
// Histograms
private readonly Histogram<double> _requestDurationMs;
private readonly Histogram<double> _cacheLatencyMs;
private readonly Histogram<double> _fingerprintMatchDurationMs;
private readonly Histogram<int> _batchSize;
private readonly Histogram<double> _confidenceScore;
// Gauges
private readonly UpDownCounter<long> _requestsInProgress;
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
public ResolutionTelemetry(IMeterFactory? meterFactory = null)
{
_meter = meterFactory?.Create(MeterName) ?? new Meter(MeterName);
_requestsTotal = _meter.CreateCounter<long>(
"binaryindex.resolution.requests.total",
unit: "{request}",
description: "Total resolution API requests");
_cacheHitsTotal = _meter.CreateCounter<long>(
"binaryindex.resolution.cache.hits.total",
unit: "{hit}",
description: "Total cache hits");
_cacheMissesTotal = _meter.CreateCounter<long>(
"binaryindex.resolution.cache.misses.total",
unit: "{miss}",
description: "Total cache misses");
_resolutionsTotal = _meter.CreateCounter<long>(
"binaryindex.resolution.resolutions.total",
unit: "{resolution}",
description: "Total successful resolutions");
_errorsTotal = _meter.CreateCounter<long>(
"binaryindex.resolution.errors.total",
unit: "{error}",
description: "Total resolution errors");
_rateLimitedTotal = _meter.CreateCounter<long>(
"binaryindex.resolution.rate_limited.total",
unit: "{request}",
description: "Total rate-limited requests");
_requestDurationMs = _meter.CreateHistogram<double>(
"binaryindex.resolution.request.duration.ms",
unit: "ms",
description: "Request duration in milliseconds");
_cacheLatencyMs = _meter.CreateHistogram<double>(
"binaryindex.resolution.cache.latency.ms",
unit: "ms",
description: "Cache lookup latency in milliseconds");
_fingerprintMatchDurationMs = _meter.CreateHistogram<double>(
"binaryindex.resolution.fingerprint_match.duration.ms",
unit: "ms",
description: "Fingerprint matching duration in milliseconds");
_batchSize = _meter.CreateHistogram<int>(
"binaryindex.resolution.batch.size",
unit: "{item}",
description: "Batch request size");
_confidenceScore = _meter.CreateHistogram<double>(
"binaryindex.resolution.confidence",
unit: "1",
description: "Resolution confidence score distribution");
_requestsInProgress = _meter.CreateUpDownCounter<long>(
"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<ResolutionTelemetry>();
return services;
}
}