dynamics-ax (hourly) lists the monitored AX databases with version, schema, companies and read access; discovery and the AX community rules (now marked dynamicsAx) use the same detection. dynamics-ax-batch (5 min) reads BATCH (3.0/4.0) or BATCHJOB/BATCHJOBHISTORY (2009+), keeps each job's own run history; dynamics-ax-sequences (hourly) reads NUMBERSEQUENCETABLE and SYSTEMSEQUENCES and forecasts from SQLInsight's own daily samples. All reads run through [db].sys.sp_executesql under read committed with a 2 s lock timeout (snapshot when allowed), bounded with TOP, never NOLOCK, never written. Blocking now captures CONTEXT_INFO for the AX user mapping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
93 lines
5.7 KiB
C#
93 lines
5.7 KiB
C#
using SQLInsight.Core.DynamicsAx;
|
|
using SQLInsight.Core.Instances;
|
|
|
|
namespace SQLInsight.Collectors.DynamicsAx;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class AxSequenceCollector(AxDatabaseDetector detector, AxThresholds? thresholds = null) : Collector<AxSequenceSnapshot>
|
|
{
|
|
private readonly AxThresholds _thresholds = thresholds ?? new AxThresholds();
|
|
|
|
public override string Name => "dynamics-ax-sequences";
|
|
public override TimeSpan DefaultInterval => TimeSpan.FromHours(1);
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
|
|
|
|
protected override async Task<AxSequenceSnapshot> CollectAsync(CollectorContext context, AxSequenceSnapshot? previous)
|
|
{
|
|
var ax = await detector.GetAsync(context).ConfigureAwait(false);
|
|
var now = context.Now.UtcDateTime;
|
|
var history = new Dictionary<string, IReadOnlyList<AxSample>>(StringComparer.Ordinal);
|
|
var databases = new List<AxSequenceDatabase>();
|
|
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<AxSample> 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<string>();
|
|
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<string, IReadOnlyList<AxSample>>())
|
|
{
|
|
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));
|
|
}
|