Seen on a customer's SQL Server 2005 SP4 (legacy ODBC): - agent-jobs declared @since with an initial value (2008+): "Incorrect syntax near CONVERT" and SQLI-JOB-001/002 never ran. Now DECLARE; SET. - plan-analysis used query_hash (2008+) and failed every 30 minutes for SQLI-PLAN-001/002/003. On 2005 a statement is now identified by sql_handle + offsets, as top SQL does. It needs sys.dm_exec_text_query_plan, so the new capability TextQueryPlan (2005 SP2, 9.0.3042) is required. - COM-DB-001 used OBJECT_SCHEMA_NAME (2005 SP2+); now joins sys.objects and sys.schemas. Audit of all collector SQL and community rules without minimumVersion 10.0: the rest is 2005 syntax or behind a version/capability check. Sql2005CompatibilityTests (2005 cannot run in CI): every collector that can run on 2005 is scanned for 2008+ syntax in its SQL (inline DECLARE, +=, MERGE, CONCAT/IIF/FORMAT/TRY_*, OFFSET/FETCH, date/time types, multi-row VALUES), version-branched files listed explicitly; the same for community rules; and the 2005 variants of plan analysis and top SQL must not use query_hash. Verified to fail on the old agent-jobs SQL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
58 lines
3.1 KiB
C#
58 lines
3.1 KiB
C#
using System.Globalization;
|
|
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Instances;
|
|
|
|
namespace SQLInsight.Collectors.Instance;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class AgentJobCollector : Collector<AgentJobSnapshot>
|
|
{
|
|
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<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
|
|
|
|
protected override async Task<AgentJobSnapshot> 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 };
|
|
}
|
|
|
|
/// <summary>msdb stores 2026-09-29 13:05:07 as run_date 20260929 and run_time 130507 (server local time).</summary>
|
|
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;
|
|
}
|