using SQLInsight.Core.DynamicsAx; using SQLInsight.Core.Instances; namespace SQLInsight.Collectors.DynamicsAx; /// /// Number sequences (NUMBERSEQUENCETABLE) and RecId allocation (SYSTEMSEQUENCES) in every monitored AX database where /// the agent may read them, every hour. SQLInsight samples NEXTREC and NEXTVAL once a day into its own repository /// (insight.AxSequenceSamples, bounded retention) and forecasts from that history when each runs out. Bounded with TOP /// (the most used first); never written to. /// public sealed class AxSequenceCollector(AxDatabaseDetector detector, AxThresholds? thresholds = null) : Collector { private readonly AxThresholds _thresholds = thresholds ?? new AxThresholds(); public override string Name => "dynamics-ax-sequences"; public override TimeSpan DefaultInterval => TimeSpan.FromHours(1); public override IReadOnlyCollection RequiredCapabilities => [Capability.DynamicManagementViews]; protected override async Task CollectAsync(CollectorContext context, AxSequenceSnapshot? previous) { var ax = await detector.GetAsync(context).ConfigureAwait(false); var now = context.Now.UtcDateTime; var history = new Dictionary>(StringComparer.Ordinal); var databases = new List(); foreach (var database in ax.Databases) { var name = database.Database; var earlier = previous?.Databases.FirstOrDefault(d => string.Equals(d.Database, name, StringComparison.OrdinalIgnoreCase)); if (AxSchemaAdapter.For(database.Version) is not { } adapter) { const string unknown = "Ikke målbar: AX-versionen kunne ikke bestemmes ud fra skemaet."; databases.Add(new AxSequenceDatabase { Database = name, Version = database.Version, NumberSequencesNotMeasurable = unknown, RecIdsNotMeasurable = unknown }); continue; } IReadOnlyList Samples(char kind, string key, long value) { var historyKey = AxSequenceSnapshot.HistoryKey(name, kind, key); var samples = (previous?.History.TryGetValue(historyKey, out var stored) == true ? stored : []) .Where(s => s.SampledUtc >= now.AddDays(-(_thresholds.ForecastBasisDays + 1)) && s.SampledUtc.Date != now.Date) .Append(new AxSample(now, value)) .ToList(); history[historyKey] = samples; return samples; } var notes = new List(); var numbersQuery = adapter.NumberSequences(database); notes.AddRange(numbersQuery.Notes); var (numberRows, numberProblem) = await AxReader.ReadAsync(context, database, numbersQuery, MapNumberSequence).ConfigureAwait(false); var sequences = numberRows?.Select(r => adapter.Interpret(r, Samples(AxSequenceSnapshot.NumberSequenceKind, adapter.SequenceKey(r), r.Next), now, _thresholds.ForecastBasisDays)) .ToList(); var recIdQuery = adapter.RecIds(database); notes.AddRange(recIdQuery.Notes); var nextValIsInt = string.Equals(database.Table("SYSTEMSEQUENCES")?.Column("NEXTVAL")?.Type, "int", StringComparison.OrdinalIgnoreCase); var (recIdRows, recIdProblem) = await AxReader.ReadAsync(context, database, recIdQuery, MapRecId).ConfigureAwait(false); var recIds = recIdRows?.Select(r => AxSchemaAdapter.Interpret(r, nextValIsInt, Samples(AxSequenceSnapshot.RecIdKind, AxSchemaAdapter.RecIdKey(r), r.NextVal), now, _thresholds.ForecastBasisDays)).ToList(); // A lock timeout keeps the previous values, so findings do not flap. databases.Add(new AxSequenceDatabase { Database = name, Version = database.Version, NumberSequences = sequences ?? (AxReader.IsTransient(numberProblem) ? earlier?.NumberSequences ?? [] : []), NumberSequencesNotMeasurable = sequences is null && !(AxReader.IsTransient(numberProblem) && earlier?.NumberSequencesNotMeasurable is null && earlier is not null) ? numberProblem : null, RecIds = recIds ?? (AxReader.IsTransient(recIdProblem) ? earlier?.RecIds ?? [] : []), RecIdsNotMeasurable = recIds is null && !(AxReader.IsTransient(recIdProblem) && earlier?.RecIdsNotMeasurable is null && earlier is not null) ? recIdProblem : null, Notes = notes, }); // Sequences not read this time keep their history (a lock timeout must not lose it). foreach (var (key, samples) in previous?.History ?? new Dictionary>()) { if (key.StartsWith(name + "\u001f", StringComparison.Ordinal) && !history.ContainsKey(key)) { history[key] = samples.Where(s => s.SampledUtc >= now.AddDays(-(_thresholds.ForecastBasisDays + 1))).ToList(); } } } return new AxSequenceSnapshot { CapturedAt = context.Now, Thresholds = _thresholds, Databases = databases, History = history }; } private static AxNumberSequenceRow MapNumberSequence(Row r) => new( r.Long(0), r.String(1), r.NullableString(2), r.NullableString(3), r.Long(4), r.Long(5), r.Long(6), r.NullableString(7), r.Bool(8), r.Bool(9), r.Bool(10), r.Bool(11), r.IsNull(12) ? null : r.Bool(12), r.NullableDateTime(13), r.NullableInt(14), r.IsNull(15) ? null : r.Double(15), r.Int(16)); private static AxRecIdRow MapRecId(Row r) => new(r.Int(0), r.NullableString(1), r.Long(2), r.NullableLong(3), r.NullableString(4)); }