Skip to content

Commit

Permalink
Use logger abstraction in CLI + client libraries (#30497)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sshader authored and Convex, Inc. committed Oct 9, 2024
1 parent 9936c21 commit f50685b
Show file tree
Hide file tree
Showing 29 changed files with 306 additions and 164 deletions.
3 changes: 3 additions & 0 deletions npm-packages/convex/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export default [
fixable: false,
},
],

// Use `logMessage` and friends (CLI specific) or `logger.log` instead.
"no-console": "error",
},
},
{
Expand Down
32 changes: 23 additions & 9 deletions npm-packages/convex/src/browser/http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
convexToJson,
jsonToConvex,
} from "../values/index.js";
import { logToConsole } from "./logging.js";
import { instantiateDefaultLogger, logForFunction, Logger } from "./logging.js";
import { FunctionArgs, UserIdentityAttributes } from "../server/index.js";

export const STATUS_CODE_OK = 200;
Expand Down Expand Up @@ -46,20 +46,34 @@ export class ConvexHttpClient {
private encodedTsPromise?: Promise<string>;
private debug: boolean;
private fetchOptions?: FetchOptions;

private logger: Logger;
/**
* Create a new {@link ConvexHttpClient}.
*
* @param address - The url of your Convex deployment, often provided
* by an environment variable. E.g. `https://small-mouse-123.convex.cloud`.
* @param skipConvexDeploymentUrlCheck - Skip validating that the Convex deployment URL looks like
* @param options - An object of options.
* - `skipConvexDeploymentUrlCheck` - Skip validating that the Convex deployment URL looks like
* `https://happy-animal-123.convex.cloud` or localhost. This can be useful if running a self-hosted
* Convex backend that uses a different URL.
* - `logger` - A logger. If not provided, logs to the console.
* You can construct your own logger to customize logging to log elsewhere
* or not log at all.
*/
constructor(address: string, skipConvexDeploymentUrlCheck?: boolean) {
if (skipConvexDeploymentUrlCheck !== true) {
constructor(
address: string,
options?: { skipConvexDeploymentUrlCheck?: boolean; logger?: Logger },
) {
if (typeof options === "boolean") {
throw new Error(
"skipConvexDeploymentUrlCheck as the second argument is no longer supported. Please pass an options object, `{ skipConvexDeploymentUrlCheck: true }`.",
);
}
const opts = options ?? {};
if (opts.skipConvexDeploymentUrlCheck !== true) {
validateDeploymentUrl(address);
}
this.logger = opts.logger ?? instantiateDefaultLogger({ verbose: false });
this.address = address;
this.debug = true;
}
Expand Down Expand Up @@ -255,7 +269,7 @@ export class ConvexHttpClient {

if (this.debug) {
for (const line of respJSON.logLines ?? []) {
logToConsole("info", "query", name, line);
logForFunction(this.logger, "info", "query", name, line);
}
}
switch (respJSON.status) {
Expand Down Expand Up @@ -316,7 +330,7 @@ export class ConvexHttpClient {
const respJSON = await response.json();
if (this.debug) {
for (const line of respJSON.logLines ?? []) {
logToConsole("info", "mutation", name, line);
logForFunction(this.logger, "info", "mutation", name, line);
}
}
switch (respJSON.status) {
Expand Down Expand Up @@ -377,7 +391,7 @@ export class ConvexHttpClient {
const respJSON = await response.json();
if (this.debug) {
for (const line of respJSON.logLines ?? []) {
logToConsole("info", "action", name, line);
logForFunction(this.logger, "info", "action", name, line);
}
}
switch (respJSON.status) {
Expand Down Expand Up @@ -447,7 +461,7 @@ export class ConvexHttpClient {
const respJSON = await response.json();
if (this.debug) {
for (const line of respJSON.logLines ?? []) {
logToConsole("info", "any", name, line);
logForFunction(this.logger, "info", "any", name, line);
}
}
switch (respJSON.status) {
Expand Down
107 changes: 97 additions & 10 deletions npm-packages/convex/src/browser/logging.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-console */ // This is the one file where we can `console.log` for the default logger implementation.
import { ConvexError, Value } from "../values/index.js";
import { FunctionFailure } from "./sync/function_result.js";

Expand All @@ -20,7 +21,97 @@ function prefix_for_source(source: UdfType) {
}
}

export function logToConsole(
export type LogLevel = "debug" | "info" | "warn" | "error";

/**
* A logger that can be used to log messages. By default, this is a wrapper
* around `console`, but can be configured to not log at all or to log somewhere
* else.
*/
export class Logger {
private _onLogLineFuncs: Record<
string,
(level: LogLevel, ...args: any[]) => void
>;
private _verbose: boolean;

constructor(options: { verbose: boolean }) {
this._onLogLineFuncs = {};
this._verbose = options.verbose;
}

addLogLineListener(
func: (level: LogLevel, ...args: any[]) => void,
): () => void {
let id = Math.random().toString(36).substring(2, 15);
for (let i = 0; i < 10; i++) {
if (this._onLogLineFuncs[id] === undefined) {
break;
}
id = Math.random().toString(36).substring(2, 15);
}
this._onLogLineFuncs[id] = func;
return () => {
delete this._onLogLineFuncs[id];
};
}

logVerbose(...args: any[]) {
if (this._verbose) {
for (const func of Object.values(this._onLogLineFuncs)) {
func("debug", `${new Date().toISOString()}`, ...args);
}
}
}

log(...args: any[]) {
for (const func of Object.values(this._onLogLineFuncs)) {
func("info", ...args);
}
}

warn(...args: any[]) {
for (const func of Object.values(this._onLogLineFuncs)) {
func("warn", ...args);
}
}

error(...args: any[]) {
for (const func of Object.values(this._onLogLineFuncs)) {
func("error", ...args);
}
}
}

export function instantiateDefaultLogger(options: {
verbose: boolean;
}): Logger {
const logger = new Logger(options);
logger.addLogLineListener((level, ...args) => {
switch (level) {
case "debug":
console.debug(...args);
break;
case "info":
console.log(...args);
break;
case "warn":
console.warn(...args);
break;
case "error":
console.error(...args);
break;
default: {
const _typecheck: never = level;
console.log(...args);
}
}
});
return logger;
}

export function logForFunction(
logger: Logger,
type: "info" | "error",
source: UdfType,
udfPath: string,
Expand All @@ -34,27 +125,23 @@ export function logToConsole(
if (type === "info") {
const match = message.match(/^\[.*?\] /);
if (match === null) {
console.error(
logger.error(
`[CONVEX ${prefix}(${udfPath})] Could not parse console.log`,
);
return;
}
const level = message.slice(1, match[0].length - 2);
const args = message.slice(match[0].length);

console.log(
`%c[CONVEX ${prefix}(${udfPath})] [${level}]`,
INFO_COLOR,
args,
);
logger.log(`%c[CONVEX ${prefix}(${udfPath})] [${level}]`, INFO_COLOR, args);
} else {
console.error(`[CONVEX ${prefix}(${udfPath})] ${message}`);
logger.error(`[CONVEX ${prefix}(${udfPath})] ${message}`);
}
}

export function logFatalError(message: string): Error {
export function logFatalError(logger: Logger, message: string): Error {
const errorMessage = `[CONVEX FATAL ERROR] ${message}`;
console.error(errorMessage);
logger.error(errorMessage);
return new Error(errorMessage);
}

Expand Down
25 changes: 12 additions & 13 deletions npm-packages/convex/src/browser/sync/authentication_manager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Logger } from "../logging.js";
import { LocalSyncState } from "./local_state.js";
import { AuthError, Transition } from "./protocol.js";
import { jwtDecode } from "jwt-decode";
Expand Down Expand Up @@ -89,7 +90,7 @@ export class AuthenticationManager {
private readonly resumeSocket: () => void;
// Passed down by BaseClient, sends a message to the server
private readonly clearAuth: () => void;
private readonly verbose: boolean;
private readonly logger: Logger;

constructor(
syncState: LocalSyncState,
Expand All @@ -100,15 +101,15 @@ export class AuthenticationManager {
pauseSocket,
resumeSocket,
clearAuth,
verbose,
logger,
}: {
authenticate: (token: string) => void;
stopSocket: () => Promise<void>;
restartSocket: () => void;
pauseSocket: () => void;
resumeSocket: () => void;
clearAuth: () => void;
verbose: boolean;
logger: Logger;
},
) {
this.syncState = syncState;
Expand All @@ -118,7 +119,7 @@ export class AuthenticationManager {
this.pauseSocket = pauseSocket;
this.resumeSocket = resumeSocket;
this.clearAuth = clearAuth;
this.verbose = verbose;
this.logger = logger;
}

async setConfig(
Expand Down Expand Up @@ -216,7 +217,7 @@ export class AuthenticationManager {
// We failed on a fresh token, trying another one won't help
this.authState.state === "waitingForServerConfirmationOfFreshToken"
) {
console.error(
this.logger.error(
`Failed to authenticate: "${serverMessage.error}", check your server auth config`,
);
if (this.syncState.hasAuth()) {
Expand Down Expand Up @@ -316,14 +317,16 @@ export class AuthenticationManager {
// This is no longer really possible, because
// we wait on server response before scheduling token refetch,
// and the server currently requires JWT tokens.
console.error("Auth token is not a valid JWT, cannot refetch the token");
this.logger.error(
"Auth token is not a valid JWT, cannot refetch the token",
);
return;
}
// iat: issued at time, UTC seconds timestamp at which the JWT was issued
// exp: expiration time, UTC seconds timestamp at which the JWT will expire
const { iat, exp } = decodedToken as { iat?: number; exp?: number };
if (!iat || !exp) {
console.error(
this.logger.error(
"Auth token does not have required fields, cannot refetch the token",
);
return;
Expand All @@ -338,7 +341,7 @@ export class AuthenticationManager {
(exp - iat - leewaySeconds) * 1000,
);
if (delay <= 0) {
console.error(
this.logger.error(
"Auth token does not live long enough, cannot refetch the token",
);
return;
Expand Down Expand Up @@ -413,10 +416,6 @@ export class AuthenticationManager {
}

private _logVerbose(message: string) {
if (this.verbose) {
console.debug(
`${new Date().toISOString()} ${message} [v${this.configVersion}]`,
);
}
this.logger.logVerbose(`${message} [v${this.configVersion}]`);
}
}
Loading

0 comments on commit f50685b

Please sign in to comment.