Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ await baker.saveState();
await baker.restoreState();
```

#### Using a Custom Logger

You can provide a custom logger that implements the `Logger` interface to log messages from Cronbake:

```typescript
import { getLogger } from '@logtape/logtape';
import { Baker, FilePersistenceProvider } from 'cronbake';

const logger = getLogger(['mirar', 'cron']);

export const baker = Baker.create({
logger
});
```

#### Persistence Providers (File and Redis)

Cronbake uses pluggable providers for persistence. A file provider is included by default. A Redis provider is available; you inject your Redis client.
Expand Down
26 changes: 17 additions & 9 deletions lib/baker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
JobMetrics,
SchedulerConfig,
PersistenceOptions,
Logger,
} from "./types";
import { FilePersistenceProvider } from "./persistence/file";
import type { PersistenceProvider } from "./persistence/types";
Expand All @@ -27,8 +28,10 @@ class Baker implements IBaker {
private persistenceProvider?: PersistenceProvider;
private enableMetrics: boolean;
private onError?: (error: Error, jobName: string) => void;
private logger: Logger;

constructor(options: IBakerOptions = {}) {
this.logger = options.logger ?? console;
this.config = {
pollingInterval: options.schedulerConfig?.pollingInterval ?? 1000,
useCalculatedTimeouts:
Expand Down Expand Up @@ -65,7 +68,7 @@ class Baker implements IBaker {
if (this.persistence.enabled && this.persistence.autoRestore) {
const restorePromise = this.restoreState();
restorePromise.catch((err) => {
console.warn("Failed to restore state:", err);
this.logger.warn("Failed to restore state:", err);
});
this.initialRestorePromise = restorePromise;
}
Expand Down Expand Up @@ -94,14 +97,19 @@ class Baker implements IBaker {
pollingInterval: this.config.pollingInterval,
};

const jobLogger = options.logger ?? this.logger;

const enhancedOptions: CronOptions<T> = {
...options,
maxHistory: options.maxHistory ?? this.config.maxHistoryEntries,
logger: jobLogger,
onError:
options.onError ??
((error: Error) => {
if (this.onError) {
this.onError(error, options.name);
} else {
jobLogger.warn(`Cron job '${options.name}' failed:`, error);
}
}),
};
Expand All @@ -113,7 +121,7 @@ class Baker implements IBaker {

if (this.persistence.enabled && !this.isRestoring) {
this.saveState().catch((err) => {
console.warn("Failed to save state:", err);
this.logger.warn("Failed to save state:", err);
});
}

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

if (this.persistence.enabled) {
this.saveState().catch((err) => {
console.warn("Failed to save state:", err);
this.logger.warn("Failed to save state:", err);
});
}
}
Expand Down Expand Up @@ -174,7 +182,7 @@ class Baker implements IBaker {

if (this.persistence.enabled) {
this.saveState().catch((err) => {
console.warn("Failed to save state:", err);
this.logger.warn("Failed to save state:", err);
});
}
}
Expand Down Expand Up @@ -259,7 +267,7 @@ class Baker implements IBaker {

if (this.persistence.enabled) {
this.saveState().catch((err) => {
console.warn("Failed to save state:", err);
this.logger.warn("Failed to save state:", err);
});
}
}
Expand Down Expand Up @@ -314,7 +322,7 @@ class Baker implements IBaker {
let restoredCount = 0;
for (const jobData of state.jobs) {
if (!jobData.name || !jobData.cron) {
console.warn("Skipping invalid job data:", jobData);
this.logger.warn("Skipping invalid job data:", jobData);
continue;
}
if (jobData.persist === false) {
Expand All @@ -329,7 +337,7 @@ class Baker implements IBaker {
name: jobData.name,
cron: jobData.cron,
callback: () => {
console.warn(
this.logger.warn(
`Restored job '${jobData.name}' executed but no callback was provided`
);
},
Expand All @@ -356,11 +364,11 @@ class Baker implements IBaker {
this.restoredJobs.add(jobData.name);
restoredCount++;
} catch (error) {
console.warn(`Failed to restore job '${jobData.name}':`, error);
this.logger.warn(`Failed to restore job '${jobData.name}':`, error);
}
}
if (restoredCount > 0) {
console.log(`Restored ${restoredCount} cron jobs from persistence`);
this.logger.info(`Restored ${restoredCount} cron jobs from persistence`);
}
} catch (error) {
throw new Error(`Failed to restore state: ${error}`);
Expand Down
11 changes: 7 additions & 4 deletions lib/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type Status,
type ExecutionHistory,
type JobMetrics,
type Logger,
} from "./types";
import CronParser from "./parser";
import { CBResolver, resolveIfPromise } from "./utils";
Expand Down Expand Up @@ -42,6 +43,7 @@ class Cron<T extends string = string> implements ICron<T> {
private immediate: boolean;
private initialDelayMs?: number;
private lastRunTime: Date | null = null;
private logger: Logger;

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

this.logger = options.logger ?? console;
this.name = options.name;
this.cron = options.cron;
this.callback = options.callback;
this.onTick = CBResolver.bind(this, options.onTick);
this.onComplete = CBResolver.bind(this, options.onComplete);
this.onTick = CBResolver.bind(this, options.onTick, undefined, this.logger);
this.onComplete = CBResolver.bind(this, options.onComplete, undefined, this.logger);
this.onError = options.onError;
this.priority = options.priority ?? 0;
this.maxHistory = options.maxHistory ?? 100;
Expand Down Expand Up @@ -269,10 +272,10 @@ class Cron<T extends string = string> implements ICron<T> {
try {
this.onError(err instanceof Error ? err : new Error(String(err)));
} catch (handlerError) {
console.warn("Error handler failed:", handlerError);
this.logger.warn("Error handler failed:", handlerError);
}
} else {
console.warn(`Cron job '${this.name}' failed:`, err);
this.logger.warn(`Cron job '${this.name}' failed:`, err);
}
this.metrics.lastError = error;
} finally {
Expand Down
103 changes: 103 additions & 0 deletions lib/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, it, expect, jest } from "bun:test";
import Baker from "./baker";
import Cron from "./cron";
import { Logger } from "./types";

describe("Custom Logger", () => {
it("should use custom logger in Cron when passed via Baker", async () => {
const mockLogger: Logger = {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};

const baker = new Baker({
logger: mockLogger,
});

const cron = baker.add({
name: "test-cron-logger",
cron: "* * * * * *",
callback: () => {
throw new Error("Test error");
},
start: true,
immediate: true,
});

await new Promise((r) => setTimeout(r, 30));

cron.stop();
baker.destroyAll();

// Should have logged a warning
expect(mockLogger.warn).toHaveBeenCalled();
});

it("should use custom logger in Cron when passed directly", async () => {
const mockLogger: Logger = {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};

const cron = new Cron({
name: "test-cron-direct-logger",
cron: "* * * * * *",
callback: () => {
throw new Error("Test error direct");
},
logger: mockLogger,
start: true,
immediate: true,
});

await new Promise((r) => setTimeout(r, 30));

cron.stop();
cron.destroy();

expect(mockLogger.warn).toHaveBeenCalled();
});

it("should allow overriding logger per job in Baker", async () => {
const bakerLogger: Logger = {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};

const jobLogger: Logger = {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};

const baker = new Baker({
logger: bakerLogger,
});

const cron = baker.add({
name: "test-override-logger",
cron: "* * * * * *",
callback: () => {
throw new Error("Test error override");
},
logger: jobLogger,
start: true,
immediate: true,
});

await new Promise((r) => setTimeout(r, 30));

cron.stop();
baker.destroyAll();

expect(jobLogger.warn).toHaveBeenCalled();
expect(bakerLogger.warn).not.toHaveBeenCalled();
});
});
15 changes: 15 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ type CronTime = {
dayOfWeek?: number[];
};

/**
* Interface for a logger that implements standard logging methods.
*/
export interface Logger {
info(message: any, ...args: any[]): void;
error(message: any, ...args: any[]): void;
warn(message: any, ...args: any[]): void;
debug(message: any, ...args: any[]): void;
}

/**
* Interface for a cron parser that can parse a cron expression and provide
* the next and previous execution times.
Expand Down Expand Up @@ -272,6 +282,10 @@ type CronOptions<T extends string = string> = {
* If true, run the first callback immediately on start (before schedule)
*/
immediate?: boolean;
/**
* Custom logger instance
*/
logger?: Logger;
/**
* If set with `immediate: true`, delay the first run by this amount.
* Accepts number (ms) or strings like '500ms', '10s', '2m', '1h'.
Expand Down Expand Up @@ -485,6 +499,7 @@ interface IBakerOptions {
persistence?: PersistenceOptions;
enableMetrics?: boolean;
onError?: (error: Error, jobName: string) => void;
logger?: Logger;
}

export {
Expand Down
8 changes: 6 additions & 2 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Logger } from "./types";

const resolveIfPromise = async (value: any) =>
value instanceof Promise ? await value : value;

/**
* Resolves a callback function, handling both sync and async functions
* @param callback - The callback function to execute
* @param onError - Optional error handler
* @param logger - Optional logger
*/
const CBResolver = async (
callback?: () => void | Promise<void>,
onError?: (error: Error) => void
onError?: (error: Error) => void,
logger: Logger = console
) => {
try {
if (callback) {
Expand All @@ -18,7 +22,7 @@ const CBResolver = async (
if (onError) {
onError(error as Error);
} else {
console.warn('Callback execution failed:', error);
logger.warn('Callback execution failed:', error);
}
}
};
Expand Down