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; }