Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
350f039
feat: add Sentry.Quartz
michaelmairegger Oct 6, 2025
be40ccc
refactor: optimize cron information retrieval in SentryCronJobListener
michaelmairegger Oct 7, 2025
ed08560
refactor: swap ILogger dependency from SentryCronJobListener with IDi…
michaelmairegger Oct 7, 2025
5b0911c
refactor: simplify Quartz configuration by removing redundant service…
michaelmairegger Oct 7, 2025
7927c33
refactor: migrate to Quartz v4.0.0 with .NET 10
michaelmairegger Aug 24, 2026
6545156
feat: include Sentry.Quartz in solution filter and generation scripts
michaelmairegger Aug 24, 2026
864de52
feat: add Quartz sample showcasing Sentry integration
michaelmairegger Aug 24, 2026
77ad99a
fix: cursor github comments
michaelmairegger Aug 24, 2026
489d073
refactor: simplify Quartz configuration and update project to use Web…
michaelmairegger Aug 24, 2026
5c2c060
refactor: replace SentryCronJobListener with SentryCronJobMiddleware …
michaelmairegger Aug 31, 2026
e1def11
fix: handling issue where current region might throw a CultureNotFoun…
michaelmairegger Aug 31, 2026
e8669e8
fix: sample project has different nuget package version
michaelmairegger Aug 31, 2026
b30f97a
fix: skip middleware execution for non-cron triggers
michaelmairegger Sep 1, 2026
9c7f36e
refactor: rename and update Quartz extension method to AddSentryCronJob
michaelmairegger Sep 1, 2026
c882dc0
feat: add SentryMetricsMiddleware to track Quartz job execution durat…
michaelmairegger Sep 1, 2026
65fc9ab
refactor: simplify AddSentryCronJob method by removing unused service…
michaelmairegger Sep 1, 2026
d1082e7
feat: add SentryScopeMiddleware and extend Quartz integration with Ad…
michaelmairegger Sep 1, 2026
e32a27e
refactor: move non-cron trigger check to StartQuartz method
michaelmairegger Sep 1, 2026
25d22a7
feat: add missing options parameter
michaelmairegger Sep 1, 2026
2718a74
refactor: remove unused project reference to Sentry.AspNetCore in Sen…
michaelmairegger Sep 1, 2026
1d13fd2
fix: Quatz.Net and Sentry handling cron expression for days differently
michaelmairegger Sep 1, 2026
7f0a48a
feat: add XML documenation
michaelmairegger Sep 7, 2026
6a91e70
feat: update Quartz to stable v4.0.0
michaelmairegger Sep 7, 2026
202c475
feat: add Quartz Dashboard and HTTP API integration, update launch URL
michaelmairegger Sep 7, 2026
8278009
feat: add customizable SentryMonitorOptions via callback
michaelmairegger Sep 7, 2026
83e92d2
style: apply dotnet format to Sentry.Quartz
jamescrosswell Sep 8, 2026
045f8d9
ci(craft): add Sentry.Quartz to the release registry
jamescrosswell Sep 8, 2026
be3fe21
test: add Sentry.Quartz.Tests
michaelmairegger Sep 8, 2026
16792a1
feat: add configurable SentryScopeMiddleware options
michaelmairegger Sep 14, 2026
0f8e237
refactor: migrate extension methods to extension block
michaelmairegger Sep 14, 2026
f0eebaf
refactor: remove SentryMetricsMiddleware
michaelmairegger Sep 14, 2026
1b46d8b
fix: use JobDetail.Key.ToString()
michaelmairegger Sep 14, 2026
15b62bb
fix: replace job type with job ID in SentryCronJobMiddleware dictiona…
michaelmairegger Sep 14, 2026
8a765b2
feat(quartz): Rework Sentry.Quartz into Sentry.Quartz4
wedamija Oct 6, 2026
fd4beb4
ref(quartz): Rename Sentry.Quartz4 to Sentry.Quartz
wedamija Oct 6, 2026
739ce97
fix(quartz): Address review feedback on Sentry.Quartz
wedamija Oct 8, 2026
fe97276
ref(quartz): Drop warn-once state and discard options from a failed c…
wedamija Oct 8, 2026
73e5551
fix(quartz): Capture job exceptions in the job scope and rename the s…
wedamija Oct 13, 2026
ccf6da4
docs(quartz): Say that AddSentry captures job exceptions
jamescrosswell Oct 15, 2026
c768096
feat(quartz): Add an option to monitor every scheduled job
wedamija Oct 17, 2026
6bb5320
Merge branch 'main' into feature/sentry.quartz
wedamija Oct 19, 2026
e957e7b
ref(quartz): Remove a leftover comment from JobMonitor
wedamija Oct 20, 2026
787ab1e
fix(quartz): Drop the schedule when jobs share a slug with different …
wedamija Oct 20, 2026
ef8b0e6
test(quartz): Skip the time zone test when there's no time zone database
wedamija Oct 20, 2026
b59c8d9
docs(quartz): Drop the stale warning from the slug attribute docs
jamescrosswell Oct 20, 2026
6c73800
Merge branch 'main' into feature/sentry.quartz
jamescrosswell Oct 21, 2026
68c1476
Merge branch 'danf/quartz-5505-rebased' into danf/quartz-bulk-cron
wedamija Oct 21, 2026
edc9757
fix(quartz): Skip jobs whose type can't be resolved when reading trig…
wedamija Oct 21, 2026
e892565
test(quartz): Pin the order of the Sentry job middleware
wedamija Oct 21, 2026
0b3b755
docs(quartz): Update the sample's comment on how job exceptions are c…
wedamija Oct 21, 2026
1383e8a
fix(quartz): Capture each job exception once with the logging integra…
wedamija Oct 21, 2026
59f4da0
Merge branch 'danf/quartz-5505-rebased' into danf/quartz-bulk-cron
wedamija Oct 21, 2026
3a84027
test(quartz): Check that AddSentry leaves unrelated options resolvable
wedamija Oct 21, 2026
b7dbd44
Merge branch 'danf/quartz-5505-rebased' into danf/quartz-bulk-cron
wedamija Oct 21, 2026
81e1888
Update src/Sentry.Quartz/QuartzErrorLogFilterSetup.cs
wedamija Oct 21, 2026
f3d7c36
ref(quartz): Drop the stale constraint wording and assertion for the …
wedamija Oct 21, 2026
5b64b1e
Merge branch 'danf/quartz-5505-rebased' into danf/quartz-bulk-cron
wedamija Oct 21, 2026
fde31be
Merge branch 'main' into danf/quartz-bulk-cron
wedamija Oct 21, 2026
9e187f6
Merge remote-tracking branch 'origin/main' into danf/quartz-bulk-cron
wedamija Oct 29, 2026
7d4293e
fix(quartz): Skip jobs with unsupported schedules when monitoring all…
wedamija Oct 29, 2026
70816f8
fix(quartz): Skip default-monitored jobs whose triggers have differen…
wedamija Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 89 additions & 22 deletions src/Sentry.Quartz/Core/JobMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

