diff --git a/app/api/cron/blob-prune/route.ts b/app/api/cron/blob-prune/route.ts index e1087c0b..8b6f7ac4 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({ module: "cron:blob-prune" }); + export const dynamic = "force-dynamic"; export async function GET(request: Request) { @@ -11,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 }, @@ -18,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 }); } @@ -25,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) { - console.warn("[blob-prune] completed with errors", { + log.warn("completed.with.errors", { deletedCount: result.deletedCount, errorCount: result.errorCount, - duration_ms: Date.now() - startedAt, + durationMs, }); } else { - console.info("[blob-prune] completed", { + log.info("completed", { deletedCount: result.deletedCount, - duration_ms: Date.now() - startedAt, + durationMs, }); } @@ -42,14 +48,16 @@ export async function GET(request: Request) { success: true, deletedCount: result.deletedCount, errorCount: result.errorCount, - duration_ms: Date.now() - startedAt, + durationMs, + }); + } catch (err) { + log.error("cron.failed", { + err: err instanceof Error ? err : new Error(String(err)), }); - } catch (error) { - console.error("[blob-prune] cron failed", 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 f8467f4d..1c193a5c 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({ module: "cron:due-drain" }); + export const dynamic = "force-dynamic"; export async function GET(request: Request) { @@ -50,24 +53,34 @@ export async function GET(request: Request) { e.data.sections.map((s) => redis.zrem(ns("due", s), e.data.domain)), ), ); - } catch (e) { - console.warn("[due-drain] cleanup failed", e); + } catch (err) { + log.warn("cleanup.failed", { + err: err instanceof Error ? err : new Error(String(err)), + }); } emitted += chunk.length; } + log.info("cron.ok", { + emitted, + groups: result.groups, + durationMs: Date.now() - startedAt, + }); + return NextResponse.json({ success: true, emitted, groups: result.groups, - duration_ms: Date.now() - startedAt, + durationMs: Date.now() - startedAt, + }); + } catch (err) { + log.error("cron.failed", { + err: err instanceof Error ? err : new Error(String(err)), }); - } catch (error) { - console.error("[due-drain] cron failed", 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/trpc/[trpc]/route.ts b/app/api/trpc/[trpc]/route.ts index 314cade2..e88513dd 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({ module: "trpc:handler" }); + log.error("unhandled", { path, err: error }); }, }); diff --git a/instrumentation.ts b/instrumentation.ts index 1b07b791..a51be0ba 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,11 +1,34 @@ import type { Instrumentation } from "next"; +// 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" }); + + // Process-level error hooks (Node only) + process.on("uncaughtException", (err) => + log.error("uncaughtException", { err }), + ); + process.on("unhandledRejection", (reason) => + log.error("unhandledRejection", { + err: reason instanceof Error ? reason : new Error(String(reason)), + }), + ); + } +}; + export const onRequestError: Instrumentation.onRequestError = async ( err, request, ) => { 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(); @@ -13,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" @@ -26,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) { - console.error("Error parsing PostHog cookie:", e); + } catch (err) { + log.error("cookie.parse.error", { + err: err instanceof Error ? err : new Error(String(err)), + }); } } } @@ -38,12 +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 tracking failed:", - instrumentationError, - ); + console.error("Instrumentation error", err); } } }; 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 42577d69..0cb7c704 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,6 +1,8 @@ -import { captureServer } from "@/lib/analytics/server"; +import { logger } from "@/lib/logger"; import { ns, redis } from "@/lib/redis"; +const log = logger({ module: "cache" }); + function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -45,22 +47,22 @@ export async function acquireLockOrWaitForResult(options: { nx: true, ex: lockTtl, }); - const acquired = setRes === "OK" || setRes === undefined; + const acquired = Boolean(setRes); 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: err instanceof Error ? err : new Error(String(err)), }); // If Redis is down, fail open (don't wait) return { acquired: true, cachedResult: null }; @@ -76,11 +78,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, + durationMs: Date.now() - startTime, }); return { acquired: false, cachedResult: result }; } @@ -88,7 +90,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, @@ -99,27 +101,27 @@ 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 }; } } } catch (err) { - console.warn("[redis] error polling for result", { + log.warn("redis.polling.error", { lockKey, resultKey, - error: (err as Error)?.message, + err: err instanceof Error ? err : new Error(String(err)), }); } await sleep(pollIntervalMs); } - console.warn("[redis] wait timeout, no result found", { + log.warn("redis.wait.timeout", { lockKey, resultKey, pollCount, - waitedMs: Date.now() - startTime, + durationMs: Date.now() - startTime, }); return { acquired: false, cachedResult: null }; @@ -129,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 */ @@ -149,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 { @@ -166,27 +158,18 @@ 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 }; } } - } catch {} + } catch (err) { + log.debug("redis.index.read.failed", { + indexKey, + err: err instanceof Error ? err : new Error(String(err)), + }); + } // 2) Acquire lock or wait const lockResult = await acquireLockOrWaitForResult<{ url: string | null }>({ @@ -199,13 +182,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 }; @@ -228,19 +204,23 @@ export async function getOrCreateCachedAsset>( member: produced.key, }); } - } catch {} - - await captureServer(eventName, { - ...baseMetrics, - ...(produced.metrics ?? {}), - duration_ms: Date.now() - startedAt, - outcome: produced.url ? "ok" : "not_found", - cache: "store", - }); + } catch (err) { + log.warn("redis.cache.store.failed", { + indexKey, + purgeQueue, + err: err instanceof Error ? err : new Error(String(err)), + }); + } + return { url: produced.url }; } finally { try { await redis.del(lockKey); - } catch {} + } catch (err) { + log.debug("redis.lock.release.failed", { + lockKey, + err: err instanceof Error ? err : new Error(String(err)), + }); + } } } diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 00000000..557cc1cd --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,140 @@ +// 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"; + +// Safe env access for Edge compatibility +const env = globalThis?.process?.env ?? {}; +const isProd = env.NODE_ENV === "production"; +const defaultLevel = + (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 && env.LOG_PRETTY !== "0" + ? { + target: "pino-pretty", + options: { colorize: true, singleLine: true }, + } + : undefined; + + nodeRoot = pino.default({ + level: defaultLevel, + base: { + app: "domainstack", + env: env.NODE_ENV, + commit: env.VERCEL_GIT_COMMIT_SHA, + region: env.VERCEL_REGION, + }, + messageKey: "msg", + timestamp: pino.default.stdTimeFunctions.isoTime, + transport, + serializers: { + err: pino.default.stdSerializers.err, + }, + }); + + return nodeRoot; +} + +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) => { + 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) => { + 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 | null; + vercelId?: string | null; +}) { + return logger({ + method: opts.method, + path: opts.path, + ip: opts.ip, + requestId: opts.requestId, + vercelId: opts.vercelId ?? undefined, + }); +} diff --git a/lib/r2.ts b/lib/r2.ts index a9416883..13c2b94e 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({ module: "r2" }); 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("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..325fb340 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({ module: "ratelimit" }); + 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("blocked", { service, ip, limit: res.limit, 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/lib/storage.ts b/lib/storage.ts index a8a421ba..1abf1f05 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({ module: "storage" }); + 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("upload.attempt", { key, attempt: attempt + 1, maxAttempts, @@ -81,7 +84,7 @@ async function uploadWithRetry( cacheControl, }); - console.info("[storage] upload success", { + log.info("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("upload.attempt.failed", { key, attempt: attempt + 1, - error: lastError.message, + err: lastError, }); // Don't sleep on last attempt @@ -103,7 +106,7 @@ async function uploadWithRetry( UPLOAD_BACKOFF_BASE_MS, UPLOAD_BACKOFF_MAX_MS, ); - console.debug("[storage] retrying after delay", { + log.debug("retrying.after.delay", { key, delayMs: delay, }); @@ -112,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: { diff --git a/package.json b/package.json index 31421787..f0e65dd2 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "motion": "^12.23.24", "next": "15.6.0-canary.39", "next-themes": "^0.4.6", + "pino": "^10.1.0", "postgres": "^3.4.7", "posthog-js": "^1.280.1", "posthog-node": "^5.10.3", @@ -104,6 +105,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..911f6a7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ 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 postgres: specifier: ^3.4.7 version: 3.4.7 @@ -222,6 +225,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 +1669,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 +3214,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 +3397,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 +3472,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 +3785,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 +3964,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 +4128,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 +4418,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 +4499,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 +4570,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 +4611,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 +4702,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 +4762,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 +4776,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 +4841,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 +4880,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 +4920,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 +4991,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 +7118,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 +8791,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + axios-proxy-builder@0.1.2: dependencies: tunnel: 0.0.6 @@ -8902,6 +8987,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -8979,6 +9066,8 @@ snapshots: date-fns@4.1.0: {} + dateformat@4.6.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -9241,8 +9330,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 +9506,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 +9669,8 @@ snapshots: jiti@2.6.1: {} + joycon@3.1.1: {} + jpeg-js@0.4.4: {} js-tokens@4.0.0: {} @@ -9839,6 +9936,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 +10025,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 +10115,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 +10194,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 +10324,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 +10400,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -10267,6 +10410,8 @@ snapshots: scheduler@0.26.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -10346,6 +10491,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 +10528,8 @@ snapshots: dependencies: extend-shallow: 3.0.2 + split2@4.2.0: {} + stackback@0.0.2: {} std-env@3.10.0: {} @@ -10424,6 +10575,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-json-comments@5.0.3: {} + strnum@2.1.1: {} strtok3@10.3.4: @@ -10497,6 +10650,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..a604153d 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"; @@ -9,12 +8,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({ module: "certificates" }); + export async function getCertificates(domain: string): Promise { - console.debug("[certificates] start", { domain }); + log.debug("start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); const d = registrable @@ -64,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) => { @@ -94,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); }); }, @@ -118,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 @@ -164,30 +155,22 @@ export async function getCertificates(domain: string): Promise { dueAtMs, ); } catch (err) { - console.warn("[certificates] schedule failed", { + log.warn("schedule.failed", { domain: registrable ?? domain, - error: (err as Error)?.message, + err: err instanceof Error ? err : new Error(String(err)), }); } } - console.info("[certificates] ok", { + log.info("ok", { domain: registrable ?? domain, - chain_length: out.length, - duration_ms: Date.now() - startedAt, + chainLength: out.length, }); return out; } catch (err) { - console.warn("[certificates] error", { - domain: registrable ?? domain, - error: (err as Error)?.message, - }); - await captureServer("tls_probe", { + log.warn("error", { domain: registrable ?? domain, - chain_length: 0, - duration_ms: Date.now() - startedAt, - outcome, - error: String(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/dns.ts b/server/services/dns.ts index d0e48128..efe5e44a 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"; @@ -10,6 +9,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 +19,8 @@ import { DnsTypeSchema, } from "@/lib/schemas"; +const log = logger({ module: "dns" }); + export type DohProvider = { key: DnsResolver; buildUrl: (domain: string, type: DnsType) => URL; @@ -54,8 +56,7 @@ export const DOH_PROVIDERS: DohProvider[] = [ ]; export async function resolveAll(domain: string): Promise { - const startedAt = Date.now(); - console.debug("[dns] start", { domain }); + log.debug("start", { domain }); const providers = providerOrderForLookup(domain); const durationByProvider: Record = {}; let lastError: unknown = null; @@ -134,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 }; } @@ -229,9 +209,9 @@ export async function resolveAll(domain: string): Promise { soonest, ); } catch (err) { - console.warn("[dns] schedule failed (partial)", { + log.warn("schedule.failed.partial", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); } } @@ -258,35 +238,21 @@ 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", { + log.info("ok.partial", { 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", - }); - console.info("[dns] ok (partial)", { - domain: registrable, counts, resolver: pinnedProvider.key, - duration_ms_total: Date.now() - startedAt, + durationMs: durationByProvider[pinnedProvider.key], }); return { records: merged, resolver: pinnedProvider.key, } as DnsResolveResult; } catch (err) { - console.warn("[dns] partial refresh failed; falling back", { - domain: registrable, + log.warn("partial.refresh.failed", { + domain: registrable ?? domain, provider: pinnedProvider.key, - error: (err as Error)?.message, + err, }); // Fall through to full provider loop below } @@ -312,9 +278,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 @@ -366,35 +329,24 @@ 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("schedule.failed.full", { domain: registrable ?? domain, - error: (err as Error)?.message, + err, }); } } - await captureServer("dns_resolve_all", { + log.info("ok", { 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", - }); - console.info("[dns] ok", { - domain: registrable, counts, resolver: resolverUsed, - duration_ms_total: Date.now() - startedAt, + durationMs: durationByProvider, }); return { records: flat, resolver: resolverUsed } as DnsResolveResult; } catch (err) { - console.warn("[dns] provider attempt failed", { - domain: registrable, + log.warn("provider.attempt.failed", { + domain: registrable ?? domain, provider: provider.key, - error: (err as Error)?.message, + err, }); durationByProvider[provider.key] = Date.now() - attemptStart; lastError = err; @@ -403,16 +355,10 @@ export async function resolveAll(domain: string): Promise { } // All providers failed - await captureServer("dns_resolve_all", { + log.error("all.providers.failed", { domain: registrable ?? domain, - duration_ms_total: Date.now() - startedAt, - failure: true, - provider_attempts: providers.length, - }); - console.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/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 b8a427a3..94e693cb 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"; @@ -8,12 +7,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({ module: "headers" }); + export async function probeHeaders(domain: string): Promise { const url = `https://${domain}/`; - console.debug("[headers] start", { domain }); + log.debug("start", { domain }); // Fast path: read from Postgres if fresh const registrable = toRegistrableDomain(domain); const d = registrable @@ -40,8 +42,8 @@ export async function probeHeaders(domain: string): Promise { const normalized = normalize( existing.map((h) => ({ name: h.name, value: h.value })), ); - console.info("[headers] db hit", { - domain: registrable, + log.info("cache.hit", { + domain: registrable ?? domain, count: normalized.length, }); return normalized; @@ -63,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) { @@ -85,25 +81,23 @@ 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)), + }); + } } - console.info("[headers] ok", { - domain: registrable, + log.info("ok", { + domain: registrable ?? domain, status: final.status, count: normalized.length, }); return normalized; } catch (err) { - console.warn("[headers] error", { - domain: registrable ?? domain, - error: (err as Error)?.message, - }); - await captureServer("headers_probe", { + log.error("error", { domain: registrable ?? domain, - status: -1, - used_method: "ERROR", - final_url: url, - error: String(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/hosting.ts b/server/services/hosting.ts index 33da325b..bbcaebe2 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"; @@ -12,6 +11,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 +23,10 @@ import { resolveAll } from "@/server/services/dns"; import { probeHeaders } from "@/server/services/headers"; import { lookupIpMeta } from "@/server/services/ip"; +const log = logger({ module: "hosting" }); + export async function detectHosting(domain: string): Promise { - const startedAt = Date.now(); - console.debug("[hosting] start", { domain }); + log.debug("start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); @@ -118,12 +119,11 @@ export async function detectHosting(domain: string): Promise { lon: row.geoLon ?? null, }, }; - console.info("[hosting] cache", { + log.info("cache.hit", { domain, hosting: info.hostingProvider.name, email: info.emailProvider.name, dns_provider: info.dnsProvider.name, - duration_ms: Date.now() - startedAt, }); return info; } @@ -207,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) { @@ -254,14 +245,18 @@ 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)), + }); + } } - console.info("[hosting] ok", { + log.info("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..0d2d1ef8 100644 --- a/server/services/ip.ts +++ b/server/services/ip.ts @@ -1,3 +1,7 @@ +import { logger } from "@/lib/logger"; + +const log = logger({ module: "ip" }); + 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("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("ipwhois.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("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 }); + } 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 0730e098..a23f3dc0 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({ module: "pricing" }); + 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("fetch.ok", { cached: false }); } else { - console.error("[pricing] upstream error", { status: res.status }); + log.error("upstream.error", { status: res.status }); } } catch (err) { - console.error("[pricing] fetch error", { - error: (err as Error)?.message, - }); + log.error("fetch.error", { err }); } } else { payload = lock.cachedResult; diff --git a/server/services/registration.ts b/server/services/registration.ts index 6cffd42c..afc0f263 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"; @@ -12,6 +11,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 +20,13 @@ import type { RegistrationSource, } from "@/lib/schemas"; +const log = logger({ module: "registration" }); + /** * 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("start", { domain }); // Try current snapshot const registrable = toRegistrableDomain(domain); @@ -102,18 +103,10 @@ 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, - }); - console.info("[registration] ok (cached)", { + log.info("ok.cached", { domain: registrable ?? domain, registered: row.isRegistered, registrar: registrarProvider.name, - duration_ms: Date.now() - startedAt, }); return response; @@ -125,21 +118,18 @@ export async function getRegistration(domain: string): Promise { }); if (!ok || !record) { - console.warn("[registration] error", { - domain: registrable ?? domain, - error: error || "unknown", - }); - await captureServer("registration_lookup", { + const err = new Error( + `Registration lookup failed for ${registrable ?? domain}: ${error || "unknown error"}`, + ); + log.error("error", { domain: registrable ?? domain, - outcome: "error", - cached: false, - error: error || "unknown", + err, }); - throw new Error(error || "Registration lookup failed"); + throw err; } // Log raw rdapper record for observability (safe; already public data) - console.debug("[registration] rdapper result", { + log.debug("rdapper.result", { ...record, }); @@ -213,24 +203,16 @@ export async function getRegistration(domain: string): Promise { expiresAt.getTime(), ); } catch (err) { - console.warn("[registration] schedule failed", { + log.warn("schedule.failed", { domain: registrable ?? domain, - error: (err as Error)?.message, + err: err instanceof Error ? err : new Error(String(err)), }); } } - await captureServer("registration_lookup", { - domain: registrable ?? domain, - outcome: record.isRegistered ? "ok" : "unregistered", - cached: false, - duration_ms: Date.now() - startedAt, - source: record.source, - }); - console.info("[registration] ok", { + log.info("ok", { domain: registrable ?? domain, registered: record.isRegistered, registrar: withProvider.registrarProvider.name, - duration_ms: Date.now() - startedAt, }); return withProvider; 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 39319a33..2b2c1501 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"; @@ -11,6 +10,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 +23,13 @@ import type { import { parseHtmlMeta, parseRobotsTxt, selectPreview } from "@/lib/seo"; import { storeImage } from "@/lib/storage"; +const log = logger({ module: "seo" }); + const SOCIAL_WIDTH = 1200; const SOCIAL_HEIGHT = 630; export async function getSeo(domain: string): Promise { - console.debug("[seo] start", { domain }); + log.debug("start", { domain }); // Fast path: DB const registrable = toRegistrableDomain(domain); const d = registrable @@ -237,15 +239,7 @@ 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), - }); - - console.info("[seo] ok", { + log.info("ok", { domain: registrable ?? domain, status: status ?? -1, has_meta: !!meta, @@ -260,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", @@ -283,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 { @@ -307,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 }; @@ -364,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 }; diff --git a/trpc/init.ts b/trpc/init.ts index 5d059244..8d3295c0 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 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({ + 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("start", { rpcPath: path, rpcType: type }); + try { + const result = await next(); + ctx.log.info("ok", { + rpcPath: path, + rpcType: type, + durationMs: Math.round(performance.now() - start), + }); + return result; + } catch (err) { + ctx.log.error("error", { + rpcPath: path, + rpcType: type, + durationMs: Math.round(performance.now() - start), + err: err instanceof Error ? err : new Error(String(err)), + }); + throw err; + } +}); + +export const publicProcedure = t.procedure.use(withLogging);