📄
CheckMonitoringJob.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
using System.Diagnostics; using BfiMonitor; using Microsoft.Extensions.Logging; using Microsoft.Playwright; using Quartz; [DisallowConcurrentExecution] internal sealed class CheckMonitoringJob( PlaywrightBrowserService browserService, ScreeningRepository repository, ILogger<CheckMonitoringJob> logger, TimeProvider timeProvider ) : IJob { public async Task Execute(IJobExecutionContext context) { var monitorId = context.MergedJobDataMap.GetInt("monitorId"); var force = context.MergedJobDataMap.GetBoolean("force"); using var activity = Tracing.StartCheckerJobForMonitor(monitorId); activity?.SetTag("Force", force); var monitor = await repository.GetMonitoringByIdAsync(monitorId, context.CancellationToken); if (monitor is null) { logger.LogMonitoringNotFound(monitorId); activity?.SetTag("EndReason", "MonitoringNotFound"); activity?.SetStatus(ActivityStatusCode.Ok); return; } activity?.SetTag("MonitorUrl", monitor.Url); if (monitor.ArchivedAt is not null) { logger.LogMonitoringArchived(monitorId); activity?.SetTag("EndReason", "MonitoringArchived"); activity?.SetStatus(ActivityStatusCode.Ok); return; } if (monitor.PausedAt is not null && !force) { logger.LogMonitoringPaused(monitorId); activity?.SetTag("EndReason", "MonitoringPaused"); activity?.SetStatus(ActivityStatusCode.Ok); return; } var latestHtml = await repository.LatestHtml(monitor.Id, context.CancellationToken); logger.LogLatestHtml(monitor.Id, latestHtml); var page = await browserService.NewPageAsync(); try { logger.LogFetchingSite(monitor.Id, monitor.Url); await page.GotoAsync( monitor.Url, new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = TimeSpan.FromSeconds(45).Milliseconds } ); await Task.Delay(TimeSpan.FromSeconds(2)); var resultsElement = await page.WaitForSelectorAsync(monitor.Selector); if (resultsElement is null) { logger.LogNoResultsElementFound(monitor.Id, monitor.Selector); activity?.SetTag("EndReason", "NoResultsElementFound"); activity?.SetStatus(ActivityStatusCode.Ok); return; } var currentHtml = await resultsElement.InnerHTMLAsync(); logger.LogCurrentHtml(monitor.Id, currentHtml); if (currentHtml == latestHtml) { logger.LogHtmlNotUpdated(monitor.Id); activity?.SetTag("EndReason", "HtmlNotUpdated"); activity?.SetStatus(ActivityStatusCode.Ok); return; } logger.LogHtmlUpdated(monitor.Id); await repository.InsertNewDetection( monitor.Id, currentHtml, timeProvider.GetUtcNow(), context.CancellationToken ); var label = string.IsNullOrWhiteSpace(monitor.Name) ? monitor.Url : monitor.Name; var message = $"Site updated: {label}"; foreach (var phoneNumber in monitor.PhoneNumbers) { var dataMap = new JobDataMap { { "phoneNumber", phoneNumber }, { "message", message } }; var trigger = TriggerBuilder.Create().ForJob(SendSmsJob.Key).UsingJobData(dataMap).StartNow().Build(); await context.Scheduler.ScheduleJob(trigger, context.CancellationToken); logger.LogSmsScheduled(phoneNumber); } activity?.SetTag("EndReason", "ChangeDetected"); activity?.SetStatus(ActivityStatusCode.Ok); } catch (Exception ex) { activity?.SetTag("EndReason", "Exception"); activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); throw; } finally { await page.CloseAsync(); } } } internal static partial class CheckMonitoringJobLoggerExtensions { [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: not found, skipping check")] public static partial void LogMonitoringNotFound(this ILogger logger, int monitorId); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: archived, skipping check")] public static partial void LogMonitoringArchived(this ILogger logger, int monitorId); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: paused, skipping scheduled check")] public static partial void LogMonitoringPaused(this ILogger logger, int monitorId); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: latest HTML was {LatestHtml}")] public static partial void LogLatestHtml(this ILogger logger, int monitorId, string? latestHtml); [LoggerMessage(LogLevel.Warning, "Monitor {MonitorId}: no {ElementSelector} element found")] public static partial void LogNoResultsElementFound(this ILogger logger, int monitorId, string elementSelector); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: fetching {Url}")] public static partial void LogFetchingSite(this ILogger logger, int monitorId, string url); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: current HTML is {CurrentHtml}")] public static partial void LogCurrentHtml(this ILogger logger, int monitorId, string? currentHtml); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: HTML has not changed")] public static partial void LogHtmlNotUpdated(this ILogger logger, int monitorId); [LoggerMessage(LogLevel.Information, "Monitor {MonitorId}: HTML has changed")] public static partial void LogHtmlUpdated(this ILogger logger, int monitorId); [LoggerMessage(LogLevel.Information, "Scheduled SMS to {PhoneNumber}")] public static partial void LogSmsScheduled(this ILogger logger, string phoneNumber); }