From 226208b444ecd9ad11d8dfc836380ceeba8a5b42 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 16:39:35 -0300 Subject: [PATCH 1/7] feat: structured JSON logging + K8s probe docs (COW-994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structured logging: - Add src/application/helpers/cowLogger.ts — thin JSON logger that emits one JSON line per call with { time, level, msg, ...fields } - Migrate all console.log/warn calls in blockHandler.ts and settlement.ts to cowLog(); each line now carries chainId and block as proper JSON fields so log aggregators can filter by chain without regex parsing - Add --log-format json to pnpm start so Ponder's own log lines are also JSON in production; pnpm dev keeps pretty format for local readability K8s probe docs (docs/deployment.md): - Document /healthz (liveness) vs /ready (readiness) distinction - Add sample K8s manifest snippet with correct probe mapping - Explain why /ready must NOT be used as a liveness probe - Add structured logging section explaining --log-format json behavior Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 36 ++++++++++++ package.json | 2 +- src/application/handlers/blockHandler.ts | 71 +++++++----------------- src/application/handlers/settlement.ts | 42 +++++++------- src/application/helpers/cowLogger.ts | 19 +++++++ 5 files changed, 96 insertions(+), 74 deletions(-) create mode 100644 src/application/helpers/cowLogger.ts diff --git a/docs/deployment.md b/docs/deployment.md index 29a4171..05ce647 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -88,6 +88,42 @@ deployment/ The `Dockerfile` in the project root builds the Ponder image: two-stage Node 22 Alpine, installs dependencies with `--frozen-lockfile`, exposes port 3000, runs `pnpm start`. The health check hits `/ready` with a 24-hour start period (initial sync takes hours). +### Kubernetes Probes + +The indexer exposes two health endpoints with distinct semantics: + +| Endpoint | Semantic | Returns 200 when | +|----------|----------|-----------------| +| `/healthz` | **Liveness** — is the process alive? | Always, once the server starts | +| `/ready` | **Readiness** — is the index fully synced? | Only when fully synced | + +Map these to different K8s probe types: + +```yaml +livenessProbe: + httpGet: + path: /healthz + port: 3000 + periodSeconds: 30 + failureThreshold: 3 +readinessProbe: + httpGet: + path: /ready + port: 3000 + periodSeconds: 10 + failureThreshold: 18 # 3-minute window before marking unready +``` + +**Do not** use `/ready` as the liveness probe. A pod that is still indexing (which takes hours on a cold start) returns 200 on `/healthz` but not on `/ready`. Using `/ready` for liveness would kill the pod before it ever finishes syncing. + +The Docker Compose health check uses `/ready` with a 24-hour start period as a pragmatic fallback for single-container deployments, not as a K8s-style probe. + +### Structured Logging + +`pnpm start` runs with `--log-format json`, which makes both Ponder's internal log lines and the handler log lines (via `cowLog`) emit newline-delimited JSON. Each handler log line includes `chainId` and `block` as top-level fields, enabling log aggregators (Datadog, CloudWatch, Loki) to filter and alert by chain. + +`pnpm dev` uses Ponder's default pretty format for readability during local development. + ### PostgreSQL Auto-Tuning `start-db.sh` tunes memory settings from `POSTGRES_MEMORY_LIMIT`. With the default 1G: diff --git a/package.json b/package.json index 257684c..6d06e55 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "ponder dev", - "start": "ponder start -p 3000 --schema ${DATABASE_SCHEMA:-public}", + "start": "ponder start -p 3000 --schema ${DATABASE_SCHEMA:-public} --log-format json", "db": "ponder db", "codegen": "ponder codegen", "lint": "eslint . --ext .ts", diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index 817453c..d648d33 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -41,6 +41,7 @@ import { parsePollError, } from "../helpers/pollResultErrors"; import { computeOrderUid, type GPv2OrderData } from "../helpers/orderUid"; +import { cowLog } from "../helpers/cowLogger"; const NON_DETERMINISTIC_TYPES = ["PerpetualSwap", "GoodAfterTime", "TradeAboveThreshold", "Unknown"] as const; const SINGLE_SHOT_NON_DETERMINISTIC = ["GoodAfterTime", "TradeAboveThreshold"] as const; @@ -119,9 +120,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { if (dueOrders.length === 0) return; - console.log( - `[COW:C1] ENTER block=${currentBlock} chain=${chainId} due=${dueOrders.length}`, - ); + cowLog("info", "C1:ENTER", { block: String(currentBlock), chainId, due: dueOrders.length }); const c1MulticallPromise = context.client.multicall({ contracts: dueOrders.map((order) => ({ @@ -147,9 +146,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { ); } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:C1] multicall timeout block=${currentBlock} chain=${chainId} due=${dueOrders.length}`, - ); + cowLog("warn", "C1:multicall_timeout", { block: String(currentBlock), chainId, due: dueOrders.length }); return; } throw err; @@ -263,9 +260,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { eq(conditionalOrderGenerator.eventId, order.generatorId), ), ); - console.log( - `[COW:C1] NEVER generatorId=${order.generatorId} reason=${pollResult.reason} block=${currentBlock} chain=${chainId}`, - ); + cowLog("info", "C1:NEVER", { block: String(currentBlock), chainId, generatorId: order.generatorId, reason: pollResult.reason }); neverCount++; break; @@ -284,9 +279,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { eq(conditionalOrderGenerator.eventId, order.generatorId), ), ); - console.log( - `[COW:C1] CANCELLED generatorId=${order.generatorId} block=${currentBlock} chain=${chainId}`, - ); + cowLog("info", "C1:CANCELLED", { block: String(currentBlock), chainId, generatorId: order.generatorId }); break; } } @@ -295,9 +288,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { await Promise.all(successPromises); const capped = dueOrders.length === maxGeneratorsPerBlock; - console.log( - `[COW:C1] DONE block=${currentBlock} chain=${chainId} due=${dueOrders.length} success=${successCount} never=${neverCount} backedOff=${backedOffCount}${capped ? " CAPPED" : ""}`, - ); + cowLog("info", "C1:DONE", { block: String(currentBlock), chainId, due: dueOrders.length, success: successCount, never: neverCount, backedOff: backedOffCount, capped }); }); // ─── C2: Candidate Confirmer ───────────────────────────────────────────────── @@ -386,9 +377,7 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { ), ); - console.log( - `[COW:C2] block=${event.block.number} chain=${chainId} parent-cancelled=${orphanCandidates.length}`, - ); + cowLog("info", "C2:parent_cancelled", { block: String(event.block.number), chainId, parentCancelled: orphanCandidates.length }); } } @@ -548,9 +537,7 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { } if (confirmed > 0 || stale.length > 0) { - console.log( - `[COW:C2] block=${event.block.number} chain=${chainId} candidates=${unconfirmed.length} confirmed=${confirmed} expired=${stale.length}`, - ); + cowLog("info", "C2:DONE", { block: String(event.block.number), chainId, candidates: unconfirmed.length, confirmed, expired: stale.length }); } }); @@ -602,9 +589,7 @@ ponder.on("StatusUpdater:block", async ({ event, context }) => { } if (updated > 0) { - console.log( - `[COW:C3] block=${event.block.number} chain=${chainId} open=${openOrders.length} updated=${updated}`, - ); + cowLog("info", "C3:DONE", { block: String(event.block.number), chainId, open: openOrders.length, updated }); } } @@ -668,9 +653,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { .from(bootstrapRetryQueue) .where(eq(bootstrapRetryQueue.chainId, chainId)); - console.log( - `[COW:C4] block=${currentBlock} chain=${chainId} pending_retry=${queued.length}`, - ); + cowLog("info", "C4:START", { block: String(currentBlock), chainId, pendingRetry: queued.length }); let totalDiscovered = 0; const retriedOwners = new Set(); @@ -690,9 +673,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { .where(and(eq(bootstrapRetryQueue.chainId, chainId), eq(bootstrapRetryQueue.owner, owner as Hex))); } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:C4] owner retry timeout owner=${owner} chain=${chainId} retry_count=${retryCount + 1} after=${BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS}ms`, - ); + cowLog("warn", "C4:owner_retry_timeout", { block: String(currentBlock), chainId, owner, retryCount: retryCount + 1, timeoutMs: BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS }); await context.db.sql .update(bootstrapRetryQueue) .set({ retryCount: retryCount + 1, lastRetryAt: currentBlock }) @@ -735,14 +716,12 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { const freshOwners = new Set(generators.map((g) => g.owner).filter((o) => !retriedOwners.has(o))); if (freshOwners.size === 0 && retriedOwners.size === 0) { - console.log(`[COW:C4] block=${currentBlock} chain=${chainId} no generators need bootstrap`); + cowLog("info", "C4:no_bootstrap_needed", { block: String(currentBlock), chainId }); return; } if (freshOwners.size > 0) { - console.log( - `[COW:C4] block=${currentBlock} chain=${chainId} generators=${generators.length} fresh_owners=${freshOwners.size}`, - ); + cowLog("info", "C4:bootstrap_start", { block: String(currentBlock), chainId, generators: generators.length, freshOwners: freshOwners.size }); } for (const owner of freshOwners) { @@ -756,9 +735,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { totalDiscovered += count; } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:C4] owner timeout owner=${owner} chain=${chainId} after=${BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS}ms`, - ); + cowLog("warn", "C4:owner_timeout", { block: String(currentBlock), chainId, owner, timeoutMs: BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS }); await context.db.sql .insert(bootstrapRetryQueue) .values({ chainId, owner, firstTimeoutAt: currentBlock, retryCount: 1, lastRetryAt: currentBlock }) @@ -769,9 +746,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { } } - console.log( - `[COW:C4] DONE block=${currentBlock} chain=${chainId} discovered=${totalDiscovered}`, - ); + cowLog("info", "C4:DONE", { block: String(currentBlock), chainId, discovered: totalDiscovered }); }); // ─── C5: Deterministic Cancellation Sweeper ────────────────────────────────── @@ -825,9 +800,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = if (dueGenerators.length === 0) return; - console.log( - `[COW:C5] ENTER block=${currentBlock} chain=${chainId} due=${dueGenerators.length}`, - ); + cowLog("info", "C5:ENTER", { block: String(currentBlock), chainId, due: dueGenerators.length }); const c5MulticallPromise = context.client.multicall({ contracts: dueGenerators.map((g) => ({ @@ -848,9 +821,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = ); } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:C5] multicall timeout block=${currentBlock} chain=${chainId} due=${dueGenerators.length}`, - ); + cowLog("warn", "C5:multicall_timeout", { block: String(currentBlock), chainId, due: dueGenerators.length }); return; } throw err; @@ -887,9 +858,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = eq(conditionalOrderGenerator.eventId, gen.generatorId), ), ); - console.log( - `[COW:C5] CANCELLED generatorId=${gen.generatorId} orderType=${gen.orderType} block=${currentBlock} chain=${chainId}`, - ); + cowLog("info", "C5:CANCELLED", { block: String(currentBlock), chainId, generatorId: gen.generatorId, orderType: gen.orderType }); cancelledCount++; } else { await context.db.sql @@ -909,9 +878,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = } } - console.log( - `[COW:C5] DONE block=${currentBlock} chain=${chainId} due=${dueGenerators.length} cancelled=${cancelledCount} stillActive=${stillActiveCount} errors=${errorCount}`, - ); + cowLog("info", "C5:DONE", { block: String(currentBlock), chainId, due: dueGenerators.length, cancelled: cancelledCount, stillActive: stillActiveCount, errors: errorCount }); }); // ─── Shared helpers ────────────────────────────────────────────────────────── diff --git a/src/application/handlers/settlement.ts b/src/application/handlers/settlement.ts index 7304b07..ff6ba17 100644 --- a/src/application/handlers/settlement.ts +++ b/src/application/handlers/settlement.ts @@ -2,6 +2,7 @@ import { ponder } from "ponder:registry"; import { AddressType, conditionalOrderGenerator, ownerMapping, transaction } from "ponder:schema"; import { and, eq } from "ponder"; import { decodeAbiParameters, keccak256, toBytes } from "viem"; +import { cowLog } from "../helpers/cowLogger"; import { AaveV3AdapterHelperAbi } from "../../../abis/AaveV3AdapterHelperAbi"; import { AAVE_V3_ADAPTER_FACTORY_ADDRESSES, @@ -31,15 +32,15 @@ function logStatsIfIntervalPassed() { if (Date.now() - statsLastLogAt < LOG_INTERVAL_MS) return; const contractAddresses = stats.tradeLogsFound - stats.skippedAlreadyMapped - stats.skippedEOA; - console.log( - `[SETTLEMENT:STATS] settlements=${stats.total}` + - ` tradeLogs=${stats.tradeLogsFound}` + - ` alreadyMapped=${stats.skippedAlreadyMapped}` + - ` eoa=${stats.skippedEOA}` + - ` notAdapter=${stats.skippedNotAdapter}` + - ` mapped=${stats.mapped}` + - ` | avgFactory=${contractAddresses > 0 ? (stats.msFactory / contractAddresses).toFixed(1) : 0}ms`, - ); + cowLog("info", "settlement:stats", { + settlements: stats.total, + tradeLogs: stats.tradeLogsFound, + alreadyMapped: stats.skippedAlreadyMapped, + eoa: stats.skippedEOA, + notAdapter: stats.skippedNotAdapter, + mapped: stats.mapped, + avgFactoryMs: contractAddresses > 0 ? Number((stats.msFactory / contractAddresses).toFixed(1)) : 0, + }); statsLastLogAt = Date.now(); } @@ -208,18 +209,17 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { stats.mapped++; logStatsIfIntervalPassed(); - console.log( - `[COW:SETTLEMENT:TRADE] AAVE_ADAPTER_MAPPED` + - ` adapter=${ownerAddress}` + - ` eoa=${eoaOwner.toLowerCase()}` + - ` orderUid=${orderUid}` + - ` sellToken=${sellToken.toLowerCase()}` + - ` buyToken=${buyToken.toLowerCase()}` + - ` sellAmount=${sellAmount}` + - ` buyAmount=${buyAmount}` + - ` block=${event.block.number}` + - ` chain=${chainId}`, - ); + cowLog("info", "settlement:aave_adapter_mapped", { + block: String(event.block.number), + chainId, + adapter: ownerAddress, + eoa: eoaOwner.toLowerCase(), + orderUid: String(orderUid), + sellToken: sellToken.toLowerCase(), + buyToken: buyToken.toLowerCase(), + sellAmount: String(sellAmount), + buyAmount: String(buyAmount), + }); } logStatsIfIntervalPassed(); diff --git a/src/application/helpers/cowLogger.ts b/src/application/helpers/cowLogger.ts new file mode 100644 index 0000000..5a2fbaa --- /dev/null +++ b/src/application/helpers/cowLogger.ts @@ -0,0 +1,19 @@ +/** + * Structured JSON logger for handler code. Outputs one JSON line per call so + * log aggregators (Datadog, CloudWatch, etc.) can filter by chainId, handler, + * block number, or any other field without regex parsing. + * + * Ponder's own log lines are controlled by --log-format (pretty|json) on the + * CLI. These handler lines are always JSON so they remain parseable regardless + * of Ponder's format setting. + */ + +type LogLevel = "info" | "warn" | "error"; + +export function cowLog( + level: LogLevel, + msg: string, + fields: Record = {}, +): void { + console.log(JSON.stringify({ time: Date.now(), level, msg, ...fields })); +} From b7d8838f69f07ae9ad455837794eb1a87f04f50f Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 2 Jun 2026 16:24:31 -0300 Subject: [PATCH 2/7] fix: remove decode-only-for-logging block from settlement.ts cowLog (COW-994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decodeAbiParameters call (orderUid, sellToken, buyToken, sellAmount, buyAmount) was only used for logging. COW-991 removes this block separately; removing it here too prevents a conflict when both PRs land — otherwise the cowLog fields reference variables that no longer exist, crashing the indexer. Co-Authored-By: Claude Sonnet 4.6 --- src/application/handlers/settlement.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/application/handlers/settlement.ts b/src/application/handlers/settlement.ts index ff6ba17..00677b5 100644 --- a/src/application/handlers/settlement.ts +++ b/src/application/handlers/settlement.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; import { AddressType, conditionalOrderGenerator, ownerMapping, transaction } from "ponder:schema"; import { and, eq } from "ponder"; -import { decodeAbiParameters, keccak256, toBytes } from "viem"; +import { keccak256, toBytes } from "viem"; import { cowLog } from "../helpers/cowLogger"; import { AaveV3AdapterHelperAbi } from "../../../abis/AaveV3AdapterHelperAbi"; import { @@ -192,20 +192,6 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { ), ); - // Decode non-indexed Trade log fields: sellToken, buyToken, amounts, orderUid - const [sellToken, buyToken, sellAmount, buyAmount, , orderUid] = - decodeAbiParameters( - [ - { type: "address" }, - { type: "address" }, - { type: "uint256" }, - { type: "uint256" }, - { type: "uint256" }, - { type: "bytes" }, - ], - log.data, - ); - stats.mapped++; logStatsIfIntervalPassed(); @@ -214,11 +200,6 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { chainId, adapter: ownerAddress, eoa: eoaOwner.toLowerCase(), - orderUid: String(orderUid), - sellToken: sellToken.toLowerCase(), - buyToken: buyToken.toLowerCase(), - sellAmount: String(sellAmount), - buyAmount: String(buyAmount), }); } From 414ae6f164b60da6e98d5f345a15145128e74538 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 2 Jun 2026 17:07:36 -0300 Subject: [PATCH 3/7] fix: route warn/error to stderr in cowLog, add initialDelaySeconds to K8s probes (COW-994) Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 2 ++ src/application/helpers/cowLogger.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/deployment.md b/docs/deployment.md index 05ce647..8036ef1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -104,12 +104,14 @@ livenessProbe: httpGet: path: /healthz port: 3000 + initialDelaySeconds: 30 periodSeconds: 30 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 3000 + initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 18 # 3-minute window before marking unready ``` diff --git a/src/application/helpers/cowLogger.ts b/src/application/helpers/cowLogger.ts index 5a2fbaa..3969837 100644 --- a/src/application/helpers/cowLogger.ts +++ b/src/application/helpers/cowLogger.ts @@ -15,5 +15,10 @@ export function cowLog( msg: string, fields: Record = {}, ): void { - console.log(JSON.stringify({ time: Date.now(), level, msg, ...fields })); + const line = JSON.stringify({ time: Date.now(), level, msg, ...fields }); + if (level === "warn" || level === "error") { + console.error(line); + } else { + console.log(line); + } } From 6e140fd0a3f1a31f2b328b1e00f2d917f1e1982b Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 10:47:47 -0300 Subject: [PATCH 4/7] fix: reduce cowLogger JSDoc to single-line comment per project convention Co-Authored-By: Claude Sonnet 4.6 --- src/application/helpers/cowLogger.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/application/helpers/cowLogger.ts b/src/application/helpers/cowLogger.ts index 3969837..91a2c0f 100644 --- a/src/application/helpers/cowLogger.ts +++ b/src/application/helpers/cowLogger.ts @@ -1,12 +1,4 @@ -/** - * Structured JSON logger for handler code. Outputs one JSON line per call so - * log aggregators (Datadog, CloudWatch, etc.) can filter by chainId, handler, - * block number, or any other field without regex parsing. - * - * Ponder's own log lines are controlled by --log-format (pretty|json) on the - * CLI. These handler lines are always JSON so they remain parseable regardless - * of Ponder's format setting. - */ +// Structured JSON logger for handler code — always emits one JSON line per call regardless of Ponder's --log-format setting. type LogLevel = "info" | "warn" | "error"; From d873a8618ead1af11febffcc1e280e2804d49f58 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 21:00:00 -0300 Subject: [PATCH 5/7] fix: rename cowLog->log/logger.ts, use /health for liveness, clarify readiness semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename cowLogger.ts to logger.ts and cowLog() to log() — the module name already provides context; the cow prefix was misleading - Rename `for (const log of receipt.logs)` loop variable to txLog to avoid shadowing the newly imported log function - K8s liveness probe: use /health (Ponder built-in) instead of /healthz - readinessProbe: fix misleading failureThreshold comment; add paragraph clarifying that NotReady does not kill the pod — cold-start sync takes hours Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 14 ++++--- src/application/handlers/blockHandler.ts | 38 +++++++++---------- src/application/handlers/settlement.ts | 14 +++---- .../helpers/{cowLogger.ts => logger.ts} | 2 +- 4 files changed, 35 insertions(+), 33 deletions(-) rename src/application/helpers/{cowLogger.ts => logger.ts} (94%) diff --git a/docs/deployment.md b/docs/deployment.md index 8036ef1..e14f560 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -94,7 +94,7 @@ The indexer exposes two health endpoints with distinct semantics: | Endpoint | Semantic | Returns 200 when | |----------|----------|-----------------| -| `/healthz` | **Liveness** — is the process alive? | Always, once the server starts | +| `/health` | **Liveness** — is the process alive? | Always, once the server starts | | `/ready` | **Readiness** — is the index fully synced? | Only when fully synced | Map these to different K8s probe types: @@ -102,7 +102,7 @@ Map these to different K8s probe types: ```yaml livenessProbe: httpGet: - path: /healthz + path: /health port: 3000 initialDelaySeconds: 30 periodSeconds: 30 @@ -112,17 +112,19 @@ readinessProbe: path: /ready port: 3000 initialDelaySeconds: 30 - periodSeconds: 10 - failureThreshold: 18 # 3-minute window before marking unready + periodSeconds: 30 + failureThreshold: 3 # marks pod unready (not killed) — cold-start sync takes hours ``` -**Do not** use `/ready` as the liveness probe. A pod that is still indexing (which takes hours on a cold start) returns 200 on `/healthz` but not on `/ready`. Using `/ready` for liveness would kill the pod before it ever finishes syncing. +**Do not** use `/ready` as the liveness probe. A pod that is still indexing (which takes hours on a cold start) returns 200 on `/health` but not on `/ready`. Using `/ready` for liveness would kill the pod before it ever finishes syncing. + +A pod in `NotReady` state is not killed — it is simply removed from load-balancer rotation. On a cold start (no existing database), the pod will be `NotReady` for the duration of the historical backfill (hours). That is expected: the old pod (if any) keeps serving traffic during this window, and once the new pod catches up, K8s starts routing to it. The Docker Compose health check uses `/ready` with a 24-hour start period as a pragmatic fallback for single-container deployments, not as a K8s-style probe. ### Structured Logging -`pnpm start` runs with `--log-format json`, which makes both Ponder's internal log lines and the handler log lines (via `cowLog`) emit newline-delimited JSON. Each handler log line includes `chainId` and `block` as top-level fields, enabling log aggregators (Datadog, CloudWatch, Loki) to filter and alert by chain. +`pnpm start` runs with `--log-format json`, which makes both Ponder's internal log lines and the handler log lines (via `log()` in `src/application/helpers/logger.ts`) emit newline-delimited JSON. Each handler log line includes `chainId` and `block` as top-level fields, enabling log aggregators (Datadog, CloudWatch, Loki) to filter and alert by chain. `pnpm dev` uses Ponder's default pretty format for readability during local development. diff --git a/src/application/handlers/blockHandler.ts b/src/application/handlers/blockHandler.ts index d648d33..653eaa6 100644 --- a/src/application/handlers/blockHandler.ts +++ b/src/application/handlers/blockHandler.ts @@ -41,7 +41,7 @@ import { parsePollError, } from "../helpers/pollResultErrors"; import { computeOrderUid, type GPv2OrderData } from "../helpers/orderUid"; -import { cowLog } from "../helpers/cowLogger"; +import { log } from "../helpers/logger"; const NON_DETERMINISTIC_TYPES = ["PerpetualSwap", "GoodAfterTime", "TradeAboveThreshold", "Unknown"] as const; const SINGLE_SHOT_NON_DETERMINISTIC = ["GoodAfterTime", "TradeAboveThreshold"] as const; @@ -120,7 +120,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { if (dueOrders.length === 0) return; - cowLog("info", "C1:ENTER", { block: String(currentBlock), chainId, due: dueOrders.length }); + log("info", "C1:ENTER", { block: String(currentBlock), chainId, due: dueOrders.length }); const c1MulticallPromise = context.client.multicall({ contracts: dueOrders.map((order) => ({ @@ -146,7 +146,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { ); } catch (err) { if (err instanceof TimeoutError) { - cowLog("warn", "C1:multicall_timeout", { block: String(currentBlock), chainId, due: dueOrders.length }); + log("warn", "C1:multicall_timeout", { block: String(currentBlock), chainId, due: dueOrders.length }); return; } throw err; @@ -260,7 +260,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { eq(conditionalOrderGenerator.eventId, order.generatorId), ), ); - cowLog("info", "C1:NEVER", { block: String(currentBlock), chainId, generatorId: order.generatorId, reason: pollResult.reason }); + log("info", "C1:NEVER", { block: String(currentBlock), chainId, generatorId: order.generatorId, reason: pollResult.reason }); neverCount++; break; @@ -279,7 +279,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { eq(conditionalOrderGenerator.eventId, order.generatorId), ), ); - cowLog("info", "C1:CANCELLED", { block: String(currentBlock), chainId, generatorId: order.generatorId }); + log("info", "C1:CANCELLED", { block: String(currentBlock), chainId, generatorId: order.generatorId }); break; } } @@ -288,7 +288,7 @@ ponder.on("ContractPoller:block", async ({ event, context }) => { await Promise.all(successPromises); const capped = dueOrders.length === maxGeneratorsPerBlock; - cowLog("info", "C1:DONE", { block: String(currentBlock), chainId, due: dueOrders.length, success: successCount, never: neverCount, backedOff: backedOffCount, capped }); + log("info", "C1:DONE", { block: String(currentBlock), chainId, due: dueOrders.length, success: successCount, never: neverCount, backedOff: backedOffCount, capped }); }); // ─── C2: Candidate Confirmer ───────────────────────────────────────────────── @@ -377,7 +377,7 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { ), ); - cowLog("info", "C2:parent_cancelled", { block: String(event.block.number), chainId, parentCancelled: orphanCandidates.length }); + log("info", "C2:parent_cancelled", { block: String(event.block.number), chainId, parentCancelled: orphanCandidates.length }); } } @@ -537,7 +537,7 @@ ponder.on("CandidateConfirmer:block", async ({ event, context }) => { } if (confirmed > 0 || stale.length > 0) { - cowLog("info", "C2:DONE", { block: String(event.block.number), chainId, candidates: unconfirmed.length, confirmed, expired: stale.length }); + log("info", "C2:DONE", { block: String(event.block.number), chainId, candidates: unconfirmed.length, confirmed, expired: stale.length }); } }); @@ -589,7 +589,7 @@ ponder.on("StatusUpdater:block", async ({ event, context }) => { } if (updated > 0) { - cowLog("info", "C3:DONE", { block: String(event.block.number), chainId, open: openOrders.length, updated }); + log("info", "C3:DONE", { block: String(event.block.number), chainId, open: openOrders.length, updated }); } } @@ -653,7 +653,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { .from(bootstrapRetryQueue) .where(eq(bootstrapRetryQueue.chainId, chainId)); - cowLog("info", "C4:START", { block: String(currentBlock), chainId, pendingRetry: queued.length }); + log("info", "C4:START", { block: String(currentBlock), chainId, pendingRetry: queued.length }); let totalDiscovered = 0; const retriedOwners = new Set(); @@ -673,7 +673,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { .where(and(eq(bootstrapRetryQueue.chainId, chainId), eq(bootstrapRetryQueue.owner, owner as Hex))); } catch (err) { if (err instanceof TimeoutError) { - cowLog("warn", "C4:owner_retry_timeout", { block: String(currentBlock), chainId, owner, retryCount: retryCount + 1, timeoutMs: BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS }); + log("warn", "C4:owner_retry_timeout", { block: String(currentBlock), chainId, owner, retryCount: retryCount + 1, timeoutMs: BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS }); await context.db.sql .update(bootstrapRetryQueue) .set({ retryCount: retryCount + 1, lastRetryAt: currentBlock }) @@ -716,12 +716,12 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { const freshOwners = new Set(generators.map((g) => g.owner).filter((o) => !retriedOwners.has(o))); if (freshOwners.size === 0 && retriedOwners.size === 0) { - cowLog("info", "C4:no_bootstrap_needed", { block: String(currentBlock), chainId }); + log("info", "C4:no_bootstrap_needed", { block: String(currentBlock), chainId }); return; } if (freshOwners.size > 0) { - cowLog("info", "C4:bootstrap_start", { block: String(currentBlock), chainId, generators: generators.length, freshOwners: freshOwners.size }); + log("info", "C4:bootstrap_start", { block: String(currentBlock), chainId, generators: generators.length, freshOwners: freshOwners.size }); } for (const owner of freshOwners) { @@ -735,7 +735,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { totalDiscovered += count; } catch (err) { if (err instanceof TimeoutError) { - cowLog("warn", "C4:owner_timeout", { block: String(currentBlock), chainId, owner, timeoutMs: BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS }); + log("warn", "C4:owner_timeout", { block: String(currentBlock), chainId, owner, timeoutMs: BOOTSTRAP_OWNER_FETCH_TIMEOUT_MS }); await context.db.sql .insert(bootstrapRetryQueue) .values({ chainId, owner, firstTimeoutAt: currentBlock, retryCount: 1, lastRetryAt: currentBlock }) @@ -746,7 +746,7 @@ ponder.on("HistoricalBootstrap:block", async ({ event, context }) => { } } - cowLog("info", "C4:DONE", { block: String(currentBlock), chainId, discovered: totalDiscovered }); + log("info", "C4:DONE", { block: String(currentBlock), chainId, discovered: totalDiscovered }); }); // ─── C5: Deterministic Cancellation Sweeper ────────────────────────────────── @@ -800,7 +800,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = if (dueGenerators.length === 0) return; - cowLog("info", "C5:ENTER", { block: String(currentBlock), chainId, due: dueGenerators.length }); + log("info", "C5:ENTER", { block: String(currentBlock), chainId, due: dueGenerators.length }); const c5MulticallPromise = context.client.multicall({ contracts: dueGenerators.map((g) => ({ @@ -821,7 +821,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = ); } catch (err) { if (err instanceof TimeoutError) { - cowLog("warn", "C5:multicall_timeout", { block: String(currentBlock), chainId, due: dueGenerators.length }); + log("warn", "C5:multicall_timeout", { block: String(currentBlock), chainId, due: dueGenerators.length }); return; } throw err; @@ -858,7 +858,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = eq(conditionalOrderGenerator.eventId, gen.generatorId), ), ); - cowLog("info", "C5:CANCELLED", { block: String(currentBlock), chainId, generatorId: gen.generatorId, orderType: gen.orderType }); + log("info", "C5:CANCELLED", { block: String(currentBlock), chainId, generatorId: gen.generatorId, orderType: gen.orderType }); cancelledCount++; } else { await context.db.sql @@ -878,7 +878,7 @@ ponder.on("DeterministicCancellationSweeper:block", async ({ event, context }) = } } - cowLog("info", "C5:DONE", { block: String(currentBlock), chainId, due: dueGenerators.length, cancelled: cancelledCount, stillActive: stillActiveCount, errors: errorCount }); + log("info", "C5:DONE", { block: String(currentBlock), chainId, due: dueGenerators.length, cancelled: cancelledCount, stillActive: stillActiveCount, errors: errorCount }); }); // ─── Shared helpers ────────────────────────────────────────────────────────── diff --git a/src/application/handlers/settlement.ts b/src/application/handlers/settlement.ts index 00677b5..fe4825b 100644 --- a/src/application/handlers/settlement.ts +++ b/src/application/handlers/settlement.ts @@ -2,7 +2,7 @@ import { ponder } from "ponder:registry"; import { AddressType, conditionalOrderGenerator, ownerMapping, transaction } from "ponder:schema"; import { and, eq } from "ponder"; import { keccak256, toBytes } from "viem"; -import { cowLog } from "../helpers/cowLogger"; +import { log } from "../helpers/logger"; import { AaveV3AdapterHelperAbi } from "../../../abis/AaveV3AdapterHelperAbi"; import { AAVE_V3_ADAPTER_FACTORY_ADDRESSES, @@ -32,7 +32,7 @@ function logStatsIfIntervalPassed() { if (Date.now() - statsLastLogAt < LOG_INTERVAL_MS) return; const contractAddresses = stats.tradeLogsFound - stats.skippedAlreadyMapped - stats.skippedEOA; - cowLog("info", "settlement:stats", { + log("info", "settlement:stats", { settlements: stats.total, tradeLogs: stats.tradeLogsFound, alreadyMapped: stats.skippedAlreadyMapped, @@ -80,15 +80,15 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { hash: event.transaction.hash, }); - for (const log of receipt.logs) { + for (const txLog of receipt.logs) { // Only Trade logs emitted by GPv2Settlement in this same transaction - if (log.address.toLowerCase() !== settlementAddress) continue; - if (log.topics[0] !== TRADE_TOPIC) continue; + if (txLog.address.toLowerCase() !== settlementAddress) continue; + if (txLog.topics[0] !== TRADE_TOPIC) continue; stats.tradeLogsFound++; // Decode owner from topics[1] — ABI-encoded 32-byte padded address - const owner = `0x${log.topics[1]!.slice(26)}` as `0x${string}`; + const owner = `0x${txLog.topics[1]!.slice(26)}` as `0x${string}`; const ownerAddress = owner.toLowerCase() as `0x${string}`; // Skip if already mapped (adapter seen in a prior settlement) @@ -195,7 +195,7 @@ ponder.on("GPv2Settlement:Settlement", async ({ event, context }) => { stats.mapped++; logStatsIfIntervalPassed(); - cowLog("info", "settlement:aave_adapter_mapped", { + log("info", "settlement:aave_adapter_mapped", { block: String(event.block.number), chainId, adapter: ownerAddress, diff --git a/src/application/helpers/cowLogger.ts b/src/application/helpers/logger.ts similarity index 94% rename from src/application/helpers/cowLogger.ts rename to src/application/helpers/logger.ts index 91a2c0f..272c2a5 100644 --- a/src/application/helpers/cowLogger.ts +++ b/src/application/helpers/logger.ts @@ -2,7 +2,7 @@ type LogLevel = "info" | "warn" | "error"; -export function cowLog( +export function log( level: LogLevel, msg: string, fields: Record = {}, From 843c83c675b16c208d15924966e27139bdc23c16 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 8 Jun 2026 09:56:51 -0300 Subject: [PATCH 6/7] feat: migrate all src/application/ log calls to structured logger (COW-994) Extends the log() migration from blockHandler.ts and settlement.ts to all remaining call sites in composableCow.ts, setup.ts, orderbookClient.ts, and uidPrecompute.ts. No console.log/warn/error remain in src/application/ outside logger.ts itself. Documents the scope and usage convention in deployment.md. Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 13 ++++++++- src/application/handlers/composableCow.ts | 18 ++++--------- src/application/handlers/setup.ts | 5 ++-- src/application/helpers/orderbookClient.ts | 31 +++++++++------------- src/application/helpers/uidPrecompute.ts | 15 +++++------ 5 files changed, 38 insertions(+), 44 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 437cfe0..3252575 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -131,10 +131,21 @@ The Docker Compose health check uses `/ready` with a 24-hour start period as a p ### Structured Logging -`pnpm start` runs with `--log-format json`, which makes both Ponder's internal log lines and the handler log lines (via `log()` in `src/application/helpers/logger.ts`) emit newline-delimited JSON. Each handler log line includes `chainId` and `block` as top-level fields, enabling log aggregators (Datadog, CloudWatch, Loki) to filter and alert by chain. +`pnpm start` runs with `--log-format json`, which makes both Ponder's internal log lines and the handler log lines emit newline-delimited JSON. Each handler log line includes structured fields (e.g. `chainId`, `block`) enabling log aggregators (Datadog, CloudWatch, Loki) to filter and alert by chain. `pnpm dev` uses Ponder's default pretty format for readability during local development. +**Convention:** all code under `src/application/` uses `log()` from `src/application/helpers/logger.ts` instead of `console.log/warn/error` directly. The `src/api/` layer (Hono routes) is exempt — Hono handles its own logging. Example: + +```ts +import { log } from "../helpers/logger"; + +log("info", "c2:confirmed", { chainId, orderUid, block: String(event.block.number) }); +log("warn", "c2:timeout", { chainId, block: String(event.block.number) }); +``` + +`warn` and `error` level messages go to `stderr`; `info` goes to `stdout`. The `level` field in the JSON payload is what log aggregators use to route and alert. + ### PostgreSQL Memory Flags Memory settings are hardcoded in the `command:` block of `docker-compose.yml`, tuned for 1G RAM: diff --git a/src/application/handlers/composableCow.ts b/src/application/handlers/composableCow.ts index 44f7858..c2b7057 100644 --- a/src/application/handlers/composableCow.ts +++ b/src/application/handlers/composableCow.ts @@ -45,6 +45,7 @@ import { getOrderTypeFromHandler } from "../../utils/order-types"; import { decodeStaticInput } from "../../decoders/index"; import { precomputeAndDiscover } from "../helpers/uidPrecompute"; import { CirclesBackingOrderAbi } from "../../../abis/CirclesBackingOrderAbi"; +import { log } from "../helpers/logger"; // ─── CirclesBackingOrder immutables cache ─────────────────────────────────── // @@ -128,14 +129,9 @@ async function insertGenerator( const orderType = getOrderTypeFromHandler(handler, chainId); if (orderType === "Unknown") { - console.warn( - `[ComposableCow] Unknown handler ${handler} on chain ${chainId}, ` + - `saving as Unknown — event=${event.id}`, - ); + log("warn", "composableCow:unknownHandler", { handler, chainId, event: event.id }); } else { - console.log( - `[ComposableCow] ConditionalOrderCreated event=${event.id} chain=${chainId} orderType=${orderType} block=${event.block.number}`, - ); + log("info", "composableCow:created", { event: event.id, chainId, orderType, block: String(event.block.number) }); } // Decode staticInput; for CirclesBackingOrder, also merge in handler immutables. @@ -169,13 +165,9 @@ async function insertGenerator( }; } - console.log( - `[ComposableCow] Decoded event=${event.id} orderType=${orderType} decodedParams=${decodedParams ? "ok" : "null"}`, - ); + log("info", "composableCow:decoded", { event: event.id, orderType, decodedParams: decodedParams ? "ok" : "null" }); } catch (err) { - console.warn( - `[ComposableCow] Decode failed event=${event.id} orderType=${orderType} err=${err}`, - ); + log("warn", "composableCow:decodeFailed", { event: event.id, orderType, err: String(err) }); decodedParams = null; decodeError = "invalid_static_input"; } diff --git a/src/application/handlers/setup.ts b/src/application/handlers/setup.ts index 95a9ea6..e69f25c 100644 --- a/src/application/handlers/setup.ts +++ b/src/application/handlers/setup.ts @@ -1,5 +1,6 @@ import { ponder } from "ponder:registry"; import { sql } from "ponder"; +import { log } from "../helpers/logger"; /** * Creates the cow_cache schema and persistent cache tables on startup. @@ -37,7 +38,5 @@ ponder.on("ComposableCow:setup", async ({ context }) => { ) as { count: number }[]; const count = result[0]?.count ?? 0; - console.log( - `[COW:SETUP] cow_cache.order_uid_cache ready — ${count} entr${count === 1 ? "y" : "ies"} from previous run`, - ); + log("info", "setup:cacheReady", { count, entries: `${count} entr${count === 1 ? "y" : "ies"} from previous run` }); }); diff --git a/src/application/helpers/orderbookClient.ts b/src/application/helpers/orderbookClient.ts index 16a6cf6..8d9c74d 100644 --- a/src/application/helpers/orderbookClient.ts +++ b/src/application/helpers/orderbookClient.ts @@ -25,6 +25,7 @@ import { COMPOSABLE_COW_HANDLER_ADDRESSES, ORDERBOOK_API_URLS } from "../../data import { ORDERBOOK_HTTP_TIMEOUT_MS, SIGNING_SCHEME_EIP1271 } from "../../constants"; import { decodeEip1271Signature } from "../decoders/erc1271Signature"; import { fetchWithTimeout, TimeoutError, withTimeout } from "./withTimeout"; +import { log } from "./logger"; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -86,16 +87,16 @@ export async function fetchComposableOrders( ): Promise { const apiBaseUrl = ORDERBOOK_API_URLS[chainId]; if (!apiBaseUrl) { - console.warn(`[COW:OB] No API URL for chainId=${chainId}`); + log("warn", "ob:noApiUrl", { chainId }); return []; } - console.log(`[COW:OB] FETCH owner=${owner} chain=${chainId}`); + log("info", "ob:fetch", { owner, chainId }); const allApiOrders = await fetchAccountOrders(apiBaseUrl, owner); const composable = await filterAndProcess(context, chainId, allApiOrders); if (composable.length === 0) { - console.log(`[COW:OB] owner=${owner} chain=${chainId} apiTotal=${allApiOrders.length} composable=0`); + log("info", "ob:fetchResult", { owner, chainId, apiTotal: allApiOrders.length, composable: 0 }); return []; } @@ -140,9 +141,7 @@ export async function fetchComposableOrders( } } - console.log( - `[COW:OB] owner=${owner} chain=${chainId} apiTotal=${allApiOrders.length} composable=${composable.length} cached=${composable.length - toRefresh.length} refreshed=${toRefresh.length}`, - ); + log("info", "ob:fetchResult", { owner, chainId, apiTotal: allApiOrders.length, composable: composable.length, cached: composable.length - toRefresh.length, refreshed: toRefresh.length }); return results; } @@ -253,9 +252,7 @@ export async function fetchOrderStatusByUids( ); } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:OB] statusByUids timeout chain=${chainId} toFetch=${toFetch.length} after=${ORDERBOOK_HTTP_TIMEOUT_MS * 2}ms`, - ); + log("warn", "ob:statusByUidsTimeout", { chainId, toFetch: toFetch.length, after: ORDERBOOK_HTTP_TIMEOUT_MS * 2 }); return result; // cache-only map — caller treats missing UIDs as "not on API yet" } throw err; @@ -316,7 +313,7 @@ async function fetchAccountOrders( "ob:account", ); if (!response.ok) { - console.warn(`[COW:OB] API ${response.status} owner=${owner}`); + log("warn", "ob:accountError", { status: response.status, owner }); break; } const page = (await response.json()) as OrderbookOrder[]; @@ -325,12 +322,10 @@ async function fetchAccountOrders( offset += page.length; } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:OB] Account fetch timeout owner=${owner} offset=${offset} after=${ORDERBOOK_HTTP_TIMEOUT_MS}ms`, - ); + log("warn", "ob:accountTimeout", { owner, offset, after: ORDERBOOK_HTTP_TIMEOUT_MS }); break; } - console.warn(`[COW:OB] Fetch failed owner=${owner} err=${err}`); + log("warn", "ob:accountFetchFailed", { owner, err: String(err) }); break; } } @@ -362,19 +357,17 @@ async function fetchOrdersByUids( "ob:byUids", ); if (!response.ok) { - console.warn(`[COW:OB] Batch fetch ${response.status} uids=${chunk.length} offset=${i}`); + log("warn", "ob:batchFetchError", { status: response.status, uids: chunk.length, offset: i }); continue; } const raw = (await response.json()) as { order: OrderbookOrder }[]; results.push(...raw.map((item) => item.order)); } catch (err) { if (err instanceof TimeoutError) { - console.warn( - `[COW:OB] Batch fetch timeout uids=${chunk.length} offset=${i} after=${ORDERBOOK_HTTP_TIMEOUT_MS}ms`, - ); + log("warn", "ob:batchFetchTimeout", { uids: chunk.length, offset: i, after: ORDERBOOK_HTTP_TIMEOUT_MS }); continue; } - console.warn(`[COW:OB] Batch fetch failed err=${err} offset=${i}`); + log("warn", "ob:batchFetchFailed", { err: String(err), offset: i }); } } diff --git a/src/application/helpers/uidPrecompute.ts b/src/application/helpers/uidPrecompute.ts index 80ca37e..cb84407 100644 --- a/src/application/helpers/uidPrecompute.ts +++ b/src/application/helpers/uidPrecompute.ts @@ -21,6 +21,7 @@ import { candidateDiscreteOrder, conditionalOrderGenerator, discreteOrder } from import { computeOrderUid, type GPv2OrderData } from "./orderUid"; import { fetchOrderStatusByUids } from "./orderbookClient"; import { isDeterministicOrderType } from "../../utils/order-types"; +import { log } from "./logger"; // GPv2Order.sol constant hashes const KIND_SELL = "0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775" as Hex; @@ -61,7 +62,7 @@ export function precomputeOrderUids( ): PrecomputedOrder[] | null { if (!decodedParams) { if (isDeterministicOrderType(orderType)) { - console.warn(`[COW:PRECOMPUTE] SKIP type=${orderType} owner=${owner} chain=${chainId} reason=decodedParams_null`); + log("warn", "precompute:skip", { orderType, owner, chainId, reason: "decodedParams_null" }); } return null; } @@ -167,9 +168,7 @@ export async function precomputeAndDiscover( eq(conditionalOrderGenerator.eventId, generatorEventId), ), ); - console.log( - `[ComposableCow] All ${precomputed.length} pre-computed orders terminal on API — generator=${generatorEventId} marked Completed`, - ); + log("info", "precompute:allTerminal", { count: precomputed.length, generatorEventId }); return true; } @@ -221,7 +220,7 @@ function precomputeTwapUids( const appData = params["appData"] as Hex | undefined; if (!sellToken || !buyToken || !partSellAmount || !minPartLimit || !n || !t || !appData) { - console.warn(`[COW:PRECOMPUTE] SKIP type=TWAP owner=${owner} chain=${chainId} reason=missing_params missing=${[!sellToken && "sellToken", !buyToken && "buyToken", !partSellAmount && "partSellAmount", !minPartLimit && "minPartLimit", !n && "n", !t && "t", !appData && "appData"].filter(Boolean).join(",")}`); + log("warn", "precompute:skip", { orderType: "TWAP", owner, chainId, reason: "missing_params", missing: [!sellToken && "sellToken", !buyToken && "buyToken", !partSellAmount && "partSellAmount", !minPartLimit && "minPartLimit", !n && "n", !t && "t", !appData && "appData"].filter(Boolean).join(",") }); return null; } @@ -232,11 +231,11 @@ function precomputeTwapUids( const t0 = BigInt(t0Raw ?? "0") === 0n ? blockTimestamp : BigInt(t0Raw!); if (nParts <= 0 || tSeconds <= 0n) { - console.warn(`[COW:PRECOMPUTE] SKIP type=TWAP owner=${owner} chain=${chainId} reason=invalid_math nParts=${nParts} tSeconds=${tSeconds}`); + log("warn", "precompute:skip", { orderType: "TWAP", owner, chainId, reason: "invalid_math", nParts, tSeconds: String(tSeconds) }); return null; } if (nParts > 100000) { - console.warn(`[COW:PRECOMPUTE] SKIP type=TWAP owner=${owner} chain=${chainId} reason=too_many_parts nParts=${nParts}`); + log("warn", "precompute:skip", { orderType: "TWAP", owner, chainId, reason: "too_many_parts", nParts }); return null; } @@ -318,7 +317,7 @@ function precomputeStopLossUid( const validTo = params["validTo"]; if (!sellToken || !buyToken || !sellAmount || !buyAmount || !appData || !validTo) { - console.warn(`[COW:PRECOMPUTE] SKIP type=StopLoss owner=${owner} chain=${chainId} reason=missing_params missing=${[!sellToken && "sellToken", !buyToken && "buyToken", !sellAmount && "sellAmount", !buyAmount && "buyAmount", !appData && "appData", !validTo && "validTo"].filter(Boolean).join(",")}`); + log("warn", "precompute:skip", { orderType: "StopLoss", owner, chainId, reason: "missing_params", missing: [!sellToken && "sellToken", !buyToken && "buyToken", !sellAmount && "sellAmount", !buyAmount && "buyAmount", !appData && "appData", !validTo && "validTo"].filter(Boolean).join(",") }); return null; } From 960c8633d5b3369a2cfde0ee4313b740654e59d8 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 8 Jun 2026 10:20:10 -0300 Subject: [PATCH 7/7] docs: remove specific K8s probe timing values from deployment.md (COW-994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep only path and port — the operator decides periodSeconds, failureThreshold, and initialDelaySeconds based on their cluster SLOs. Add a note explaining this. Co-Authored-By: Claude Sonnet 4.6 --- docs/deployment.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 3252575..22be3cc 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -104,23 +104,17 @@ The indexer exposes two health endpoints with distinct semantics: | `/health` | **Liveness** — is the process alive? | Always, once the server starts | | `/ready` | **Readiness** — is the index fully synced? | Only when fully synced | -Map these to different K8s probe types: +Map these to different K8s probe types. The specific timing values (`periodSeconds`, `failureThreshold`, `initialDelaySeconds`) depend on your cluster's SLOs; what matters is which path and port to use: ```yaml livenessProbe: httpGet: path: /health port: 3000 - initialDelaySeconds: 30 - periodSeconds: 30 - failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 3000 - initialDelaySeconds: 30 - periodSeconds: 30 - failureThreshold: 3 # marks pod unready (not killed) — cold-start sync takes hours ``` **Do not** use `/ready` as the liveness probe. A pod that is still indexing (which takes hours on a cold start) returns 200 on `/health` but not on `/ready`. Using `/ready` for liveness would kill the pod before it ever finishes syncing.