-
Notifications
You must be signed in to change notification settings - Fork 51
Add retention sweep metrics for cycle duration, rows deleted, and consecutive failures #5831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| namespace ServiceControl.Infrastructure; | ||
|
|
||
| /// <summary> | ||
| /// The meters each instance publishes on. Shared because persisters publish onto the meter their | ||
| /// host has already registered with the exporter, and the two assemblies cannot reference each | ||
| /// other. | ||
| /// </summary> | ||
| public static class ServiceControlMeters | ||
| { | ||
| public const string Error = "Particular.ServiceControl"; | ||
| public const string Audit = "Particular.ServiceControl.Audit"; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,120 @@ | ||||||||||||||||||||||
| namespace ServiceControl.Persistence.EFCore.Infrastructure; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| using System.Diagnostics; | ||||||||||||||||||||||
| using System.Diagnostics.Metrics; | ||||||||||||||||||||||
| using ServiceControl.Infrastructure; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public enum RetentionEntity | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| FailedMessages, | ||||||||||||||||||||||
| EventLog, | ||||||||||||||||||||||
| GroupComments | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public class RetentionMetrics | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| public const string MeterName = ServiceControlMeters.Error; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public static readonly string CycleDurationInstrumentName = $"{InstrumentPrefix}.cycle_duration_seconds"; | ||||||||||||||||||||||
| public static readonly string RowsDeletedInstrumentName = $"{InstrumentPrefix}.rows_deleted_total"; | ||||||||||||||||||||||
| public static readonly string ConsecutiveFailuresInstrumentName = $"{InstrumentPrefix}.consecutive_failures_total"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public RetentionMetrics(IMeterFactory meterFactory) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var meter = meterFactory.Create(MeterName, MeterVersion); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| cycleDuration = meter.CreateHistogram( | ||||||||||||||||||||||
| CycleDurationInstrumentName, | ||||||||||||||||||||||
| unit: "seconds", | ||||||||||||||||||||||
| description: "Retention sweep pass duration in seconds", | ||||||||||||||||||||||
| tags: null, | ||||||||||||||||||||||
| // A sweep pass is sub-second when it is keeping up and minutes long when it is working | ||||||||||||||||||||||
| // through a backlog, so the default boundaries resolve neither end. | ||||||||||||||||||||||
| advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.1, 0.5, 1, 5, 15, 60, 300, 900] }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| rowsDeleted = meter.CreateCounter<long>(RowsDeletedInstrumentName, description: "Rows deleted by the retention sweep"); | ||||||||||||||||||||||
| consecutiveFailureGauge = meter.CreateObservableGauge(ConsecutiveFailuresInstrumentName, ObserveConsecutiveFailures, description: "Consecutive retention sweep failures"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public RetentionCycleMetrics BeginCycle(RetentionEntity entity, CancellationToken cancellationToken = default) => new(this, entity, cancellationToken); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void RecordRowsDeleted(RetentionEntity entity, int rows) => rowsDeleted.Add(rows, EntityTags[(int)entity]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| internal void RecordCycle(RetentionEntity entity, TimeSpan elapsed, bool success) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var tags = EntityTags[(int)entity]; | ||||||||||||||||||||||
| tags.Add("result", success ? "success" : "failed"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| cycleDuration.Record(elapsed.TotalSeconds, tags); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (success) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| Interlocked.Exchange(ref consecutiveFailures[(int)entity], 0); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| else | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| Interlocked.Increment(ref consecutiveFailures[(int)entity]); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| IEnumerable<Measurement<long>> ObserveConsecutiveFailures() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| for (var entity = 0; entity < consecutiveFailures.Length; entity++) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| yield return new Measurement<long>(Volatile.Read(ref consecutiveFailures[entity]), EntityTags[entity]); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| static TagList EntityTag(string entity) => new() { { "retention.entity", entity } }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| readonly long[] consecutiveFailures = new long[EntityTags.Length]; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| readonly Histogram<double> cycleDuration; | ||||||||||||||||||||||
| readonly Counter<long> rowsDeleted; | ||||||||||||||||||||||
| #pragma warning disable IDE0052 | ||||||||||||||||||||||
| readonly ObservableGauge<long> consecutiveFailureGauge; | ||||||||||||||||||||||
| #pragma warning restore IDE0052 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| static readonly TagList[] EntityTags = | ||||||||||||||||||||||
| [ | ||||||||||||||||||||||
| EntityTag("failed_messages"), | ||||||||||||||||||||||
| EntityTag("event_log"), | ||||||||||||||||||||||
| EntityTag("group_comments") | ||||||||||||||||||||||
| ]; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const string MeterVersion = "0.1.0"; | ||||||||||||||||||||||
| const string InstrumentPrefix = "sc.retention"; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// One pass of the retention sweep. A pass interrupted by shutdown is not a measurement of | ||||||||||||||||||||||
| /// anything, so a cancelled cycle records neither a duration nor a failure. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| public sealed class RetentionCycleMetrics : IDisposable | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| internal RetentionCycleMetrics(RetentionMetrics metrics, RetentionEntity entity, CancellationToken cancellationToken) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| this.metrics = metrics; | ||||||||||||||||||||||
| this.entity = entity; | ||||||||||||||||||||||
| this.cancellationToken = cancellationToken; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void Complete() => completed = true; | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it's probably not material but shouldn't this also freeze the timer in case there is time between the |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void Dispose() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (cancellationToken.IsCancellationRequested) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| metrics.RecordCycle(entity, stopwatch.Elapsed, completed); | ||||||||||||||||||||||
|
Comment on lines
+106
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still want to store the cycle, if it completes, before shutdown.
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| bool completed; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| readonly RetentionMetrics metrics; | ||||||||||||||||||||||
| readonly RetentionEntity entity; | ||||||||||||||||||||||
| readonly CancellationToken cancellationToken; | ||||||||||||||||||||||
| readonly Stopwatch stopwatch = Stopwatch.StartNew(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| namespace ServiceControl.Persistence.Tests; | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.Metrics; | ||
| using System.Linq; | ||
| using ServiceControl.Persistence.EFCore.Infrastructure; | ||
|
|
||
| /// <summary> | ||
| /// Collects everything the retention instruments record, for the meter belonging to one factory. | ||
| /// Every fixture in the run shares the meter name, so the factory is what tells these instruments | ||
| /// apart from the ones another test left behind. | ||
| /// </summary> | ||
| sealed class RecordedRetentionMetrics : IDisposable | ||
| { | ||
| public RecordedRetentionMetrics(IMeterFactory meterFactory) | ||
| { | ||
| listener = new MeterListener | ||
| { | ||
| InstrumentPublished = (instrument, activeListener) => | ||
| { | ||
| if (instrument.Meter.Name == RetentionMetrics.MeterName && ReferenceEquals(instrument.Meter.Scope, meterFactory)) | ||
| { | ||
| activeListener.EnableMeasurementEvents(instrument); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, _) => Add(instrument, measurement, tags)); | ||
| listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, _) => Add(instrument, measurement, tags)); | ||
| listener.Start(); | ||
| } | ||
|
|
||
| public IReadOnlyList<Recorded> Of(string instrumentName, RetentionEntity entity) | ||
| { | ||
| lock (measurements) | ||
| { | ||
| return | ||
| [ | ||
| .. measurements.Where(measurement => | ||
| measurement.InstrumentName == instrumentName && | ||
| Equals(measurement.Tags["retention.entity"], EntityTag(entity))) | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| public IReadOnlyList<Recorded> Cycles(RetentionEntity entity) => Of(RetentionMetrics.CycleDurationInstrumentName, entity); | ||
|
|
||
| public double RowsDeleted(RetentionEntity entity) => | ||
| Of(RetentionMetrics.RowsDeletedInstrumentName, entity).Sum(measurement => measurement.Value); | ||
|
|
||
| public double ConsecutiveFailures(RetentionEntity entity) | ||
| { | ||
| listener.RecordObservableInstruments(); | ||
|
|
||
| return Of(RetentionMetrics.ConsecutiveFailuresInstrumentName, entity)[^1].Value; | ||
| } | ||
|
|
||
| public void Dispose() => listener.Dispose(); | ||
|
|
||
| void Add(Instrument instrument, double value, ReadOnlySpan<KeyValuePair<string, object>> tags) | ||
| { | ||
| var copied = new Dictionary<string, object>(); | ||
|
|
||
| foreach (var tag in tags) | ||
| { | ||
| copied[tag.Key] = tag.Value; | ||
| } | ||
|
|
||
| lock (measurements) | ||
| { | ||
| measurements.Add(new Recorded(instrument.Name, value, copied)); | ||
| } | ||
| } | ||
|
|
||
| static string EntityTag(RetentionEntity entity) => entity switch | ||
| { | ||
| RetentionEntity.FailedMessages => "failed_messages", | ||
| RetentionEntity.EventLog => "event_log", | ||
| RetentionEntity.GroupComments => "group_comments", | ||
| _ => throw new ArgumentOutOfRangeException(nameof(entity)) | ||
| }; | ||
|
|
||
| readonly MeterListener listener; | ||
| readonly List<Recorded> measurements = []; | ||
|
|
||
| public sealed record Recorded(string InstrumentName, double Value, Dictionary<string, object> Tags) | ||
| { | ||
| public object Result => Tags["result"]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these classes be in separate files, and possibly in their own namespace/folder?