diff --git a/__tests__/unit/logger.test.ts b/__tests__/unit/logger.test.ts index e1839b68..2f078afc 100644 --- a/__tests__/unit/logger.test.ts +++ b/__tests__/unit/logger.test.ts @@ -1,4 +1,5 @@ -import { LOG_LEVELS, parseDebugLevel } from "../../src"; +import { ConsoleLogHandler, LOG_LEVELS, parseDebugLevel } from "../../src"; +import { LogHandler } from "../../src/util/logging"; describe("logging", () => { describe("parseDebugLevel", () => { @@ -32,3 +33,69 @@ describe("logging", () => { ); }); }); + +describe("levels", () => { + describe("TRACE log level", () => { + const logger: LogHandler = new ConsoleLogHandler(LOG_LEVELS.DEBUG); + it("always log", () => { + logger.trace("Should log trace"); + logger.debug("Should log debug"); + logger.warn("Should log warnings"); + logger.error("Should log errors"); + }); + }); + describe("DEBUG log level", () => { + const logger: LogHandler = new ConsoleLogHandler(LOG_LEVELS.DEBUG); + it("skipped", () => { + logger.trace("Should not log", "foo"); + }); + it("debug", () => { + logger.debug("Should log something"); + }); + it("warn", () => { + logger.warn("Should also log (%s substitution!)", "with"); + }); + }); + describe("ERROR log level", () => { + const logger: LogHandler = new ConsoleLogHandler(LOG_LEVELS.ERROR); + it("skipped", () => { + logger.trace("Should not log", "foo"); + logger.debug("Should not log", "bar"); + logger.warn("Should not log"); + }); + it("logged", () => { + logger.error("Should log (%s substitution!)", "with"); + }); + }); +}); + +describe("Log messages", () => { + const logger: LogHandler = new ConsoleLogHandler(LOG_LEVELS.TRACE); + describe("Log message construction", () => { + it("trace", () => { + logger.trace("hello %s world", "foo", "bar"); // hello foo world bar + logger.trace("hello %s %s world", "foo", "bar"); // hello foo bar world + logger.trace("hello %s %s %s world", "foo", "bar"); // hello foo bar %s world + }); + it("debug", () => { + logger.debug("hello %s world", "foo", "bar"); // hello foo world bar + logger.debug("hello %s %s world", "foo", "bar"); // hello foo bar world + logger.debug("hello %s %s %s world", "foo", "bar"); // hello foo bar %s world + }); + it("info", () => { + logger.info("hello %s world", "foo", "bar"); // hello foo world bar + logger.info("hello %s %s world", "foo", "bar"); // hello foo bar world + logger.info("hello %s %s %s world", "foo", "bar"); // hello foo bar %s world + }); + it("warn", () => { + logger.warn("hello %s world", "foo", "bar"); // hello foo world bar + logger.warn("hello %s %s world", "foo", "bar"); // hello foo bar world + logger.warn("hello %s %s %s world", "foo", "bar"); // hello foo bar %s world + }); + it("error", () => { + logger.error("hello %s world", "foo", "bar"); // hello foo world bar + logger.error("hello %s %s world", "foo", "bar"); // hello foo bar world + logger.error("hello %s %s %s world", "foo", "bar"); // hello foo bar %s world + }); + }); +}); diff --git a/src/client-configuration.ts b/src/client-configuration.ts index 050dc44a..60bc9e9f 100644 --- a/src/client-configuration.ts +++ b/src/client-configuration.ts @@ -67,6 +67,9 @@ export interface ClientConfiguration { */ fetch_keepalive?: boolean; + /** + * A log handler instance. + */ logger?: LogHandler; /** @@ -199,6 +202,11 @@ export type StreamClientConfiguration = { */ secret: string; + /** + * A log handler instance. + */ + logger: LogHandler; + /** * Indicates if stream should include "status" events, periodic events that * update the client with the latest valid timestamp (in the event of a @@ -226,10 +234,12 @@ export type FeedClientConfiguration = Required< | "client_timeout_buffer_ms" | "query_timeout_ms" | "secret" + | "logger" + | "endpoint" > > & { /** - * The underlying {@link HTTPClient} that will execute the actual HTTP calls + * The underlying {@link HTTPClient} that will execute the actual HTTP calls. */ httpClient: HTTPClient; diff --git a/src/client.ts b/src/client.ts index 721e873b..90f98a4d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -23,6 +23,7 @@ import { isHTTPResponse, isStreamClient, type HTTPClient, + HTTPResponse, } from "./http-client"; import { Query } from "./query-builder"; import { TaggedTypeFormat } from "./tagged-type"; @@ -103,6 +104,8 @@ export class Client { readonly #clientConfiguration: RequiredClientConfig; /** The underlying {@link HTTPClient} client. */ readonly #httpClient: HTTPClient & Partial; + /** A LogHandler instance. */ + readonly #logger: LogHandler; /** The last transaction timestamp this client has seen */ #lastTxnTs?: number; /** true if this client is closed false otherwise */ @@ -135,6 +138,12 @@ export class Client { logger: this.#getLogger(clientConfiguration), }; + if (clientConfiguration && clientConfiguration.logger) { + this.#logger = clientConfiguration.logger; + } else { + this.#logger = new ConsoleLogHandler(LOG_LEVELS.ERROR); + } + this.#validateConfiguration(); if (!httpClient) { @@ -362,6 +371,7 @@ export class Client { const streamClientConfig: StreamClientConfiguration = { ...this.#clientConfiguration, httpStreamClient: streamClient, + logger: this.#logger, ...options, }; @@ -438,6 +448,7 @@ export class Client { const clientConfiguration: FeedClientConfiguration = { ...this.#clientConfiguration, httpClient: this.#httpClient, + logger: this.#logger, ...options, }; @@ -606,19 +617,34 @@ in an environmental variable named FAUNA_SECRET or pass it to the Client\ }; this.#setHeaders(requestConfig, headers); - const isTaggedFormat = requestConfig.format === "tagged"; + const isTaggedFormat: boolean = requestConfig.format === "tagged"; - const client_timeout_ms = + const client_timeout_ms: number = requestConfig.query_timeout_ms + this.#clientConfiguration.client_timeout_buffer_ms; + const method = "POST"; + this.#logger.debug( + "Fauna HTTP %s Request to %s (timeout: %s), headers: %s", + method, + this.#clientConfiguration.endpoint.toString(), + client_timeout_ms.toString(), + JSON.stringify(headers), + ); - const response = await this.#httpClient.request({ + const response: HTTPResponse = await this.#httpClient.request({ client_timeout_ms, data: queryRequest, headers, - method: "POST", + method, }); + this.#logger.debug( + "Fauna HTTP Response %s from %s, headers: %s", + response.status, + this.#clientConfiguration.endpoint.toString(), + JSON.stringify(response.headers), + ); + let parsedResponse; try { parsedResponse = { @@ -764,6 +790,8 @@ export class StreamClient { #streamAdapter?: StreamAdapter; /** A saved copy of the EventSource once received */ #eventSource?: EventSource; + /** A LogHandler instance. */ + #logger: LogHandler; /** * @@ -785,6 +813,7 @@ export class StreamClient { } this.#clientConfiguration = clientConfiguration; + this.#logger = clientConfiguration.logger; this.#validateConfiguration(); } @@ -973,6 +1002,8 @@ export class FeedClient { #query: () => Promise; /** The event feed's client options */ #clientConfiguration: FeedClientConfiguration; + /** A LogHandler instance */ + #logger: LogHandler; /** The last `cursor` value received for the current page */ #lastCursor?: string; /** A saved copy of the EventSource once received */ @@ -1001,6 +1032,7 @@ export class FeedClient { this.#clientConfiguration = clientConfiguration; this.#lastCursor = clientConfiguration.cursor; + this.#logger = clientConfiguration.logger; this.#validateConfiguration(); } @@ -1024,15 +1056,16 @@ export class FeedClient { const headers = this.#getHeaders(); + const client_timeout_ms: number = + this.#clientConfiguration.client_timeout_buffer_ms + + this.#clientConfiguration.query_timeout_ms; + const method: string = "POST"; + const req: HTTPRequest = { headers, - client_timeout_ms: - this.#clientConfiguration.client_timeout_buffer_ms + - this.#clientConfiguration.query_timeout_ms, - data: { - token: this.#eventSource.token, - }, - method: "POST", + client_timeout_ms, + method, + data: { token: this.#eventSource.token }, path: FaunaAPIPaths.EVENT_FEED, }; @@ -1069,12 +1102,26 @@ export class FeedClient { const { httpClient } = this.#clientConfiguration; - const request = await this.#nextPageHttpRequest(); + const request: HTTPRequest = await this.#nextPageHttpRequest(); + + this.#logger.debug( + "Fauna HTTP %s Request to %s (timeout: %s), headers: %s", + request.method, + this.#clientConfiguration.endpoint.toString(), + request.client_timeout_ms, + JSON.stringify(request.headers), + ); const response = await withRetries(() => httpClient.request(request), { maxAttempts: this.#clientConfiguration.max_attempts, maxBackoff: this.#clientConfiguration.max_backoff, shouldRetry: (error) => error instanceof ThrottlingError, }); + this.#logger.debug( + "Fauna HTTP Response %s from %s, headers: %s", + response.status, + this.#clientConfiguration.endpoint.toString(), + JSON.stringify(response.headers), + ); let body: FeedSuccess | FeedError; diff --git a/src/http-client/http-client.ts b/src/http-client/http-client.ts index 0ffb7984..8ba4da14 100644 --- a/src/http-client/http-client.ts +++ b/src/http-client/http-client.ts @@ -20,7 +20,7 @@ export type HTTPRequest = { headers: Record; /** HTTP method to use */ - method: "POST"; + method: string; /** The path of the endpoint to call if not using the default */ path?: SupportedFaunaAPIPaths; diff --git a/src/util/logging.ts b/src/util/logging.ts index ca326982..4bbebbba 100644 --- a/src/util/logging.ts +++ b/src/util/logging.ts @@ -20,12 +20,13 @@ export type LogLevel = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS]; */ export function parseDebugLevel(debug_level: string | undefined): LogLevel { switch (debug_level) { - case LOG_LEVELS.TRACE: - case LOG_LEVELS.DEBUG: - case LOG_LEVELS.INFO: - case LOG_LEVELS.WARN: - case LOG_LEVELS.ERROR: - case LOG_LEVELS.FATAL: + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": return debug_level; default: return LOG_LEVELS.OFF; @@ -33,12 +34,12 @@ export function parseDebugLevel(debug_level: string | undefined): LogLevel { } export interface LogHandler { - trace(msg?: string, args?: string[]): void; - debug(msg?: string, args?: string[]): void; - info(msg?: string, args?: string[]): void; - warn(msg?: string, args?: string[]): void; - error(msg?: string, args?: string[]): void; - fatal(msg?: string, args?: string[]): void; + trace(msg: string, ...args: any[]): void; + debug(msg: string, ...args: any[]): void; + info(msg: string, ...args: any[]): void; + warn(msg: string, ...args: any[]): void; + error(msg: string, ...args: any[]): void; + fatal(msg: string, ...args: any[]): void; } export class ConsoleLogHandler implements LogHandler { @@ -47,37 +48,37 @@ export class ConsoleLogHandler implements LogHandler { this.#level = level; } - trace(msg?: string, args?: string[]): void { + trace(msg: string, ...args: any[]): void { if (this.#level >= LOG_LEVELS.TRACE) { console.trace(msg, args); } } - debug(msg?: string, args?: string[]): void { + debug(msg: string, ...args: any[]): void { if (this.#level >= LOG_LEVELS.DEBUG) { console.debug(msg, args); } } - info(msg?: string, args?: string[]): void { + info(msg: string, ...args: any[]): void { if (this.#level >= LOG_LEVELS.INFO) { console.info(msg, args); } } - warn(msg?: string, args?: string[]): void { + warn(msg: string, ...args: any[]): void { if (this.#level >= LOG_LEVELS.WARN) { console.warn(msg, args); } } - error(msg?: string, args?: string[]): void { + error(msg: string, ...args: any[]): void { if (this.#level >= LOG_LEVELS.ERROR) { console.error(msg, args); } } - fatal(msg?: string, args?: string[]): void { + fatal(msg: string, args?: any[]): void { if (this.#level >= LOG_LEVELS.FATAL) { console.error(msg, args); }