using System; using Svrnty.CQRS.Events.Abstractions.Streaming; using System.Collections.Generic; using System.Diagnostics.Metrics; using Svrnty.CQRS.Events.Abstractions; namespace Svrnty.CQRS.Events.Metrics; /// /// Implementation of event stream metrics using System.Diagnostics.Metrics API. /// Compatible with OpenTelemetry and Prometheus exporters. /// /// /// /// Metric Naming Convention: /// All metrics use the prefix "svrnty.cqrs.events." for consistency. /// Tags/labels are used for stream name, subscription ID, and event type dimensions. /// /// /// OpenTelemetry Integration: /// This implementation uses .NET's built-in Meter API which is automatically /// discovered by OpenTelemetry's .NET instrumentation. /// /// /// Prometheus Integration: /// Use OpenTelemetry's Prometheus exporter to expose these metrics at /metrics endpoint. /// /// public sealed class EventStreamMetrics : IEventStreamMetrics, IDisposable { private const string MeterName = "Svrnty.CQRS.Events"; private const string MeterVersion = "1.0.0"; private readonly Meter _meter; // Counters (cumulative values that only increase) private readonly Counter _eventsPublishedCounter; private readonly Counter _eventsConsumedCounter; private readonly Counter _errorsCounter; private readonly Counter _retriesCounter; // Histograms (distribution of values) private readonly Histogram _processingLatencyHistogram; // Observable Gauges (current point-in-time values) private readonly Dictionary _consumerLagCache = new(); private readonly Dictionary _streamLengthCache = new(); private readonly Dictionary _activeConsumersCache = new(); private readonly object _cacheLock = new(); /// /// Initializes a new instance of the class. /// public EventStreamMetrics() { _meter = new Meter(MeterName, MeterVersion); // Counter: Total events published _eventsPublishedCounter = _meter.CreateCounter( name: "svrnty.cqrs.events.published", unit: "events", description: "Total number of events published to streams"); // Counter: Total events consumed _eventsConsumedCounter = _meter.CreateCounter( name: "svrnty.cqrs.events.consumed", unit: "events", description: "Total number of events consumed from subscriptions"); // Counter: Total errors _errorsCounter = _meter.CreateCounter( name: "svrnty.cqrs.events.errors", unit: "errors", description: "Total number of errors during event processing"); // Counter: Total retries _retriesCounter = _meter.CreateCounter( name: "svrnty.cqrs.events.retries", unit: "retries", description: "Total number of retry attempts for failed events"); // Histogram: Processing latency distribution _processingLatencyHistogram = _meter.CreateHistogram( name: "svrnty.cqrs.events.processing_latency", unit: "ms", description: "Event processing latency from publish to acknowledgment"); // Observable Gauge: Consumer lag _meter.CreateObservableGauge( name: "svrnty.cqrs.events.consumer_lag", observeValues: () => { lock (_cacheLock) { var measurements = new List>(_consumerLagCache.Count); foreach (var kvp in _consumerLagCache) { var parts = kvp.Key.Split(':', 2); if (parts.Length == 2) { var tags = new KeyValuePair[] { new("stream", parts[0]), new("subscription", parts[1]) }; measurements.Add(new Measurement(kvp.Value, tags)); } } return measurements; } }, unit: "events", description: "Number of events the consumer is behind the stream head"); // Observable Gauge: Stream length _meter.CreateObservableGauge( name: "svrnty.cqrs.events.stream_length", observeValues: () => { lock (_cacheLock) { var measurements = new List>(_streamLengthCache.Count); foreach (var kvp in _streamLengthCache) { var tags = new KeyValuePair[] { new("stream", kvp.Key) }; measurements.Add(new Measurement(kvp.Value, tags)); } return measurements; } }, unit: "events", description: "Current length of the event stream (total events)"); // Observable Gauge: Active consumers _meter.CreateObservableGauge( name: "svrnty.cqrs.events.active_consumers", observeValues: () => { lock (_cacheLock) { var measurements = new List>(_activeConsumersCache.Count); foreach (var kvp in _activeConsumersCache) { var parts = kvp.Key.Split(':', 2); if (parts.Length == 2) { var tags = new KeyValuePair[] { new("stream", parts[0]), new("subscription", parts[1]) }; measurements.Add(new Measurement(kvp.Value, tags)); } } return measurements; } }, unit: "consumers", description: "Number of active consumers for a subscription"); } /// public void RecordEventPublished(string streamName, string eventType) { var tags = new KeyValuePair[] { new("stream", streamName), new("event_type", eventType) }; _eventsPublishedCounter.Add(1, tags); } /// public void RecordEventConsumed(string streamName, string subscriptionId, string eventType) { var tags = new KeyValuePair[] { new("stream", streamName), new("subscription", subscriptionId), new("event_type", eventType) }; _eventsConsumedCounter.Add(1, tags); } /// public void RecordProcessingLatency(string streamName, string subscriptionId, TimeSpan latency) { var tags = new KeyValuePair[] { new("stream", streamName), new("subscription", subscriptionId) }; _processingLatencyHistogram.Record(latency.TotalMilliseconds, tags); } /// public void RecordConsumerLag(string streamName, string subscriptionId, long lag) { var key = $"{streamName}:{subscriptionId}"; lock (_cacheLock) { _consumerLagCache[key] = lag; } } /// public void RecordError(string streamName, string? subscriptionId, string errorType) { var tags = subscriptionId != null ? new KeyValuePair[] { new("stream", streamName), new("subscription", subscriptionId), new("error_type", errorType) } : new KeyValuePair[] { new("stream", streamName), new("error_type", errorType) }; _errorsCounter.Add(1, tags); } /// public void RecordRetry(string streamName, string subscriptionId, int attemptNumber) { var tags = new KeyValuePair[] { new("stream", streamName), new("subscription", subscriptionId), new("attempt", attemptNumber) }; _retriesCounter.Add(1, tags); } /// public void RecordStreamLength(string streamName, long length) { lock (_cacheLock) { _streamLengthCache[streamName] = length; } } /// public void RecordActiveConsumers(string streamName, string subscriptionId, int consumerCount) { var key = $"{streamName}:{subscriptionId}"; lock (_cacheLock) { _activeConsumersCache[key] = consumerCount; } } /// /// Disposes the meter and releases resources. /// public void Dispose() { _meter?.Dispose(); } }