| File: Model\MetricPinStore.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Configuration; using Aspire.Dashboard.Otlp.Model; using Microsoft.Extensions.Options; namespace Aspire.Dashboard.Model; /// <summary> /// Stores pinned metric instruments in the application settings database, independently of individual runs. /// </summary> /// <remarks> /// Pins are keyed by meter and instrument name, not by resource. Pinning <c>http.server.request.duration</c> /// once pins it for every service that reports it, which matches how the same instrument is compared across /// services, and keeps pins valid when resources are renamed or replicas get new instance names. /// </remarks> public sealed class MetricPinStore { private const string SettingsName = "pinned metrics"; private readonly Lock _lock = new(); private readonly List<OtlpInstrumentKey> _pins = []; private readonly DashboardSettingsDatabase _database; private bool _loaded; public MetricPinStore(IOptions<DashboardOptions> options, ILogger<MetricPinStore> logger) : this(DashboardSettingsDatabase.GetDatabasePath(options.Value), logger) { } internal MetricPinStore(string? databasePath, ILogger logger) { _database = new DashboardSettingsDatabase(databasePath, logger); } /// <summary> /// Raised after pins change. Handlers may run on any thread. /// </summary> public event Action? Changed; /// <summary> /// Gets the pinned instruments in the order they were pinned. /// </summary> public IReadOnlyList<OtlpInstrumentKey> GetPins() { lock (_lock) { EnsureLoaded(); return [.. _pins]; } } /// <summary> /// Gets the pinned instruments among <paramref name="instruments"/>, in the order they were pinned. /// Pins for instruments that aren't in the list, such as ones only another resource reports, are skipped. /// </summary> public List<OtlpInstrumentSummary> GetPinnedInstruments(IEnumerable<OtlpInstrumentSummary> instruments) { // The same instrument can come from several resources, and each of them is pinned along with it. var instrumentsByKey = instruments.ToLookup(i => i.GetKey()); return [.. GetPins().SelectMany(key => instrumentsByKey[key])]; } public bool IsPinned(OtlpInstrumentKey key) { lock (_lock) { EnsureLoaded(); return _pins.Contains(key); } } /// <summary> /// Pins or unpins an instrument. Returns <see langword="false"/> if the change couldn't be saved, /// in which case the pins are unchanged. /// </summary> public bool SetPinned(OtlpInstrumentKey key, bool pinned) { lock (_lock) { EnsureLoaded(); if (_pins.Contains(key) == pinned) { return true; } if (!Save(key, pinned)) { return false; } if (pinned) { _pins.Add(key); } else { _pins.Remove(key); } } Changed?.Invoke(); return true; } private void EnsureLoaded() { if (_loaded) { return; } _loaded = true; _database.Load(SettingsName, connection => { using var command = connection.CreateCommand(); // pin_id is the rowid, which increases with each insert, so ordering by it lists pins in the // order they were pinned. command.CommandText = """ CREATE TABLE IF NOT EXISTS metric_pins ( pin_id INTEGER NOT NULL PRIMARY KEY, meter_name TEXT NOT NULL, instrument_name TEXT NOT NULL, UNIQUE (meter_name, instrument_name) ); SELECT meter_name, instrument_name FROM metric_pins ORDER BY pin_id; """; using var reader = command.ExecuteReader(); while (reader.Read()) { _pins.Add(new OtlpInstrumentKey(reader.GetString(0), reader.GetString(1))); } }); } private bool Save(OtlpInstrumentKey key, bool pinned) { return _database.TrySave(SettingsName, connection => { using var command = connection.CreateCommand(); command.CommandText = pinned ? "INSERT OR IGNORE INTO metric_pins (meter_name, instrument_name) VALUES ($meter, $instrument);" : "DELETE FROM metric_pins WHERE meter_name = $meter AND instrument_name = $instrument;"; command.Parameters.AddWithValue("$meter", key.MeterName); command.Parameters.AddWithValue("$instrument", key.InstrumentName); command.ExecuteNonQuery(); }); } }