using System.Globalization;
using SQLInsight.Core.Data;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.Instance;
///
/// SQL Agent jobs from msdb. Uses only the tables granted by the least-privilege script (no msdb functions, which
/// would need EXECUTE); run_date/run_time integers are converted here.
///
public sealed class AgentJobCollector : Collector
{
internal const string RunningSql = "SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE program_name LIKE N'SQLAgent - %'";
internal const string JobsSql = """
DECLARE @since int;
SET @since = CONVERT(int, CONVERT(char(8), DATEADD(day, -1, GETDATE()), 112));
SELECT j.name, j.enabled, last_run.run_status, last_run.run_date, last_run.run_time,
failed_step.step_name, LEFT(failed_step.message, 2000), failures.n
FROM msdb.dbo.sysjobs j
OUTER APPLY (SELECT TOP (1) h.run_status, h.run_date, h.run_time FROM msdb.dbo.sysjobhistory h
WHERE h.job_id = j.job_id AND h.step_id = 0 ORDER BY h.instance_id DESC) last_run
OUTER APPLY (SELECT TOP (1) h.step_name, h.message FROM msdb.dbo.sysjobhistory h
WHERE h.job_id = j.job_id AND h.step_id > 0 AND h.run_status = 0 ORDER BY h.instance_id DESC) failed_step
OUTER APPLY (SELECT COUNT(*) AS n FROM msdb.dbo.sysjobhistory h
WHERE h.job_id = j.job_id AND h.step_id = 0 AND h.run_status = 0 AND h.run_date >= @since) failures
""";
public override string Name => "agent-jobs";
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(5);
public override IReadOnlyCollection RequiredCapabilities => [Capability.DynamicManagementViews];
protected override async Task CollectAsync(CollectorContext context, AgentJobSnapshot? previous)
{
var ct = context.CancellationToken;
var sessions = await Db.QueryAsync(context.Connection, RunningSql, r => r.Int(0), ct).ConfigureAwait(false);
var jobs = await Db.QueryAsync(context.Connection, JobsSql, r => new AgentJob
{
Name = r.String(0),
IsEnabled = r.Bool(1),
LastRunStatus = r.NullableInt(2),
LastRunAt = r.IsNull(3) ? null : ToDateTime(r.Int(3), r.Int(4)),
LastFailedStepName = r.NullableString(5),
LastFailedStepMessage = r.NullableString(6),
FailuresLastDay = r.NullableInt(7) ?? 0,
}, ct).ConfigureAwait(false);
return new AgentJobSnapshot { CapturedAt = context.Now, IsAgentRunning = sessions[0] > 0, Jobs = jobs };
}
/// msdb stores 2026-09-29 13:05:07 as run_date 20260929 and run_time 130507 (server local time).
internal static DateTime? ToDateTime(int runDate, int runTime) =>
DateTime.TryParseExact(
runDate.ToString("00000000", CultureInfo.InvariantCulture) + runTime.ToString("000000", CultureInfo.InvariantCulture),
"yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)
? value
: null;
}