266 lines
9.3 KiB
C#
266 lines
9.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Implementation of event stream metrics using System.Diagnostics.Metrics API.
|
|
/// Compatible with OpenTelemetry and Prometheus exporters.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Metric Naming Convention:</strong>
|
|
/// All metrics use the prefix "svrnty.cqrs.events." for consistency.
|
|
/// Tags/labels are used for stream name, subscription ID, and event type dimensions.
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>OpenTelemetry Integration:</strong>
|
|
/// This implementation uses .NET's built-in Meter API which is automatically
|
|
/// discovered by OpenTelemetry's .NET instrumentation.
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Prometheus Integration:</strong>
|
|
/// Use OpenTelemetry's Prometheus exporter to expose these metrics at /metrics endpoint.
|
|
/// </para>
|
|
/// </remarks>
|
|
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<long> _eventsPublishedCounter;
|
|
private readonly Counter<long> _eventsConsumedCounter;
|
|
private readonly Counter<long> _errorsCounter;
|
|
private readonly Counter<long> _retriesCounter;
|
|
|
|
// Histograms (distribution of values)
|
|
private readonly Histogram<double> _processingLatencyHistogram;
|
|
|
|
// Observable Gauges (current point-in-time values)
|
|
private readonly Dictionary<string, long> _consumerLagCache = new();
|
|
private readonly Dictionary<string, long> _streamLengthCache = new();
|
|
private readonly Dictionary<string, int> _activeConsumersCache = new();
|
|
private readonly object _cacheLock = new();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="EventStreamMetrics"/> class.
|
|
/// </summary>
|
|
public EventStreamMetrics()
|
|
{
|
|
_meter = new Meter(MeterName, MeterVersion);
|
|
|
|
// Counter: Total events published
|
|
_eventsPublishedCounter = _meter.CreateCounter<long>(
|
|
name: "svrnty.cqrs.events.published",
|
|
unit: "events",
|
|
description: "Total number of events published to streams");
|
|
|
|
// Counter: Total events consumed
|
|
_eventsConsumedCounter = _meter.CreateCounter<long>(
|
|
name: "svrnty.cqrs.events.consumed",
|
|
unit: "events",
|
|
description: "Total number of events consumed from subscriptions");
|
|
|
|
// Counter: Total errors
|
|
_errorsCounter = _meter.CreateCounter<long>(
|
|
name: "svrnty.cqrs.events.errors",
|
|
unit: "errors",
|
|
description: "Total number of errors during event processing");
|
|
|
|
// Counter: Total retries
|
|
_retriesCounter = _meter.CreateCounter<long>(
|
|
name: "svrnty.cqrs.events.retries",
|
|
unit: "retries",
|
|
description: "Total number of retry attempts for failed events");
|
|
|
|
// Histogram: Processing latency distribution
|
|
_processingLatencyHistogram = _meter.CreateHistogram<double>(
|
|
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<Measurement<long>>(_consumerLagCache.Count);
|
|
foreach (var kvp in _consumerLagCache)
|
|
{
|
|
var parts = kvp.Key.Split(':', 2);
|
|
if (parts.Length == 2)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", parts[0]),
|
|
new("subscription", parts[1])
|
|
};
|
|
measurements.Add(new Measurement<long>(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<Measurement<long>>(_streamLengthCache.Count);
|
|
foreach (var kvp in _streamLengthCache)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", kvp.Key)
|
|
};
|
|
measurements.Add(new Measurement<long>(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<Measurement<int>>(_activeConsumersCache.Count);
|
|
foreach (var kvp in _activeConsumersCache)
|
|
{
|
|
var parts = kvp.Key.Split(':', 2);
|
|
if (parts.Length == 2)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", parts[0]),
|
|
new("subscription", parts[1])
|
|
};
|
|
measurements.Add(new Measurement<int>(kvp.Value, tags));
|
|
}
|
|
}
|
|
return measurements;
|
|
}
|
|
},
|
|
unit: "consumers",
|
|
description: "Number of active consumers for a subscription");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordEventPublished(string streamName, string eventType)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", streamName),
|
|
new("event_type", eventType)
|
|
};
|
|
_eventsPublishedCounter.Add(1, tags);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordEventConsumed(string streamName, string subscriptionId, string eventType)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", streamName),
|
|
new("subscription", subscriptionId),
|
|
new("event_type", eventType)
|
|
};
|
|
_eventsConsumedCounter.Add(1, tags);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordProcessingLatency(string streamName, string subscriptionId, TimeSpan latency)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", streamName),
|
|
new("subscription", subscriptionId)
|
|
};
|
|
_processingLatencyHistogram.Record(latency.TotalMilliseconds, tags);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordConsumerLag(string streamName, string subscriptionId, long lag)
|
|
{
|
|
var key = $"{streamName}:{subscriptionId}";
|
|
lock (_cacheLock)
|
|
{
|
|
_consumerLagCache[key] = lag;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordError(string streamName, string? subscriptionId, string errorType)
|
|
{
|
|
var tags = subscriptionId != null
|
|
? new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", streamName),
|
|
new("subscription", subscriptionId),
|
|
new("error_type", errorType)
|
|
}
|
|
: new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", streamName),
|
|
new("error_type", errorType)
|
|
};
|
|
_errorsCounter.Add(1, tags);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordRetry(string streamName, string subscriptionId, int attemptNumber)
|
|
{
|
|
var tags = new KeyValuePair<string, object?>[]
|
|
{
|
|
new("stream", streamName),
|
|
new("subscription", subscriptionId),
|
|
new("attempt", attemptNumber)
|
|
};
|
|
_retriesCounter.Add(1, tags);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordStreamLength(string streamName, long length)
|
|
{
|
|
lock (_cacheLock)
|
|
{
|
|
_streamLengthCache[streamName] = length;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void RecordActiveConsumers(string streamName, string subscriptionId, int consumerCount)
|
|
{
|
|
var key = $"{streamName}:{subscriptionId}";
|
|
lock (_cacheLock)
|
|
{
|
|
_activeConsumersCache[key] = consumerCount;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the meter and releases resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_meter?.Dispose();
|
|
}
|
|
}
|