namespace Sentry.Quartz;

// Captures the check-ins of jobs with SentryMonitorSlugAttribute
// Captures the check-ins of jobs with SentryMonitorSlugAttribute, or of every scheduled job with MonitorAllJobs
internal sealed class JobMonitor
{
// How long the schedules of all monitored triggers are reused before they're read from the scheduler again
internal static readonly TimeSpan TriggerSchedulesTtl = TimeSpan.FromMinutes(1);

private static readonly ConcurrentDictionary<Type, SentryMonitorSlugAttribute?> Attributes = new();
private static readonly SentryMonitorSlugAttribute DefaultAttribute = new();

private readonly SentryQuartzOptions _options;
private readonly IHub _hub;
Expand All @@ -38,7 +39,8 @@ public JobMonitor(SentryQuartzOptions options, IHub hub, IDiagnosticLogger? logg
try
{
var jobType = QuartzApi.GetJobType(context.JobDetail);
var attribute = GetAttribute(jobType);
var attribute = GetAttribute(jobType)
?? (IsMonitoredByDefault(context) ? DefaultAttribute : null);
if (attribute is null)
{
return null;
Expand All @@ -52,9 +54,27 @@ public JobMonitor(SentryQuartzOptions options, IHub hub, IDiagnosticLogger? logg
return null;
}

configureMonitorOptions = _options.SendMonitorConfig && attribute.SendMonitorConfig
? await GetMonitorConfigAsync(context, monitorSlug, cancellationToken).ConfigureAwait(false)
: null;
var monitoredByDefault = ReferenceEquals(attribute, DefaultAttribute);
bool hasNoSchedule;
if (_options.SendMonitorConfig && attribute.SendMonitorConfig)
{
configureMonitorOptions = await GetMonitorConfigAsync(context, monitorSlug, cancellationToken)
.ConfigureAwait(false);
hasNoSchedule = configureMonitorOptions is null;
}
else
{
configureMonitorOptions = null;
hasNoSchedule = monitoredByDefault
&& await GetTriggerScheduleAsync(context, monitorSlug, cancellationToken).ConfigureAwait(false) is null;
}

if (monitoredByDefault && hasNoSchedule)
{
Logger?.LogDebug("Skipping the check-in for job '{0}'. MonitorAllJobs only monitors jobs whose " +
"schedule Sentry supports.", context.JobDetail.Key);
return null;
}
}
catch (Exception e)
{
Expand Down Expand Up @@ -153,6 +173,52 @@ public void Finish(JobCheckIn checkIn, bool failed, TimeSpan duration)
}
}

private bool IsMonitoredByDefault(IJobExecutionContext context)
{
if (!_options.MonitorAllJobs)
{
return false;
}

// Only jobs on a schedule Sentry can represent, and whose key stays the same across restarts
var key = context.JobDetail.Key;
if (!HasSentrySchedule(context.Trigger) || !HasIdentity(key))
{
return false;
}

if (IsExcluded(key))
{
Logger?.LogDebug("Skipping the check-in for job '{0}'. It matches ExcludedJobs.", key);
return false;
}

return true;
}

// Whether MonitorAllJobs covers the job's scheduled runs
private bool IsMonitoredByDefault(IJobDetail job) =>
_options.MonitorAllJobs && HasIdentity(job.Key) && !IsExcluded(job.Key);

private static bool HasSentrySchedule(ITrigger trigger) =>
TriggerSchedule.ToSchedule(trigger) is not null;

private bool IsExcluded(JobKey key)
{
var fullKey = key.ToString();
foreach (var pattern in _options.ExcludedJobs)
{
if (pattern._regex is { } regex
? regex.IsMatch(fullKey) || regex.IsMatch(key.Name)
: pattern._string == fullKey || pattern._string == key.Name)
{
return true;
}
}

return false;
}

// Reading every trigger takes several scheduler queries, so the result is reused for a minute.
private async Task<TriggerSchedules> GetTriggerSchedulesAsync(
IScheduler scheduler,
Expand All @@ -166,7 +232,7 @@ private async Task<TriggerSchedules> GetTriggerSchedulesAsync(
if (_triggerSchedules is not { } schedules || schedules.ReadAt + TriggerSchedulesTtl <= now
|| schedules.Scheduler != scheduler || !schedules.Contains(triggerKey))
{
schedules = await TriggerSchedules.ReadAsync(scheduler, now, cancellationToken).ConfigureAwait(false);
schedules = await TriggerSchedules.ReadAsync(this, scheduler, now, cancellationToken).ConfigureAwait(false);
_triggerSchedules = schedules;
}

Expand Down Expand Up @@ -196,19 +262,20 @@ private TriggerSchedules(IScheduler scheduler, DateTimeOffset readAt,
_keys = keys;
}

public static async Task<TriggerSchedules> ReadAsync(IScheduler scheduler, DateTimeOffset readAt,
CancellationToken cancellationToken)
public static async Task<TriggerSchedules> ReadAsync(JobMonitor monitor, IScheduler scheduler,
DateTimeOffset readAt, CancellationToken cancellationToken)
{
var triggersBySlug = new Dictionary<string, List<(TriggerKey Key, MonitorSchedule Schedule)>>();
var keys = new HashSet<TriggerKey>();
var triggers = await QuartzApi.GetTriggersAsync(scheduler, IsMonitored, cancellationToken).ConfigureAwait(false);
var triggers = await QuartzApi.GetTriggersAsync(scheduler, monitor.IsMonitored, cancellationToken)
.ConfigureAwait(false);
foreach (var (job, trigger) in triggers)
{
keys.Add(trigger.Key);

// Triggers whose schedule isn't sent don't change the monitor
var attribute = GetAttribute(QuartzApi.GetJobType(job))!;
if (!attribute.SendMonitorConfig
// Triggers whose runs aren't monitored, or whose schedule isn't sent, don't change the monitor
var attribute = GetAttribute(QuartzApi.GetJobType(job)) ?? DefaultAttribute;
if (attribute is not { SendMonitorConfig: true }
|| TriggerSchedule.ToSchedule(trigger) is not { } schedule
|| GetMonitorSlug(job, trigger, attribute) is not { } slug)
{
Expand Down Expand Up @@ -240,18 +307,18 @@ public List<TriggerKey> WithDifferentSchedule(string monitorSlug, TriggerKey tri
.Select(trigger => trigger.Key)
.ToList();
}
}

// Jobs whose type can't be resolved, such as a deleted job class still in a persistent store, are skipped
private static bool IsMonitored(IJobDetail job)
// Jobs whose type can't be resolved, such as a deleted job class still in a persistent store, are skipped
private bool IsMonitored(IJobDetail job)
{
try
{
try
{
return GetAttribute(QuartzApi.GetJobType(job)) is not null;
}
catch (Exception)
{
return false;
}
return GetAttribute(QuartzApi.GetJobType(job)) is not null || IsMonitoredByDefault(job);
}
catch (Exception)
{
return false;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Quartz/Core/SentryMonitorSlugAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Sentry.Quartz;
/// </summary>
/// <remarks>
/// <para>
/// Jobs without this attribute are not monitored.
/// Jobs without this attribute are not monitored, unless <see cref="SentryQuartzOptions.MonitorAllJobs"/> is enabled.
/// </para>
/// <para>
/// When no monitor slug is given, the slug is the job's group and name joined by a hyphen, normalized the way Sentry
Expand Down
28 changes: 28 additions & 0 deletions src/Sentry.Quartz/Core/SentryQuartzOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ namespace Sentry.Quartz;
/// </summary>
public class SentryQuartzOptions
{
/// <summary>
/// When enabled, every job fired by a trigger whose schedule Sentry can represent sends check-ins, as if it had
/// <see cref="SentryMonitorSlugAttribute"/> without a monitor slug. Defaults to <c>false</c>.
/// </summary>
/// <remarks>
/// <para>
/// The monitor slug is the slug of the job's key, as described on <see cref="SentryMonitorSlugAttribute"/>. Jobs
/// built without an identity are skipped, and so are runs fired by other triggers, such as a job triggered once by
/// hand or a trigger with a calendar. <see cref="SendMonitorConfig"/> lists the schedules Sentry can represent.
/// Jobs with several triggers on different schedules are skipped, and so are jobs that share a monitor slug with
/// another job on a different schedule. Jobs with <see cref="SentryMonitorSlugAttribute"/> keep its settings and
/// still send check-ins.
/// </para>
/// <para>
/// To skip some jobs, add them to <see cref="ExcludedJobs"/>.
/// </para>
/// </remarks>
public bool MonitorAllJobs { get; set; }

/// <summary>
/// Jobs that <see cref="MonitorAllJobs"/> doesn't monitor. A string matches a job whose name or full key
/// (<c>group.name</c>) is equal to it, and a regular expression a job whose name or full key it matches.
/// </summary>
/// <remarks>
/// Jobs with <see cref="SentryMonitorSlugAttribute"/> are monitored even when they match.
/// </remarks>
public IList<StringOrRegex> ExcludedJobs { get; set; } = new List<StringOrRegex>();

/// <summary>
/// When enabled, the in-progress check-in of each job with <see cref="SentryMonitorSlugAttribute"/> includes
/// the schedule of the trigger that fired the job as the monitor config, so Sentry creates the monitor or updates
Expand Down
3 changes: 2 additions & 1 deletion src/Sentry.Quartz/SentryQuartzBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public static class SentryQuartzBuilderExtensions
/// <para>
/// Jobs with <see cref="SentryMonitorSlugAttribute"/> also send a check-in each time they run, with the
/// schedule of the trigger that fired them as the monitor config. A job that throws or is cancelled sends an
/// error check-in. See <see cref="SentryQuartzOptions.SendMonitorConfig"/>.
/// error check-in. See <see cref="SentryQuartzOptions.SendMonitorConfig"/>. To monitor every scheduled job,
/// enable <see cref="SentryQuartzOptions.MonitorAllJobs"/>.
/// </para>
/// </remarks>
/// <param name="configure">Configures the options.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
{
public SentryQuartzOptions() { }
public System.Action<Quartz.IJobExecutionContext, Sentry.SentryMonitorOptions>? ConfigureMonitorOptions { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> ExcludedJobs { get; set; }
public bool MonitorAllJobs { get; set; }
public bool SendMonitorConfig { get; set; }
}
}
Loading
Loading