Skip to content

Commit 6608734

Browse files
authored
Merge pull request #17 from nemvince/custom-logger-support
Add custom logger support
2 parents c91a906 + 5bb7b22 commit 6608734

File tree

6 files changed

+163
-15
lines changed

6 files changed

+163
-15
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,21 @@ await baker.saveState();
327327
await baker.restoreState();
328328
```
329329

330+
#### Using a Custom Logger
331+
332+
You can provide a custom logger that implements the `Logger` interface to log messages from Cronbake:
333+
334+
```typescript
335+
import { getLogger } from '@logtape/logtape';
336+
import { Baker, FilePersistenceProvider } from 'cronbake';
337+
338+
const logger = getLogger(['mirar', 'cron']);
339+
340+
export const baker = Baker.create({
341+
logger
342+
});
343+
```
344+
330345
#### Persistence Providers (File and Redis)
331346

332347
Cronbake uses pluggable providers for persistence. A file provider is included by default. A Redis provider is available; you inject your Redis client.

lib/baker.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
JobMetrics,
1010
SchedulerConfig,
1111
PersistenceOptions,
12+
Logger,
1213
} from "./types";
1314
import { FilePersistenceProvider } from "./persistence/file";
1415
import type { PersistenceProvider } from "./persistence/types";
@@ -27,8 +28,10 @@ class Baker implements IBaker {
2728
private persistenceProvider?: PersistenceProvider;
2829
private enableMetrics: boolean;
2930
private onError?: (error: Error, jobName: string) => void;
31+
private logger: Logger;
3032

3133
constructor(options: IBakerOptions = {}) {
34+
this.logger = options.logger ?? console;
3235
this.config = {
3336
pollingInterval: options.schedulerConfig?.pollingInterval ?? 1000,
3437
useCalculatedTimeouts:
@@ -65,7 +68,7 @@ class Baker implements IBaker {
6568
if (this.persistence.enabled && this.persistence.autoRestore) {
6669
const restorePromise = this.restoreState();
6770
restorePromise.catch((err) => {
68-
console.warn("Failed to restore state:", err);
71+
this.logger.warn("Failed to restore state:", err);
6972
});
7073
this.initialRestorePromise = restorePromise;
7174
}
@@ -94,14 +97,19 @@ class Baker implements IBaker {
9497
pollingInterval: this.config.pollingInterval,
9598
};
9699

100+
const jobLogger = options.logger ?? this.logger;
101+
97102
const enhancedOptions: CronOptions<T> = {
98103
...options,
99104
maxHistory: options.maxHistory ?? this.config.maxHistoryEntries,
105+
logger: jobLogger,
100106
onError:
101107
options.onError ??
102108
((error: Error) => {
103109
if (this.onError) {
104110
this.onError(error, options.name);
111+
} else {
112+
jobLogger.warn(`Cron job '${options.name}' failed:`, error);
105113
}
106114
}),
107115
};
@@ -113,7 +121,7 @@ class Baker implements IBaker {
113121

114122
if (this.persistence.enabled && !this.isRestoring) {
115123
this.saveState().catch((err) => {
116-
console.warn("Failed to save state:", err);
124+
this.logger.warn("Failed to save state:", err);
117125
});
118126
}
119127

@@ -130,7 +138,7 @@ class Baker implements IBaker {
130138

131139
if (this.persistence.enabled) {
132140
this.saveState().catch((err) => {
133-
console.warn("Failed to save state:", err);
141+
this.logger.warn("Failed to save state:", err);
134142
});
135143
}
136144
}
@@ -174,7 +182,7 @@ class Baker implements IBaker {
174182

175183
if (this.persistence.enabled) {
176184
this.saveState().catch((err) => {
177-
console.warn("Failed to save state:", err);
185+
this.logger.warn("Failed to save state:", err);
178186
});
179187
}
180188
}
@@ -259,7 +267,7 @@ class Baker implements IBaker {
259267

260268
if (this.persistence.enabled) {
261269
this.saveState().catch((err) => {
262-
console.warn("Failed to save state:", err);
270+
this.logger.warn("Failed to save state:", err);
263271
});
264272
}
265273
}
@@ -314,7 +322,7 @@ class Baker implements IBaker {
314322
let restoredCount = 0;
315323
for (const jobData of state.jobs) {
316324
if (!jobData.name || !jobData.cron) {
317-
console.warn("Skipping invalid job data:", jobData);
325+
this.logger.warn("Skipping invalid job data:", jobData);
318326
continue;
319327
}
320328
if (jobData.persist === false) {
@@ -329,7 +337,7 @@ class Baker implements IBaker {
329337
name: jobData.name,
330338
cron: jobData.cron,
331339
callback: () => {
332-
console.warn(
340+
this.logger.warn(
333341
`Restored job '${jobData.name}' executed but no callback was provided`
334342
);
335343
},
@@ -356,11 +364,11 @@ class Baker implements IBaker {
356364
this.restoredJobs.add(jobData.name);
357365
restoredCount++;
358366
} catch (error) {
359-
console.warn(`Failed to restore job '${jobData.name}':`, error);
367+
this.logger.warn(`Failed to restore job '${jobData.name}':`, error);
360368
}
361369
}
362370
if (restoredCount > 0) {
363-
console.log(`Restored ${restoredCount} cron jobs from persistence`);
371+
this.logger.info(`Restored ${restoredCount} cron jobs from persistence`);
364372
}
365373
} catch (error) {
366374
throw new Error(`Failed to restore state: ${error}`);

lib/cron.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type Status,
88
type ExecutionHistory,
99
type JobMetrics,
10+
type Logger,
1011
} from "./types";
1112
import CronParser from "./parser";
1213
import { CBResolver, resolveIfPromise } from "./utils";
@@ -42,6 +43,7 @@ class Cron<T extends string = string> implements ICron<T> {
4243
private immediate: boolean;
4344
private initialDelayMs?: number;
4445
private lastRunTime: Date | null = null;
46+
private logger: Logger;
4547

4648
/**
4749
* Creates a new instance of the `Cron` class.
@@ -54,11 +56,12 @@ class Cron<T extends string = string> implements ICron<T> {
5456
throw new Error("Cron job name is required and must be a string");
5557
}
5658

59+
this.logger = options.logger ?? console;
5760
this.name = options.name;
5861
this.cron = options.cron;
5962
this.callback = options.callback;
60-
this.onTick = CBResolver.bind(this, options.onTick);
61-
this.onComplete = CBResolver.bind(this, options.onComplete);
63+
this.onTick = CBResolver.bind(this, options.onTick, undefined, this.logger);
64+
this.onComplete = CBResolver.bind(this, options.onComplete, undefined, this.logger);
6265
this.onError = options.onError;
6366
this.priority = options.priority ?? 0;
6467
this.maxHistory = options.maxHistory ?? 100;
@@ -269,10 +272,10 @@ class Cron<T extends string = string> implements ICron<T> {
269272
try {
270273
this.onError(err instanceof Error ? err : new Error(String(err)));
271274
} catch (handlerError) {
272-
console.warn("Error handler failed:", handlerError);
275+
this.logger.warn("Error handler failed:", handlerError);
273276
}
274277
} else {
275-
console.warn(`Cron job '${this.name}' failed:`, err);
278+
this.logger.warn(`Cron job '${this.name}' failed:`, err);
276279
}
277280
this.metrics.lastError = error;
278281
} finally {

lib/logger.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, it, expect, jest } from "bun:test";
2+
import Baker from "./baker";
3+
import Cron from "./cron";
4+
import { Logger } from "./types";
5+
6+
describe("Custom Logger", () => {
7+
it("should use custom logger in Cron when passed via Baker", async () => {
8+
const mockLogger: Logger = {
9+
info: jest.fn(),
10+
error: jest.fn(),
11+
warn: jest.fn(),
12+
debug: jest.fn(),
13+
};
14+
15+
const baker = new Baker({
16+
logger: mockLogger,
17+
});
18+
19+
const cron = baker.add({
20+
name: "test-cron-logger",
21+
cron: "* * * * * *",
22+
callback: () => {
23+
throw new Error("Test error");
24+
},
25+
start: true,
26+
immediate: true,
27+
});
28+
29+
await new Promise((r) => setTimeout(r, 30));
30+
31+
cron.stop();
32+
baker.destroyAll();
33+
34+
// Should have logged a warning
35+
expect(mockLogger.warn).toHaveBeenCalled();
36+
});
37+
38+
it("should use custom logger in Cron when passed directly", async () => {
39+
const mockLogger: Logger = {
40+
info: jest.fn(),
41+
error: jest.fn(),
42+
warn: jest.fn(),
43+
debug: jest.fn(),
44+
};
45+
46+
const cron = new Cron({
47+
name: "test-cron-direct-logger",
48+
cron: "* * * * * *",
49+
callback: () => {
50+
throw new Error("Test error direct");
51+
},
52+
logger: mockLogger,
53+
start: true,
54+
immediate: true,
55+
});
56+
57+
await new Promise((r) => setTimeout(r, 30));
58+
59+
cron.stop();
60+
cron.destroy();
61+
62+
expect(mockLogger.warn).toHaveBeenCalled();
63+
});
64+
65+
it("should allow overriding logger per job in Baker", async () => {
66+
const bakerLogger: Logger = {
67+
info: jest.fn(),
68+
error: jest.fn(),
69+
warn: jest.fn(),
70+
debug: jest.fn(),
71+
};
72+
73+
const jobLogger: Logger = {
74+
info: jest.fn(),
75+
error: jest.fn(),
76+
warn: jest.fn(),
77+
debug: jest.fn(),
78+
};
79+
80+
const baker = new Baker({
81+
logger: bakerLogger,
82+
});
83+
84+
const cron = baker.add({
85+
name: "test-override-logger",
86+
cron: "* * * * * *",
87+
callback: () => {
88+
throw new Error("Test error override");
89+
},
90+
logger: jobLogger,
91+
start: true,
92+
immediate: true,
93+
});
94+
95+
await new Promise((r) => setTimeout(r, 30));
96+
97+
cron.stop();
98+
baker.destroyAll();
99+
100+
expect(jobLogger.warn).toHaveBeenCalled();
101+
expect(bakerLogger.warn).not.toHaveBeenCalled();
102+
});
103+
});

lib/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ type CronTime = {
77
dayOfWeek?: number[];
88
};
99

10+
/**
11+
* Interface for a logger that implements standard logging methods.
12+
*/
13+
export interface Logger {
14+
info(message: any, ...args: any[]): void;
15+
error(message: any, ...args: any[]): void;
16+
warn(message: any, ...args: any[]): void;
17+
debug(message: any, ...args: any[]): void;
18+
}
19+
1020
/**
1121
* Interface for a cron parser that can parse a cron expression and provide
1222
* the next and previous execution times.
@@ -272,6 +282,10 @@ type CronOptions<T extends string = string> = {
272282
* If true, run the first callback immediately on start (before schedule)
273283
*/
274284
immediate?: boolean;
285+
/**
286+
* Custom logger instance
287+
*/
288+
logger?: Logger;
275289
/**
276290
* If set with `immediate: true`, delay the first run by this amount.
277291
* Accepts number (ms) or strings like '500ms', '10s', '2m', '1h'.
@@ -485,6 +499,7 @@ interface IBakerOptions {
485499
persistence?: PersistenceOptions;
486500
enableMetrics?: boolean;
487501
onError?: (error: Error, jobName: string) => void;
502+
logger?: Logger;
488503
}
489504

490505
export {

lib/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import { Logger } from "./types";
2+
13
const resolveIfPromise = async (value: any) =>
24
value instanceof Promise ? await value : value;
35

46
/**
57
* Resolves a callback function, handling both sync and async functions
68
* @param callback - The callback function to execute
79
* @param onError - Optional error handler
10+
* @param logger - Optional logger
811
*/
912
const CBResolver = async (
1013
callback?: () => void | Promise<void>,
11-
onError?: (error: Error) => void
14+
onError?: (error: Error) => void,
15+
logger: Logger = console
1216
) => {
1317
try {
1418
if (callback) {
@@ -18,7 +22,7 @@ const CBResolver = async (
1822
if (onError) {
1923
onError(error as Error);
2024
} else {
21-
console.warn('Callback execution failed:', error);
25+
logger.warn('Callback execution failed:', error);
2226
}
2327
}
2428
};

0 commit comments

Comments
 (0)