From d1e9268f29dfd7449d8fba2817f39438919bf36b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 2 Apr 2026 14:10:51 +0200 Subject: [PATCH 01/13] Add `logger: PonderAppLogger` field to `PonderAppContext` data model --- .../src/deserialize/ponder-app-context.ts | 30 +++++ .../src/local-ponder-client.mock.ts | 7 + packages/ponder-sdk/src/ponder-app-context.ts | 126 ++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts index 2939185e1..2f12de226 100644 --- a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts +++ b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts @@ -14,6 +14,7 @@ import { type PonderAppCommand, PonderAppCommands, type PonderAppContext, + type PonderAppLogger, } from "../ponder-app-context"; import type { Unvalidated } from "./utils"; @@ -26,6 +27,32 @@ export const schemaPortNumber = z .min(1, { error: "Port must be greater than or equal to 1." }) .max(65535, { error: "Port must be less than or equal to 65535." }); +/** + * Represents the Ponder app logger method + */ +const schemaPonderAppLoggerMethod = z.function({ + input: [ + z.looseObject({ + msg: z.string({ error: "Log message must be a string." }), + error: z.optional(z.instanceof(Error, { error: "Error must be an instance of Error." })), + }), + ], + output: z.void(), +}); + +/** + * Represents the logger provided by the Ponder runtime to a local Ponder app. + */ +const schemaPonderAppLogger = z + .looseObject({ + error: schemaPonderAppLoggerMethod, + warn: schemaPonderAppLoggerMethod, + info: schemaPonderAppLoggerMethod, + debug: schemaPonderAppLoggerMethod, + trace: schemaPonderAppLoggerMethod, + }) + .transform((logger) => logger as PonderAppLogger); + /** * Type representing the "raw" context of a local Ponder app. */ @@ -34,6 +61,7 @@ const schemaRawPonderAppContext = z.object({ command: z.enum(PonderAppCommands), port: schemaPortNumber, }), + logger: schemaPonderAppLogger, }); /** @@ -47,6 +75,7 @@ export type RawPonderAppContext = z.infer; const schemaPonderAppContext = z.object({ command: z.enum(PonderAppCommands), localPonderAppUrl: z.instanceof(URL, { error: "localPonderAppUrl must be a valid URL." }), + logger: schemaPonderAppLogger, }); /** @@ -62,6 +91,7 @@ function buildUnvalidatedPonderAppContext( return { command: rawPonderAppContext.options.command as Unvalidated, localPonderAppUrl: new URL(`http://localhost:${rawPonderAppContext.options.port}`), + logger: rawPonderAppContext.logger, }; } diff --git a/packages/ponder-sdk/src/local-ponder-client.mock.ts b/packages/ponder-sdk/src/local-ponder-client.mock.ts index a1e6daa88..708f7f402 100644 --- a/packages/ponder-sdk/src/local-ponder-client.mock.ts +++ b/packages/ponder-sdk/src/local-ponder-client.mock.ts @@ -38,6 +38,13 @@ export function createLocalPonderClientMock(overrides?: { const ponderAppContext = { command: overrides?.ponderAppContext?.command ?? PonderAppCommands.Start, localPonderAppUrl: new URL("http://localhost:3000"), + logger: { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, + trace: () => {}, + }, } satisfies PonderAppContext; return new LocalPonderClient( diff --git a/packages/ponder-sdk/src/ponder-app-context.ts b/packages/ponder-sdk/src/ponder-app-context.ts index 0afb48745..904298463 100644 --- a/packages/ponder-sdk/src/ponder-app-context.ts +++ b/packages/ponder-sdk/src/ponder-app-context.ts @@ -10,6 +10,127 @@ export const PonderAppCommands = { export type PonderAppCommand = (typeof PonderAppCommands)[keyof typeof PonderAppCommands]; +/** + * Represents a single log entry for the Ponder app logger. + * + * It is a loose object that: + * - must contain a `msg` property of type string, and + * - can optionally include an `error` property of type Error, and + * - can optionally include any additional properties relevant to + * the log message. The additional properties can be used to provide more + * context about the log message, and will be included in the log output. + */ +type PonderAppLog = { + /** + * Log message + */ + msg: string; + + /** + * Optional error object to log. + * + * If provided, the logger will log the error's stack trace and message. + */ + error?: Error; +} & Record; + +/** + * Ponder app logger + * + * Represents the logger provided by the Ponder runtime to a local Ponder app. + * @see https://github.com/ponder-sh/ponder/blob/6fcc15d4234e43862cb6e21c05f3c57f4c2f7464/packages/core/src/internal/logger.ts#L8-L31 + */ +export interface PonderAppLogger { + /** + * Logs a message at the "error" level. + * + * @param options - The log message and additional properties to log. + * + * @example + * ```ts + * logger.error({ + * msg: "Incorrect omnichain status", + * error: new Error("The omnichain status be either 'omnichain-backfill' or 'omnichain-following'"), + * expected: "omnichain-backfill or omnichain-following", + * actual: "omnichain-unstarted" + * }); + * + * logger.error({ + * msg: "Incorrect omnichain status", + * error: new Error("The omnichain status be either 'omnichain-backfill' or 'omnichain-following'"), + * }); + * + * logger.error({ + * msg: "The omnichain status be either 'omnichain-backfill' or 'omnichain-following'" + * }); + * ``` + */ + error(options: T): void; + + /** + * Logs a message at the "warn" level. + * + * @param options - The log message and additional properties to log. + * + * @example + * ```ts + * logger.warn({ + * msg: "Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled.", + * effects: "This results in the availability of both the legacy Subgraph-Compatible GraphQL API (/subgraph) _and_ ENSNode's Omnigraph API (/api/omnigraph), and comes with an associated increase in indexing time. If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use." + * }); + * + * logger.warn({ + * msg: "Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled." + * }); + * ``` + */ + warn(options: T): void; + + /** + * Logs a message at the "info" level. + * @param options + * + * @example + * ```ts + * logger.info({ + * msg: "An informational message", + * details: "Here are some details about the info" + * }); + * ``` + */ + info(options: T): void; + + /** + * Logs a message at the "debug" level. + * @param options + * + * @example + * ```ts + * logger.debug({ + * msg: "A debug message", + * arg1: "Here is some debug information about arg1", + * arg2: "Here is some debug information about arg2" + * }); + * ``` + */ + debug(options: T): void; + + /** + * Logs a message at the "trace" level. + * @param options + * + * @example + * ```ts + * logger.trace({ + * msg: "A trace message", + * detailA: "Here are some details about the trace message", + * detailB: "Here are some more details about the trace message" + * }); + * ``` + */ + trace(options: T): void; +} + /** * Ponder app context * @@ -25,4 +146,9 @@ export interface PonderAppContext { * URL of the local Ponder app. */ localPonderAppUrl: URL; + + /** + * Logger provided by the Ponder runtime + */ + logger: PonderAppLogger; } From e26bdb8257dc84cf295c5db62d45c2f351fafee6 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 2 Apr 2026 14:11:16 +0200 Subject: [PATCH 02/13] docs(changeset): Added `logger` field to `PonderAppContext` data model. --- .changeset/warm-geese-fall.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/warm-geese-fall.md diff --git a/.changeset/warm-geese-fall.md b/.changeset/warm-geese-fall.md new file mode 100644 index 000000000..eae67144c --- /dev/null +++ b/.changeset/warm-geese-fall.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ponder-sdk": minor +--- + +Added `logger` field to `PonderAppContext` data model. From ad9ad552a53882e661331d48f903600863c6f4c3 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 2 Apr 2026 14:12:08 +0200 Subject: [PATCH 03/13] Intergrated `logger` from Ponder SDK into ENSIndexer --- apps/ensindexer/src/lib/logger.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/ensindexer/src/lib/logger.ts diff --git a/apps/ensindexer/src/lib/logger.ts b/apps/ensindexer/src/lib/logger.ts new file mode 100644 index 000000000..fea0b328e --- /dev/null +++ b/apps/ensindexer/src/lib/logger.ts @@ -0,0 +1,23 @@ +import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; + +if (!globalThis.PONDER_COMMON?.logger) { + throw new Error( + "Ponder Common Logger must be provided by Ponder runtime at globalThis.PONDER_COMMON.logger", + ); +} + +/** + * Logger instance for ENSIndexer to use. + */ +export const logger = globalThis.PONDER_COMMON.logger; + +/** + * Formats a log parameter as a pretty-printed JSON string. + * This is useful for logging complex objects in a readable format. + */ +export const formatLogParam = (param: unknown): string => + prettyPrintJson(param) + .split("\n") + .map((line) => line.trim().replace(": ", ":")) + .join("") + .trim(); From b00f1ab68c9137f643f300f984901dbe138e642d Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 2 Apr 2026 14:13:51 +0200 Subject: [PATCH 04/13] Updated ENSIndexer code to use the `logger` module --- apps/ensindexer/ponder/ponder.config.ts | 19 +++-- .../ponder/src/api/handlers/ensnode-api.ts | 9 ++- apps/ensindexer/ponder/src/api/index.ts | 14 +++- apps/ensindexer/src/lib/dns-helpers.ts | 19 ++--- .../ensdb-writer-worker.ts | 72 +++++++++++++------ .../src/lib/ensdb-writer-worker/singleton.ts | 3 +- .../src/lib/ensdb/migrate-ensnode-schema.ts | 12 +++- .../src/lib/ensraibow-api-client.ts | 11 ++- apps/ensindexer/src/lib/graphnode-helpers.ts | 11 +-- apps/ensindexer/src/lib/version-info.ts | 9 ++- .../ensv2/handlers/ensv1/NameWrapper.ts | 3 +- 11 files changed, 128 insertions(+), 54 deletions(-) diff --git a/apps/ensindexer/ponder/ponder.config.ts b/apps/ensindexer/ponder/ponder.config.ts index bdb21adcc..fd073a0de 100644 --- a/apps/ensindexer/ponder/ponder.config.ts +++ b/apps/ensindexer/ponder/ponder.config.ts @@ -1,23 +1,28 @@ import config from "@/config"; import { PluginName } from "@ensnode/ensnode-sdk"; -import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; import { redactENSIndexerConfig } from "@/config/redact"; +import { formatLogParam, logger } from "@/lib/logger"; import ponderConfig from "@/ponder/config"; //////// // Log redacted ENSIndexerConfig for debugging. //////// -console.log("ENSIndexer running with config:"); -console.log(prettyPrintJson(redactENSIndexerConfig(config))); +logger.info({ + msg: "ENSIndexer starting", + config: formatLogParam(redactENSIndexerConfig(config)), +}); -// log warning about dual activation of subgraph and ensv2 plugins +// Log warning about dual activation of subgraph and ensv2 plugins if (config.plugins.includes(PluginName.Subgraph) && config.plugins.includes(PluginName.ENSv2)) { - console.warn( - `Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled. This results in the availability of both the legacy Subgraph-Compatible GraphQL API (/subgraph) _and_ ENSNode's Omnigraph API (/api/omnigraph), and comes with an associated increase in indexing time. If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use.`, - ); + logger.warn({ + msg: `Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled. This results in the availability of both the legacy Subgraph-Compatible GraphQL API (/subgraph) _and_ ENSNode's Omnigraph API (/api/omnigraph), and comes with an associated increase in indexing time.`, + advice: formatLogParam( + `If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use.`, + ), + }); } //////// diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index fc6a46e79..d3461488d 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -11,6 +11,7 @@ import { } from "@ensnode/ensnode-sdk"; import { ensDbClient } from "@/lib/ensdb/singleton"; +import { formatLogParam, logger } from "@/lib/logger"; const app = new Hono(); @@ -55,8 +56,12 @@ app.get("/indexing-status", async (c) => { } satisfies IndexingStatusResponseOk), ); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - console.error(`Indexing Status Snapshot is currently not available: ${errorMessage}`); + logger.error({ + msg: "Indexing status snapshot unavailable", + error: error instanceof Error ? error : undefined, + module: formatLogParam("ensnode-api"), + endpoint: formatLogParam("/indexing-status"), + }); return c.json( serializeIndexingStatusResponse({ diff --git a/apps/ensindexer/ponder/src/api/index.ts b/apps/ensindexer/ponder/src/api/index.ts index 9b184f128..cd38aa23c 100644 --- a/apps/ensindexer/ponder/src/api/index.ts +++ b/apps/ensindexer/ponder/src/api/index.ts @@ -7,6 +7,7 @@ import type { ErrorResponse } from "@ensnode/ensnode-sdk"; import { migrateEnsNodeSchema } from "@/lib/ensdb/migrate-ensnode-schema"; import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; +import { formatLogParam, logger } from "@/lib/logger"; import ensNodeApi from "./handlers/ensnode-api"; @@ -19,7 +20,11 @@ import ensNodeApi from "./handlers/ensnode-api"; migrateEnsNodeSchema() .then(startEnsDbWriterWorker) .catch((error) => { - console.error("Failed to migrate ENSNode Schema — ", error); + logger.error({ + msg: "Failed to initialize ENSNode metadata", + error: error instanceof Error ? error : undefined, + module: formatLogParam("ponder-api"), + }); process.exit(1); }); @@ -39,7 +44,12 @@ app.route("/api", ensNodeApi); // log hono errors to console app.onError((error, ctx) => { - console.error(error); + logger.error({ + msg: "Internal server error", + error: error instanceof Error ? error : undefined, + path: formatLogParam(ctx.req.path), + module: formatLogParam("ponder-api"), + }); return ctx.json({ message: "Internal Server Error" } satisfies ErrorResponse, 500); }); diff --git a/apps/ensindexer/src/lib/dns-helpers.ts b/apps/ensindexer/src/lib/dns-helpers.ts index fc4b77290..0e4680f41 100644 --- a/apps/ensindexer/src/lib/dns-helpers.ts +++ b/apps/ensindexer/src/lib/dns-helpers.ts @@ -14,6 +14,7 @@ import { } from "@ensnode/ensnode-sdk"; import { interpretTextRecordKey, interpretTextRecordValue } from "@ensnode/ensnode-sdk/internal"; +import { formatLogParam, logger } from "@/lib/logger"; import { isLabelSubgraphIndexable } from "@/lib/subgraph/is-label-subgraph-indexable"; /** @@ -111,15 +112,16 @@ export function decodeTXTData(data: Buffer[]): string | null { // soft-invariant: we never receive 0 data results in a TXT record if (decoded.length === 0) { - console.warn(`decodeTXTData zero 'data' results, this is unexpected.`); + logger.warn({ msg: `decodeTXTData zero 'data' results, this is unexpected.` }); return null; } // soft-invariant: we never receive more than 1 data result in a TXT record if (decoded.length > 1) { - console.warn( - `decodeTXTData received multiple 'data' results, this is unexpected. data = '${decoded.join(",")}'`, - ); + logger.warn({ + msg: `decodeTXTData received multiple 'data' results, this is unexpected.`, + data: formatLogParam(decoded), + }); } // biome-ignore lint/style/noNonNullAssertion: guaranteed to exist due to length check above @@ -166,16 +168,17 @@ export function parseDnsTxtRecordArgs({ }); if (txtDatas.length === 0) { - console.warn(`parseDNSRecordArgs: No TXT answers found in DNS record for key '${key}'`); + logger.warn({ msg: `parseDNSRecordArgs: No TXT answers found in DNS record for key '${key}'` }); // no text answers? interpret as deletion return { key, value: null }; } if (txtDatas.length > 1) { - console.warn( - `parseDNSRecordArgs: received multiple TXT answers, this is unexpected. answers = '${txtDatas.join(",")}'. Only using the first one.`, - ); + logger.warn({ + msg: `parseDNSRecordArgs: received multiple TXT answers, this is unexpected. Only using the first one.`, + answers: `[${txtDatas.join(",")}]`, + }); } // biome-ignore lint/style/noNonNullAssertion: ok due to checks above diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index 1645d196a..5c3002aa1 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -14,6 +14,7 @@ import { import type { LocalPonderClient } from "@ensnode/ponder-sdk"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; +import { formatLogParam, logger } from "@/lib/logger"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; /** @@ -100,16 +101,24 @@ export class EnsDbWriterWorker { const inMemoryConfig = await this.getValidatedEnsIndexerPublicConfig(); // Task 1: upsert ENSDb version into ENSDb. - console.log(`[EnsDbWriterWorker]: Upserting ENSDb version into ENSDb...`); + logger.debug({ msg: "Upserting ENSDb version", module: formatLogParam("EnsDbWriterWorker") }); await this.ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); - console.log( - `[EnsDbWriterWorker]: ENSDb version upserted successfully: ${inMemoryConfig.versionInfo.ensDb}`, - ); + logger.info({ + msg: "Upserted ENSDb version", + ensDbVersion: formatLogParam(inMemoryConfig.versionInfo.ensDb), + module: formatLogParam("EnsDbWriterWorker"), + }); // Task 2: upsert of EnsIndexerPublicConfig into ENSDb. - console.log(`[EnsDbWriterWorker]: Upserting ENSIndexer Public Config into ENSDb...`); + logger.debug({ + msg: "Upserting ENSIndexer public config", + module: formatLogParam("EnsDbWriterWorker"), + }); await this.ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig); - console.log(`[EnsDbWriterWorker]: ENSIndexer Public Config upserted successfully`); + logger.info({ + msg: "Upserted ENSIndexer public config", + module: formatLogParam("EnsDbWriterWorker"), + }); // Task 3: recurring upsert of Indexing Status Snapshot into ENSDb. this.indexingStatusInterval = setInterval( @@ -163,12 +172,23 @@ export class EnsDbWriterWorker { * will be thrown and the worker will not start, as the ENSIndexer Public Config * is a critical dependency for the worker's tasks. */ + const configFetchRetries = 3; + + logger.debug({ + msg: "Fetching ENSIndexer public config", + retries: configFetchRetries, + module: formatLogParam("EnsDbWriterWorker"), + }); + const inMemoryConfigPromise = pRetry(() => this.publicConfigBuilder.getPublicConfig(), { - retries: 3, - onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { - console.warn( - `ENSIndexer Config fetch attempt ${attemptNumber} failed (${error.message}). ${retriesLeft} retries left.`, - ); + retries: configFetchRetries, + onFailedAttempt: ({ attemptNumber, retriesLeft }) => { + logger.warn({ + msg: "Config fetch attempt failed", + attempt: attemptNumber, + retriesLeft, + module: formatLogParam("EnsDbWriterWorker"), + }); }, }); @@ -180,12 +200,19 @@ export class EnsDbWriterWorker { this.ensDbClient.getEnsIndexerPublicConfig(), inMemoryConfigPromise, ]); + logger.info({ + msg: "Fetched ENSIndexer public config", + module: formatLogParam("EnsDbWriterWorker"), + config: formatLogParam(inMemoryConfig), + }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - console.error( - `[EnsDbWriterWorker]: Failed to fetch ENSIndexer Public Config: ${errorMessage}`, - ); + logger.error({ + msg: "Failed to fetch ENSIndexer public config", + error: error instanceof Error ? error : undefined, + module: formatLogParam("EnsDbWriterWorker"), + }); // Throw the error to terminate the ENSIndexer process due to failed fetch of critical dependency throw new Error(errorMessage, { @@ -205,9 +232,11 @@ export class EnsDbWriterWorker { } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - console.error( - `[EnsDbWriterWorker]: In-memory ENSIndexer Public Config object is not compatible with its counterpart stored in ENSDb. Cause: ${errorMessage}`, - ); + logger.error({ + msg: "In-memory config incompatible with stored config", + error: error instanceof Error ? error : undefined, + module: formatLogParam("EnsDbWriterWorker"), + }); // Throw the error to terminate the ENSIndexer process due to // found config incompatibility @@ -240,10 +269,11 @@ export class EnsDbWriterWorker { await this.ensDbClient.upsertIndexingStatusSnapshot(crossChainSnapshot); } catch (error) { - console.error( - `[EnsDbWriterWorker]: Error retrieving or validating Indexing Status Snapshot:`, - error, - ); + logger.error({ + msg: "Failed to upsert indexing status snapshot", + error: error instanceof Error ? error : undefined, + module: formatLogParam("EnsDbWriterWorker"), + }); // Do not throw the error, as failure to retrieve the Indexing Status // should not cause the ENSDb Writer Worker to stop functioning. } diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index 5e0a9d9df..8b9168ce0 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,6 +1,7 @@ import { ensDbClient } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; import { localPonderClient } from "@/lib/local-ponder-client"; +import { logger } from "@/lib/logger"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; import { EnsDbWriterWorker } from "./ensdb-writer-worker"; @@ -35,7 +36,7 @@ export function startEnsDbWriterWorker() { // Abort the worker on error to trigger cleanup ensDbWriterWorker.stop(); - console.error("EnsDbWriterWorker encountered an error:", error); + logger.error({ msg: "EnsDbWriterWorker encountered an error", error }); // Re-throw the error to ensure the application shuts down with a non-zero exit code. process.exitCode = 1; diff --git a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts index 6a3efdebb..fe755288b 100644 --- a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts +++ b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts @@ -1,6 +1,8 @@ import { createRequire } from "node:module"; import { join } from "node:path"; +import { formatLogParam, logger } from "@/lib/logger"; + import { ensDbClient } from "./singleton"; // Resolve the path to the migrations directory within the ENSDb SDK package @@ -13,7 +15,13 @@ const migrationsDirPath = join( * Execute database migrations for ENSNode Schema in ENSDb. */ export async function migrateEnsNodeSchema(): Promise { - console.log(`Running database migrations for ENSNode Schema in ENSDb.`); + logger.debug({ + msg: "Started database migrations", + module: formatLogParam("migrate-ensnode-schema"), + }); await ensDbClient.migrateEnsNodeSchema(migrationsDirPath); - console.log(`Database migrations for ENSNode Schema in ENSDb completed successfully.`); + logger.info({ + msg: "Completed database migrations", + module: formatLogParam("migrate-ensnode-schema"), + }); } diff --git a/apps/ensindexer/src/lib/ensraibow-api-client.ts b/apps/ensindexer/src/lib/ensraibow-api-client.ts index 877943b43..310c65ce0 100644 --- a/apps/ensindexer/src/lib/ensraibow-api-client.ts +++ b/apps/ensindexer/src/lib/ensraibow-api-client.ts @@ -2,6 +2,8 @@ import config from "@/config"; import { type EnsRainbow, EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk"; +import { formatLogParam, logger } from "@/lib/logger"; + export function getENSRainbowApiClient(): EnsRainbow.ApiClient { const ensRainbowApiClient = new EnsRainbowApiClient({ endpointUrl: config.ensRainbowUrl, @@ -12,9 +14,12 @@ export function getENSRainbowApiClient(): EnsRainbow.ApiClient { ensRainbowApiClient.getOptions().endpointUrl === EnsRainbowApiClient.defaultOptions().endpointUrl ) { - console.warn( - `Using default public ENSRainbow server which may cause increased network latency. For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.`, - ); + logger.warn({ + msg: `Using default public ENSRainbow server which may cause increased network latency`, + advice: formatLogParam( + "For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.", + ), + }); } return ensRainbowApiClient; diff --git a/apps/ensindexer/src/lib/graphnode-helpers.ts b/apps/ensindexer/src/lib/graphnode-helpers.ts index df8d4a82e..d46f31336 100644 --- a/apps/ensindexer/src/lib/graphnode-helpers.ts +++ b/apps/ensindexer/src/lib/graphnode-helpers.ts @@ -4,6 +4,7 @@ import type { LabelHash, LiteralLabel } from "@ensnode/ensnode-sdk"; import { type EnsRainbow, ErrorCode, isHealError } from "@ensnode/ensrainbow-sdk"; import { getENSRainbowApiClient } from "@/lib/ensraibow-api-client"; +import { logger } from "@/lib/logger"; const ensRainbowApiClient = getENSRainbowApiClient(); @@ -63,10 +64,12 @@ export async function labelByLabelHash(labelHash: LabelHash): Promise Date: Thu, 2 Apr 2026 14:15:10 +0200 Subject: [PATCH 05/13] Update testing suite --- apps/ensindexer/src/lib/__test__/mockLogger.ts | 16 ++++++++++++++++ apps/ensindexer/src/lib/dns-helpers.test.ts | 4 ++++ .../ensdb-writer-worker.test.ts | 4 ++++ .../ensindexer/src/lib/graphnode-helpers.test.ts | 12 ++++++++---- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 apps/ensindexer/src/lib/__test__/mockLogger.ts diff --git a/apps/ensindexer/src/lib/__test__/mockLogger.ts b/apps/ensindexer/src/lib/__test__/mockLogger.ts new file mode 100644 index 000000000..799a30661 --- /dev/null +++ b/apps/ensindexer/src/lib/__test__/mockLogger.ts @@ -0,0 +1,16 @@ +import { vi } from "vitest"; + +/** + * Mock the logger module to avoid the globalThis.PONDER_COMMON check. + */ +export function setupLoggerMock() { + vi.mock("@/lib/logger", () => ({ + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, + formatLogParam: vi.fn((param: unknown) => JSON.stringify(param)), + })); +} diff --git a/apps/ensindexer/src/lib/dns-helpers.test.ts b/apps/ensindexer/src/lib/dns-helpers.test.ts index 38eaac0a1..401466402 100644 --- a/apps/ensindexer/src/lib/dns-helpers.test.ts +++ b/apps/ensindexer/src/lib/dns-helpers.test.ts @@ -3,6 +3,10 @@ import { bytesToHex, decodeEventLog, stringToHex, zeroHash } from "viem"; import { packetToBytes } from "viem/ens"; import { describe, expect, it } from "vitest"; +import { setupLoggerMock } from "@/lib/__test__/mockLogger"; + +setupLoggerMock(); + import { getDatasource } from "@ensnode/datasources"; import type { DNSEncodedLiteralName } from "@ensnode/ensnode-sdk"; diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts index f55d75247..d2f5a98a2 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts @@ -6,6 +6,10 @@ import { validateEnsIndexerPublicConfigCompatibility, } from "@ensnode/ensnode-sdk"; +import { setupLoggerMock } from "@/lib/__test__/mockLogger"; + +setupLoggerMock(); + import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; diff --git a/apps/ensindexer/src/lib/graphnode-helpers.test.ts b/apps/ensindexer/src/lib/graphnode-helpers.test.ts index aeb4af1e3..9d2c9db16 100644 --- a/apps/ensindexer/src/lib/graphnode-helpers.test.ts +++ b/apps/ensindexer/src/lib/graphnode-helpers.test.ts @@ -3,7 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { LabelHash } from "@ensnode/ensnode-sdk"; import { setupConfigMock } from "@/lib/__test__/mockConfig"; +import { setupLoggerMock } from "@/lib/__test__/mockLogger"; +setupLoggerMock(); setupConfigMock(); // setup config mock before importing dependent modules // Use real p-retry logic but with 0 timeouts so tests don't incur actual backoff delays. @@ -20,6 +22,8 @@ vi.mock("p-retry", async () => { // Mock fetch globally to prevent real network calls global.fetch = vi.fn(); +import { logger } from "@/lib/logger"; + import { labelByLabelHash } from "./graphnode-helpers"; describe("labelByLabelHash", () => { @@ -153,7 +157,7 @@ describe("labelByLabelHash", () => { // carrying over cacheable responses (HealSuccess, HealNotFoundError) and bypassing fetch. it("retries on network/fetch failure and succeeds on a later attempt", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); (fetch as any) .mockRejectedValueOnce(new Error("network error")) @@ -173,7 +177,7 @@ describe("labelByLabelHash", () => { }); it("retries on HealServerError and succeeds on a later attempt", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); (fetch as any) .mockResolvedValueOnce({ @@ -229,7 +233,7 @@ describe("labelByLabelHash", () => { }); it("throws after exhausting retries on persistent network/fetch failures", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); (fetch as any).mockRejectedValue(new Error("network error")); @@ -245,7 +249,7 @@ describe("labelByLabelHash", () => { }); it("throws after exhausting retries on persistent HealServerError responses", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {}); (fetch as any).mockResolvedValue({ ok: true, From 58494321939b5a770be4722e8c4fea9e265376ca Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 2 Apr 2026 16:09:19 +0200 Subject: [PATCH 06/13] docs(changeset): Enhanced application logging approach to use a streamlined logger implementation across ENSIndexer app. --- .changeset/fast-coins-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fast-coins-grab.md diff --git a/.changeset/fast-coins-grab.md b/.changeset/fast-coins-grab.md new file mode 100644 index 000000000..cc17a83a4 --- /dev/null +++ b/.changeset/fast-coins-grab.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +Enhanced application logging approach to use a streamlined logger implementation across ENSIndexer app. From b8161e79c9a474423abe266fe8f84849c19bf41c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 2 Apr 2026 16:30:04 +0200 Subject: [PATCH 07/13] Apply AI PR feedback --- apps/ensindexer/ponder/src/api/index.ts | 6 ++-- .../ensindexer/src/lib/__test__/mockLogger.ts | 31 ++++++++++++------- apps/ensindexer/src/lib/dns-helpers.test.ts | 4 +-- apps/ensindexer/src/lib/dns-helpers.ts | 12 +++++-- .../ensdb-writer-worker.test.ts | 4 +-- .../ensdb-writer-worker.ts | 8 ++--- .../src/lib/ensdb-writer-worker/singleton.ts | 7 +++-- .../src/lib/graphnode-helpers.test.ts | 3 +- apps/ensindexer/src/lib/logger.ts | 25 +++++++++++++-- apps/ensindexer/src/lib/version-info.ts | 4 +-- packages/ponder-sdk/src/ponder-app-context.ts | 6 ++-- 11 files changed, 71 insertions(+), 39 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/index.ts b/apps/ensindexer/ponder/src/api/index.ts index cd38aa23c..2f23b1d2b 100644 --- a/apps/ensindexer/ponder/src/api/index.ts +++ b/apps/ensindexer/ponder/src/api/index.ts @@ -7,7 +7,7 @@ import type { ErrorResponse } from "@ensnode/ensnode-sdk"; import { migrateEnsNodeSchema } from "@/lib/ensdb/migrate-ensnode-schema"; import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; -import { formatLogParam, logger } from "@/lib/logger"; +import { buildLogError, formatLogParam, logger } from "@/lib/logger"; import ensNodeApi from "./handlers/ensnode-api"; @@ -22,7 +22,7 @@ migrateEnsNodeSchema() .catch((error) => { logger.error({ msg: "Failed to initialize ENSNode metadata", - error: error instanceof Error ? error : undefined, + error: buildLogError(error), module: formatLogParam("ponder-api"), }); process.exit(1); @@ -46,7 +46,7 @@ app.route("/api", ensNodeApi); app.onError((error, ctx) => { logger.error({ msg: "Internal server error", - error: error instanceof Error ? error : undefined, + error: buildLogError(error), path: formatLogParam(ctx.req.path), module: formatLogParam("ponder-api"), }); diff --git a/apps/ensindexer/src/lib/__test__/mockLogger.ts b/apps/ensindexer/src/lib/__test__/mockLogger.ts index 799a30661..c24f2698d 100644 --- a/apps/ensindexer/src/lib/__test__/mockLogger.ts +++ b/apps/ensindexer/src/lib/__test__/mockLogger.ts @@ -1,16 +1,25 @@ import { vi } from "vitest"; +// Set up the global PONDER_COMMON.logger before mocking to allow importOriginal to work +const mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), +}; + +(globalThis as any).PONDER_COMMON = { logger: mockLogger }; + /** * Mock the logger module to avoid the globalThis.PONDER_COMMON check. + * Uses real implementations for formatLogParam and buildLogError. */ -export function setupLoggerMock() { - vi.mock("@/lib/logger", () => ({ - logger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - formatLogParam: vi.fn((param: unknown) => JSON.stringify(param)), - })); -} +vi.mock("@/lib/logger", async (importOriginal) => { + const { buildLogError, formatLogParam } = await importOriginal(); + + return { + logger: mockLogger, + formatLogParam, + buildLogError, + }; +}); diff --git a/apps/ensindexer/src/lib/dns-helpers.test.ts b/apps/ensindexer/src/lib/dns-helpers.test.ts index 401466402..3e6a7cd13 100644 --- a/apps/ensindexer/src/lib/dns-helpers.test.ts +++ b/apps/ensindexer/src/lib/dns-helpers.test.ts @@ -3,9 +3,7 @@ import { bytesToHex, decodeEventLog, stringToHex, zeroHash } from "viem"; import { packetToBytes } from "viem/ens"; import { describe, expect, it } from "vitest"; -import { setupLoggerMock } from "@/lib/__test__/mockLogger"; - -setupLoggerMock(); +import "@/lib/__test__/mockLogger"; import { getDatasource } from "@ensnode/datasources"; import type { DNSEncodedLiteralName } from "@ensnode/ensnode-sdk"; diff --git a/apps/ensindexer/src/lib/dns-helpers.ts b/apps/ensindexer/src/lib/dns-helpers.ts index 0e4680f41..79973d0ae 100644 --- a/apps/ensindexer/src/lib/dns-helpers.ts +++ b/apps/ensindexer/src/lib/dns-helpers.ts @@ -168,7 +168,11 @@ export function parseDnsTxtRecordArgs({ }); if (txtDatas.length === 0) { - logger.warn({ msg: `parseDNSRecordArgs: No TXT answers found in DNS record for key '${key}'` }); + logger.warn({ + msg: "No TXT answers found in DNS record", + fn: "parseDnsTxtRecordArgs", + textRecordKey: formatLogParam(key), + }); // no text answers? interpret as deletion return { key, value: null }; @@ -176,8 +180,10 @@ export function parseDnsTxtRecordArgs({ if (txtDatas.length > 1) { logger.warn({ - msg: `parseDNSRecordArgs: received multiple TXT answers, this is unexpected. Only using the first one.`, - answers: `[${txtDatas.join(",")}]`, + msg: `Received multiple TXT answers, this is unexpected. Only using the first one.`, + fn: "parseDnsTxtRecordArgs", + textRecordKey: formatLogParam(key), + answers: formatLogParam(txtDatas), }); } diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts index d2f5a98a2..ba0f0bee5 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts @@ -6,9 +6,7 @@ import { validateEnsIndexerPublicConfigCompatibility, } from "@ensnode/ensnode-sdk"; -import { setupLoggerMock } from "@/lib/__test__/mockLogger"; - -setupLoggerMock(); +import "@/lib/__test__/mockLogger"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index 5c3002aa1..5478934c7 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -14,7 +14,7 @@ import { import type { LocalPonderClient } from "@ensnode/ponder-sdk"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; -import { formatLogParam, logger } from "@/lib/logger"; +import { buildLogError, formatLogParam, logger } from "@/lib/logger"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; /** @@ -210,7 +210,7 @@ export class EnsDbWriterWorker { logger.error({ msg: "Failed to fetch ENSIndexer public config", - error: error instanceof Error ? error : undefined, + error: buildLogError(error), module: formatLogParam("EnsDbWriterWorker"), }); @@ -234,7 +234,7 @@ export class EnsDbWriterWorker { logger.error({ msg: "In-memory config incompatible with stored config", - error: error instanceof Error ? error : undefined, + error: buildLogError(error), module: formatLogParam("EnsDbWriterWorker"), }); @@ -271,7 +271,7 @@ export class EnsDbWriterWorker { } catch (error) { logger.error({ msg: "Failed to upsert indexing status snapshot", - error: error instanceof Error ? error : undefined, + error: buildLogError(error), module: formatLogParam("EnsDbWriterWorker"), }); // Do not throw the error, as failure to retrieve the Indexing Status diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index 8b9168ce0..9b66691d2 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,7 +1,7 @@ import { ensDbClient } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; import { localPonderClient } from "@/lib/local-ponder-client"; -import { logger } from "@/lib/logger"; +import { buildLogError, logger } from "@/lib/logger"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; import { EnsDbWriterWorker } from "./ensdb-writer-worker"; @@ -36,7 +36,10 @@ export function startEnsDbWriterWorker() { // Abort the worker on error to trigger cleanup ensDbWriterWorker.stop(); - logger.error({ msg: "EnsDbWriterWorker encountered an error", error }); + logger.error({ + msg: "EnsDbWriterWorker encountered an error", + error: buildLogError(error), + }); // Re-throw the error to ensure the application shuts down with a non-zero exit code. process.exitCode = 1; diff --git a/apps/ensindexer/src/lib/graphnode-helpers.test.ts b/apps/ensindexer/src/lib/graphnode-helpers.test.ts index 9d2c9db16..cc8d62324 100644 --- a/apps/ensindexer/src/lib/graphnode-helpers.test.ts +++ b/apps/ensindexer/src/lib/graphnode-helpers.test.ts @@ -3,9 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { LabelHash } from "@ensnode/ensnode-sdk"; import { setupConfigMock } from "@/lib/__test__/mockConfig"; -import { setupLoggerMock } from "@/lib/__test__/mockLogger"; +import "@/lib/__test__/mockLogger"; -setupLoggerMock(); setupConfigMock(); // setup config mock before importing dependent modules // Use real p-retry logic but with 0 timeouts so tests don't incur actual backoff delays. diff --git a/apps/ensindexer/src/lib/logger.ts b/apps/ensindexer/src/lib/logger.ts index fea0b328e..ac1775084 100644 --- a/apps/ensindexer/src/lib/logger.ts +++ b/apps/ensindexer/src/lib/logger.ts @@ -12,12 +12,31 @@ if (!globalThis.PONDER_COMMON?.logger) { export const logger = globalThis.PONDER_COMMON.logger; /** - * Formats a log parameter as a pretty-printed JSON string. - * This is useful for logging complex objects in a readable format. + * Formats a log parameter as a single-line, compact JSON string. + * This is useful for logging complex objects inline without extra whitespace + * or newlines. */ export const formatLogParam = (param: unknown): string => prettyPrintJson(param) .split("\n") - .map((line) => line.trim().replace(": ", ":")) + .map((line) => + line + .trim() + // This regex only removes the whitespace after + // a top-level JSON key's closing quote, leaving any colons inside + // string values untouched. + .replace(/^("(?:[^"\\]|\\.)*"):\s/, "$1:"), + ) .join("") .trim(); + +/** + * Builds the value to be used for the `error` property in log messages. + */ +export const buildLogError = (error: unknown): Error | undefined => { + if (error instanceof Error) { + return error; + } + + return undefined; +}; diff --git a/apps/ensindexer/src/lib/version-info.ts b/apps/ensindexer/src/lib/version-info.ts index 961fadacf..283256982 100644 --- a/apps/ensindexer/src/lib/version-info.ts +++ b/apps/ensindexer/src/lib/version-info.ts @@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { logger } from "@/lib/logger"; +import { buildLogError, logger } from "@/lib/logger"; /** * Get ENSIndexer version @@ -66,7 +66,7 @@ export function getPackageVersion(packageName: string) { } catch (error) { logger.error({ msg: `Could not find version for ${packageName}`, - error: error instanceof Error ? error : undefined, + error: buildLogError(error), }); return "unknown"; diff --git a/packages/ponder-sdk/src/ponder-app-context.ts b/packages/ponder-sdk/src/ponder-app-context.ts index 904298463..49bb75a73 100644 --- a/packages/ponder-sdk/src/ponder-app-context.ts +++ b/packages/ponder-sdk/src/ponder-app-context.ts @@ -50,18 +50,18 @@ export interface PonderAppLogger { * ```ts * logger.error({ * msg: "Incorrect omnichain status", - * error: new Error("The omnichain status be either 'omnichain-backfill' or 'omnichain-following'"), + * error: new Error("The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'"), * expected: "omnichain-backfill or omnichain-following", * actual: "omnichain-unstarted" * }); * * logger.error({ * msg: "Incorrect omnichain status", - * error: new Error("The omnichain status be either 'omnichain-backfill' or 'omnichain-following'"), + * error: new Error("The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'"), * }); * * logger.error({ - * msg: "The omnichain status be either 'omnichain-backfill' or 'omnichain-following'" + * msg: "The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'" * }); * ``` */ From de77f11507ef93520c3c462600bc342b69a97f02 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 3 Apr 2026 07:34:06 +0200 Subject: [PATCH 08/13] Update `PonderAppContext[logger]` instance Improve log message formatting by making the "raw" Ponder app logger to use custom formatting helpers. --- .../src/deserialize/ponder-app-context.ts | 6 +- .../src/deserialize/ponder-app-logger.ts | 101 ++++++++++++++ packages/ponder-sdk/src/index.ts | 1 + packages/ponder-sdk/src/ponder-app-context.ts | 123 +----------------- packages/ponder-sdk/src/ponder-app-logger.ts | 120 +++++++++++++++++ 5 files changed, 227 insertions(+), 124 deletions(-) create mode 100644 packages/ponder-sdk/src/deserialize/ponder-app-logger.ts create mode 100644 packages/ponder-sdk/src/ponder-app-logger.ts diff --git a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts index 2f12de226..8e1379e1e 100644 --- a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts +++ b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts @@ -14,8 +14,8 @@ import { type PonderAppCommand, PonderAppCommands, type PonderAppContext, - type PonderAppLogger, } from "../ponder-app-context"; +import { wrapPonderAppLogger } from "./ponder-app-logger"; import type { Unvalidated } from "./utils"; /** @@ -34,7 +34,7 @@ const schemaPonderAppLoggerMethod = z.function({ input: [ z.looseObject({ msg: z.string({ error: "Log message must be a string." }), - error: z.optional(z.instanceof(Error, { error: "Error must be an instance of Error." })), + error: z.optional(z.unknown()), }), ], output: z.void(), @@ -51,7 +51,7 @@ const schemaPonderAppLogger = z debug: schemaPonderAppLoggerMethod, trace: schemaPonderAppLoggerMethod, }) - .transform((logger) => logger as PonderAppLogger); + .transform(wrapPonderAppLogger); /** * Type representing the "raw" context of a local Ponder app. diff --git a/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts b/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts new file mode 100644 index 000000000..a6d4e21bb --- /dev/null +++ b/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts @@ -0,0 +1,101 @@ +import type { PonderAppLog, PonderAppLogger } from "../ponder-app-logger"; + +/** + * Represents a primitive value type that can be logged directly + * without formatting. + */ +type Primitive = string | number | boolean | bigint | symbol | null | undefined; + +/** + * Type guard helper to check if a value is a primitive type. + * + * Used with {@link formatLogValue}. + */ +function isPrimitive(value: unknown): value is Primitive { + return value === null || (typeof value !== "object" && typeof value !== "function"); +} + +/** + * JSON replacer function that handles special types for serialization. + * - URL objects are converted to their href string + * - Map objects are converted to plain objects + * - Set objects are converted to arrays + * + * Used with {@link formatLogValue}. + */ +function replacer(_key: string, value: unknown): unknown { + // stringify a URL object + if (value instanceof URL) return value.href; + + // convert Map to plain object for serialization + if (value instanceof Map) return Object.fromEntries(value); + + // convert Set to array for serialization + if (value instanceof Set) return Array.from(value); + + // pass-through value + return value; +} + +/** + * Formats a value for logging. + * - Primitives and Errors are returned as-is + * - Objects are JSON stringified with the replacer and collapsed to single line + * + * Used with {@link wrapLogMethod} to automatically format log parameters before + * passing to the underlying logger. + */ +function formatLogValue(value: unknown): unknown { + // Primitives pass through + if (isPrimitive(value)) return value; + + // Error instances pass through (handled specially by logger) + if (value instanceof Error) return value; + + // Otherwise JSON stringify with replacer + return JSON.stringify(value, replacer); +} + +/** + * Wraps a logger method to provide automatic parameter formatting. + * - Non-Error values in the `error` field are filtered out + * - Complex values are automatically JSON stringified + */ +function wrapLogMethod(fn: (options: Log) => void) { + return (options: Log) => { + const formattedOptions = Object.fromEntries( + Object.entries(options) + // Filter out non-Error values in the `error` field + .filter(([key, value]) => { + if (key === "error" && !(value instanceof Error)) return false; + return true; + }) + // Format values + .map(([key, value]) => [key, formatLogValue(value)]), + ) as Log; + + return fn(formattedOptions); + }; +} + +/** + * Wraps the raw Ponder App Logger provided by the Ponder runtime to + * automatically format log parameters: + * + * - Primitives are passed through as-is + * - Error instances are passed through as-is (and handled specially by the logger) + * - Objects are JSON stringified (with special handling for URL, Map, Set) + * - Non-Error `error` values are automatically filtered out + * + * This maintains full compatibility with the {@link PonderAppLogger} interface. + */ +export function wrapPonderAppLogger(rawLogger: PonderAppLogger): PonderAppLogger { + return Object.freeze({ + ...rawLogger, + error: wrapLogMethod(rawLogger.error.bind(rawLogger)), + warn: wrapLogMethod(rawLogger.warn.bind(rawLogger)), + info: wrapLogMethod(rawLogger.info.bind(rawLogger)), + debug: wrapLogMethod(rawLogger.debug.bind(rawLogger)), + trace: wrapLogMethod(rawLogger.trace.bind(rawLogger)), + }); +} diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index a3be62ccc..2dc1103ed 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -10,4 +10,5 @@ export * from "./local-indexing-metrics"; export * from "./local-ponder-client"; export * from "./numbers"; export * from "./ponder-app-context"; +export * from "./ponder-app-logger"; export * from "./time"; diff --git a/packages/ponder-sdk/src/ponder-app-context.ts b/packages/ponder-sdk/src/ponder-app-context.ts index 49bb75a73..4e560f1aa 100644 --- a/packages/ponder-sdk/src/ponder-app-context.ts +++ b/packages/ponder-sdk/src/ponder-app-context.ts @@ -1,3 +1,5 @@ +import type { PonderAppLogger } from "./ponder-app-logger"; + /** * Ponder app commands * @@ -10,127 +12,6 @@ export const PonderAppCommands = { export type PonderAppCommand = (typeof PonderAppCommands)[keyof typeof PonderAppCommands]; -/** - * Represents a single log entry for the Ponder app logger. - * - * It is a loose object that: - * - must contain a `msg` property of type string, and - * - can optionally include an `error` property of type Error, and - * - can optionally include any additional properties relevant to - * the log message. The additional properties can be used to provide more - * context about the log message, and will be included in the log output. - */ -type PonderAppLog = { - /** - * Log message - */ - msg: string; - - /** - * Optional error object to log. - * - * If provided, the logger will log the error's stack trace and message. - */ - error?: Error; -} & Record; - -/** - * Ponder app logger - * - * Represents the logger provided by the Ponder runtime to a local Ponder app. - * @see https://github.com/ponder-sh/ponder/blob/6fcc15d4234e43862cb6e21c05f3c57f4c2f7464/packages/core/src/internal/logger.ts#L8-L31 - */ -export interface PonderAppLogger { - /** - * Logs a message at the "error" level. - * - * @param options - The log message and additional properties to log. - * - * @example - * ```ts - * logger.error({ - * msg: "Incorrect omnichain status", - * error: new Error("The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'"), - * expected: "omnichain-backfill or omnichain-following", - * actual: "omnichain-unstarted" - * }); - * - * logger.error({ - * msg: "Incorrect omnichain status", - * error: new Error("The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'"), - * }); - * - * logger.error({ - * msg: "The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'" - * }); - * ``` - */ - error(options: T): void; - - /** - * Logs a message at the "warn" level. - * - * @param options - The log message and additional properties to log. - * - * @example - * ```ts - * logger.warn({ - * msg: "Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled.", - * effects: "This results in the availability of both the legacy Subgraph-Compatible GraphQL API (/subgraph) _and_ ENSNode's Omnigraph API (/api/omnigraph), and comes with an associated increase in indexing time. If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use." - * }); - * - * logger.warn({ - * msg: "Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled." - * }); - * ``` - */ - warn(options: T): void; - - /** - * Logs a message at the "info" level. - * @param options - * - * @example - * ```ts - * logger.info({ - * msg: "An informational message", - * details: "Here are some details about the info" - * }); - * ``` - */ - info(options: T): void; - - /** - * Logs a message at the "debug" level. - * @param options - * - * @example - * ```ts - * logger.debug({ - * msg: "A debug message", - * arg1: "Here is some debug information about arg1", - * arg2: "Here is some debug information about arg2" - * }); - * ``` - */ - debug(options: T): void; - - /** - * Logs a message at the "trace" level. - * @param options - * - * @example - * ```ts - * logger.trace({ - * msg: "A trace message", - * detailA: "Here are some details about the trace message", - * detailB: "Here are some more details about the trace message" - * }); - * ``` - */ - trace(options: T): void; -} - /** * Ponder app context * diff --git a/packages/ponder-sdk/src/ponder-app-logger.ts b/packages/ponder-sdk/src/ponder-app-logger.ts new file mode 100644 index 000000000..7fb16cb3f --- /dev/null +++ b/packages/ponder-sdk/src/ponder-app-logger.ts @@ -0,0 +1,120 @@ +/** + * Represents a single log entry for the Ponder app logger. + * + * It is a loose object that: + * - must contain a `msg` property of type string, and + * - can optionally include an `error` property of type Error, and + * - can optionally include any additional properties relevant to + * the log message. The additional properties can be used to provide more + * context about the log message, and will be included in the log output. + */ +export type PonderAppLog = { + /** + * Log message + */ + msg: string; + + /** + * Optional error object to log. + * + * If provided, the logger will log the error's stack trace and message. + */ + error?: unknown; +} & Record; + +/** + * Ponder app logger + * + * Represents the logger provided by the Ponder runtime to a local Ponder app. + * @see https://github.com/ponder-sh/ponder/blob/6fcc15d4234e43862cb6e21c05f3c57f4c2f7464/packages/core/src/internal/logger.ts#L8-L31 + */ +export interface PonderAppLogger { + /** + * Logs a message at the "error" level. + * + * @param options - The log message and additional properties to log. + * + * @example + * ```ts + * logger.error({ + * msg: "Incorrect omnichain status", + * error: new Error("The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'"), + * expected: "omnichain-backfill or omnichain-following", + * actual: "omnichain-unstarted" + * }); + * + * logger.error({ + * msg: "Incorrect omnichain status", + * error: new Error("The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'"), + * }); + * + * logger.error({ + * msg: "The omnichain status must be either 'omnichain-backfill' or 'omnichain-following'" + * }); + * ``` + */ + error(options: T): void; + + /** + * Logs a message at the "warn" level. + * + * @param options - The log message and additional properties to log. + * + * @example + * ```ts + * logger.warn({ + * msg: "Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled.", + * effects: "This results in the availability of both the legacy Subgraph-Compatible GraphQL API (/subgraph) _and_ ENSNode's Omnigraph API (/api/omnigraph), and comes with an associated increase in indexing time. If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use." + * }); + * + * logger.warn({ + * msg: "Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled." + * }); + * ``` + */ + warn(options: T): void; + + /** + * Logs a message at the "info" level. + * @param options + * + * @example + * ```ts + * logger.info({ + * msg: "An informational message", + * details: "Here are some details about the info" + * }); + * ``` + */ + info(options: T): void; + + /** + * Logs a message at the "debug" level. + * @param options + * + * @example + * ```ts + * logger.debug({ + * msg: "A debug message", + * arg1: "Here is some debug information about arg1", + * arg2: "Here is some debug information about arg2" + * }); + * ``` + */ + debug(options: T): void; + + /** + * Logs a message at the "trace" level. + * @param options + * + * @example + * ```ts + * logger.trace({ + * msg: "A trace message", + * detailA: "Here are some details about the trace message", + * detailB: "Here are some more details about the trace message" + * }); + * ``` + */ + trace(options: T): void; +} From 3e3f068d2896a48b9e763593cdd6639848002f68 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 3 Apr 2026 07:35:48 +0200 Subject: [PATCH 09/13] Create a separate file to share `localPonderContext` instance --- apps/ensindexer/src/lib/local-ponder-client.ts | 9 +++------ apps/ensindexer/src/lib/local-ponder-context.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 apps/ensindexer/src/lib/local-ponder-context.ts diff --git a/apps/ensindexer/src/lib/local-ponder-client.ts b/apps/ensindexer/src/lib/local-ponder-client.ts index e30fc6e47..fcfe293a9 100644 --- a/apps/ensindexer/src/lib/local-ponder-client.ts +++ b/apps/ensindexer/src/lib/local-ponder-client.ts @@ -3,15 +3,12 @@ import config from "@/config"; import { publicClients } from "ponder:api"; import { buildIndexedBlockranges } from "@ensnode/ensnode-sdk"; -import { deserializePonderAppContext, LocalPonderClient } from "@ensnode/ponder-sdk"; +import { LocalPonderClient } from "@ensnode/ponder-sdk"; import { getPluginsAllDatasourceNames } from "@/lib/plugin-helpers"; -if (!globalThis.PONDER_COMMON) { - throw new Error("PONDER_COMMON must be defined by Ponder at runtime as a global variable."); -} +import { localPonderContext } from "./local-ponder-context"; -const ponderAppContext = deserializePonderAppContext(globalThis.PONDER_COMMON); const pluginsAllDatasourceNames = getPluginsAllDatasourceNames(config.plugins); const indexedBlockranges = buildIndexedBlockranges(config.namespace, pluginsAllDatasourceNames); @@ -19,5 +16,5 @@ export const localPonderClient = new LocalPonderClient( config.indexedChainIds, indexedBlockranges, publicClients, - ponderAppContext, + localPonderContext, ); diff --git a/apps/ensindexer/src/lib/local-ponder-context.ts b/apps/ensindexer/src/lib/local-ponder-context.ts new file mode 100644 index 000000000..ed66a4088 --- /dev/null +++ b/apps/ensindexer/src/lib/local-ponder-context.ts @@ -0,0 +1,14 @@ +import { deserializePonderAppContext, type PonderAppContext } from "@ensnode/ponder-sdk"; + +if (!globalThis.PONDER_COMMON) { + throw new Error("PONDER_COMMON must be defined by Ponder at runtime as a global variable."); +} + +/** + * Local Ponder app context + * + * Represents the {@link PonderAppContext} object provided by Ponder runtime to + * the local Ponder app. Useful for accessing internal Ponder app configuration + * and utilities such as the logger. + */ +export const localPonderContext = deserializePonderAppContext(globalThis.PONDER_COMMON); From 009b53f4ce086198951e0489b35f36ca6f2959d4 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 3 Apr 2026 07:40:30 +0200 Subject: [PATCH 10/13] Remove `formatLogParam` and `buildLogError` helpers Functionality of those was covered by updates in Ponder SDK --- apps/ensindexer/ponder/ponder.config.ts | 8 ++-- .../ponder/src/api/handlers/ensnode-api.ts | 8 ++-- apps/ensindexer/ponder/src/api/index.ts | 12 ++--- .../ensindexer/src/lib/__test__/mockLogger.ts | 8 +--- apps/ensindexer/src/lib/dns-helpers.ts | 10 ++--- .../ensdb-writer-worker.ts | 32 ++++++------- .../src/lib/ensdb-writer-worker/singleton.ts | 4 +- .../src/lib/ensdb/migrate-ensnode-schema.ts | 6 +-- .../src/lib/ensrainbow/singleton.ts | 32 ++++++++----- apps/ensindexer/src/lib/logger.ts | 45 +++---------------- apps/ensindexer/src/lib/version-info.ts | 4 +- 11 files changed, 70 insertions(+), 99 deletions(-) diff --git a/apps/ensindexer/ponder/ponder.config.ts b/apps/ensindexer/ponder/ponder.config.ts index fd073a0de..5fbb2be5e 100644 --- a/apps/ensindexer/ponder/ponder.config.ts +++ b/apps/ensindexer/ponder/ponder.config.ts @@ -3,7 +3,7 @@ import config from "@/config"; import { PluginName } from "@ensnode/ensnode-sdk"; import { redactENSIndexerConfig } from "@/config/redact"; -import { formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; import ponderConfig from "@/ponder/config"; //////// @@ -12,16 +12,14 @@ import ponderConfig from "@/ponder/config"; logger.info({ msg: "ENSIndexer starting", - config: formatLogParam(redactENSIndexerConfig(config)), + config: redactENSIndexerConfig(config), }); // Log warning about dual activation of subgraph and ensv2 plugins if (config.plugins.includes(PluginName.Subgraph) && config.plugins.includes(PluginName.ENSv2)) { logger.warn({ msg: `Both the '${PluginName.Subgraph}' and '${PluginName.ENSv2}' plugins are enabled. This results in the availability of both the legacy Subgraph-Compatible GraphQL API (/subgraph) _and_ ENSNode's Omnigraph API (/api/omnigraph), and comes with an associated increase in indexing time.`, - advice: formatLogParam( - `If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use.`, - ), + advice: `If your intent is to have both APIs available in parallel, excellent, otherwise you may benefit from only enabling the plugin for the API you plan to use.`, }); } diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index d3461488d..af826cb61 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -11,7 +11,7 @@ import { } from "@ensnode/ensnode-sdk"; import { ensDbClient } from "@/lib/ensdb/singleton"; -import { formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; const app = new Hono(); @@ -58,9 +58,9 @@ app.get("/indexing-status", async (c) => { } catch (error) { logger.error({ msg: "Indexing status snapshot unavailable", - error: error instanceof Error ? error : undefined, - module: formatLogParam("ensnode-api"), - endpoint: formatLogParam("/indexing-status"), + error, + module: "ensnode-api", + endpoint: "/indexing-status", }); return c.json( diff --git a/apps/ensindexer/ponder/src/api/index.ts b/apps/ensindexer/ponder/src/api/index.ts index 2f23b1d2b..4b7573d75 100644 --- a/apps/ensindexer/ponder/src/api/index.ts +++ b/apps/ensindexer/ponder/src/api/index.ts @@ -7,7 +7,7 @@ import type { ErrorResponse } from "@ensnode/ensnode-sdk"; import { migrateEnsNodeSchema } from "@/lib/ensdb/migrate-ensnode-schema"; import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; -import { buildLogError, formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; import ensNodeApi from "./handlers/ensnode-api"; @@ -22,8 +22,8 @@ migrateEnsNodeSchema() .catch((error) => { logger.error({ msg: "Failed to initialize ENSNode metadata", - error: buildLogError(error), - module: formatLogParam("ponder-api"), + error, + module: "ponder-api", }); process.exit(1); }); @@ -46,9 +46,9 @@ app.route("/api", ensNodeApi); app.onError((error, ctx) => { logger.error({ msg: "Internal server error", - error: buildLogError(error), - path: formatLogParam(ctx.req.path), - module: formatLogParam("ponder-api"), + error, + path: ctx.req.path, + module: "ponder-api", }); return ctx.json({ message: "Internal Server Error" } satisfies ErrorResponse, 500); }); diff --git a/apps/ensindexer/src/lib/__test__/mockLogger.ts b/apps/ensindexer/src/lib/__test__/mockLogger.ts index c24f2698d..d2c1e356d 100644 --- a/apps/ensindexer/src/lib/__test__/mockLogger.ts +++ b/apps/ensindexer/src/lib/__test__/mockLogger.ts @@ -2,6 +2,7 @@ import { vi } from "vitest"; // Set up the global PONDER_COMMON.logger before mocking to allow importOriginal to work const mockLogger = { + trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), @@ -12,14 +13,9 @@ const mockLogger = { /** * Mock the logger module to avoid the globalThis.PONDER_COMMON check. - * Uses real implementations for formatLogParam and buildLogError. */ -vi.mock("@/lib/logger", async (importOriginal) => { - const { buildLogError, formatLogParam } = await importOriginal(); - +vi.mock("@/lib/logger", async () => { return { logger: mockLogger, - formatLogParam, - buildLogError, }; }); diff --git a/apps/ensindexer/src/lib/dns-helpers.ts b/apps/ensindexer/src/lib/dns-helpers.ts index 79973d0ae..d2e831e2c 100644 --- a/apps/ensindexer/src/lib/dns-helpers.ts +++ b/apps/ensindexer/src/lib/dns-helpers.ts @@ -14,7 +14,7 @@ import { } from "@ensnode/ensnode-sdk"; import { interpretTextRecordKey, interpretTextRecordValue } from "@ensnode/ensnode-sdk/internal"; -import { formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; import { isLabelSubgraphIndexable } from "@/lib/subgraph/is-label-subgraph-indexable"; /** @@ -120,7 +120,7 @@ export function decodeTXTData(data: Buffer[]): string | null { if (decoded.length > 1) { logger.warn({ msg: `decodeTXTData received multiple 'data' results, this is unexpected.`, - data: formatLogParam(decoded), + data: decoded, }); } @@ -171,7 +171,7 @@ export function parseDnsTxtRecordArgs({ logger.warn({ msg: "No TXT answers found in DNS record", fn: "parseDnsTxtRecordArgs", - textRecordKey: formatLogParam(key), + textRecordKey: key, }); // no text answers? interpret as deletion @@ -182,8 +182,8 @@ export function parseDnsTxtRecordArgs({ logger.warn({ msg: `Received multiple TXT answers, this is unexpected. Only using the first one.`, fn: "parseDnsTxtRecordArgs", - textRecordKey: formatLogParam(key), - answers: formatLogParam(txtDatas), + textRecordKey: key, + answers: txtDatas, }); } diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index 5478934c7..7204dd535 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -14,7 +14,7 @@ import { import type { LocalPonderClient } from "@ensnode/ponder-sdk"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; -import { buildLogError, formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; /** @@ -101,23 +101,23 @@ export class EnsDbWriterWorker { const inMemoryConfig = await this.getValidatedEnsIndexerPublicConfig(); // Task 1: upsert ENSDb version into ENSDb. - logger.debug({ msg: "Upserting ENSDb version", module: formatLogParam("EnsDbWriterWorker") }); + logger.debug({ msg: "Upserting ENSDb version", module: "EnsDbWriterWorker" }); await this.ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); logger.info({ msg: "Upserted ENSDb version", - ensDbVersion: formatLogParam(inMemoryConfig.versionInfo.ensDb), - module: formatLogParam("EnsDbWriterWorker"), + ensDbVersion: inMemoryConfig.versionInfo.ensDb, + module: "EnsDbWriterWorker", }); // Task 2: upsert of EnsIndexerPublicConfig into ENSDb. logger.debug({ msg: "Upserting ENSIndexer public config", - module: formatLogParam("EnsDbWriterWorker"), + module: "EnsDbWriterWorker", }); await this.ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig); logger.info({ msg: "Upserted ENSIndexer public config", - module: formatLogParam("EnsDbWriterWorker"), + module: "EnsDbWriterWorker", }); // Task 3: recurring upsert of Indexing Status Snapshot into ENSDb. @@ -177,7 +177,7 @@ export class EnsDbWriterWorker { logger.debug({ msg: "Fetching ENSIndexer public config", retries: configFetchRetries, - module: formatLogParam("EnsDbWriterWorker"), + module: "EnsDbWriterWorker", }); const inMemoryConfigPromise = pRetry(() => this.publicConfigBuilder.getPublicConfig(), { @@ -187,7 +187,7 @@ export class EnsDbWriterWorker { msg: "Config fetch attempt failed", attempt: attemptNumber, retriesLeft, - module: formatLogParam("EnsDbWriterWorker"), + module: "EnsDbWriterWorker", }); }, }); @@ -202,16 +202,16 @@ export class EnsDbWriterWorker { ]); logger.info({ msg: "Fetched ENSIndexer public config", - module: formatLogParam("EnsDbWriterWorker"), - config: formatLogParam(inMemoryConfig), + module: "EnsDbWriterWorker", + config: inMemoryConfig, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; logger.error({ msg: "Failed to fetch ENSIndexer public config", - error: buildLogError(error), - module: formatLogParam("EnsDbWriterWorker"), + error, + module: "EnsDbWriterWorker", }); // Throw the error to terminate the ENSIndexer process due to failed fetch of critical dependency @@ -234,8 +234,8 @@ export class EnsDbWriterWorker { logger.error({ msg: "In-memory config incompatible with stored config", - error: buildLogError(error), - module: formatLogParam("EnsDbWriterWorker"), + error, + module: "EnsDbWriterWorker", }); // Throw the error to terminate the ENSIndexer process due to @@ -271,8 +271,8 @@ export class EnsDbWriterWorker { } catch (error) { logger.error({ msg: "Failed to upsert indexing status snapshot", - error: buildLogError(error), - module: formatLogParam("EnsDbWriterWorker"), + error, + module: "EnsDbWriterWorker", }); // Do not throw the error, as failure to retrieve the Indexing Status // should not cause the ENSDb Writer Worker to stop functioning. diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index 9b66691d2..22fd6a5e9 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,7 +1,7 @@ import { ensDbClient } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; import { localPonderClient } from "@/lib/local-ponder-client"; -import { buildLogError, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; import { EnsDbWriterWorker } from "./ensdb-writer-worker"; @@ -38,7 +38,7 @@ export function startEnsDbWriterWorker() { logger.error({ msg: "EnsDbWriterWorker encountered an error", - error: buildLogError(error), + error, }); // Re-throw the error to ensure the application shuts down with a non-zero exit code. diff --git a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts index fe755288b..b5c1116c5 100644 --- a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts +++ b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts @@ -1,7 +1,7 @@ import { createRequire } from "node:module"; import { join } from "node:path"; -import { formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; import { ensDbClient } from "./singleton"; @@ -17,11 +17,11 @@ const migrationsDirPath = join( export async function migrateEnsNodeSchema(): Promise { logger.debug({ msg: "Started database migrations", - module: formatLogParam("migrate-ensnode-schema"), + module: "migrate-ensnode-schema", }); await ensDbClient.migrateEnsNodeSchema(migrationsDirPath); logger.info({ msg: "Completed database migrations", - module: formatLogParam("migrate-ensnode-schema"), + module: "migrate-ensnode-schema", }); } diff --git a/apps/ensindexer/src/lib/ensrainbow/singleton.ts b/apps/ensindexer/src/lib/ensrainbow/singleton.ts index ed0db2fdf..c6785560c 100644 --- a/apps/ensindexer/src/lib/ensrainbow/singleton.ts +++ b/apps/ensindexer/src/lib/ensrainbow/singleton.ts @@ -5,16 +5,14 @@ import pRetry from "p-retry"; import { EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk"; -import { buildLogError, formatLogParam, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; const { ensRainbowUrl, labelSet } = config; if (ensRainbowUrl.href === EnsRainbowApiClient.defaultOptions().endpointUrl.href) { logger.warn({ msg: `Using default public ENSRainbow server which may cause increased network latency`, - advice: formatLogParam( - "For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.", - ), + advice: `For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.`, }); } @@ -53,7 +51,10 @@ export function waitForEnsRainbowToBeReady(): Promise { return waitForEnsRainbowToBeReadyPromise; } - console.log(`Waiting for ENSRainbow instance to be ready at '${ensRainbowUrl}'...`); + logger.info({ + msg: `Waiting for ENSRainbow instance to be ready`, + ensRainbowInstance: ensRainbowUrl.href, + }); waitForEnsRainbowToBeReadyPromise = pRetry(async () => ensRainbowClient.health(), { retries: 60, // This allows for a total of over 1 hour of retries with 1 minute between attempts. @@ -64,19 +65,26 @@ export function waitForEnsRainbowToBeReady(): Promise { msg: `ENSRainbow health check failed`, attempt: attemptNumber, retriesLeft, - error: buildLogError(retriesLeft === 0 ? error : undefined), - ensRainbowInstance: formatLogParam(ensRainbowUrl.href), - advice: formatLogParam( - `This might be due to ENSRainbow having a cold start, which can take 30+ minutes.`, - ), + error: retriesLeft === 0 ? error : undefined, + ensRainbowInstance: ensRainbowUrl.href, + advice: `This might be due to ENSRainbow having a cold start, which can take 30+ minutes.`, }); }, }) - .then(() => console.log(`ENSRainbow instance is ready at '${ensRainbowUrl}'.`)) + .then(() => { + logger.info({ + msg: `ENSRainbow instance is ready`, + ensRainbowInstance: ensRainbowUrl.href, + }); + }) .catch((error) => { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - console.error(`ENSRainbow health check failed after multiple attempts: ${errorMessage}`); + logger.error({ + msg: `ENSRainbow health check failed after multiple attempts`, + error, + ensRainbowInstance: ensRainbowUrl.href, + }); // Throw the error to terminate the ENSIndexer process due to the failed health check of a critical dependency throw new Error(errorMessage, { diff --git a/apps/ensindexer/src/lib/logger.ts b/apps/ensindexer/src/lib/logger.ts index ac1775084..a98fe9536 100644 --- a/apps/ensindexer/src/lib/logger.ts +++ b/apps/ensindexer/src/lib/logger.ts @@ -1,42 +1,11 @@ -import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; +import type { PonderAppLogger } from "@ensnode/ponder-sdk"; -if (!globalThis.PONDER_COMMON?.logger) { - throw new Error( - "Ponder Common Logger must be provided by Ponder runtime at globalThis.PONDER_COMMON.logger", - ); -} +import { localPonderContext } from "@/lib/local-ponder-context"; /** - * Logger instance for ENSIndexer to use. + * Logger for the ENSIndexer app + * + * Represents the {@link PonderAppLogger} provided by + * the Ponder runtime to the ENSIndexer app. */ -export const logger = globalThis.PONDER_COMMON.logger; - -/** - * Formats a log parameter as a single-line, compact JSON string. - * This is useful for logging complex objects inline without extra whitespace - * or newlines. - */ -export const formatLogParam = (param: unknown): string => - prettyPrintJson(param) - .split("\n") - .map((line) => - line - .trim() - // This regex only removes the whitespace after - // a top-level JSON key's closing quote, leaving any colons inside - // string values untouched. - .replace(/^("(?:[^"\\]|\\.)*"):\s/, "$1:"), - ) - .join("") - .trim(); - -/** - * Builds the value to be used for the `error` property in log messages. - */ -export const buildLogError = (error: unknown): Error | undefined => { - if (error instanceof Error) { - return error; - } - - return undefined; -}; +export const logger = localPonderContext.logger; diff --git a/apps/ensindexer/src/lib/version-info.ts b/apps/ensindexer/src/lib/version-info.ts index 283256982..284d1ec1b 100644 --- a/apps/ensindexer/src/lib/version-info.ts +++ b/apps/ensindexer/src/lib/version-info.ts @@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { buildLogError, logger } from "@/lib/logger"; +import { logger } from "@/lib/logger"; /** * Get ENSIndexer version @@ -66,7 +66,7 @@ export function getPackageVersion(packageName: string) { } catch (error) { logger.error({ msg: `Could not find version for ${packageName}`, - error: buildLogError(error), + error, }); return "unknown"; From 624729258fd4ef311b6493f7be3f1a7d0476ef76 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 3 Apr 2026 07:45:55 +0200 Subject: [PATCH 11/13] Replaced `prettyPrintJson` with `stringifyConfig` Part of streamling formatting for logging config objects --- apps/ensrainbow/src/commands/server-command.ts | 6 +++--- .../ensnode-sdk/src/shared/config/pretty-printing.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/ensrainbow/src/commands/server-command.ts b/apps/ensrainbow/src/commands/server-command.ts index d53b7931d..1288c9b46 100644 --- a/apps/ensrainbow/src/commands/server-command.ts +++ b/apps/ensrainbow/src/commands/server-command.ts @@ -2,7 +2,7 @@ import type { ServeCommandConfig } from "@/config"; import { serve } from "@hono/node-server"; -import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; +import { stringifyConfig } from "@ensnode/ensnode-sdk/internal"; import { buildEnsRainbowPublicConfig } from "@/config/public"; import { createApi } from "@/lib/api"; @@ -15,7 +15,7 @@ export type ServerCommandOptions = ServeCommandConfig; export async function serverCommand(options: ServerCommandOptions): Promise { // console.log is used so it can't be skipped by the logger console.log("ENSRainbow running with config:"); - console.log(prettyPrintJson(options)); + console.log(stringifyConfig(options, { pretty: true })); logger.info(`ENS Rainbow server starting on port ${options.port}...`); @@ -28,7 +28,7 @@ export async function serverCommand(options: ServerCommandOptions): Promise JSON.stringify(json, configJSONReplacer, 2); +export const stringifyConfig = (json: any, options: { pretty: boolean } = { pretty: false }) => + JSON.stringify(json, configJSONReplacer, options.pretty ? 2 : undefined); From 90c8aec1df73a5fb0d51e1ce4a669e39d4eeb733 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 3 Apr 2026 07:52:07 +0200 Subject: [PATCH 12/13] Apply AI PR feedback --- .../src/deserialize/ponder-app-context.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts index 8e1379e1e..f1e20fcfb 100644 --- a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts +++ b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts @@ -41,17 +41,21 @@ const schemaPonderAppLoggerMethod = z.function({ }); /** - * Represents the logger provided by the Ponder runtime to a local Ponder app. + * Represents the "raw" logger provided by the Ponder runtime to a local Ponder app. */ -const schemaPonderAppLogger = z - .looseObject({ - error: schemaPonderAppLoggerMethod, - warn: schemaPonderAppLoggerMethod, - info: schemaPonderAppLoggerMethod, - debug: schemaPonderAppLoggerMethod, - trace: schemaPonderAppLoggerMethod, - }) - .transform(wrapPonderAppLogger); +const schemaRawPonderAppLogger = z.looseObject({ + error: schemaPonderAppLoggerMethod, + warn: schemaPonderAppLoggerMethod, + info: schemaPonderAppLoggerMethod, + debug: schemaPonderAppLoggerMethod, + trace: schemaPonderAppLoggerMethod, +}); + +/** + * Represents the "wrapper" logger that formats log parameters + * before passing to the underlying logger. + */ +const schemaPonderAppLogger = schemaRawPonderAppLogger.transform(wrapPonderAppLogger); /** * Type representing the "raw" context of a local Ponder app. @@ -61,7 +65,7 @@ const schemaRawPonderAppContext = z.object({ command: z.enum(PonderAppCommands), port: schemaPortNumber, }), - logger: schemaPonderAppLogger, + logger: schemaRawPonderAppLogger, }); /** From c6b7cf770f1140619ec0cf48fc428e64ab4238c6 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 3 Apr 2026 10:37:39 -0500 Subject: [PATCH 13/13] nit: tidy up based on bot notes --- .../ponder-sdk/src/deserialize/ponder-app-logger.ts | 13 +++++++++++-- packages/ponder-sdk/src/ponder-app-logger.ts | 9 ++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts b/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts index a6d4e21bb..c7ad9cada 100644 --- a/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts +++ b/packages/ponder-sdk/src/deserialize/ponder-app-logger.ts @@ -17,6 +17,7 @@ function isPrimitive(value: unknown): value is Primitive { /** * JSON replacer function that handles special types for serialization. + * - bigints are converted to strings * - URL objects are converted to their href string * - Map objects are converted to plain objects * - Set objects are converted to arrays @@ -24,6 +25,9 @@ function isPrimitive(value: unknown): value is Primitive { * Used with {@link formatLogValue}. */ function replacer(_key: string, value: unknown): unknown { + // stringify bigints + if (typeof value === "bigint") return value.toString(); + // stringify a URL object if (value instanceof URL) return value.href; @@ -53,7 +57,12 @@ function formatLogValue(value: unknown): unknown { if (value instanceof Error) return value; // Otherwise JSON stringify with replacer - return JSON.stringify(value, replacer); + try { + return JSON.stringify(value, replacer); + } catch { + // And if JSON.stringify throws, fall back to String() + return String(value); + } } /** @@ -84,7 +93,7 @@ function wrapLogMethod(fn: (options: Log) => void) { * * - Primitives are passed through as-is * - Error instances are passed through as-is (and handled specially by the logger) - * - Objects are JSON stringified (with special handling for URL, Map, Set) + * - Objects are JSON stringified (with special handling for bigint, URL, Map, Set) * - Non-Error `error` values are automatically filtered out * * This maintains full compatibility with the {@link PonderAppLogger} interface. diff --git a/packages/ponder-sdk/src/ponder-app-logger.ts b/packages/ponder-sdk/src/ponder-app-logger.ts index 7fb16cb3f..66cb90f5d 100644 --- a/packages/ponder-sdk/src/ponder-app-logger.ts +++ b/packages/ponder-sdk/src/ponder-app-logger.ts @@ -20,7 +20,14 @@ export type PonderAppLog = { * If provided, the logger will log the error's stack trace and message. */ error?: unknown; -} & Record; + + /** + * Optional additional properties. + * + * If provided, they will be included in the log output. + */ + [key: string]: unknown; +}; /** * Ponder app logger