Repository navigation
feat(node): Add cronJobIntegration to monitor cron jobs #25477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wedamija
wants to merge
21
commits into
danf/node-schedule-monitor-jobs
Choose a base branch
from
danf/cron-monitor-jobs
base: danf/node-schedule-monitor-jobs
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+329
−21
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
cf8b5be
feat(node): Add cronJobIntegration to monitor cron jobs
wedamija 1908bf3
test(node): Skip cron integration suite on bun run
wedamija 487dc0c
chore(deps): Update pnpm-lock.yaml for cron 4 and dedupe luxon
wedamija 0fa3047
Merge branch 'danf/node-schedule-monitor-jobs' into danf/cron-monitor…
wedamija c11e2fd
ref(node): Use the shared job-creation helpers in cronJobIntegration
wedamija be15930
Merge branch 'danf/node-schedule-monitor-jobs' into danf/cron-monitor…
wedamija dbd9ecb
ref(node): Mark monitored cron jobs without a double cast
wedamija 0b0c3e1
Merge branch 'danf/node-schedule-monitor-jobs' into danf/cron-monitor…
wedamija 8cd6820
Merge remote-tracking branch 'origin/danf/node-schedule-monitor-jobs'…
wedamija aa654d6
test(node): Cover cron jobs sent without a config or skipped as one-off
wedamija 2c35a5c
Merge remote-tracking branch 'origin/danf/node-schedule-monitor-jobs'…
wedamija 01a7c4a
Merge danf/node-schedule-monitor-jobs into danf/cron-monitor-jobs
wedamija 4c444cf
Merge remote-tracking branch 'origin/danf/node-schedule-monitor-jobs'…
wedamija a1f362d
ref(node): Pass cronJobIntegration options to the subscriber
wedamija c5ea0a9
Merge danf/node-schedule-monitor-jobs into danf/cron-monitor-jobs
wedamija d13ffdd
ref(node): Export cronJobIntegration from Deno
wedamija b4bbbe4
Merge danf/node-schedule-monitor-jobs into danf/cron-monitor-jobs
wedamija 86acf7f
fix(node): Send the new schedule after a cron job's setTime
wedamija 92466ee
Merge remote-tracking branch 'origin/danf/node-schedule-monitor-jobs'…
wedamija b065781
Merge branch 'danf/node-schedule-monitor-jobs' into danf/cron-monitor…
wedamija cba31f0
Merge branch 'danf/node-schedule-monitor-jobs' into danf/cron-monitor…
wedamija File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
dev-packages/node-integration-tests/suites/cron/cron-integration/instrument.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
|
||
| Sentry.init({ | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '1.0', | ||
| transport: loggingTransport, | ||
| integrations: [Sentry.cronJobIntegration({ excludeJobs: ['excluded-job'] })], | ||
| }); |
37 changes: 37 additions & 0 deletions
37
dev-packages/node-integration-tests/suites/cron/cron-integration/scenario.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { CronJob, CronTime } from 'cron'; | ||
|
|
||
| const excluded = CronJob.from({ cronTime: '0 0 * * *', onTick: () => {}, name: 'excluded-job' }); | ||
| const unnamed = new CronJob('0 0 * * *', () => {}); | ||
| const oneOff = CronJob.from({ cronTime: new Date(Date.now() + 86_400_000), onTick: () => {}, name: 'one-off-job' }); | ||
| const offset = CronJob.from({ cronTime: '0 0 * * *', utcOffset: 60, onTick: () => {}, name: 'offset-job' }); | ||
| const subMinute = CronJob.from({ cronTime: '*/10 * * * * *', onTick: () => {}, name: 'sub-minute-job' }); | ||
|
|
||
| let runs = 0; | ||
| const job = CronJob.from({ | ||
| cronTime: '0 30 9 * * 1-5', | ||
| timeZone: 'Europe/Vienna', | ||
| name: 'my-cron-job', | ||
| waitForCompletion: true, | ||
| errorHandler: () => {}, | ||
| onTick: async () => { | ||
| runs++; | ||
| if (runs === 2) { | ||
| throw new Error('Error in cron job'); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Run the jobs without waiting for their schedule. | ||
| (async () => { | ||
| await excluded.fireOnTick(); | ||
| await unnamed.fireOnTick(); | ||
| await oneOff.fireOnTick(); | ||
| await offset.fireOnTick(); | ||
| await subMinute.fireOnTick(); | ||
| await job.fireOnTick(); | ||
| await job.fireOnTick(); | ||
| job.setTime(new CronTime('0 8 * * *', 'UTC')); | ||
| await job.fireOnTick(); | ||
| })(); | ||
|
|
||
| setTimeout(() => process.exit(), 10_000); |
68 changes: 68 additions & 0 deletions
68
dev-packages/node-integration-tests/suites/cron/cron-integration/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import type { SerializedCheckIn } from '@sentry/core'; | ||
| import { afterAll, describe, expect } from 'vitest'; | ||
| import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; | ||
|
|
||
| const MONITOR_CONFIG = { | ||
| schedule: { type: 'crontab', value: '30 9 * * 1-5' }, | ||
| timezone: 'Europe/Vienna', | ||
| }; | ||
|
|
||
| const expectNoConfig = (slug: string) => (checkIn: SerializedCheckIn) => { | ||
| expect(checkIn).toMatchObject({ monitor_slug: slug, status: 'in_progress' }); | ||
| expect(checkIn.monitor_config).toBeUndefined(); | ||
| }; | ||
|
|
||
| describe('cronJobIntegration', () => { | ||
| afterAll(() => { | ||
| cleanupChildProcesses(); | ||
| }); | ||
|
|
||
| createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { | ||
| test('sends check-ins for named jobs', async () => { | ||
| await createRunner() | ||
| .ignore('event') | ||
| .expect({ check_in: expectNoConfig('offset-job') }) | ||
| .expect({ check_in: { monitor_slug: 'offset-job', status: 'ok' } }) | ||
| .expect({ check_in: expectNoConfig('sub-minute-job') }) | ||
| .expect({ check_in: { monitor_slug: 'sub-minute-job', status: 'ok' } }) | ||
| .expect({ | ||
| check_in: { monitor_slug: 'my-cron-job', status: 'in_progress', monitor_config: MONITOR_CONFIG }, | ||
| }) | ||
| .expect({ check_in: { monitor_slug: 'my-cron-job', status: 'ok', duration: expect.any(Number) } }) | ||
| .expect({ | ||
| check_in: { monitor_slug: 'my-cron-job', status: 'in_progress', monitor_config: MONITOR_CONFIG }, | ||
| }) | ||
| .expect({ check_in: { monitor_slug: 'my-cron-job', status: 'error', duration: expect.any(Number) } }) | ||
| .expect({ | ||
| check_in: { | ||
| monitor_slug: 'my-cron-job', | ||
| status: 'in_progress', | ||
| monitor_config: { schedule: { type: 'crontab', value: '0 8 * * *' }, timezone: 'UTC' }, | ||
| }, | ||
| }) | ||
| .expect({ check_in: { monitor_slug: 'my-cron-job', status: 'ok', duration: expect.any(Number) } }) | ||
| .start() | ||
| .completed(); | ||
| }); | ||
|
|
||
| test('captures job errors', async () => { | ||
| await createRunner() | ||
| .unordered() | ||
| .expect({ | ||
| event: { | ||
| exception: { | ||
| values: [ | ||
| { | ||
| type: 'Error', | ||
| value: 'Error in cron job', | ||
| mechanism: { type: 'auto.function.cron', handled: false }, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }) | ||
| .start() | ||
| .completed(); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import type { IntegrationFn, MonitorConfig } from '@sentry/core'; | ||
| import { defineIntegration } from '@sentry/core'; | ||
| import { CHANNELS } from '../orchestrion/channels'; | ||
| import { cronModuleNames } from '../orchestrion/config/cron'; | ||
| import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; | ||
| import type { AnyFn, ScheduleChange } from '../utils/cronMonitoring'; | ||
| import { | ||
| cronExpressionToMonitorConfig, | ||
| shouldMonitorJob, | ||
| subscribeToJobCreation, | ||
| subscribeToScheduleChange, | ||
| updateMonitorConfig, | ||
| wrapScheduledJob, | ||
| } from '../utils/cronMonitoring'; | ||
|
|
||
| const INTEGRATION_NAME = 'CronJob' as const; | ||
|
|
||
| export interface CronJobIntegrationOptions { | ||
| /** Job names not to monitor. Strings must match the name exactly. */ | ||
| excludeJobs?: Array<string | RegExp>; | ||
| } | ||
|
|
||
| const _cronJobIntegration = ((options: CronJobIntegrationOptions = {}) => { | ||
| return { | ||
| name: INTEGRATION_NAME, | ||
| setup(client) { | ||
| invokeOrchestrionInstrumentation(client, cronModuleNames, subscribe, [options], { | ||
| requiresTracingChannelBinding: false, | ||
| }); | ||
| }, | ||
| }; | ||
| }) satisfies IntegrationFn; | ||
|
|
||
| /** | ||
| * Sends Crons check-ins for every `CronJob` of the `cron` package (4.3+) that has a `name`. | ||
| * The monitor slug is the job name; jobs without a name, and one-off jobs, are not monitored. | ||
| */ | ||
| export const cronJobIntegration = defineIntegration(_cronJobIntegration); | ||
|
|
||
| function subscribe(options: CronJobIntegrationOptions): void { | ||
| subscribeToJobCreation(CHANNELS.CRON_JOB_CONSTRUCTOR, INTEGRATION_NAME, args => wrapJob(args, options)); | ||
| subscribeToScheduleChange(CHANNELS.CRON_JOB_SET_TIME, INTEGRATION_NAME, onSetTime); | ||
| } | ||
|
|
||
| /** Sends the new time's config after `job.setTime(cronTime)`. */ | ||
| export function onSetTime({ arguments: [cronTime], self }: ScheduleChange): void { | ||
| const job = self as { cronTime?: unknown; _callbacks?: unknown[] } | undefined; | ||
| // `setTime` throws and keeps the old time when it isn't given a `CronTime`. | ||
| if (!job || job.cronTime !== cronTime) { | ||
| return; | ||
| } | ||
| const { source, timeZone, utcOffset } = cronTime as { source?: unknown; timeZone?: unknown; utcOffset?: unknown }; | ||
| const monitorConfig: MonitorConfig | undefined = | ||
| typeof source === 'string' && utcOffset == null ? cronExpressionToMonitorConfig(source, timeZone, 'or') : undefined; | ||
| for (const callback of job._callbacks || []) { | ||
| updateMonitorConfig(callback, monitorConfig); | ||
| } | ||
| } | ||
|
|
||
| // `new CronJob(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, ..., name)` | ||
| function wrapJob(args: unknown[], options: CronJobIntegrationOptions): void { | ||
| const [cronTime, onTick, , , timeZone] = args; | ||
| const utcOffset = args[7]; | ||
| const name = args[11]; | ||
| if (typeof cronTime !== 'string' || !shouldMonitorJob(name, onTick, options.excludeJobs)) { | ||
| return; | ||
| } | ||
| const monitorConfig = utcOffset == null ? cronExpressionToMonitorConfig(cronTime, timeZone, 'or') : undefined; | ||
| args[1] = wrapScheduledJob(onTick as AnyFn, name, monitorConfig, 'auto.function.cron'); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import type { InstrumentationConfig } from '../apmTypes'; | ||
| import { getModuleNames } from './module-names'; | ||
|
|
||
| // `CronJob.from()` calls the constructor. Job names were added in 4.3.0. | ||
| export const cronConfig = [ | ||
| { | ||
| channelName: 'cronJobConstructor', | ||
| module: { name: 'cron', versionRange: '>=4.3.0 <5', filePath: 'dist/job.js' }, | ||
| functionQuery: { className: 'CronJob' }, | ||
| }, | ||
| { | ||
| channelName: 'cronJobSetTime', | ||
| module: { name: 'cron', versionRange: '>=4.3.0 <5', filePath: 'dist/job.js' }, | ||
| functionQuery: { className: 'CronJob', methodName: 'setTime', kind: 'Sync' }, | ||
| }, | ||
| ] satisfies InstrumentationConfig[]; | ||
|
|
||
| export const cronModuleNames = getModuleNames(cronConfig); | ||
|
|
||
| export const cronChannels = { | ||
| CRON_JOB_CONSTRUCTOR: 'orchestrion:cron:cronJobConstructor', | ||
| CRON_JOB_SET_TIME: 'orchestrion:cron:cronJobSetTime', | ||
| } as const; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.