Skip to content

Commit 125709e

Browse files
sshaderConvex, Inc.
authored and
Convex, Inc.
committedOct 9, 2024·
Use logger abstraction in CLI + client libraries (#30497)
I'd like to make it easier to configure logging levels + some day attach listeners for log events. This adds a `Logger` class that all our logging goes through, and defaults to logging to the console (so identical behavior as before). GitOrigin-RevId: 8369e5ba460ec62d94f7cebea1cef3f2a8adb2ea
1 parent 76dfc0c commit 125709e

29 files changed

+306
-164
lines changed
 

‎eslint.config.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export default [
126126
fixable: false,
127127
},
128128
],
129+
130+
// Use `logMessage` and friends (CLI specific) or `logger.log` instead.
131+
"no-console": "error",
129132
},
130133
},
131134
{

‎src/browser/http_client.ts

+23-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
convexToJson,
1313
jsonToConvex,
1414
} from "../values/index.js";
15-
import { logToConsole } from "./logging.js";
15+
import { instantiateDefaultLogger, logForFunction, Logger } from "./logging.js";
1616
import { FunctionArgs, UserIdentityAttributes } from "../server/index.js";
1717

1818
export const STATUS_CODE_OK = 200;
@@ -46,20 +46,34 @@ export class ConvexHttpClient {
4646
private encodedTsPromise?: Promise<string>;
4747
private debug: boolean;
4848
private fetchOptions?: FetchOptions;
49-
49+
private logger: Logger;
5050
/**
5151
* Create a new {@link ConvexHttpClient}.
5252
*
5353
* @param address - The url of your Convex deployment, often provided
5454
* by an environment variable. E.g. `https://small-mouse-123.convex.cloud`.
55-
* @param skipConvexDeploymentUrlCheck - Skip validating that the Convex deployment URL looks like
55+
* @param options - An object of options.
56+
* - `skipConvexDeploymentUrlCheck` - Skip validating that the Convex deployment URL looks like
5657
* `https://happy-animal-123.convex.cloud` or localhost. This can be useful if running a self-hosted
5758
* Convex backend that uses a different URL.
59+
* - `logger` - A logger. If not provided, logs to the console.
60+
* You can construct your own logger to customize logging to log elsewhere
61+
* or not log at all.
5862
*/
59-
constructor(address: string, skipConvexDeploymentUrlCheck?: boolean) {
60-
if (skipConvexDeploymentUrlCheck !== true) {
63+
constructor(
64+
address: string,
65+
options?: { skipConvexDeploymentUrlCheck?: boolean; logger?: Logger },
66+
) {
67+
if (typeof options === "boolean") {
68+
throw new Error(
69+
"skipConvexDeploymentUrlCheck as the second argument is no longer supported. Please pass an options object, `{ skipConvexDeploymentUrlCheck: true }`.",
70+
);
71+
}
72+
const opts = options ?? {};
73+
if (opts.skipConvexDeploymentUrlCheck !== true) {
6174
validateDeploymentUrl(address);
6275
}
76+
this.logger = opts.logger ?? instantiateDefaultLogger({ verbose: false });
6377
this.address = address;
6478
this.debug = true;
6579
}
@@ -255,7 +269,7 @@ export class ConvexHttpClient {
255269

256270
if (this.debug) {
257271
for (const line of respJSON.logLines ?? []) {
258-
logToConsole("info", "query", name, line);
272+
logForFunction(this.logger, "info", "query", name, line);
259273
}
260274
}
261275
switch (respJSON.status) {
@@ -316,7 +330,7 @@ export class ConvexHttpClient {
316330
const respJSON = await response.json();
317331
if (this.debug) {
318332
for (const line of respJSON.logLines ?? []) {
319-
logToConsole("info", "mutation", name, line);
333+
logForFunction(this.logger, "info", "mutation", name, line);
320334
}
321335
}
322336
switch (respJSON.status) {
@@ -377,7 +391,7 @@ export class ConvexHttpClient {
377391
const respJSON = await response.json();
378392
if (this.debug) {
379393
for (const line of respJSON.logLines ?? []) {
380-
logToConsole("info", "action", name, line);
394+
logForFunction(this.logger, "info", "action", name, line);
381395
}
382396
}
383397
switch (respJSON.status) {
@@ -447,7 +461,7 @@ export class ConvexHttpClient {
447461
const respJSON = await response.json();
448462
if (this.debug) {
449463
for (const line of respJSON.logLines ?? []) {
450-
logToConsole("info", "any", name, line);
464+
logForFunction(this.logger, "info", "any", name, line);
451465
}
452466
}
453467
switch (respJSON.status) {

‎src/browser/logging.ts

+97-10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-console */ // This is the one file where we can `console.log` for the default logger implementation.
12
import { ConvexError, Value } from "../values/index.js";
23
import { FunctionFailure } from "./sync/function_result.js";
34

@@ -20,7 +21,97 @@ function prefix_for_source(source: UdfType) {
2021
}
2122
}
2223

23-
export function logToConsole(
24+
export type LogLevel = "debug" | "info" | "warn" | "error";
25+
26+
/**
27+
* A logger that can be used to log messages. By default, this is a wrapper
28+
* around `console`, but can be configured to not log at all or to log somewhere
29+
* else.
30+
*/
31+
export class Logger {
32+
private _onLogLineFuncs: Record<
33+
string,
34+
(level: LogLevel, ...args: any[]) => void
35+
>;
36+
private _verbose: boolean;
37+
38+
constructor(options: { verbose: boolean }) {
39+
this._onLogLineFuncs = {};
40+
this._verbose = options.verbose;
41+
}
42+
43+
addLogLineListener(
44+
func: (level: LogLevel, ...args: any[]) => void,
45+
): () => void {
46+
let id = Math.random().toString(36).substring(2, 15);
47+
for (let i = 0; i < 10; i++) {
48+
if (this._onLogLineFuncs[id] === undefined) {
49+
break;
50+
}
51+
id = Math.random().toString(36).substring(2, 15);
52+
}
53+
this._onLogLineFuncs[id] = func;
54+
return () => {
55+
delete this._onLogLineFuncs[id];
56+
};
57+
}
58+
59+
logVerbose(...args: any[]) {
60+
if (this._verbose) {
61+
for (const func of Object.values(this._onLogLineFuncs)) {
62+
func("debug", `${new Date().toISOString()}`, ...args);
63+
}
64+
}
65+
}
66+
67+
log(...args: any[]) {
68+
for (const func of Object.values(this._onLogLineFuncs)) {
69+
func("info", ...args);
70+
}
71+
}
72+
73+
warn(...args: any[]) {
74+
for (const func of Object.values(this._onLogLineFuncs)) {
75+
func("warn", ...args);
76+
}
77+
}
78+
79+
error(...args: any[]) {
80+
for (const func of Object.values(this._onLogLineFuncs)) {
81+
func("error", ...args);
82+
}
83+
}
84+
}
85+
86+
export function instantiateDefaultLogger(options: {
87+
verbose: boolean;
88+
}): Logger {
89+
const logger = new Logger(options);
90+
logger.addLogLineListener((level, ...args) => {
91+
switch (level) {
92+
case "debug":
93+
console.debug(...args);
94+
break;
95+
case "info":
96+
console.log(...args);
97+
break;
98+
case "warn":
99+
console.warn(...args);
100+
break;
101+
case "error":
102+
console.error(...args);
103+
break;
104+
default: {
105+
const _typecheck: never = level;
106+
console.log(...args);
107+
}
108+
}
109+
});
110+
return logger;
111+
}
112+
113+
export function logForFunction(
114+
logger: Logger,
24115
type: "info" | "error",
25116
source: UdfType,
26117
udfPath: string,
@@ -34,27 +125,23 @@ export function logToConsole(
34125
if (type === "info") {
35126
const match = message.match(/^\[.*?\] /);
36127
if (match === null) {
37-
console.error(
128+
logger.error(
38129
`[CONVEX ${prefix}(${udfPath})] Could not parse console.log`,
39130
);
40131
return;
41132
}
42133
const level = message.slice(1, match[0].length - 2);
43134
const args = message.slice(match[0].length);
44135

45-
console.log(
46-
`%c[CONVEX ${prefix}(${udfPath})] [${level}]`,
47-
INFO_COLOR,
48-
args,
49-
);
136+
logger.log(`%c[CONVEX ${prefix}(${udfPath})] [${level}]`, INFO_COLOR, args);
50137
} else {
51-
console.error(`[CONVEX ${prefix}(${udfPath})] ${message}`);
138+
logger.error(`[CONVEX ${prefix}(${udfPath})] ${message}`);
52139
}
53140
}
54141

55-
export function logFatalError(message: string): Error {
142+
export function logFatalError(logger: Logger, message: string): Error {
56143
const errorMessage = `[CONVEX FATAL ERROR] ${message}`;
57-
console.error(errorMessage);
144+
logger.error(errorMessage);
58145
return new Error(errorMessage);
59146
}
60147

‎src/browser/sync/authentication_manager.ts

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Logger } from "../logging.js";
12
import { LocalSyncState } from "./local_state.js";
23
import { AuthError, Transition } from "./protocol.js";
34
import { jwtDecode } from "jwt-decode";
@@ -89,7 +90,7 @@ export class AuthenticationManager {
8990
private readonly resumeSocket: () => void;
9091
// Passed down by BaseClient, sends a message to the server
9192
private readonly clearAuth: () => void;
92-
private readonly verbose: boolean;
93+
private readonly logger: Logger;
9394

9495
constructor(
9596
syncState: LocalSyncState,
@@ -100,15 +101,15 @@ export class AuthenticationManager {
100101
pauseSocket,
101102
resumeSocket,
102103
clearAuth,
103-
verbose,
104+
logger,
104105
}: {
105106
authenticate: (token: string) => void;
106107
stopSocket: () => Promise<void>;
107108
restartSocket: () => void;
108109
pauseSocket: () => void;
109110
resumeSocket: () => void;
110111
clearAuth: () => void;
111-
verbose: boolean;
112+
logger: Logger;
112113
},
113114
) {
114115
this.syncState = syncState;
@@ -118,7 +119,7 @@ export class AuthenticationManager {
118119
this.pauseSocket = pauseSocket;
119120
this.resumeSocket = resumeSocket;
120121
this.clearAuth = clearAuth;
121-
this.verbose = verbose;
122+
this.logger = logger;
122123
}
123124

124125
async setConfig(
@@ -216,7 +217,7 @@ export class AuthenticationManager {
216217
// We failed on a fresh token, trying another one won't help
217218
this.authState.state === "waitingForServerConfirmationOfFreshToken"
218219
) {
219-
console.error(
220+
this.logger.error(
220221
`Failed to authenticate: "${serverMessage.error}", check your server auth config`,
221222
);
222223
if (this.syncState.hasAuth()) {
@@ -316,14 +317,16 @@ export class AuthenticationManager {
316317
// This is no longer really possible, because
317318
// we wait on server response before scheduling token refetch,
318319
// and the server currently requires JWT tokens.
319-
console.error("Auth token is not a valid JWT, cannot refetch the token");
320+
this.logger.error(
321+
"Auth token is not a valid JWT, cannot refetch the token",
322+
);
320323
return;
321324
}
322325
// iat: issued at time, UTC seconds timestamp at which the JWT was issued
323326
// exp: expiration time, UTC seconds timestamp at which the JWT will expire
324327
const { iat, exp } = decodedToken as { iat?: number; exp?: number };
325328
if (!iat || !exp) {
326-
console.error(
329+
this.logger.error(
327330
"Auth token does not have required fields, cannot refetch the token",
328331
);
329332
return;
@@ -338,7 +341,7 @@ export class AuthenticationManager {
338341
(exp - iat - leewaySeconds) * 1000,
339342
);
340343
if (delay <= 0) {
341-
console.error(
344+
this.logger.error(
342345
"Auth token does not live long enough, cannot refetch the token",
343346
);
344347
return;
@@ -413,10 +416,6 @@ export class AuthenticationManager {
413416
}
414417

415418
private _logVerbose(message: string) {
416-
if (this.verbose) {
417-
console.debug(
418-
`${new Date().toISOString()} ${message} [v${this.configVersion}]`,
419-
);
420-
}
419+
this.logger.logVerbose(`${message} [v${this.configVersion}]`);
421420
}
422421
}

0 commit comments

Comments
 (0)
Please sign in to comment.