From fc8b52681c2bd6ccfeeb531d0df733c7ecf8ba8d Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 01:06:55 -0400 Subject: [PATCH 1/9] Implement unified logging with Pino across the application --- app/api/cron/blob-prune/route.ts | 9 +- app/api/cron/due-drain/route.ts | 7 +- app/api/trpc/[trpc]/route.ts | 9 +- instrumentation.ts | 23 ++++- lib/cache.ts | 25 ++--- lib/logger.ts | 134 ++++++++++++++++++++++++++ lib/r2.ts | 7 +- lib/ratelimit.ts | 5 +- lib/storage.ts | 15 +-- package.json | 3 + pnpm-lock.yaml | 160 +++++++++++++++++++++++++++++++ server/services/certificates.ts | 16 ++-- server/services/dns.ts | 31 +++--- server/services/headers.ts | 13 ++- server/services/hosting.ts | 11 ++- server/services/ip.ts | 14 +-- server/services/pricing.ts | 11 ++- server/services/registration.ts | 19 ++-- server/services/seo.ts | 7 +- trpc/init.ts | 41 +++++++- 20 files changed, 468 insertions(+), 92 deletions(-) create mode 100644 lib/logger.ts diff --git a/app/api/cron/blob-prune/route.ts b/app/api/cron/blob-prune/route.ts index e1087c0b..3c10cd53 100644 --- a/app/api/cron/blob-prune/route.ts +++ b/app/api/cron/blob-prune/route.ts @@ -1,6 +1,9 @@ import { NextResponse } from "next/server"; +import { logger } from "@/lib/logger"; import { pruneDueBlobsOnce } from "@/lib/storage"; +const log = logger(); + export const dynamic = "force-dynamic"; export async function GET(request: Request) { @@ -26,13 +29,13 @@ export async function GET(request: Request) { const result = await pruneDueBlobsOnce(startedAt); if (result.errorCount > 0) { - console.warn("[blob-prune] completed with errors", { + log.warn("blob-prune.completed.with.errors", { deletedCount: result.deletedCount, errorCount: result.errorCount, duration_ms: Date.now() - startedAt, }); } else { - console.info("[blob-prune] completed", { + log.info("blob-prune.completed", { deletedCount: result.deletedCount, duration_ms: Date.now() - startedAt, }); @@ -45,7 +48,7 @@ export async function GET(request: Request) { duration_ms: Date.now() - startedAt, }); } catch (error) { - console.error("[blob-prune] cron failed", error); + log.error("blob-prune.cron.failed", { err: error }); return NextResponse.json( { error: "Internal error", diff --git a/app/api/cron/due-drain/route.ts b/app/api/cron/due-drain/route.ts index f8467f4d..7c99ff92 100644 --- a/app/api/cron/due-drain/route.ts +++ b/app/api/cron/due-drain/route.ts @@ -1,8 +1,11 @@ import { NextResponse } from "next/server"; import { inngest } from "@/lib/inngest/client"; +import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; import { drainDueDomainsOnce } from "@/lib/schedule"; +const log = logger(); + export const dynamic = "force-dynamic"; export async function GET(request: Request) { @@ -51,7 +54,7 @@ export async function GET(request: Request) { ), ); } catch (e) { - console.warn("[due-drain] cleanup failed", e); + log.warn("due-drain.cleanup.failed", { err: e }); } emitted += chunk.length; } @@ -63,7 +66,7 @@ export async function GET(request: Request) { duration_ms: Date.now() - startedAt, }); } catch (error) { - console.error("[due-drain] cron failed", error); + log.error("due-drain.cron.failed", { err: error }); return NextResponse.json( { error: "Internal error", diff --git a/app/api/trpc/[trpc]/route.ts b/app/api/trpc/[trpc]/route.ts index 314cade2..ac7080bb 100644 --- a/app/api/trpc/[trpc]/route.ts +++ b/app/api/trpc/[trpc]/route.ts @@ -1,4 +1,5 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { logger } from "@/lib/logger"; import { appRouter } from "@/server/routers/_app"; import { createContext } from "@/trpc/init"; @@ -33,12 +34,8 @@ const handler = (req: Request) => return { headers, status: 429 }; }, onError: ({ path, error }) => { - // Development logging - if (process.env.NODE_ENV === "development") { - console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}`, - ); - } + const log = logger(); + log.error("trpc.unhandled", { path, err: error }); }, }); diff --git a/instrumentation.ts b/instrumentation.ts index 1b07b791..84aa1d42 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,4 +1,18 @@ import type { Instrumentation } from "next"; +import { logger } from "@/lib/logger"; + +// Process-level error hooks (Node only) +if (typeof process !== "undefined" && process?.on) { + const log = logger(); + process.on("uncaughtException", (err) => + log.error("uncaughtException", { err }), + ); + process.on("unhandledRejection", (reason) => + log.error("unhandledRejection", { err: reason }), + ); +} + +const log = logger(); export const onRequestError: Instrumentation.onRequestError = async ( err, @@ -27,7 +41,7 @@ export const onRequestError: Instrumentation.onRequestError = async ( const postHogData = JSON.parse(decodedCookie); distinctId = postHogData.distinct_id; } catch (e) { - console.error("Error parsing PostHog cookie:", e); + log.error("posthog.cookie.parse.error", { err: e }); } } } @@ -40,10 +54,9 @@ export const onRequestError: Instrumentation.onRequestError = async ( await phClient.shutdown(); } catch (instrumentationError) { // Graceful degradation - log error but don't throw to avoid breaking the request - console.error( - "Instrumentation error tracking failed:", - instrumentationError, - ); + log.error("instrumentation.error.tracking.failed", { + err: instrumentationError, + }); } } }; diff --git a/lib/cache.ts b/lib/cache.ts index 42577d69..e6e30a1a 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,6 +1,9 @@ import { captureServer } from "@/lib/analytics/server"; +import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; +const log = logger(); + function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -48,19 +51,19 @@ export async function acquireLockOrWaitForResult(options: { const acquired = setRes === "OK" || setRes === undefined; if (acquired) { - console.debug("[redis] lock acquired", { lockKey }); + log.debug("redis.lock.acquired", { lockKey }); return { acquired: true, cachedResult: null }; } - console.debug("[redis] lock not acquired, waiting for result", { + log.debug("redis.lock.not.acquired", { lockKey, resultKey, maxWaitMs, }); } catch (err) { - console.warn("[redis] lock acquisition failed", { + log.warn("redis.lock.acquisition.failed", { lockKey, - error: (err as Error)?.message, + err, }); // If Redis is down, fail open (don't wait) return { acquired: true, cachedResult: null }; @@ -76,11 +79,11 @@ export async function acquireLockOrWaitForResult(options: { const result = (await redis.get(resultKey)) as T | null; if (result !== null) { - console.debug("[redis] found cached result while waiting", { + log.debug("redis.cache.hit.waiting", { lockKey, resultKey, pollCount, - waitedMs: Date.now() - startTime, + wait_ms: Date.now() - startTime, }); return { acquired: false, cachedResult: result }; } @@ -88,7 +91,7 @@ export async function acquireLockOrWaitForResult(options: { // Check if lock still exists - if not, the other process may have failed const lockExists = await redis.exists(lockKey); if (!lockExists) { - console.warn("[redis] lock disappeared without result", { + log.warn("redis.lock.disappeared", { lockKey, resultKey, pollCount, @@ -105,21 +108,21 @@ export async function acquireLockOrWaitForResult(options: { } } } catch (err) { - console.warn("[redis] error polling for result", { + log.warn("redis.polling.error", { lockKey, resultKey, - error: (err as Error)?.message, + err, }); } await sleep(pollIntervalMs); } - console.warn("[redis] wait timeout, no result found", { + log.warn("redis.wait.timeout", { lockKey, resultKey, pollCount, - waitedMs: Date.now() - startTime, + wait_ms: Date.now() - startTime, }); return { acquired: false, cachedResult: null }; diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 00000000..3667b5fb --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,134 @@ +// Unified logger for Node (Pino), Edge (Middleware/Edge runtime), and Browser. +// - Node runtime: dynamic imports Pino and emits structured JSON to stdout. +// - Edge/Browser: console-based shim with same API. +// - Supports child bindings via logger.with({ ... }). + +type Level = "debug" | "info" | "warn" | "error"; +export type LogFields = Record; + +export type Logger = { + debug: (msg: string, fields?: LogFields) => void; + info: (msg: string, fields?: LogFields) => void; + warn: (msg: string, fields?: LogFields) => void; + error: (msg: string, fields?: LogFields) => void; + with: (bindings: LogFields) => Logger; +}; + +const RUNTIME: "node" | "edge" | "browser" = + typeof window !== "undefined" + ? "browser" + : (globalThis as Record).EdgeRuntime + ? "edge" + : "node"; + +const isProd = process.env.NODE_ENV === "production"; +const defaultLevel = + (process.env.LOG_LEVEL as Level | undefined) ?? (isProd ? "info" : "debug"); + +// ---------- console-based fallback (Edge/Browser) ---------- +function makeConsoleLogger(base: LogFields = {}): Logger { + const emit = (level: Level, msg: string, fields?: LogFields) => { + const line = { level, msg, ...base, ...(fields ?? {}) }; + const fn = + (console[level] as typeof console.log | undefined) ?? console.log; + fn(line); + }; + return { + debug: (m, f) => emit("debug", m, f), + info: (m, f) => emit("info", m, f), + warn: (m, f) => emit("warn", m, f), + error: (m, f) => emit("error", m, f), + with: (bindings) => makeConsoleLogger({ ...base, ...bindings }), + }; +} + +// ---------- Pino (Node runtime) ---------- +type PinoLogger = { + child: (bindings: LogFields) => PinoLogger; + debug: (obj: LogFields, msg: string) => void; + info: (obj: LogFields, msg: string) => void; + warn: (obj: LogFields, msg: string) => void; + error: (obj: LogFields, msg: string) => void; +}; + +let nodeRoot: PinoLogger | null = null; + +async function getPinoRoot(): Promise { + if (nodeRoot) return nodeRoot; + + const pino = await import("pino"); + const transport = + !isProd && process.env.LOG_PRETTY !== "0" + ? { + target: "pino-pretty", + options: { colorize: true, singleLine: true }, + } + : undefined; + + nodeRoot = pino.default({ + level: defaultLevel, + base: { + app: "domainstack", + env: process.env.NODE_ENV, + commit: process.env.VERCEL_GIT_COMMIT_SHA, + region: process.env.VERCEL_REGION, + }, + messageKey: "msg", + timestamp: pino.default.stdTimeFunctions.isoTime, + transport, + serializers: { + err: pino.default.stdSerializers.err, + }, + }); + + return nodeRoot; +} + +function makeNodeLogger(base: LogFields = {}): Logger { + const emit = async (level: Level, msg: string, fields?: LogFields) => { + const root = await getPinoRoot(); + const child = Object.keys(base).length ? root.child(base) : root; + child[level]({ ...(fields ?? {}) }, msg); + }; + // Sync facade; logs flush after first dynamic import resolves. + return { + debug: (m, f) => { + void emit("debug", m, f); + }, + info: (m, f) => { + void emit("info", m, f); + }, + warn: (m, f) => { + void emit("warn", m, f); + }, + error: (m, f) => { + void emit("error", m, f); + }, + with: (bindings) => makeNodeLogger({ ...base, ...bindings }), + }; +} + +// ---------- public API ---------- +export function logger(bindings?: LogFields): Logger { + return RUNTIME === "node" + ? makeNodeLogger(bindings) + : makeConsoleLogger(bindings); +} + +export function createRequestLogger(opts: { + method?: string; + path?: string; + ip?: string; + requestId?: string; + userId?: string; + vercelId?: string | null; +}) { + return logger({ + method: opts.method, + path: opts.path, + ip: opts.ip, + requestId: opts.requestId, + userId: opts.userId, + vercelId: opts.vercelId ?? undefined, + }); +} diff --git a/lib/r2.ts b/lib/r2.ts index a9416883..f461d2b2 100644 --- a/lib/r2.ts +++ b/lib/r2.ts @@ -6,6 +6,9 @@ import { PutObjectCommand, S3Client, } from "@aws-sdk/client-s3"; +import { logger } from "@/lib/logger"; + +const log = logger(); function getEnvOrThrow(name: string): string { const v = process.env[name]; @@ -168,9 +171,9 @@ export async function deleteObjects(keys: string[]): Promise { } } catch (err) { const message = (err as Error)?.message || "unknown"; - console.error("[r2] deleteObjects failed", { + log.error("r2.deleteObjects.failed", { keys: slice, - error: message, + err, }); for (const k of slice) { results.push({ key: k, deleted: false, error: message }); diff --git a/lib/ratelimit.ts b/lib/ratelimit.ts index 0a89760b..e820ea4e 100644 --- a/lib/ratelimit.ts +++ b/lib/ratelimit.ts @@ -3,9 +3,12 @@ import "server-only"; import { TRPCError } from "@trpc/server"; import { Ratelimit } from "@upstash/ratelimit"; import { waitUntil } from "@vercel/functions"; +import { logger } from "@/lib/logger"; import { redis } from "@/lib/redis"; import { t } from "@/trpc/init"; +const log = logger(); + export const SERVICE_LIMITS = { dns: { points: 60, window: "1 m" }, headers: { points: 60, window: "1 m" }, @@ -43,7 +46,7 @@ export async function assertRateLimit(service: ServiceName, ip: string) { Math.ceil((res.reset - Date.now()) / 1000), ); - console.warn("[ratelimit] blocked", { + log.warn("ratelimit.blocked", { service, ip, limit: res.limit, diff --git a/lib/storage.ts b/lib/storage.ts index a8a421ba..4c28b2d6 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -1,9 +1,12 @@ import "server-only"; import { createHmac } from "node:crypto"; +import { logger } from "@/lib/logger"; import { makePublicUrl, putObject } from "@/lib/r2"; import type { StorageKind } from "@/lib/schemas"; +const log = logger(); + const UPLOAD_MAX_ATTEMPTS = 3; const UPLOAD_BACKOFF_BASE_MS = 100; const UPLOAD_BACKOFF_MAX_MS = 2000; @@ -68,7 +71,7 @@ async function uploadWithRetry( for (let attempt = 0; attempt < maxAttempts; attempt++) { try { - console.debug("[storage] upload attempt", { + log.debug("storage.upload.attempt", { key, attempt: attempt + 1, maxAttempts, @@ -81,7 +84,7 @@ async function uploadWithRetry( cacheControl, }); - console.info("[storage] upload success", { + log.info("storage.upload.ok", { key, attempt: attempt + 1, }); @@ -90,10 +93,10 @@ async function uploadWithRetry( } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); - console.warn("[storage] upload attempt failed", { + log.warn("storage.upload.attempt.failed", { key, attempt: attempt + 1, - error: lastError.message, + err: lastError, }); // Don't sleep on last attempt @@ -103,9 +106,9 @@ async function uploadWithRetry( UPLOAD_BACKOFF_BASE_MS, UPLOAD_BACKOFF_MAX_MS, ); - console.debug("[storage] retrying after delay", { + log.debug("storage.retrying.after.delay", { key, - delayMs: delay, + delay_ms: delay, }); await sleep(delay); } diff --git a/package.json b/package.json index 31421787..7d96752b 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,8 @@ "motion": "^12.23.24", "next": "15.6.0-canary.39", "next-themes": "^0.4.6", + "pino": "^10.1.0", + "pino-std-serializers": "^7.0.0", "postgres": "^3.4.7", "posthog-js": "^1.280.1", "posthog-node": "^5.10.3", @@ -104,6 +106,7 @@ "bufferutil": "^4.0.9", "drizzle-kit": "^0.31.5", "jsdom": "^27.0.1", + "pino-pretty": "^13.1.2", "puppeteer": "24.22.3", "tailwindcss": "^4.1.16", "tsx": "^4.20.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8826d7ed..ec16406e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,12 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + pino: + specifier: ^10.1.0 + version: 10.1.0 + pino-std-serializers: + specifier: ^7.0.0 + version: 7.0.0 postgres: specifier: ^3.4.7 version: 3.4.7 @@ -222,6 +228,9 @@ importers: jsdom: specifier: ^27.0.1 version: 27.0.1(bufferutil@4.0.9)(postcss@8.5.6) + pino-pretty: + specifier: ^13.1.2 + version: 13.1.2 puppeteer: specifier: 24.22.3 version: 24.22.3(bufferutil@4.0.9)(typescript@5.9.3) @@ -1663,6 +1672,9 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3205,6 +3217,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + axios-proxy-builder@0.1.2: resolution: {integrity: sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==} @@ -3384,6 +3400,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3456,6 +3475,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -3766,9 +3788,15 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-xml-parser@5.2.5: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true @@ -3939,6 +3967,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -4100,6 +4131,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} @@ -4386,6 +4421,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4463,6 +4502,20 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-pretty@13.1.2: + resolution: {integrity: sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==} + hasBin: true + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@10.1.0: + resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==} + hasBin: true + pixelmatch@4.0.2: resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} hasBin: true @@ -4520,6 +4573,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -4558,6 +4614,9 @@ packages: engines: {node: '>=18'} hasBin: true + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quickselect@3.0.0: resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} @@ -4646,6 +4705,10 @@ packages: resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} engines: {node: '>=8'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -4702,6 +4765,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4712,6 +4779,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4774,6 +4844,9 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -4810,6 +4883,10 @@ packages: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4846,6 +4923,10 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strnum@2.1.1: resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} @@ -4913,6 +4994,9 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -7037,6 +7121,8 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@pinojs/redact@0.4.0': {} + '@polka/url@1.0.0-next.29': {} '@posthog/cli@0.5.5': @@ -8708,6 +8794,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + axios-proxy-builder@0.1.2: dependencies: tunnel: 0.0.6 @@ -8902,6 +8990,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -8979,6 +9069,8 @@ snapshots: date-fns@4.1.0: {} + dateformat@4.6.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -9241,8 +9333,12 @@ snapshots: transitivePeerDependencies: - supports-color + fast-copy@3.0.2: {} + fast-fifo@1.3.2: {} + fast-safe-stringify@2.1.1: {} + fast-xml-parser@5.2.5: dependencies: strnum: 2.1.1 @@ -9413,6 +9509,8 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -9574,6 +9672,8 @@ snapshots: jiti@2.6.1: {} + joycon@3.1.1: {} + jpeg-js@0.4.4: {} js-tokens@4.0.0: {} @@ -9839,6 +9939,8 @@ snapshots: dependencies: boolbase: 1.0.0 + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -9926,6 +10028,42 @@ snapshots: picomatch@4.0.3: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.2: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pump: 3.0.3 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.0 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.0.0: {} + + pino@10.1.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pixelmatch@4.0.2: dependencies: pngjs: 3.4.0 @@ -9980,6 +10118,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + process-warning@5.0.0: {} + process@0.11.10: {} progress@2.0.3: {} @@ -10057,6 +10197,8 @@ snapshots: - typescript - utf-8-validate + quick-format-unescaped@4.0.4: {} + quickselect@3.0.0: {} radix-ui@1.4.3(@types/react-dom@19.1.9(@types/react@19.1.16))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): @@ -10185,6 +10327,8 @@ snapshots: dependencies: readable-stream: 4.7.0 + real-require@0.2.0: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -10259,6 +10403,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -10267,6 +10413,8 @@ snapshots: scheduler@0.26.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -10346,6 +10494,10 @@ snapshots: ip-address: 10.0.1 smart-buffer: 4.2.0 + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + sonner@2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -10379,6 +10531,8 @@ snapshots: dependencies: extend-shallow: 3.0.2 + split2@4.2.0: {} + stackback@0.0.2: {} std-env@3.10.0: {} @@ -10424,6 +10578,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-json-comments@5.0.3: {} + strnum@2.1.1: {} strtok3@10.3.4: @@ -10497,6 +10653,10 @@ snapshots: transitivePeerDependencies: - react-native-b4a + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinycolor2@1.6.0: {} diff --git a/server/services/certificates.ts b/server/services/certificates.ts index d91eff92..b101ded7 100644 --- a/server/services/certificates.ts +++ b/server/services/certificates.ts @@ -9,12 +9,15 @@ import { resolveOrCreateProviderId } from "@/lib/db/repos/providers"; import { certificates as certTable } from "@/lib/db/schema"; import { ttlForCertificates } from "@/lib/db/ttl"; import { toRegistrableDomain } from "@/lib/domain-server"; +import { logger } from "@/lib/logger"; import { detectCertificateAuthority } from "@/lib/providers/detection"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import type { Certificate } from "@/lib/schemas"; +const log = logger(); + export async function getCertificates(domain: string): Promise { - console.debug("[certificates] start", { domain }); + log.debug("certificates.start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); const d = registrable @@ -164,23 +167,22 @@ export async function getCertificates(domain: string): Promise { dueAtMs, ); } catch (err) { - console.warn("[certificates] schedule failed", { + log.warn("certificates.schedule.failed", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); } } - console.info("[certificates] ok", { + log.info("certificates.ok", { domain: registrable ?? domain, chain_length: out.length, - duration_ms: Date.now() - startedAt, }); return out; } catch (err) { - console.warn("[certificates] error", { + log.warn("certificates.error", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); await captureServer("tls_probe", { domain: registrable ?? domain, diff --git a/server/services/dns.ts b/server/services/dns.ts index d0e48128..19e97a3e 100644 --- a/server/services/dns.ts +++ b/server/services/dns.ts @@ -10,6 +10,7 @@ import { dnsRecords } from "@/lib/db/schema"; import { ttlForDnsRecord } from "@/lib/db/ttl"; import { toRegistrableDomain } from "@/lib/domain-server"; import { fetchWithTimeout } from "@/lib/fetch"; +import { logger } from "@/lib/logger"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import { type DnsRecord, @@ -19,6 +20,8 @@ import { DnsTypeSchema, } from "@/lib/schemas"; +const log = logger(); + export type DohProvider = { key: DnsResolver; buildUrl: (domain: string, type: DnsType) => URL; @@ -55,7 +58,7 @@ export const DOH_PROVIDERS: DohProvider[] = [ export async function resolveAll(domain: string): Promise { const startedAt = Date.now(); - console.debug("[dns] start", { domain }); + log.debug("dns.start", { domain }); const providers = providerOrderForLookup(domain); const durationByProvider: Record = {}; let lastError: unknown = null; @@ -229,9 +232,9 @@ export async function resolveAll(domain: string): Promise { soonest, ); } catch (err) { - console.warn("[dns] schedule failed (partial)", { + log.warn("dns.schedule.failed.partial", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); } } @@ -272,21 +275,20 @@ export async function resolveAll(domain: string): Promise { cache_hit: false, cache_source: "partial", }); - console.info("[dns] ok (partial)", { + log.info("dns.ok.partial", { domain: registrable, counts, resolver: pinnedProvider.key, - duration_ms_total: Date.now() - startedAt, }); return { records: merged, resolver: pinnedProvider.key, } as DnsResolveResult; } catch (err) { - console.warn("[dns] partial refresh failed; falling back", { + log.warn("dns.partial.refresh.failed", { domain: registrable, provider: pinnedProvider.key, - error: (err as Error)?.message, + err, }); // Fall through to full provider loop below } @@ -366,9 +368,9 @@ export async function resolveAll(domain: string): Promise { const soonest = times.length > 0 ? Math.min(...times) : now.getTime(); await scheduleSectionIfEarlier("dns", registrable ?? domain, soonest); } catch (err) { - console.warn("[dns] schedule failed (full)", { + log.warn("dns.schedule.failed.full", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); } } @@ -383,18 +385,17 @@ export async function resolveAll(domain: string): Promise { cache_hit: false, cache_source: "fresh", }); - console.info("[dns] ok", { + log.info("dns.ok", { domain: registrable, counts, resolver: resolverUsed, - duration_ms_total: Date.now() - startedAt, }); return { records: flat, resolver: resolverUsed } as DnsResolveResult; } catch (err) { - console.warn("[dns] provider attempt failed", { + log.warn("dns.provider.attempt.failed", { domain: registrable, provider: provider.key, - error: (err as Error)?.message, + err, }); durationByProvider[provider.key] = Date.now() - attemptStart; lastError = err; @@ -409,10 +410,10 @@ export async function resolveAll(domain: string): Promise { failure: true, provider_attempts: providers.length, }); - console.error("[dns] all providers failed", { + log.error("dns.all.providers.failed", { domain: registrable, providers: providers.map((p) => p.key), - error: String(lastError), + err: lastError, }); throw new Error( `All DoH providers failed for ${registrable ?? domain}: ${String(lastError)}`, diff --git a/server/services/headers.ts b/server/services/headers.ts index b8a427a3..5a4caeb9 100644 --- a/server/services/headers.ts +++ b/server/services/headers.ts @@ -8,12 +8,15 @@ import { httpHeaders } from "@/lib/db/schema"; import { ttlForHeaders } from "@/lib/db/ttl"; import { toRegistrableDomain } from "@/lib/domain-server"; import { fetchWithTimeout } from "@/lib/fetch"; +import { logger } from "@/lib/logger"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import type { HttpHeader } from "@/lib/schemas"; +const log = logger(); + export async function probeHeaders(domain: string): Promise { const url = `https://${domain}/`; - console.debug("[headers] start", { domain }); + log.debug("headers.start", { domain }); // Fast path: read from Postgres if fresh const registrable = toRegistrableDomain(domain); const d = registrable @@ -40,7 +43,7 @@ export async function probeHeaders(domain: string): Promise { const normalized = normalize( existing.map((h) => ({ name: h.name, value: h.value })), ); - console.info("[headers] db hit", { + log.info("headers.cache.hit", { domain: registrable, count: normalized.length, }); @@ -87,16 +90,16 @@ export async function probeHeaders(domain: string): Promise { ); } catch {} } - console.info("[headers] ok", { + log.info("headers.ok", { domain: registrable, status: final.status, count: normalized.length, }); return normalized; } catch (err) { - console.warn("[headers] error", { + log.warn("headers.error", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); await captureServer("headers_probe", { domain: registrable ?? domain, diff --git a/server/services/hosting.ts b/server/services/hosting.ts index 33da325b..a257071e 100644 --- a/server/services/hosting.ts +++ b/server/services/hosting.ts @@ -12,6 +12,7 @@ import { } from "@/lib/db/schema"; import { ttlForHosting } from "@/lib/db/ttl"; import { toRegistrableDomain } from "@/lib/domain-server"; +import { logger } from "@/lib/logger"; import { detectDnsProvider, detectEmailProvider, @@ -23,9 +24,11 @@ import { resolveAll } from "@/server/services/dns"; import { probeHeaders } from "@/server/services/headers"; import { lookupIpMeta } from "@/server/services/ip"; +const log = logger(); + export async function detectHosting(domain: string): Promise { const startedAt = Date.now(); - console.debug("[hosting] start", { domain }); + log.debug("hosting.start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); @@ -118,12 +121,11 @@ export async function detectHosting(domain: string): Promise { lon: row.geoLon ?? null, }, }; - console.info("[hosting] cache", { + log.info("hosting.cache.hit", { domain, hosting: info.hostingProvider.name, email: info.emailProvider.name, dns_provider: info.dnsProvider.name, - duration_ms: Date.now() - startedAt, }); return info; } @@ -256,12 +258,11 @@ export async function detectHosting(domain: string): Promise { await scheduleSectionIfEarlier("hosting", registrable ?? domain, dueAtMs); } catch {} } - console.info("[hosting] ok", { + log.info("hosting.ok", { domain: registrable ?? domain, hosting: hostingName, email: emailName, dns_provider: dnsName, - duration_ms: Date.now() - startedAt, }); return info; } diff --git a/server/services/ip.ts b/server/services/ip.ts index e6b7aa5f..85a3432a 100644 --- a/server/services/ip.ts +++ b/server/services/ip.ts @@ -1,3 +1,7 @@ +import { logger } from "@/lib/logger"; + +const log = logger(); + export async function lookupIpMeta(ip: string): Promise<{ geo: { city: string; @@ -10,8 +14,7 @@ export async function lookupIpMeta(ip: string): Promise<{ owner: string | null; domain: string | null; }> { - const startedAt = Date.now(); - console.debug("[ip] start", { ip }); + log.debug("ip.start", { ip }); try { const res = await fetch(`https://ipwho.is/${encodeURIComponent(ip)}`); if (!res.ok) throw new Error("ipwho.is fail"); @@ -57,7 +60,7 @@ export async function lookupIpMeta(ip: string): Promise<{ }; }; - console.debug("[ip] ipwho.is result", { ip, json: j }); + log.debug("ip.ipwho.is.result", { ip, json: j }); const org = j.connection?.org?.trim(); const isp = j.connection?.isp?.trim(); @@ -72,16 +75,15 @@ export async function lookupIpMeta(ip: string): Promise<{ lon: typeof j.longitude === "number" ? j.longitude : null, }; - console.info("[ip] ok", { + log.info("ip.ok", { ip, owner: owner || undefined, domain: domain || undefined, geo: geo || undefined, - duration_ms: Date.now() - startedAt, }); return { geo, owner, domain }; } catch { - console.warn("[ip] error", { ip }); + log.warn("ip.error", { ip }); return { owner: null, domain: null, diff --git a/server/services/pricing.ts b/server/services/pricing.ts index 0730e098..c57f4cf0 100644 --- a/server/services/pricing.ts +++ b/server/services/pricing.ts @@ -1,8 +1,11 @@ import { getDomainTld } from "rdapper"; import { acquireLockOrWaitForResult } from "@/lib/cache"; +import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; import type { Pricing } from "@/lib/schemas"; +const log = logger(); + type DomainPricingResponse = { status: string; pricing?: Record< @@ -51,14 +54,12 @@ export async function getPricingForTld(domain: string): Promise { if (res.ok) { payload = (await res.json()) as DomainPricingResponse; await redis.set(resultKey, payload, { ex: 7 * 24 * 60 * 60 }); - console.info("[pricing] fetched and cached full payload"); + log.info("pricing.fetch.ok", { cached: false }); } else { - console.error("[pricing] upstream error", { status: res.status }); + log.error("pricing.upstream.error", { status: res.status }); } } catch (err) { - console.error("[pricing] fetch error", { - error: (err as Error)?.message, - }); + log.error("pricing.fetch.error", { err }); } } else { payload = lock.cachedResult; diff --git a/server/services/registration.ts b/server/services/registration.ts index 6cffd42c..3b0798b8 100644 --- a/server/services/registration.ts +++ b/server/services/registration.ts @@ -12,6 +12,7 @@ import { } from "@/lib/db/schema"; import { ttlForRegistration } from "@/lib/db/ttl"; import { toRegistrableDomain } from "@/lib/domain-server"; +import { logger } from "@/lib/logger"; import { detectRegistrar } from "@/lib/providers/detection"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import type { @@ -20,12 +21,14 @@ import type { RegistrationSource, } from "@/lib/schemas"; +const log = logger(); + /** * Fetch domain registration using rdapper and cache the normalized DomainRecord. */ export async function getRegistration(domain: string): Promise { const startedAt = Date.now(); - console.debug("[registration] start", { domain }); + log.debug("registration.start", { domain }); // Try current snapshot const registrable = toRegistrableDomain(domain); @@ -109,11 +112,10 @@ export async function getRegistration(domain: string): Promise { duration_ms: Date.now() - startedAt, source: row.source, }); - console.info("[registration] ok (cached)", { + log.info("registration.ok.cached", { domain: registrable ?? domain, registered: row.isRegistered, registrar: registrarProvider.name, - duration_ms: Date.now() - startedAt, }); return response; @@ -125,7 +127,7 @@ export async function getRegistration(domain: string): Promise { }); if (!ok || !record) { - console.warn("[registration] error", { + log.warn("registration.error", { domain: registrable ?? domain, error: error || "unknown", }); @@ -139,7 +141,7 @@ export async function getRegistration(domain: string): Promise { } // Log raw rdapper record for observability (safe; already public data) - console.debug("[registration] rdapper result", { + log.debug("registration.rdapper.result", { ...record, }); @@ -213,9 +215,9 @@ export async function getRegistration(domain: string): Promise { expiresAt.getTime(), ); } catch (err) { - console.warn("[registration] schedule failed", { + log.warn("registration.schedule.failed", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); } } @@ -226,11 +228,10 @@ export async function getRegistration(domain: string): Promise { duration_ms: Date.now() - startedAt, source: record.source, }); - console.info("[registration] ok", { + log.info("registration.ok", { domain: registrable ?? domain, registered: record.isRegistered, registrar: withProvider.registrarProvider.name, - duration_ms: Date.now() - startedAt, }); return withProvider; diff --git a/server/services/seo.ts b/server/services/seo.ts index 39319a33..b391fe2d 100644 --- a/server/services/seo.ts +++ b/server/services/seo.ts @@ -11,6 +11,7 @@ import { ttlForSeo } from "@/lib/db/ttl"; import { toRegistrableDomain } from "@/lib/domain-server"; import { fetchWithTimeout } from "@/lib/fetch"; import { optimizeImageCover } from "@/lib/image"; +import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import type { @@ -23,11 +24,13 @@ import type { import { parseHtmlMeta, parseRobotsTxt, selectPreview } from "@/lib/seo"; import { storeImage } from "@/lib/storage"; +const log = logger(); + const SOCIAL_WIDTH = 1200; const SOCIAL_HEIGHT = 630; export async function getSeo(domain: string): Promise { - console.debug("[seo] start", { domain }); + log.debug("seo.start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); const d = registrable @@ -245,7 +248,7 @@ export async function getSeo(domain: string): Promise { has_errors: Boolean(htmlError || robotsError), }); - console.info("[seo] ok", { + log.info("seo.ok", { domain: registrable ?? domain, status: status ?? -1, has_meta: !!meta, diff --git a/trpc/init.ts b/trpc/init.ts index 5d059244..2aff2ced 100644 --- a/trpc/init.ts +++ b/trpc/init.ts @@ -1,6 +1,7 @@ import { initTRPC } from "@trpc/server"; import { ipAddress } from "@vercel/functions"; import superjson from "superjson"; +import { createRequestLogger } from "@/lib/logger"; export const createContext = async (opts?: { req?: Request }) => { const req = opts?.req; @@ -11,7 +12,20 @@ export const createContext = async (opts?: { req?: Request }) => { req.headers.get("cf-connecting-ip") ?? null) : null; - return { ip, req } as const; + + const requestId = req?.headers.get("x-request-id") || crypto.randomUUID(); + const path = req ? new URL(req.url).pathname : undefined; + const vercelId = req?.headers.get("x-vercel-id"); + + const log = createRequestLogger({ + ip: ip ?? undefined, + method: req?.method, + path, + requestId, + vercelId, + }); + + return { ip, req, log } as const; }; export type Context = Awaited>; @@ -52,4 +66,27 @@ export const t = initTRPC export const createTRPCRouter = t.router; export const createCallerFactory = t.createCallerFactory; -export const publicProcedure = t.procedure; + +const withLogging = t.middleware(async ({ ctx, path, type, next }) => { + const start = performance.now(); + ctx.log.debug("rpc.start", { rpcPath: path, rpcType: type }); + try { + const result = await next(); + ctx.log.info("rpc.ok", { + rpcPath: path, + rpcType: type, + duration_ms: Math.round(performance.now() - start), + }); + return result; + } catch (err) { + ctx.log.error("rpc.error", { + rpcPath: path, + rpcType: type, + duration_ms: Math.round(performance.now() - start), + err, + }); + throw err; + } +}); + +export const publicProcedure = t.procedure.use(withLogging); From 2aaf40ada93a81cf4e782761a1ca879de2629526 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 01:20:54 -0400 Subject: [PATCH 2/9] Remove PostHog noise for ops (now handled by logger) --- app/api/cron/due-drain/route.ts | 6 ++++ lib/cache.test.ts | 8 ----- lib/cache.ts | 43 ++----------------------- lib/schedule.ts | 39 ---------------------- server/services/certificates.ts | 19 ----------- server/services/dns.ts | 57 --------------------------------- server/services/favicon.ts | 2 -- server/services/headers.ts | 14 -------- server/services/hosting.ts | 11 ------- server/services/registration.ts | 22 ------------- server/services/screenshot.ts | 2 -- server/services/seo.ts | 38 ---------------------- 12 files changed, 8 insertions(+), 253 deletions(-) diff --git a/app/api/cron/due-drain/route.ts b/app/api/cron/due-drain/route.ts index 7c99ff92..b64f2c4d 100644 --- a/app/api/cron/due-drain/route.ts +++ b/app/api/cron/due-drain/route.ts @@ -59,6 +59,12 @@ export async function GET(request: Request) { emitted += chunk.length; } + log.info("due-drain.cron.ok", { + emitted, + groups: result.groups, + duration_ms: Date.now() - startedAt, + }); + return NextResponse.json({ success: true, emitted, diff --git a/lib/cache.test.ts b/lib/cache.test.ts index adccadf4..d54ead29 100644 --- a/lib/cache.test.ts +++ b/lib/cache.test.ts @@ -42,8 +42,6 @@ describe("cached assets", () => { indexKey, lockKey, ttlSeconds: 60, - eventName: "test_asset", - baseMetrics: { domain: "example.com" }, produceAndUpload: async () => ({ url: "https://cdn/y.webp", key: "k", @@ -67,8 +65,6 @@ describe("cached assets", () => { indexKey, lockKey, ttlSeconds: 60, - eventName: "test_asset", - baseMetrics: { domain: "example.com" }, produceAndUpload: async () => ({ url: "https://cdn/unused.webp" }), }); @@ -83,8 +79,6 @@ describe("cached assets", () => { indexKey, lockKey, ttlSeconds: 60, - eventName: "test_asset", - baseMetrics: { domain: "example.com" }, purgeQueue: "purge-test", produceAndUpload: async () => ({ url: "https://cdn/new.webp", @@ -111,8 +105,6 @@ describe("cached assets", () => { indexKey, lockKey, ttlSeconds: 60, - eventName: "test_asset", - baseMetrics: { domain: "example.com" }, produceAndUpload: async () => ({ url: null }), }); diff --git a/lib/cache.ts b/lib/cache.ts index e6e30a1a..e9099bd0 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,4 +1,3 @@ -import { captureServer } from "@/lib/analytics/server"; import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; @@ -132,8 +131,6 @@ type CachedAssetOptions> = { indexKey: string; lockKey: string; ttlSeconds: number; - eventName: string; - baseMetrics?: Record; /** * Produce and upload the asset, returning { url, key } and any metrics to attach */ @@ -152,16 +149,8 @@ type CachedAssetOptions> = { export async function getOrCreateCachedAsset>( options: CachedAssetOptions, ): Promise<{ url: string | null }> { - const { - indexKey, - lockKey, - ttlSeconds, - eventName, - baseMetrics, - produceAndUpload, - purgeQueue, - } = options; - const startedAt = Date.now(); + const { indexKey, lockKey, ttlSeconds, produceAndUpload, purgeQueue } = + options; // 1) Check index try { @@ -169,23 +158,9 @@ export async function getOrCreateCachedAsset>( if (raw && typeof raw === "object") { const cachedUrl = (raw as { url?: unknown }).url; if (typeof cachedUrl === "string") { - await captureServer(eventName, { - ...baseMetrics, - source: "redis", - duration_ms: Date.now() - startedAt, - outcome: "ok", - cache: "hit", - }); return { url: cachedUrl }; } if (cachedUrl === null) { - await captureServer(eventName, { - ...baseMetrics, - source: "redis", - duration_ms: Date.now() - startedAt, - outcome: "not_found", - cache: "hit", - }); return { url: null }; } } @@ -202,13 +177,6 @@ export async function getOrCreateCachedAsset>( const cached = lockResult.cachedResult; if (cached && typeof cached === "object" && "url" in cached) { const cachedUrl = (cached as { url: string | null }).url; - await captureServer(eventName, { - ...baseMetrics, - source: "redis_wait", - duration_ms: Date.now() - startedAt, - outcome: cachedUrl ? "ok" : "not_found", - cache: "wait", - }); return { url: cachedUrl }; } return { url: null }; @@ -233,13 +201,6 @@ export async function getOrCreateCachedAsset>( } } catch {} - await captureServer(eventName, { - ...baseMetrics, - ...(produced.metrics ?? {}), - duration_ms: Date.now() - startedAt, - outcome: produced.url ? "ok" : "not_found", - cache: "store", - }); return { url: produced.url }; } finally { try { diff --git a/lib/schedule.ts b/lib/schedule.ts index 4d9a14e3..ab82a39a 100644 --- a/lib/schedule.ts +++ b/lib/schedule.ts @@ -1,6 +1,5 @@ import "server-only"; -import { captureServer } from "@/lib/analytics/server"; import { ns, redis } from "@/lib/redis"; import { BACKOFF_BASE_SECS, @@ -33,14 +32,6 @@ export async function scheduleSectionIfEarlier( ): Promise { // Validate dueAtMs before any computation or Redis writes if (!Number.isFinite(dueAtMs) || dueAtMs < 0) { - try { - await captureServer("schedule_section", { - section, - domain, - due_at_ms: dueAtMs, - outcome: "invalid_due", - }); - } catch {} return false; } const now = Date.now(); @@ -59,16 +50,6 @@ export async function scheduleSectionIfEarlier( return false; } await redis.zadd(dueKey, { score: desired, member: domain }); - try { - await captureServer("schedule_section", { - section, - domain, - due_at_ms: desired, - current_ms: current ?? null, - now_ms: now, - outcome: "scheduled", - }); - } catch {} return true; } @@ -110,14 +91,6 @@ export async function recordFailureAndBackoff( } const nextAtMs = Date.now() + backoffMsForAttempts(attempts); await redis.zadd(ns("due", section), { score: nextAtMs, member: domain }); - try { - await captureServer("schedule_backoff", { - section, - domain, - attempts, - next_at_ms: nextAtMs, - }); - } catch {} return nextAtMs; } @@ -129,9 +102,6 @@ export async function resetFailureBackoff( try { await redis.hdel(taskKey, domain); } catch {} - try { - await captureServer("schedule_backoff_reset", { section, domain }); - } catch {} } export function allSections(): Section[] { @@ -155,7 +125,6 @@ type DueDrainResult = { * Used by the Vercel cron job to trigger section revalidation via Inngest. */ export async function drainDueDomainsOnce(): Promise { - const startedAt = Date.now(); const sections = allSections(); const perSectionBatch = PER_SECTION_BATCH; const globalMax = MAX_EVENTS_PER_RUN; @@ -214,13 +183,5 @@ export async function drainDueDomainsOnce(): Promise { data: { domain, sections: Array.from(set) }, })); - try { - await captureServer("due_drain", { - duration_ms: Date.now() - startedAt, - emitted: events.length, - groups: grouped.length, - }); - } catch {} - return { events, groups: grouped.length }; } diff --git a/server/services/certificates.ts b/server/services/certificates.ts index b101ded7..908068de 100644 --- a/server/services/certificates.ts +++ b/server/services/certificates.ts @@ -1,7 +1,6 @@ import tls from "node:tls"; import { eq } from "drizzle-orm"; import { getDomainTld } from "rdapper"; -import { captureServer } from "@/lib/analytics/server"; import { db } from "@/lib/db/client"; import { replaceCertificates } from "@/lib/db/repos/certificates"; import { upsertDomain } from "@/lib/db/repos/domains"; @@ -67,8 +66,6 @@ export async function getCertificates(domain: string): Promise { // Client gating avoids calling this without A/AAAA; server does not pre-check DNS here. - const startedAt = Date.now(); - let outcome: "ok" | "timeout" | "error" = "ok"; try { const chain = await new Promise( (resolve, reject) => { @@ -97,11 +94,9 @@ export async function getCertificates(domain: string): Promise { }, ); socket.setTimeout(6000, () => { - outcome = "timeout"; socket.destroy(new Error("TLS timeout")); }); socket.on("error", (err) => { - outcome = "error"; reject(err); }); }, @@ -121,13 +116,6 @@ export async function getCertificates(domain: string): Promise { }; }); - await captureServer("tls_probe", { - domain: registrable ?? domain, - chain_length: out.length, - duration_ms: Date.now() - startedAt, - outcome, - }); - const now = new Date(); const earliestValidTo = out.length > 0 @@ -184,13 +172,6 @@ export async function getCertificates(domain: string): Promise { domain: registrable ?? domain, err, }); - await captureServer("tls_probe", { - domain: registrable ?? domain, - chain_length: 0, - duration_ms: Date.now() - startedAt, - outcome, - error: String(err), - }); // Do not treat as fatal; return empty and avoid long-lived negative cache return []; } diff --git a/server/services/dns.ts b/server/services/dns.ts index 19e97a3e..b714e383 100644 --- a/server/services/dns.ts +++ b/server/services/dns.ts @@ -1,6 +1,5 @@ import { eq } from "drizzle-orm"; import { getDomainTld } from "rdapper"; -import { captureServer } from "@/lib/analytics/server"; import { isCloudflareIpAsync } from "@/lib/cloudflare"; import { USER_AGENT } from "@/lib/constants"; import { db } from "@/lib/db/client"; @@ -57,7 +56,6 @@ export const DOH_PROVIDERS: DohProvider[] = [ ]; export async function resolveAll(domain: string): Promise { - const startedAt = Date.now(); log.debug("dns.start", { domain }); const providers = providerOrderForLookup(domain); const durationByProvider: Record = {}; @@ -137,27 +135,6 @@ export async function resolveAll(domain: string): Promise { const resolverHint = (rows[0]?.resolver ?? "cloudflare") as DnsResolver; const sorted = sortDnsRecordsByType(assembled, types); if (allFreshAcrossTypes) { - await captureServer("dns_resolve_all", { - domain: registrable ?? domain, - duration_ms_total: Date.now() - startedAt, - counts: (() => { - return (types as DnsType[]).reduce( - (acc, t) => { - acc[t] = sorted.filter((r) => r.type === t).length; - return acc; - }, - { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, - ); - })(), - cloudflare_ip_present: sorted.some( - (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, - ), - dns_provider_used: resolverHint, - provider_attempts: 0, - duration_ms_by_provider: {}, - cache_hit: true, - cache_source: "postgres", - }); return { records: sorted, resolver: resolverHint }; } @@ -261,20 +238,6 @@ export async function resolveAll(domain: string): Promise { }, { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, ); - const cloudflareIpPresent = merged.some( - (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, - ); - await captureServer("dns_resolve_all", { - domain: registrable ?? domain, - duration_ms_total: Date.now() - startedAt, - counts, - cloudflare_ip_present: cloudflareIpPresent, - dns_provider_used: pinnedProvider.key, - provider_attempts: 1, - duration_ms_by_provider: durationByProvider, - cache_hit: false, - cache_source: "partial", - }); log.info("dns.ok.partial", { domain: registrable, counts, @@ -314,9 +277,6 @@ export async function resolveAll(domain: string): Promise { }, { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, ); - const cloudflareIpPresent = flat.some( - (r) => (r.type === "A" || r.type === "AAAA") && r.isCloudflare, - ); const resolverUsed = provider.key; // Persist to Postgres @@ -374,17 +334,6 @@ export async function resolveAll(domain: string): Promise { }); } } - await captureServer("dns_resolve_all", { - domain: registrable ?? domain, - duration_ms_total: Date.now() - startedAt, - counts, - cloudflare_ip_present: cloudflareIpPresent, - dns_provider_used: resolverUsed, - provider_attempts: attemptIndex + 1, - duration_ms_by_provider: durationByProvider, - cache_hit: false, - cache_source: "fresh", - }); log.info("dns.ok", { domain: registrable, counts, @@ -404,12 +353,6 @@ export async function resolveAll(domain: string): Promise { } // All providers failed - await captureServer("dns_resolve_all", { - domain: registrable ?? domain, - duration_ms_total: Date.now() - startedAt, - failure: true, - provider_attempts: providers.length, - }); log.error("dns.all.providers.failed", { domain: registrable, providers: providers.map((p) => p.key), diff --git a/server/services/favicon.ts b/server/services/favicon.ts index 8420181f..b85d3f67 100644 --- a/server/services/favicon.ts +++ b/server/services/favicon.ts @@ -29,8 +29,6 @@ export async function getOrCreateFaviconBlobUrl( indexKey, lockKey, ttlSeconds: ttl, - eventName: "favicon_fetch", - baseMetrics: { domain, size: DEFAULT_SIZE }, purgeQueue: "favicon", produceAndUpload: async () => { const sources = buildSources(domain); diff --git a/server/services/headers.ts b/server/services/headers.ts index 5a4caeb9..11e08b06 100644 --- a/server/services/headers.ts +++ b/server/services/headers.ts @@ -1,6 +1,5 @@ import { eq } from "drizzle-orm"; import { getDomainTld } from "rdapper"; -import { captureServer } from "@/lib/analytics/server"; import { db } from "@/lib/db/client"; import { upsertDomain } from "@/lib/db/repos/domains"; import { replaceHeaders } from "@/lib/db/repos/headers"; @@ -66,12 +65,6 @@ export async function probeHeaders(domain: string): Promise { }); const normalized = normalize(headers); - await captureServer("headers_probe", { - domain: registrable ?? domain, - status: final.status, - used_method: "GET", - final_url: final.url, - }); // Persist to Postgres const now = new Date(); if (d) { @@ -101,13 +94,6 @@ export async function probeHeaders(domain: string): Promise { domain: registrable ?? domain, err, }); - await captureServer("headers_probe", { - domain: registrable ?? domain, - status: -1, - used_method: "ERROR", - final_url: url, - error: String(err), - }); // Return empty on failure without caching to avoid long-lived negatives return []; } diff --git a/server/services/hosting.ts b/server/services/hosting.ts index a257071e..8acb0326 100644 --- a/server/services/hosting.ts +++ b/server/services/hosting.ts @@ -1,7 +1,6 @@ import { eq } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { getDomainTld } from "rdapper"; -import { captureServer } from "@/lib/analytics/server"; import { db } from "@/lib/db/client"; import { upsertDomain } from "@/lib/db/repos/domains"; import { upsertHosting } from "@/lib/db/repos/hosting"; @@ -27,7 +26,6 @@ import { lookupIpMeta } from "@/server/services/ip"; const log = logger(); export async function detectHosting(domain: string): Promise { - const startedAt = Date.now(); log.debug("hosting.start", { domain }); // Fast path: DB @@ -209,15 +207,6 @@ export async function detectHosting(domain: string): Promise { dnsProvider: { name: dnsName, domain: dnsIconDomain }, geo, }; - await captureServer("hosting_detected", { - domain: registrable ?? domain, - hosting: hostingName, - email: emailName, - dns_provider: dnsName, - ip_present: Boolean(ip), - geo_country: geo.country || "", - duration_ms: Date.now() - startedAt, - }); // Persist to Postgres const now = new Date(); if (d) { diff --git a/server/services/registration.ts b/server/services/registration.ts index 3b0798b8..dfaa6dbe 100644 --- a/server/services/registration.ts +++ b/server/services/registration.ts @@ -1,6 +1,5 @@ import { eq } from "drizzle-orm"; import { getDomainTld, lookup } from "rdapper"; -import { captureServer } from "@/lib/analytics/server"; import { db } from "@/lib/db/client"; import { upsertDomain } from "@/lib/db/repos/domains"; import { resolveOrCreateProviderId } from "@/lib/db/repos/providers"; @@ -27,7 +26,6 @@ const log = logger(); * Fetch domain registration using rdapper and cache the normalized DomainRecord. */ export async function getRegistration(domain: string): Promise { - const startedAt = Date.now(); log.debug("registration.start", { domain }); // Try current snapshot @@ -105,13 +103,6 @@ export async function getRegistration(domain: string): Promise { registrarProvider, }; - await captureServer("registration_lookup", { - domain: registrable ?? domain, - outcome: row.isRegistered ? "ok" : "unregistered", - cached: true, - duration_ms: Date.now() - startedAt, - source: row.source, - }); log.info("registration.ok.cached", { domain: registrable ?? domain, registered: row.isRegistered, @@ -131,12 +122,6 @@ export async function getRegistration(domain: string): Promise { domain: registrable ?? domain, error: error || "unknown", }); - await captureServer("registration_lookup", { - domain: registrable ?? domain, - outcome: "error", - cached: false, - error: error || "unknown", - }); throw new Error(error || "Registration lookup failed"); } @@ -221,13 +206,6 @@ export async function getRegistration(domain: string): Promise { }); } } - await captureServer("registration_lookup", { - domain: registrable ?? domain, - outcome: record.isRegistered ? "ok" : "unregistered", - cached: false, - duration_ms: Date.now() - startedAt, - source: record.source, - }); log.info("registration.ok", { domain: registrable ?? domain, registered: record.isRegistered, diff --git a/server/services/screenshot.ts b/server/services/screenshot.ts index e7c21280..33d74d5f 100644 --- a/server/services/screenshot.ts +++ b/server/services/screenshot.ts @@ -66,8 +66,6 @@ export async function getOrCreateScreenshotBlobUrl( indexKey, lockKey, ttlSeconds: ttl, - eventName: "screenshot_capture", - baseMetrics: { domain, width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }, purgeQueue: "screenshot", produceAndUpload: async () => { let browser: Browser | null = null; diff --git a/server/services/seo.ts b/server/services/seo.ts index b391fe2d..70d555aa 100644 --- a/server/services/seo.ts +++ b/server/services/seo.ts @@ -1,6 +1,5 @@ import { eq } from "drizzle-orm"; import { getDomainTld } from "rdapper"; -import { captureServer } from "@/lib/analytics/server"; import { acquireLockOrWaitForResult } from "@/lib/cache"; import { SOCIAL_PREVIEW_TTL_SECONDS, USER_AGENT } from "@/lib/constants"; import { db } from "@/lib/db/client"; @@ -240,14 +239,6 @@ export async function getSeo(domain: string): Promise { } catch {} } - await captureServer("seo_fetch", { - domain: registrable ?? domain, - status: status ?? -1, - has_meta: !!meta, - has_robots: !!robots, - has_errors: Boolean(htmlError || robotsError), - }); - log.info("seo.ok", { domain: registrable ?? domain, status: status ?? -1, @@ -263,7 +254,6 @@ async function getOrCreateSocialPreviewImageUrl( domain: string, imageUrl: string, ): Promise<{ url: string | null }> { - const startedAt = Date.now(); const lower = domain.toLowerCase(); const indexKey = ns( "seo-image", @@ -286,15 +276,6 @@ async function getOrCreateSocialPreviewImageUrl( typeof raw === "object" && typeof (raw as { url?: unknown }).url === "string" ) { - await captureServer("seo_image", { - domain: lower, - width: SOCIAL_WIDTH, - height: SOCIAL_HEIGHT, - source: "redis", - duration_ms: Date.now() - startedAt, - outcome: "ok", - cache: "hit", - }); return { url: (raw as { url: string }).url }; } } catch { @@ -310,15 +291,6 @@ async function getOrCreateSocialPreviewImageUrl( if (!lockResult.acquired) { if (lockResult.cachedResult?.url) { - await captureServer("seo_image", { - domain: lower, - width: SOCIAL_WIDTH, - height: SOCIAL_HEIGHT, - source: "redis_wait", - duration_ms: Date.now() - startedAt, - outcome: "ok", - cache: "wait", - }); return { url: lockResult.cachedResult.url }; } return { url: null }; @@ -367,16 +339,6 @@ async function getOrCreateSocialPreviewImageUrl( }); } catch {} - await captureServer("seo_image", { - domain: lower, - width: SOCIAL_WIDTH, - height: SOCIAL_HEIGHT, - source: "upload", - duration_ms: Date.now() - startedAt, - outcome: "ok", - cache: "store", - }); - return { url }; } catch { return { url: null }; From 445182eb36c95679011bf5d7100206ee9bd32d8e Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 02:55:56 -0400 Subject: [PATCH 3/9] Refactor logging to include module context across various services --- app/api/cron/blob-prune/route.ts | 8 ++++---- app/api/cron/due-drain/route.ts | 8 ++++---- app/api/trpc/[trpc]/route.ts | 4 ++-- instrumentation.ts | 8 ++++---- lib/cache.ts | 2 +- lib/r2.ts | 4 ++-- lib/ratelimit.ts | 4 ++-- lib/storage.ts | 10 +++++----- package.json | 1 - pnpm-lock.yaml | 3 --- server/services/certificates.ts | 10 +++++----- server/services/dns.ts | 28 ++++++++++++++-------------- server/services/headers.ts | 14 +++++++------- server/services/hosting.ts | 8 ++++---- server/services/ip.ts | 12 ++++++------ server/services/pricing.ts | 8 ++++---- server/services/registration.ts | 19 ++++++++++--------- server/services/seo.ts | 6 +++--- trpc/init.ts | 6 +++--- 19 files changed, 80 insertions(+), 83 deletions(-) diff --git a/app/api/cron/blob-prune/route.ts b/app/api/cron/blob-prune/route.ts index 3c10cd53..8451dffd 100644 --- a/app/api/cron/blob-prune/route.ts +++ b/app/api/cron/blob-prune/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { logger } from "@/lib/logger"; import { pruneDueBlobsOnce } from "@/lib/storage"; -const log = logger(); +const log = logger({ module: "cron:blob-prune" }); export const dynamic = "force-dynamic"; @@ -29,13 +29,13 @@ export async function GET(request: Request) { const result = await pruneDueBlobsOnce(startedAt); if (result.errorCount > 0) { - log.warn("blob-prune.completed.with.errors", { + log.warn("completed.with.errors", { deletedCount: result.deletedCount, errorCount: result.errorCount, duration_ms: Date.now() - startedAt, }); } else { - log.info("blob-prune.completed", { + log.info("completed", { deletedCount: result.deletedCount, duration_ms: Date.now() - startedAt, }); @@ -48,7 +48,7 @@ export async function GET(request: Request) { duration_ms: Date.now() - startedAt, }); } catch (error) { - log.error("blob-prune.cron.failed", { err: error }); + log.error("cron.failed", { err: error }); return NextResponse.json( { error: "Internal error", diff --git a/app/api/cron/due-drain/route.ts b/app/api/cron/due-drain/route.ts index b64f2c4d..66b2903c 100644 --- a/app/api/cron/due-drain/route.ts +++ b/app/api/cron/due-drain/route.ts @@ -4,7 +4,7 @@ import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; import { drainDueDomainsOnce } from "@/lib/schedule"; -const log = logger(); +const log = logger({ module: "cron:due-drain" }); export const dynamic = "force-dynamic"; @@ -54,12 +54,12 @@ export async function GET(request: Request) { ), ); } catch (e) { - log.warn("due-drain.cleanup.failed", { err: e }); + log.warn("cleanup.failed", { err: e }); } emitted += chunk.length; } - log.info("due-drain.cron.ok", { + log.info("cron.ok", { emitted, groups: result.groups, duration_ms: Date.now() - startedAt, @@ -72,7 +72,7 @@ export async function GET(request: Request) { duration_ms: Date.now() - startedAt, }); } catch (error) { - log.error("due-drain.cron.failed", { err: error }); + log.error("cron.failed", { err: error }); return NextResponse.json( { error: "Internal error", diff --git a/app/api/trpc/[trpc]/route.ts b/app/api/trpc/[trpc]/route.ts index ac7080bb..e88513dd 100644 --- a/app/api/trpc/[trpc]/route.ts +++ b/app/api/trpc/[trpc]/route.ts @@ -34,8 +34,8 @@ const handler = (req: Request) => return { headers, status: 429 }; }, onError: ({ path, error }) => { - const log = logger(); - log.error("trpc.unhandled", { path, err: error }); + const log = logger({ module: "trpc:handler" }); + log.error("unhandled", { path, err: error }); }, }); diff --git a/instrumentation.ts b/instrumentation.ts index 84aa1d42..e37f8319 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -3,7 +3,7 @@ import { logger } from "@/lib/logger"; // Process-level error hooks (Node only) if (typeof process !== "undefined" && process?.on) { - const log = logger(); + const log = logger({ module: "instrumentation" }); process.on("uncaughtException", (err) => log.error("uncaughtException", { err }), ); @@ -12,7 +12,7 @@ if (typeof process !== "undefined" && process?.on) { ); } -const log = logger(); +const log = logger({ module: "instrumentation" }); export const onRequestError: Instrumentation.onRequestError = async ( err, @@ -41,7 +41,7 @@ export const onRequestError: Instrumentation.onRequestError = async ( const postHogData = JSON.parse(decodedCookie); distinctId = postHogData.distinct_id; } catch (e) { - log.error("posthog.cookie.parse.error", { err: e }); + log.error("cookie.parse.error", { err: e }); } } } @@ -54,7 +54,7 @@ export const onRequestError: Instrumentation.onRequestError = async ( await phClient.shutdown(); } catch (instrumentationError) { // Graceful degradation - log error but don't throw to avoid breaking the request - log.error("instrumentation.error.tracking.failed", { + log.error("error.tracking.failed", { err: instrumentationError, }); } diff --git a/lib/cache.ts b/lib/cache.ts index e9099bd0..0d9ec649 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,7 +1,7 @@ import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; -const log = logger(); +const log = logger({ module: "cache" }); function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/lib/r2.ts b/lib/r2.ts index f461d2b2..13c2b94e 100644 --- a/lib/r2.ts +++ b/lib/r2.ts @@ -8,7 +8,7 @@ import { } from "@aws-sdk/client-s3"; import { logger } from "@/lib/logger"; -const log = logger(); +const log = logger({ module: "r2" }); function getEnvOrThrow(name: string): string { const v = process.env[name]; @@ -171,7 +171,7 @@ export async function deleteObjects(keys: string[]): Promise { } } catch (err) { const message = (err as Error)?.message || "unknown"; - log.error("r2.deleteObjects.failed", { + log.error("deleteObjects.failed", { keys: slice, err, }); diff --git a/lib/ratelimit.ts b/lib/ratelimit.ts index e820ea4e..325fb340 100644 --- a/lib/ratelimit.ts +++ b/lib/ratelimit.ts @@ -7,7 +7,7 @@ import { logger } from "@/lib/logger"; import { redis } from "@/lib/redis"; import { t } from "@/trpc/init"; -const log = logger(); +const log = logger({ module: "ratelimit" }); export const SERVICE_LIMITS = { dns: { points: 60, window: "1 m" }, @@ -46,7 +46,7 @@ export async function assertRateLimit(service: ServiceName, ip: string) { Math.ceil((res.reset - Date.now()) / 1000), ); - log.warn("ratelimit.blocked", { + log.warn("blocked", { service, ip, limit: res.limit, diff --git a/lib/storage.ts b/lib/storage.ts index 4c28b2d6..09760681 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -5,7 +5,7 @@ import { logger } from "@/lib/logger"; import { makePublicUrl, putObject } from "@/lib/r2"; import type { StorageKind } from "@/lib/schemas"; -const log = logger(); +const log = logger({ module: "storage" }); const UPLOAD_MAX_ATTEMPTS = 3; const UPLOAD_BACKOFF_BASE_MS = 100; @@ -71,7 +71,7 @@ async function uploadWithRetry( for (let attempt = 0; attempt < maxAttempts; attempt++) { try { - log.debug("storage.upload.attempt", { + log.debug("upload.attempt", { key, attempt: attempt + 1, maxAttempts, @@ -84,7 +84,7 @@ async function uploadWithRetry( cacheControl, }); - log.info("storage.upload.ok", { + log.info("upload.ok", { key, attempt: attempt + 1, }); @@ -93,7 +93,7 @@ async function uploadWithRetry( } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); - log.warn("storage.upload.attempt.failed", { + log.warn("upload.attempt.failed", { key, attempt: attempt + 1, err: lastError, @@ -106,7 +106,7 @@ async function uploadWithRetry( UPLOAD_BACKOFF_BASE_MS, UPLOAD_BACKOFF_MAX_MS, ); - log.debug("storage.retrying.after.delay", { + log.debug("retrying.after.delay", { key, delay_ms: delay, }); diff --git a/package.json b/package.json index 7d96752b..f0e65dd2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "next": "15.6.0-canary.39", "next-themes": "^0.4.6", "pino": "^10.1.0", - "pino-std-serializers": "^7.0.0", "postgres": "^3.4.7", "posthog-js": "^1.280.1", "posthog-node": "^5.10.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec16406e..911f6a7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,9 +116,6 @@ importers: pino: specifier: ^10.1.0 version: 10.1.0 - pino-std-serializers: - specifier: ^7.0.0 - version: 7.0.0 postgres: specifier: ^3.4.7 version: 3.4.7 diff --git a/server/services/certificates.ts b/server/services/certificates.ts index 908068de..67b67a33 100644 --- a/server/services/certificates.ts +++ b/server/services/certificates.ts @@ -13,10 +13,10 @@ import { detectCertificateAuthority } from "@/lib/providers/detection"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import type { Certificate } from "@/lib/schemas"; -const log = logger(); +const log = logger({ module: "certificates" }); export async function getCertificates(domain: string): Promise { - log.debug("certificates.start", { domain }); + log.debug("start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); const d = registrable @@ -155,20 +155,20 @@ export async function getCertificates(domain: string): Promise { dueAtMs, ); } catch (err) { - log.warn("certificates.schedule.failed", { + log.warn("schedule.failed", { domain: registrable ?? domain, err, }); } } - log.info("certificates.ok", { + log.info("ok", { domain: registrable ?? domain, chain_length: out.length, }); return out; } catch (err) { - log.warn("certificates.error", { + log.warn("error", { domain: registrable ?? domain, err, }); diff --git a/server/services/dns.ts b/server/services/dns.ts index b714e383..c0afc56f 100644 --- a/server/services/dns.ts +++ b/server/services/dns.ts @@ -19,7 +19,7 @@ import { DnsTypeSchema, } from "@/lib/schemas"; -const log = logger(); +const log = logger({ module: "dns" }); export type DohProvider = { key: DnsResolver; @@ -56,7 +56,7 @@ export const DOH_PROVIDERS: DohProvider[] = [ ]; export async function resolveAll(domain: string): Promise { - log.debug("dns.start", { domain }); + log.debug("start", { domain }); const providers = providerOrderForLookup(domain); const durationByProvider: Record = {}; let lastError: unknown = null; @@ -209,7 +209,7 @@ export async function resolveAll(domain: string): Promise { soonest, ); } catch (err) { - log.warn("dns.schedule.failed.partial", { + log.warn("schedule.failed.partial", { domain: registrable ?? domain, err, }); @@ -238,8 +238,8 @@ export async function resolveAll(domain: string): Promise { }, { A: 0, AAAA: 0, MX: 0, TXT: 0, NS: 0 } as Record, ); - log.info("dns.ok.partial", { - domain: registrable, + log.info("ok.partial", { + domain: registrable ?? domain, counts, resolver: pinnedProvider.key, }); @@ -248,8 +248,8 @@ export async function resolveAll(domain: string): Promise { resolver: pinnedProvider.key, } as DnsResolveResult; } catch (err) { - log.warn("dns.partial.refresh.failed", { - domain: registrable, + log.warn("partial.refresh.failed", { + domain: registrable ?? domain, provider: pinnedProvider.key, err, }); @@ -328,21 +328,21 @@ export async function resolveAll(domain: string): Promise { const soonest = times.length > 0 ? Math.min(...times) : now.getTime(); await scheduleSectionIfEarlier("dns", registrable ?? domain, soonest); } catch (err) { - log.warn("dns.schedule.failed.full", { + log.warn("schedule.failed.full", { domain: registrable ?? domain, err, }); } } - log.info("dns.ok", { - domain: registrable, + log.info("ok", { + domain: registrable ?? domain, counts, resolver: resolverUsed, }); return { records: flat, resolver: resolverUsed } as DnsResolveResult; } catch (err) { - log.warn("dns.provider.attempt.failed", { - domain: registrable, + log.warn("provider.attempt.failed", { + domain: registrable ?? domain, provider: provider.key, err, }); @@ -353,8 +353,8 @@ export async function resolveAll(domain: string): Promise { } // All providers failed - log.error("dns.all.providers.failed", { - domain: registrable, + log.error("all.providers.failed", { + domain: registrable ?? domain, providers: providers.map((p) => p.key), err: lastError, }); diff --git a/server/services/headers.ts b/server/services/headers.ts index 11e08b06..be2a63f7 100644 --- a/server/services/headers.ts +++ b/server/services/headers.ts @@ -11,11 +11,11 @@ import { logger } from "@/lib/logger"; import { scheduleSectionIfEarlier } from "@/lib/schedule"; import type { HttpHeader } from "@/lib/schemas"; -const log = logger(); +const log = logger({ module: "headers" }); export async function probeHeaders(domain: string): Promise { const url = `https://${domain}/`; - log.debug("headers.start", { domain }); + log.debug("start", { domain }); // Fast path: read from Postgres if fresh const registrable = toRegistrableDomain(domain); const d = registrable @@ -42,8 +42,8 @@ export async function probeHeaders(domain: string): Promise { const normalized = normalize( existing.map((h) => ({ name: h.name, value: h.value })), ); - log.info("headers.cache.hit", { - domain: registrable, + log.info("cache.hit", { + domain: registrable ?? domain, count: normalized.length, }); return normalized; @@ -83,14 +83,14 @@ export async function probeHeaders(domain: string): Promise { ); } catch {} } - log.info("headers.ok", { - domain: registrable, + log.info("ok", { + domain: registrable ?? domain, status: final.status, count: normalized.length, }); return normalized; } catch (err) { - log.warn("headers.error", { + log.error("error", { domain: registrable ?? domain, err, }); diff --git a/server/services/hosting.ts b/server/services/hosting.ts index 8acb0326..87fb7aba 100644 --- a/server/services/hosting.ts +++ b/server/services/hosting.ts @@ -23,10 +23,10 @@ import { resolveAll } from "@/server/services/dns"; import { probeHeaders } from "@/server/services/headers"; import { lookupIpMeta } from "@/server/services/ip"; -const log = logger(); +const log = logger({ module: "hosting" }); export async function detectHosting(domain: string): Promise { - log.debug("hosting.start", { domain }); + log.debug("start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); @@ -119,7 +119,7 @@ export async function detectHosting(domain: string): Promise { lon: row.geoLon ?? null, }, }; - log.info("hosting.cache.hit", { + log.info("cache.hit", { domain, hosting: info.hostingProvider.name, email: info.emailProvider.name, @@ -247,7 +247,7 @@ export async function detectHosting(domain: string): Promise { await scheduleSectionIfEarlier("hosting", registrable ?? domain, dueAtMs); } catch {} } - log.info("hosting.ok", { + log.info("ok", { domain: registrable ?? domain, hosting: hostingName, email: emailName, diff --git a/server/services/ip.ts b/server/services/ip.ts index 85a3432a..0d2d1ef8 100644 --- a/server/services/ip.ts +++ b/server/services/ip.ts @@ -1,6 +1,6 @@ import { logger } from "@/lib/logger"; -const log = logger(); +const log = logger({ module: "ip" }); export async function lookupIpMeta(ip: string): Promise<{ geo: { @@ -14,7 +14,7 @@ export async function lookupIpMeta(ip: string): Promise<{ owner: string | null; domain: string | null; }> { - log.debug("ip.start", { ip }); + log.debug("start", { ip }); try { const res = await fetch(`https://ipwho.is/${encodeURIComponent(ip)}`); if (!res.ok) throw new Error("ipwho.is fail"); @@ -60,7 +60,7 @@ export async function lookupIpMeta(ip: string): Promise<{ }; }; - log.debug("ip.ipwho.is.result", { ip, json: j }); + log.debug("ipwhois.result", { ip, json: j }); const org = j.connection?.org?.trim(); const isp = j.connection?.isp?.trim(); @@ -75,15 +75,15 @@ export async function lookupIpMeta(ip: string): Promise<{ lon: typeof j.longitude === "number" ? j.longitude : null, }; - log.info("ip.ok", { + log.info("ok", { ip, owner: owner || undefined, domain: domain || undefined, geo: geo || undefined, }); return { geo, owner, domain }; - } catch { - log.warn("ip.error", { ip }); + } catch (err) { + log.warn("error", { ip, err }); return { owner: null, domain: null, diff --git a/server/services/pricing.ts b/server/services/pricing.ts index c57f4cf0..a23f3dc0 100644 --- a/server/services/pricing.ts +++ b/server/services/pricing.ts @@ -4,7 +4,7 @@ import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; import type { Pricing } from "@/lib/schemas"; -const log = logger(); +const log = logger({ module: "pricing" }); type DomainPricingResponse = { status: string; @@ -54,12 +54,12 @@ export async function getPricingForTld(domain: string): Promise { if (res.ok) { payload = (await res.json()) as DomainPricingResponse; await redis.set(resultKey, payload, { ex: 7 * 24 * 60 * 60 }); - log.info("pricing.fetch.ok", { cached: false }); + log.info("fetch.ok", { cached: false }); } else { - log.error("pricing.upstream.error", { status: res.status }); + log.error("upstream.error", { status: res.status }); } } catch (err) { - log.error("pricing.fetch.error", { err }); + log.error("fetch.error", { err }); } } else { payload = lock.cachedResult; diff --git a/server/services/registration.ts b/server/services/registration.ts index dfaa6dbe..ed058c5b 100644 --- a/server/services/registration.ts +++ b/server/services/registration.ts @@ -20,13 +20,13 @@ import type { RegistrationSource, } from "@/lib/schemas"; -const log = logger(); +const log = logger({ module: "registration" }); /** * Fetch domain registration using rdapper and cache the normalized DomainRecord. */ export async function getRegistration(domain: string): Promise { - log.debug("registration.start", { domain }); + log.debug("start", { domain }); // Try current snapshot const registrable = toRegistrableDomain(domain); @@ -103,7 +103,7 @@ export async function getRegistration(domain: string): Promise { registrarProvider, }; - log.info("registration.ok.cached", { + log.info("ok.cached", { domain: registrable ?? domain, registered: row.isRegistered, registrar: registrarProvider.name, @@ -118,15 +118,16 @@ export async function getRegistration(domain: string): Promise { }); if (!ok || !record) { - log.warn("registration.error", { + const err = new Error(error || "Registration lookup failed"); + log.error("error", { domain: registrable ?? domain, - error: error || "unknown", + err, }); - throw new Error(error || "Registration lookup failed"); + throw err; } // Log raw rdapper record for observability (safe; already public data) - log.debug("registration.rdapper.result", { + log.debug("rdapper.result", { ...record, }); @@ -200,13 +201,13 @@ export async function getRegistration(domain: string): Promise { expiresAt.getTime(), ); } catch (err) { - log.warn("registration.schedule.failed", { + log.warn("schedule.failed", { domain: registrable ?? domain, err, }); } } - log.info("registration.ok", { + log.info("ok", { domain: registrable ?? domain, registered: record.isRegistered, registrar: withProvider.registrarProvider.name, diff --git a/server/services/seo.ts b/server/services/seo.ts index 70d555aa..2b2c1501 100644 --- a/server/services/seo.ts +++ b/server/services/seo.ts @@ -23,13 +23,13 @@ import type { import { parseHtmlMeta, parseRobotsTxt, selectPreview } from "@/lib/seo"; import { storeImage } from "@/lib/storage"; -const log = logger(); +const log = logger({ module: "seo" }); const SOCIAL_WIDTH = 1200; const SOCIAL_HEIGHT = 630; export async function getSeo(domain: string): Promise { - log.debug("seo.start", { domain }); + log.debug("start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); const d = registrable @@ -239,7 +239,7 @@ export async function getSeo(domain: string): Promise { } catch {} } - log.info("seo.ok", { + log.info("ok", { domain: registrable ?? domain, status: status ?? -1, has_meta: !!meta, diff --git a/trpc/init.ts b/trpc/init.ts index 2aff2ced..0eef8be0 100644 --- a/trpc/init.ts +++ b/trpc/init.ts @@ -69,17 +69,17 @@ export const createCallerFactory = t.createCallerFactory; const withLogging = t.middleware(async ({ ctx, path, type, next }) => { const start = performance.now(); - ctx.log.debug("rpc.start", { rpcPath: path, rpcType: type }); + ctx.log.debug("start", { rpcPath: path, rpcType: type }); try { const result = await next(); - ctx.log.info("rpc.ok", { + ctx.log.info("ok", { rpcPath: path, rpcType: type, duration_ms: Math.round(performance.now() - start), }); return result; } catch (err) { - ctx.log.error("rpc.error", { + ctx.log.error("error", { rpcPath: path, rpcType: type, duration_ms: Math.round(performance.now() - start), From cd38517b1b906765f00046de75cff6e2668cdd83 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 03:01:57 -0400 Subject: [PATCH 4/9] Enhance logger for Edge compatibility with safe environment access --- lib/logger.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/logger.ts b/lib/logger.ts index 3667b5fb..686b9b1d 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -21,9 +21,11 @@ const RUNTIME: "node" | "edge" | "browser" = ? "edge" : "node"; -const isProd = process.env.NODE_ENV === "production"; +// Safe env access for Edge compatibility +const env = globalThis?.process?.env ?? {}; +const isProd = env.NODE_ENV === "production"; const defaultLevel = - (process.env.LOG_LEVEL as Level | undefined) ?? (isProd ? "info" : "debug"); + (env.LOG_LEVEL as Level | undefined) ?? (isProd ? "info" : "debug"); // ---------- console-based fallback (Edge/Browser) ---------- function makeConsoleLogger(base: LogFields = {}): Logger { @@ -58,7 +60,7 @@ async function getPinoRoot(): Promise { const pino = await import("pino"); const transport = - !isProd && process.env.LOG_PRETTY !== "0" + !isProd && env.LOG_PRETTY !== "0" ? { target: "pino-pretty", options: { colorize: true, singleLine: true }, @@ -69,9 +71,9 @@ async function getPinoRoot(): Promise { level: defaultLevel, base: { app: "domainstack", - env: process.env.NODE_ENV, - commit: process.env.VERCEL_GIT_COMMIT_SHA, - region: process.env.VERCEL_REGION, + env: env.NODE_ENV, + commit: env.VERCEL_GIT_COMMIT_SHA, + region: env.VERCEL_REGION, }, messageKey: "msg", timestamp: pino.default.stdTimeFunctions.isoTime, From 46c9dce02cf803ba3c1ad1da3e16f9df83b40b73 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 03:05:14 -0400 Subject: [PATCH 5/9] Optimize logger by caching child logger instances to reduce allocations --- lib/logger.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/logger.ts b/lib/logger.ts index 686b9b1d..21901dbc 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -87,11 +87,17 @@ async function getPinoRoot(): Promise { } function makeNodeLogger(base: LogFields = {}): Logger { + // Cache child logger per instance to avoid repeated allocations + let cachedChild: PinoLogger | undefined; + const emit = async (level: Level, msg: string, fields?: LogFields) => { - const root = await getPinoRoot(); - const child = Object.keys(base).length ? root.child(base) : root; - child[level]({ ...(fields ?? {}) }, msg); + if (!cachedChild) { + const root = await getPinoRoot(); + cachedChild = Object.keys(base).length ? root.child(base) : root; + } + cachedChild[level]({ ...(fields ?? {}) }, msg); }; + // Sync facade; logs flush after first dynamic import resolves. return { debug: (m, f) => { From 074164bacaf63c216bb299183b3e14531b9a0547 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 03:08:09 -0400 Subject: [PATCH 6/9] Improve error handling in caching and storage functions with detailed logging --- lib/cache.ts | 12 +++++++++--- lib/storage.ts | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/cache.ts b/lib/cache.ts index 0d9ec649..3fae686f 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -164,7 +164,9 @@ export async function getOrCreateCachedAsset>( return { url: null }; } } - } catch {} + } catch (err) { + log.debug("redis.index.read.failed", { indexKey, err }); + } // 2) Acquire lock or wait const lockResult = await acquireLockOrWaitForResult<{ url: string | null }>({ @@ -199,12 +201,16 @@ export async function getOrCreateCachedAsset>( member: produced.key, }); } - } catch {} + } catch (err) { + log.warn("redis.cache.store.failed", { indexKey, purgeQueue, err }); + } return { url: produced.url }; } finally { try { await redis.del(lockKey); - } catch {} + } catch (err) { + log.debug("redis.lock.release.failed", { lockKey, err }); + } } } diff --git a/lib/storage.ts b/lib/storage.ts index 09760681..eee3380c 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -115,9 +115,9 @@ async function uploadWithRetry( } } - throw new Error( - `Upload failed after ${maxAttempts} attempts: ${lastError?.message ?? "unknown error"}`, - ); + throw new Error(`Upload failed after ${maxAttempts} attempts.`, { + cause: lastError ?? undefined, + }); } export async function storeBlob(options: { From 0c0332c8027127b1a9be1cc740380eb55e41ded8 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 03:14:37 -0400 Subject: [PATCH 7/9] Refactor instrumentation to conditionally register Node.js-specific error handling and improve dynamic imports for logging --- instrumentation.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/instrumentation.ts b/instrumentation.ts index e37f8319..3405bddb 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,18 +1,21 @@ import type { Instrumentation } from "next"; -import { logger } from "@/lib/logger"; -// Process-level error hooks (Node only) -if (typeof process !== "undefined" && process?.on) { - const log = logger({ module: "instrumentation" }); - process.on("uncaughtException", (err) => - log.error("uncaughtException", { err }), - ); - process.on("unhandledRejection", (reason) => - log.error("unhandledRejection", { err: reason }), - ); -} +// Conditionally register Node.js-specific instrumentation +export const register = async () => { + if (process.env.NEXT_RUNTIME === "nodejs") { + // Dynamic import to avoid bundling Node.js code into Edge runtime + const { logger } = await import("@/lib/logger"); + const log = logger({ module: "instrumentation" }); -const log = logger({ module: "instrumentation" }); + // Process-level error hooks (Node only) + process.on("uncaughtException", (err) => + log.error("uncaughtException", { err }), + ); + process.on("unhandledRejection", (reason) => + log.error("unhandledRejection", { err: reason }), + ); + } +}; export const onRequestError: Instrumentation.onRequestError = async ( err, @@ -20,6 +23,10 @@ export const onRequestError: Instrumentation.onRequestError = async ( ) => { if (process.env.NEXT_RUNTIME === "nodejs") { try { + // Dynamic imports for Node.js-only code + const { logger } = await import("@/lib/logger"); + const log = logger({ module: "instrumentation" }); + const { getServerPosthog } = await import("@/lib/analytics/server"); const phClient = getServerPosthog(); @@ -54,9 +61,7 @@ export const onRequestError: Instrumentation.onRequestError = async ( await phClient.shutdown(); } catch (instrumentationError) { // Graceful degradation - log error but don't throw to avoid breaking the request - log.error("error.tracking.failed", { - err: instrumentationError, - }); + console.error("Instrumentation error", instrumentationError); } } }; From a8199b06db50b1e8e259efbf98a0420a389edb4d Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 03:39:28 -0400 Subject: [PATCH 8/9] Refactor logging to standardize error handling across services and improve duration metrics --- app/api/cron/blob-prune/route.ts | 6 +++--- app/api/cron/due-drain/route.ts | 4 ++-- instrumentation.ts | 4 +++- lib/cache.ts | 4 ++-- lib/logger.ts | 4 +--- lib/storage.ts | 2 +- server/services/certificates.ts | 4 ++-- server/services/headers.ts | 2 +- server/services/registration.ts | 4 +++- trpc/init.ts | 8 ++++---- 10 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/api/cron/blob-prune/route.ts b/app/api/cron/blob-prune/route.ts index 8451dffd..3a84211f 100644 --- a/app/api/cron/blob-prune/route.ts +++ b/app/api/cron/blob-prune/route.ts @@ -32,12 +32,12 @@ export async function GET(request: Request) { log.warn("completed.with.errors", { deletedCount: result.deletedCount, errorCount: result.errorCount, - duration_ms: Date.now() - startedAt, + durationMs: Date.now() - startedAt, }); } else { log.info("completed", { deletedCount: result.deletedCount, - duration_ms: Date.now() - startedAt, + durationMs: Date.now() - startedAt, }); } @@ -45,7 +45,7 @@ export async function GET(request: Request) { success: true, deletedCount: result.deletedCount, errorCount: result.errorCount, - duration_ms: Date.now() - startedAt, + durationMs: Date.now() - startedAt, }); } catch (error) { log.error("cron.failed", { err: error }); diff --git a/app/api/cron/due-drain/route.ts b/app/api/cron/due-drain/route.ts index 66b2903c..0bea45a5 100644 --- a/app/api/cron/due-drain/route.ts +++ b/app/api/cron/due-drain/route.ts @@ -62,14 +62,14 @@ export async function GET(request: Request) { log.info("cron.ok", { emitted, groups: result.groups, - duration_ms: Date.now() - startedAt, + durationMs: Date.now() - startedAt, }); return NextResponse.json({ success: true, emitted, groups: result.groups, - duration_ms: Date.now() - startedAt, + durationMs: Date.now() - startedAt, }); } catch (error) { log.error("cron.failed", { err: error }); diff --git a/instrumentation.ts b/instrumentation.ts index 3405bddb..e8fa5d19 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -12,7 +12,9 @@ export const register = async () => { log.error("uncaughtException", { err }), ); process.on("unhandledRejection", (reason) => - log.error("unhandledRejection", { err: reason }), + log.error("unhandledRejection", { + err: reason instanceof Error ? reason : new Error(String(reason)), + }), ); } }; diff --git a/lib/cache.ts b/lib/cache.ts index 3fae686f..c054bc95 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -82,7 +82,7 @@ export async function acquireLockOrWaitForResult(options: { lockKey, resultKey, pollCount, - wait_ms: Date.now() - startTime, + waitMs: Date.now() - startTime, }); return { acquired: false, cachedResult: result }; } @@ -121,7 +121,7 @@ export async function acquireLockOrWaitForResult(options: { lockKey, resultKey, pollCount, - wait_ms: Date.now() - startTime, + waitMs: Date.now() - startTime, }); return { acquired: false, cachedResult: null }; diff --git a/lib/logger.ts b/lib/logger.ts index 21901dbc..557cc1cd 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -127,8 +127,7 @@ export function createRequestLogger(opts: { method?: string; path?: string; ip?: string; - requestId?: string; - userId?: string; + requestId?: string | null; vercelId?: string | null; }) { return logger({ @@ -136,7 +135,6 @@ export function createRequestLogger(opts: { path: opts.path, ip: opts.ip, requestId: opts.requestId, - userId: opts.userId, vercelId: opts.vercelId ?? undefined, }); } diff --git a/lib/storage.ts b/lib/storage.ts index eee3380c..1abf1f05 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -108,7 +108,7 @@ async function uploadWithRetry( ); log.debug("retrying.after.delay", { key, - delay_ms: delay, + delayMs: delay, }); await sleep(delay); } diff --git a/server/services/certificates.ts b/server/services/certificates.ts index 67b67a33..d8689b96 100644 --- a/server/services/certificates.ts +++ b/server/services/certificates.ts @@ -157,7 +157,7 @@ export async function getCertificates(domain: string): Promise { } catch (err) { log.warn("schedule.failed", { domain: registrable ?? domain, - err, + err: err instanceof Error ? err : new Error(String(err)), }); } } @@ -170,7 +170,7 @@ export async function getCertificates(domain: string): Promise { } catch (err) { log.warn("error", { domain: registrable ?? domain, - err, + err: err instanceof Error ? err : new Error(String(err)), }); // Do not treat as fatal; return empty and avoid long-lived negative cache return []; diff --git a/server/services/headers.ts b/server/services/headers.ts index be2a63f7..3c41e618 100644 --- a/server/services/headers.ts +++ b/server/services/headers.ts @@ -92,7 +92,7 @@ export async function probeHeaders(domain: string): Promise { } catch (err) { log.error("error", { domain: registrable ?? domain, - err, + err: err instanceof Error ? err : new Error(String(err)), }); // Return empty on failure without caching to avoid long-lived negatives return []; diff --git a/server/services/registration.ts b/server/services/registration.ts index ed058c5b..0eef23ea 100644 --- a/server/services/registration.ts +++ b/server/services/registration.ts @@ -118,7 +118,9 @@ export async function getRegistration(domain: string): Promise { }); if (!ok || !record) { - const err = new Error(error || "Registration lookup failed"); + const err = new Error( + `Registration lookup failed for ${registrable ?? domain}: ${error || "unknown error"}`, + ); log.error("error", { domain: registrable ?? domain, err, diff --git a/trpc/init.ts b/trpc/init.ts index 0eef8be0..8d3295c0 100644 --- a/trpc/init.ts +++ b/trpc/init.ts @@ -13,8 +13,8 @@ export const createContext = async (opts?: { req?: Request }) => { null) : null; - const requestId = req?.headers.get("x-request-id") || crypto.randomUUID(); const path = req ? new URL(req.url).pathname : undefined; + const requestId = req?.headers.get("x-request-id"); const vercelId = req?.headers.get("x-vercel-id"); const log = createRequestLogger({ @@ -75,15 +75,15 @@ const withLogging = t.middleware(async ({ ctx, path, type, next }) => { ctx.log.info("ok", { rpcPath: path, rpcType: type, - duration_ms: Math.round(performance.now() - start), + durationMs: Math.round(performance.now() - start), }); return result; } catch (err) { ctx.log.error("error", { rpcPath: path, rpcType: type, - duration_ms: Math.round(performance.now() - start), - err, + durationMs: Math.round(performance.now() - start), + err: err instanceof Error ? err : new Error(String(err)), }); throw err; } From c1df0ab86f78b35a2ad81d11161ef00047dc5e74 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 24 Oct 2025 04:01:55 -0400 Subject: [PATCH 9/9] Standardize error handling and logging across various services, ensuring consistent error messages and improved duration metrics. --- app/api/cron/blob-prune/route.ts | 17 +++++++++++------ app/api/cron/due-drain/route.ts | 14 +++++++++----- instrumentation.ts | 14 ++++++++------ lib/cache.ts | 28 +++++++++++++++++++--------- server/services/certificates.ts | 2 +- server/services/dns.ts | 2 ++ server/services/headers.ts | 7 ++++++- server/services/hosting.ts | 7 ++++++- server/services/registration.ts | 2 +- 9 files changed, 63 insertions(+), 30 deletions(-) diff --git a/app/api/cron/blob-prune/route.ts b/app/api/cron/blob-prune/route.ts index 3a84211f..8b6f7ac4 100644 --- a/app/api/cron/blob-prune/route.ts +++ b/app/api/cron/blob-prune/route.ts @@ -14,6 +14,7 @@ export async function GET(request: Request) { : null; if (!expectedAuth) { + log.error("cron.misconfigured", { reason: "CRON_SECRET missing" }); return NextResponse.json( { error: "CRON_SECRET not configured" }, { status: 500 }, @@ -21,6 +22,7 @@ export async function GET(request: Request) { } if (authHeader !== expectedAuth) { + log.warn("cron.unauthorized", { provided: Boolean(authHeader) }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -28,16 +30,17 @@ export async function GET(request: Request) { const startedAt = Date.now(); const result = await pruneDueBlobsOnce(startedAt); + const durationMs = Date.now() - startedAt; if (result.errorCount > 0) { log.warn("completed.with.errors", { deletedCount: result.deletedCount, errorCount: result.errorCount, - durationMs: Date.now() - startedAt, + durationMs, }); } else { log.info("completed", { deletedCount: result.deletedCount, - durationMs: Date.now() - startedAt, + durationMs, }); } @@ -45,14 +48,16 @@ export async function GET(request: Request) { success: true, deletedCount: result.deletedCount, errorCount: result.errorCount, - durationMs: Date.now() - startedAt, + durationMs, + }); + } catch (err) { + log.error("cron.failed", { + err: err instanceof Error ? err : new Error(String(err)), }); - } catch (error) { - log.error("cron.failed", { err: error }); return NextResponse.json( { error: "Internal error", - message: error instanceof Error ? error.message : "unknown", + message: err instanceof Error ? err.message : "unknown", }, { status: 500 }, ); diff --git a/app/api/cron/due-drain/route.ts b/app/api/cron/due-drain/route.ts index 0bea45a5..1c193a5c 100644 --- a/app/api/cron/due-drain/route.ts +++ b/app/api/cron/due-drain/route.ts @@ -53,8 +53,10 @@ export async function GET(request: Request) { e.data.sections.map((s) => redis.zrem(ns("due", s), e.data.domain)), ), ); - } catch (e) { - log.warn("cleanup.failed", { err: e }); + } catch (err) { + log.warn("cleanup.failed", { + err: err instanceof Error ? err : new Error(String(err)), + }); } emitted += chunk.length; } @@ -71,12 +73,14 @@ export async function GET(request: Request) { groups: result.groups, durationMs: Date.now() - startedAt, }); - } catch (error) { - log.error("cron.failed", { err: error }); + } catch (err) { + log.error("cron.failed", { + err: err instanceof Error ? err : new Error(String(err)), + }); return NextResponse.json( { error: "Internal error", - message: error instanceof Error ? error.message : "unknown", + message: err instanceof Error ? err.message : "unknown", }, { status: 500 }, ); diff --git a/instrumentation.ts b/instrumentation.ts index e8fa5d19..a51be0ba 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -36,8 +36,8 @@ export const onRequestError: Instrumentation.onRequestError = async ( return; // PostHog not available, skip error tracking } - let distinctId = null; - if (request.headers.cookie) { + let distinctId: string | null = null; + if (request.headers?.cookie) { const cookieString = request.headers.cookie; const postHogCookieMatch = typeof cookieString === "string" @@ -49,8 +49,10 @@ export const onRequestError: Instrumentation.onRequestError = async ( const decodedCookie = decodeURIComponent(postHogCookieMatch[1]); const postHogData = JSON.parse(decodedCookie); distinctId = postHogData.distinct_id; - } catch (e) { - log.error("cookie.parse.error", { err: e }); + } catch (err) { + log.error("cookie.parse.error", { + err: err instanceof Error ? err : new Error(String(err)), + }); } } } @@ -61,9 +63,9 @@ export const onRequestError: Instrumentation.onRequestError = async ( }); await phClient.shutdown(); - } catch (instrumentationError) { + } catch (err) { // Graceful degradation - log error but don't throw to avoid breaking the request - console.error("Instrumentation error", instrumentationError); + console.error("Instrumentation error", err); } } }; diff --git a/lib/cache.ts b/lib/cache.ts index c054bc95..0cb7c704 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -47,7 +47,7 @@ export async function acquireLockOrWaitForResult(options: { nx: true, ex: lockTtl, }); - const acquired = setRes === "OK" || setRes === undefined; + const acquired = Boolean(setRes); if (acquired) { log.debug("redis.lock.acquired", { lockKey }); @@ -62,7 +62,7 @@ export async function acquireLockOrWaitForResult(options: { } catch (err) { log.warn("redis.lock.acquisition.failed", { lockKey, - err, + err: err instanceof Error ? err : new Error(String(err)), }); // If Redis is down, fail open (don't wait) return { acquired: true, cachedResult: null }; @@ -82,7 +82,7 @@ export async function acquireLockOrWaitForResult(options: { lockKey, resultKey, pollCount, - waitMs: Date.now() - startTime, + durationMs: Date.now() - startTime, }); return { acquired: false, cachedResult: result }; } @@ -101,7 +101,7 @@ export async function acquireLockOrWaitForResult(options: { nx: true, ex: lockTtl, }); - const retryAcquired = retryRes === "OK" || retryRes === undefined; + const retryAcquired = Boolean(retryRes); if (retryAcquired) { return { acquired: true, cachedResult: null }; } @@ -110,7 +110,7 @@ export async function acquireLockOrWaitForResult(options: { log.warn("redis.polling.error", { lockKey, resultKey, - err, + err: err instanceof Error ? err : new Error(String(err)), }); } @@ -121,7 +121,7 @@ export async function acquireLockOrWaitForResult(options: { lockKey, resultKey, pollCount, - waitMs: Date.now() - startTime, + durationMs: Date.now() - startTime, }); return { acquired: false, cachedResult: null }; @@ -165,7 +165,10 @@ export async function getOrCreateCachedAsset>( } } } catch (err) { - log.debug("redis.index.read.failed", { indexKey, err }); + log.debug("redis.index.read.failed", { + indexKey, + err: err instanceof Error ? err : new Error(String(err)), + }); } // 2) Acquire lock or wait @@ -202,7 +205,11 @@ export async function getOrCreateCachedAsset>( }); } } catch (err) { - log.warn("redis.cache.store.failed", { indexKey, purgeQueue, err }); + log.warn("redis.cache.store.failed", { + indexKey, + purgeQueue, + err: err instanceof Error ? err : new Error(String(err)), + }); } return { url: produced.url }; @@ -210,7 +217,10 @@ export async function getOrCreateCachedAsset>( try { await redis.del(lockKey); } catch (err) { - log.debug("redis.lock.release.failed", { lockKey, err }); + log.debug("redis.lock.release.failed", { + lockKey, + err: err instanceof Error ? err : new Error(String(err)), + }); } } } diff --git a/server/services/certificates.ts b/server/services/certificates.ts index d8689b96..a604153d 100644 --- a/server/services/certificates.ts +++ b/server/services/certificates.ts @@ -164,7 +164,7 @@ export async function getCertificates(domain: string): Promise { log.info("ok", { domain: registrable ?? domain, - chain_length: out.length, + chainLength: out.length, }); return out; } catch (err) { diff --git a/server/services/dns.ts b/server/services/dns.ts index c0afc56f..efe5e44a 100644 --- a/server/services/dns.ts +++ b/server/services/dns.ts @@ -242,6 +242,7 @@ export async function resolveAll(domain: string): Promise { domain: registrable ?? domain, counts, resolver: pinnedProvider.key, + durationMs: durationByProvider[pinnedProvider.key], }); return { records: merged, @@ -338,6 +339,7 @@ export async function resolveAll(domain: string): Promise { domain: registrable ?? domain, counts, resolver: resolverUsed, + durationMs: durationByProvider, }); return { records: flat, resolver: resolverUsed } as DnsResolveResult; } catch (err) { diff --git a/server/services/headers.ts b/server/services/headers.ts index 3c41e618..94e693cb 100644 --- a/server/services/headers.ts +++ b/server/services/headers.ts @@ -81,7 +81,12 @@ export async function probeHeaders(domain: string): Promise { registrable ?? domain, dueAtMs, ); - } catch {} + } catch (err) { + log.warn("schedule.failed", { + domain: registrable ?? domain, + err: err instanceof Error ? err : new Error(String(err)), + }); + } } log.info("ok", { domain: registrable ?? domain, diff --git a/server/services/hosting.ts b/server/services/hosting.ts index 87fb7aba..bbcaebe2 100644 --- a/server/services/hosting.ts +++ b/server/services/hosting.ts @@ -245,7 +245,12 @@ export async function detectHosting(domain: string): Promise { try { const dueAtMs = ttlForHosting(now).getTime(); await scheduleSectionIfEarlier("hosting", registrable ?? domain, dueAtMs); - } catch {} + } catch (err) { + log.warn("schedule.failed", { + domain: registrable ?? domain, + err: err instanceof Error ? err : new Error(String(err)), + }); + } } log.info("ok", { domain: registrable ?? domain, diff --git a/server/services/registration.ts b/server/services/registration.ts index 0eef23ea..afc0f263 100644 --- a/server/services/registration.ts +++ b/server/services/registration.ts @@ -205,7 +205,7 @@ export async function getRegistration(domain: string): Promise { } catch (err) { log.warn("schedule.failed", { domain: registrable ?? domain, - err, + err: err instanceof Error ? err : new Error(String(err)), }); } }