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();
}
}