sqlinsight-pro/src/SQLInsight.Core/Configuration/AgentRequests.cs
Søren Eggert Lundsteen Olsen 6b945295d0 Dynamics AX collectors: shared detector, batch jobs, number sequences and RecId
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>
2026-10-02 13:53:34 +02:00

150 lines
6.3 KiB
C#

using System.Text.Json;
namespace SQLInsight.Core.Configuration;
/// <summary>
/// What SQLInsight Web asks the agent to do (insight.AgentRequests). Connection tests and discovery run in the agent,
/// as the agent's identity, because that is what connects to the instances in production; the web UI only queues the
/// request and shows the result.
/// </summary>
public static class AgentRequestKind
{
/// <summary>Connect to a configured instance and check the agent's rights there.</summary>
public const string ConnectionTest = "ConnectionTest";
/// <summary>List the databases on a configured instance (state, size, recovery model, Dynamics AX …).</summary>
public const string DatabaseDiscovery = "DatabaseDiscovery";
/// <summary>Find SQL Server instances on a computer (SQL Server Browser, and the registry on the agent's own computer).</summary>
public const string InstanceDiscovery = "InstanceDiscovery";
public static readonly IReadOnlyList<string> All = [ConnectionTest, DatabaseDiscovery, InstanceDiscovery];
}
public static class AgentRequestStatus
{
public const string Pending = "Pending";
public const string Running = "Running";
public const string Done = "Done";
public const string Failed = "Failed";
}
/// <summary>JSON of request parameters and results, shared by the agent (writes) and the web UI and API (reads).</summary>
public static class AgentRequestJson
{
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
public static string Serialize<T>(T value) => JsonSerializer.Serialize(value, Options);
public static T? Deserialize<T>(string? json)
where T : class
{
if (string.IsNullOrWhiteSpace(json))
{
return null;
}
try
{
return JsonSerializer.Deserialize<T>(json, Options);
}
catch (JsonException)
{
return null;
}
}
}
/// <summary>Parameters of an <see cref="AgentRequestKind.InstanceDiscovery"/>.</summary>
/// <param name="Host">The computer to ask; empty for the agent's own computer.</param>
public sealed record InstanceDiscoveryParameters(string? Host);
/// <summary>One right the agent needs (or may have) on an instance, and whether it has it.</summary>
/// <param name="Name">The permission as written in GRANT, e.g. VIEW SERVER STATE, or SELECT ON msdb.dbo.backupset.</param>
/// <param name="Required">False for optional rights (ALTER TRACE): missing them limits a feature, not the monitoring.</param>
/// <param name="Purpose">What SQLInsight uses it for (Danish).</param>
public sealed record PermissionStatus(string Name, bool Granted, bool Required, string Purpose);
/// <summary>The outcome of a connection test, as the agent saw it.</summary>
public sealed record ConnectionTestResult
{
public bool Connected { get; init; }
public string? Error { get; init; }
/// <summary>What to do about <see cref="Error"/> (Danish).</summary>
public string? Hint { get; init; }
public string? Provider { get; init; }
public bool UsedLegacyFallback { get; init; }
public string? ProductVersion { get; init; }
public string? Release { get; init; }
public string? Edition { get; init; }
public string? ServerName { get; init; }
/// <summary>The login the agent arrived as (SUSER_SNAME()): the one the GRANT script is for.</summary>
public string? Login { get; init; }
/// <summary>The agent is sysadmin on the instance. It works, but it is more than it needs.</summary>
public bool IsSysadmin { get; init; }
/// <summary>False on SQL Server 2000, which cannot report effective permissions (no HAS_PERMS_BY_NAME).</summary>
public bool PermissionsChecked { get; init; }
public IReadOnlyList<PermissionStatus> Permissions { get; init; } = [];
/// <summary>Notes about the result, e.g. why permissions were not checked (Danish).</summary>
public IReadOnlyList<string> Notes { get; init; } = [];
public IEnumerable<PermissionStatus> MissingRequired => Permissions.Where(p => p.Required && !p.Granted);
}
/// <summary>A database as the agent found it on an instance.</summary>
public sealed record DiscoveredDatabase
{
public required string Name { get; init; }
public int DatabaseId { get; init; }
public string State { get; init; } = "ONLINE";
public long? SizeMb { get; init; }
public string? RecoveryModel { get; init; }
public int? CompatibilityLevel { get; init; }
public bool IsReadOnly { get; init; }
public bool IsSystem { get; init; }
/// <summary>A database snapshot (not monitored on its own).</summary>
public bool IsSnapshot { get; init; }
/// <summary>The agent can enter the database (HAS_DBACCESS); without it, database-specific checks cannot run there.</summary>
public bool HasAccess { get; init; }
/// <summary>Recognised as Dynamics AX by the central detection (Core/DynamicsAx/AxDetection.cs).</summary>
public bool IsDynamicsAx { get; init; }
/// <summary>The AX version as <see cref="DynamicsAx.AxVersions.ShortText"/> ("3.0", "2009", "2012 R2/R3" …), when <see cref="IsDynamicsAx"/>.</summary>
public string? DynamicsAxVersion { get; init; }
}
/// <summary>Summary of a database discovery; the databases themselves are in insight.ConfiguredDatabases.</summary>
public sealed record DatabaseDiscoveryResult
{
public int Databases { get; init; }
public IReadOnlyList<string> New { get; init; } = [];
public IReadOnlyList<string> Gone { get; init; } = [];
/// <summary>New databases were monitored at once (the instance monitors new databases automatically).</summary>
public bool NewMonitored { get; init; }
public string? Error { get; init; }
public string? Hint { get; init; }
}
/// <summary>A SQL Server instance found on a computer.</summary>
/// <param name="Server">What to type as the server: HOST or HOST\INSTANCE.</param>
/// <param name="Source">Where it was found (Danish): "SQL Server Browser" or "registreringsdatabasen".</param>
public sealed record DiscoveredInstance(string Server, string InstanceName, string? Version, int? TcpPort, bool IsClustered, string Source);
public sealed record InstanceDiscoveryResult
{
public string Host { get; init; } = string.Empty;
public IReadOnlyList<DiscoveredInstance> Instances { get; init; } = [];
public IReadOnlyList<string> Notes { get; init; } = [];
}