Skip to content

Commit 398f53a

Browse files
boredlandSkn0tt
andauthored
feat: allow passing a logger function to quirrel client (#1039)
currently the logging is really simple and very verbose. this gives users the options to define their own logger functions for the quirrel client. Co-authored-by: Simon Knott <[email protected]>
1 parent 43668e6 commit 398f53a

File tree

9 files changed

+175
-38
lines changed

9 files changed

+175
-38
lines changed

src/client/index.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,24 @@ export interface JobMeta
2323
readonly nextRepetition?: Date;
2424
}
2525

26+
export type QuirrelLogger<Payload = unknown> = {
27+
receivedJob: (route: string, data: Payload) => void;
28+
processingError: (route: string, data: Payload, error: unknown) => void;
29+
};
30+
31+
const defaultLogger: QuirrelLogger = {
32+
receivedJob: (route, data) => console.log(`Received job to ${route}`, data),
33+
processingError: (route, data, error) =>
34+
console.error(`Error in job at ${route}`, data, error),
35+
};
36+
2637
export type QuirrelJobHandler<T> = (job: T, meta: JobMeta) => Promise<void>;
2738
export type DefaultJobOptions = Pick<EnqueueJobOptions, "exclusive" | "retry">;
2839

2940
interface CreateQuirrelClientArgs<T> {
3041
route: string;
3142
handler: QuirrelJobHandler<T>;
32-
defaultJobOptions?: DefaultJobOptions;
43+
options?: QuirrelOptions<T>;
3344
config?: {
3445
/**
3546
* Recommended way to set this: process.env.QUIRREL_BASE_URL
@@ -137,10 +148,6 @@ const EnqueueJobOptionsSchema = z.object({
137148

138149
type EnqueueJobOptionsSchema = z.TypeOf<typeof EnqueueJobOptionsSchema>;
139150

140-
type EnqueueJobOptionssSchemaMatchesDocs = AssertTrue<
141-
IsExact<EnqueueJobOptions, EnqueueJobOptionsSchema>
142-
>;
143-
144151
/**
145152
* @deprecated renamed to EnqueueJobOptions
146153
*/
@@ -250,6 +257,10 @@ function getAuthHeaders(
250257
return { Authorization: `Bearer ${token}` };
251258
}
252259

260+
export interface QuirrelOptions<T = unknown> extends DefaultJobOptions {
261+
logger?: QuirrelLogger<T>;
262+
}
263+
253264
export class QuirrelClient<T> {
254265
private handler;
255266
private route;
@@ -265,17 +276,19 @@ export class QuirrelClient<T> {
265276
private fetch;
266277
private catchDecryptionErrors;
267278
private signaturePublicKey;
279+
private logger: QuirrelLogger<T>;
268280

269281
constructor(args: CreateQuirrelClientArgs<T>) {
270282
this.handler = args.handler;
271-
this.defaultJobOptions = args.defaultJobOptions;
283+
this.defaultJobOptions = args.options;
272284

273285
const token = args.config?.token ?? config.getQuirrelToken();
274286
this.defaultHeaders = {
275287
...getAuthHeaders(token),
276288
"X-QuirrelClient-Version": pack.version,
277289
};
278290

291+
this.logger = args.options?.logger ?? defaultLogger;
279292
const quirrelBaseUrl =
280293
args.config?.quirrelBaseUrl ?? config.getQuirrelBaseUrl();
281294
this.applicationBaseUrl = config.withoutTrailingSlash(
@@ -691,7 +704,7 @@ export class QuirrelClient<T> {
691704
(headers["x-quirrel-meta"] as string) ?? "{}"
692705
);
693706

694-
console.log(`Received job to ${this.route}: `, payload);
707+
this.logger.receivedJob(this.route, payload);
695708

696709
try {
697710
await this.handler(payload, {
@@ -708,7 +721,7 @@ export class QuirrelClient<T> {
708721
body: "OK",
709722
};
710723
} catch (error) {
711-
console.error(error);
724+
this.logger.processingError(this.route, payload, error);
712725
return {
713726
status: 500,
714727
headers: {},

src/client/test/logger.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { run } from "../../api/test/runQuirrel";
2+
import { getAddress, makeSignal } from "./util";
3+
import { QuirrelClient } from "..";
4+
import http from "http";
5+
6+
const errorLogMock = jest.fn();
7+
const logMock = jest.fn();
8+
const receiverMock = jest.fn();
9+
10+
global.console = {
11+
...global.console,
12+
error: errorLogMock,
13+
log: logMock,
14+
};
15+
16+
describe("client logger", () => {
17+
let quirrelBaseUrl: string;
18+
let server: Awaited<ReturnType<typeof run>>;
19+
20+
beforeAll(async () => {
21+
server = await run("Mock");
22+
quirrelBaseUrl = getAddress(server.server);
23+
});
24+
25+
afterAll(async () => {
26+
server.teardown();
27+
});
28+
29+
beforeEach(() => {
30+
errorLogMock.mockReset();
31+
logMock.mockReset();
32+
});
33+
34+
it("works without passing a custom logger function", async () => {
35+
const quirrel = new QuirrelClient<{ result: "success" | "fail" }>({
36+
route: "routeName",
37+
async handler(payload) {
38+
if (payload.result === "fail") throw new Error("yey,errors");
39+
},
40+
config: {
41+
quirrelBaseUrl,
42+
applicationBaseUrl: "http://localhost",
43+
},
44+
});
45+
await quirrel.respondTo(JSON.stringify({ result: "success" }), {});
46+
expect(logMock).toHaveBeenCalledTimes(1);
47+
expect(logMock).toHaveBeenLastCalledWith("Received job to routeName", {
48+
result: "success",
49+
});
50+
expect(errorLogMock).toHaveBeenCalledTimes(0);
51+
52+
await quirrel.respondTo(JSON.stringify({ result: "fail" }), {});
53+
expect(logMock).toHaveBeenCalledTimes(2);
54+
expect(logMock).toHaveBeenLastCalledWith("Received job to routeName", {
55+
result: "fail",
56+
});
57+
expect(errorLogMock).toHaveBeenCalledTimes(1);
58+
expect(errorLogMock).toHaveBeenLastCalledWith(
59+
"Error in job at routeName",
60+
{ result: "fail" },
61+
new Error("yey,errors")
62+
);
63+
});
64+
65+
it("allows passing a custom logger function", async () => {
66+
const receivedJob = jest.fn();
67+
const processingError = jest.fn();
68+
69+
const quirrel = new QuirrelClient<{ result: "success" | "fail" }>({
70+
route: "",
71+
async handler(payload) {
72+
if (payload.result === "fail") throw new Error("fail");
73+
},
74+
config: {
75+
quirrelBaseUrl,
76+
applicationBaseUrl: "http://localhost",
77+
},
78+
options: {
79+
logger: {
80+
receivedJob,
81+
processingError,
82+
},
83+
},
84+
});
85+
await quirrel.respondTo(JSON.stringify({ result: "success" }), {});
86+
expect(receivedJob).toBeCalledTimes(1);
87+
expect(processingError).toBeCalledTimes(0);
88+
expect(logMock).toHaveBeenCalledTimes(0);
89+
expect(errorLogMock).toHaveBeenCalledTimes(0);
90+
91+
await quirrel.respondTo(JSON.stringify({ result: "fail" }), {});
92+
expect(receivedJob).toBeCalledTimes(2);
93+
expect(processingError).toBeCalledTimes(1);
94+
expect(logMock).toHaveBeenCalledTimes(0);
95+
expect(errorLogMock).toHaveBeenCalledTimes(0);
96+
});
97+
});

src/connect.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
EnqueueJobOpts,
77
EnqueueJobOptions,
88
Job,
9+
QuirrelOptions,
910
} from "./client";
1011
import bodyParser from "body-parser";
1112

@@ -29,12 +30,12 @@ declare module "connect" {
2930
export function Queue<Payload>(
3031
route: string,
3132
handler: QuirrelJobHandler<Payload>,
32-
defaultJobOptions?: DefaultJobOptions
33+
options?: QuirrelOptions<Payload>,
3334
): Queue<Payload> {
3435
const quirrel = new QuirrelClient({
3536
route,
3637
handler,
37-
defaultJobOptions,
38+
options,
3839
});
3940

4041
const server = connect() as Queue<Payload>;
@@ -71,7 +72,8 @@ export function Queue<Payload>(
7172
export function CronJob(
7273
route: string,
7374
cronSchedule: NonNullable<NonNullable<EnqueueJobOptions["repeat"]>["cron"]>,
74-
handler: () => Promise<void>
75+
handler: () => Promise<void>,
76+
options?: QuirrelOptions
7577
) {
76-
return Queue(route, handler) as unknown;
78+
return Queue(route, handler, options) as unknown;
7779
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ export {
55
EnqueueJobOptions,
66
Job,
77
QuirrelClient,
8+
QuirrelOptions,
9+
QuirrelLogger,
810
} from "./client";
911
export { runQuirrelDev } from "./cli/commands/index";

src/next.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Job,
66
DefaultJobOptions,
77
QuirrelJobHandler,
8+
QuirrelOptions,
89
} from "./client";
910
import { registerDevelopmentDefaults } from "./client/config";
1011
import type { IncomingHttpHeaders } from "http";
@@ -45,10 +46,10 @@ export type Queue<Payload> = Omit<
4546
export function Queue<Payload>(
4647
route: string,
4748
handler: QuirrelJobHandler<Payload>,
48-
defaultJobOptions?: DefaultJobOptions
49+
options?: QuirrelOptions,
4950
): Queue<Payload> & NextApiHandler {
5051
const quirrel = new QuirrelClient<Payload>({
51-
defaultJobOptions,
52+
options,
5253
handler,
5354
route,
5455
});
@@ -83,7 +84,8 @@ export function Queue<Payload>(
8384
export function CronJob(
8485
route: string,
8586
cronSchedule: NonNullable<NonNullable<EnqueueJobOptions["repeat"]>["cron"]>,
86-
handler: () => Promise<void>
87+
handler: () => Promise<void>,
88+
options?: QuirrelOptions
8789
) {
88-
return Queue(route, handler) as unknown;
90+
return Queue(route, handler, options) as unknown;
8991
}

src/nuxt.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EnqueueJobOptions, QuirrelClient } from "./client";
1+
import { EnqueueJobOptions, QuirrelClient, QuirrelOptions } from "./client";
22
import { registerDevelopmentDefaults } from "./client/config";
33
import * as connect from "./connect";
44

@@ -9,9 +9,9 @@ registerDevelopmentDefaults({
99
export function Queue<Payload>(
1010
path: string,
1111
handler: connect.QuirrelJobHandler<Payload>,
12-
defaultJobOptions?: connect.DefaultJobOptions
12+
options?: QuirrelOptions,
1313
): Omit<QuirrelClient<Payload>, "respondTo" | "makeRequest"> {
14-
const client = connect.Queue(path, handler, defaultJobOptions);
14+
const client = connect.Queue(path, handler, options);
1515

1616
(client as any).path = path;
1717
(client as any).handler = client.handle;
@@ -22,9 +22,10 @@ export function Queue<Payload>(
2222
export function CronJob(
2323
route: string,
2424
cronSchedule: NonNullable<NonNullable<EnqueueJobOptions["repeat"]>["cron"]>,
25-
handler: () => Promise<void>
25+
handler: () => Promise<void>,
26+
options?: QuirrelOptions
2627
) {
27-
return Queue(route, handler) as unknown;
28+
return Queue(route, handler, options) as unknown;
2829
}
2930

3031
export * from "./connect";

src/redwood.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import {
55
Job,
66
DefaultJobOptions,
77
QuirrelJobHandler,
8+
QuirrelOptions,
89
} from "./client";
910
import { registerDevelopmentDefaults } from "./client/config";
1011

11-
export { Job, EnqueueJobOpts, EnqueueJobOptions, DefaultJobOptions, QuirrelJobHandler };
12+
export {
13+
Job,
14+
EnqueueJobOpts,
15+
EnqueueJobOptions,
16+
DefaultJobOptions,
17+
QuirrelJobHandler,
18+
};
1219

1320
registerDevelopmentDefaults({
1421
applicationBaseUrl: "http://localhost:8911",
@@ -38,10 +45,10 @@ export type Queue<Payload> = Omit<
3845
export function Queue<Payload>(
3946
route: string,
4047
handler: QuirrelJobHandler<Payload>,
41-
defaultJobOptions?: DefaultJobOptions
48+
options?: QuirrelOptions<Payload>,
4249
): Queue<Payload> {
4350
const quirrel = new QuirrelClient<Payload>({
44-
defaultJobOptions,
51+
options,
4552
handler,
4653
route,
4754
});
@@ -79,7 +86,8 @@ export function Queue<Payload>(
7986
export function CronJob(
8087
route: string,
8188
cronSchedule: NonNullable<NonNullable<EnqueueJobOptions["repeat"]>["cron"]>,
82-
handler: () => Promise<void>
89+
handler: () => Promise<void>,
90+
options?: QuirrelOptions
8391
) {
84-
return Queue(route, handler) as unknown;
92+
return Queue(route, handler, options) as unknown;
8593
}

src/remix.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import {
55
Job,
66
DefaultJobOptions,
77
QuirrelJobHandler,
8+
QuirrelOptions,
89
} from "./client";
910
import { registerDevelopmentDefaults } from "./client/config";
1011

1112
type DataFunctionArgs = {
1213
request: Request;
13-
}
14+
};
1415

1516
type ActionFunction = (args: DataFunctionArgs) => Promise<Response> | Response;
1617

@@ -34,26 +35,32 @@ export type Queue<Payload> = Omit<
3435
export function Queue<Payload>(
3536
route: string,
3637
handler: QuirrelJobHandler<Payload>,
37-
defaultJobOptions?: DefaultJobOptions,
38+
options?: QuirrelOptions<Payload>
3839
): ActionFunction & Queue<Payload> {
3940
const quirrel = new QuirrelClient<Payload>({
40-
defaultJobOptions,
41+
options,
4142
handler,
4243
route,
4344
});
4445

4546
async function action({ request }: DataFunctionArgs) {
4647
const body = await request.text();
47-
const response = await quirrel.respondTo(body, Object.fromEntries(request.headers.entries()));
48+
const response = await quirrel.respondTo(
49+
body,
50+
Object.fromEntries(request.headers.entries())
51+
);
4852
return new Response(response.body, {
4953
headers: response.headers,
5054
status: response.status,
5155
});
5256
}
5357

54-
action.enqueue = (payload: Payload, options: EnqueueJobOptions) => quirrel.enqueue(payload, options);
58+
action.enqueue = (payload: Payload, options: EnqueueJobOptions) =>
59+
quirrel.enqueue(payload, options);
5560

56-
action.enqueueMany = (jobs: { payload: Payload; options?: EnqueueJobOptions }[]) => quirrel.enqueueMany(jobs);
61+
action.enqueueMany = (
62+
jobs: { payload: Payload; options?: EnqueueJobOptions }[]
63+
) => quirrel.enqueueMany(jobs);
5764

5865
action.delete = (jobId: string) => quirrel.delete(jobId);
5966

@@ -70,6 +77,7 @@ export function CronJob(
7077
route: string,
7178
cronSchedule: NonNullable<NonNullable<EnqueueJobOptions["repeat"]>["cron"]>,
7279
handler: () => Promise<void>,
80+
options?: QuirrelOptions
7381
) {
74-
return Queue(route, handler) as unknown;
82+
return Queue(route, handler, options) as unknown;
7583
}

0 commit comments

Comments
 (0)