diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index 596875f8..fba057b4 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -39,6 +39,8 @@ services: # Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod) - LOG_LEVEL=${LOG_LEVEL} + - AXIOM_TOKEN=${AXIOM_TOKEN} + - AXIOM_DATASET=${AXIOM_DATASET} # Stripe - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 65eaee45..00f8589c 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -1,7 +1,9 @@ +import { randomUUID } from "crypto"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import type { NextApiRequest } from "next"; import type { OpenApiMeta } from "trpc-to-openapi"; import { initTRPC, TRPCError } from "@trpc/server"; +import { getHTTPStatusCodeFromError } from "@trpc/server/http"; import { env } from "next-runtime-env"; import superjson from "superjson"; import { ZodError } from "zod"; @@ -11,7 +13,7 @@ import { initAuth } from "@kan/auth/server"; import { createDrizzleClient } from "@kan/db/client"; import { createLogger } from "@kan/logger"; -const log = createLogger("trpc"); +const log = createLogger("api"); export interface User { id: string; @@ -50,6 +52,7 @@ interface CreateContextOptions { db: dbClient; auth: ReturnType; headers: Headers; + transport?: "trpc" | "rest"; } export const createInnerTRPCContext = (opts: CreateContextOptions) => { @@ -58,6 +61,8 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => { db: opts.db, auth: opts.auth, headers: opts.headers, + transport: opts.transport ?? "trpc", + requestId: randomUUID(), }; }; @@ -69,7 +74,13 @@ export const createTRPCContext = async ({ req }: CreateNextContextOptions) => { const session = await auth.api.getSession(); - return createInnerTRPCContext({ db, user: session?.user, auth, headers }); + return createInnerTRPCContext({ + db, + user: session?.user, + auth, + headers, + transport: "trpc", + }); }; export const createNextApiContext = async (req: NextApiRequest) => { @@ -80,7 +91,13 @@ export const createNextApiContext = async (req: NextApiRequest) => { const session = await auth.api.getSession(); - return createInnerTRPCContext({ db, user: session?.user, auth, headers }); + return createInnerTRPCContext({ + db, + user: session?.user, + auth, + headers, + transport: "trpc", + }); }; export const createRESTContext = async ({ req }: CreateNextContextOptions) => { @@ -97,7 +114,13 @@ export const createRESTContext = async ({ req }: CreateNextContextOptions) => { throw error; } - return createInnerTRPCContext({ db, user: session?.user, auth, headers }); + return createInnerTRPCContext({ + db, + user: session?.user, + auth, + headers, + transport: "rest", + }); }; const t = initTRPC @@ -126,12 +149,33 @@ const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => { const result = await next(); const duration = Date.now() - start; - const meta = { procedure: path, type, duration, userId: (ctx as { user?: { id: string } }).user?.id }; + const { user, transport, requestId } = ctx as { + user?: { id: string; email: string }; + transport?: string; + requestId?: string; + }; + const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud"; + const meta = { + requestId, + procedure: path, + type, + transport, + duration, + userId: user?.id, + ...(isCloud && { email: user?.email }), + }; + + const label = transport === "rest" ? "REST" : "tRPC"; if (result.ok) { - log.info(meta, "tRPC OK"); + log.info({ ...meta, status: 200 }, `${label} OK`); } else { - log.error({ ...meta, err: result.error }, "tRPC error"); + const status = getHTTPStatusCodeFromError(result.error); + const errorCode = result.error.code; + log.error( + { ...meta, status, errorCode, err: result.error }, + `${label} error`, + ); } return result; diff --git a/packages/logger/package.json b/packages/logger/package.json index 2e031b88..24fc7536 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -19,6 +19,7 @@ "typecheck": "tsc --noEmit --emitDeclarationOnly false" }, "dependencies": { + "@axiomhq/pino": "^1.4.0", "pino": "^9.14.0", "pino-pretty": "^13.1.3" }, diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index f4ee67d0..00c8530b 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,19 +1,40 @@ import pino from "pino"; const isDev = process.env.NODE_ENV !== "production"; +const isCloud = process.env.NEXT_PUBLIC_KAN_ENV === "cloud"; const level = process.env.LOG_LEVEL || (isDev ? "debug" : "info"); +const axiomToken = process.env.AXIOM_TOKEN; +const axiomDataset = process.env.AXIOM_DATASET; +const useAxiom = isCloud && !!axiomToken && !!axiomDataset; + +const targets: pino.TransportTargetOptions[] = []; + +if (isDev) { + targets.push({ + target: "pino-pretty", + options: { + colorize: true, + ignore: "pid,hostname", + translateTime: "HH:MM:ss", + }, + }); +} + +if (useAxiom) { + targets.push({ + target: "@axiomhq/pino", + options: { + dataset: axiomDataset, + token: axiomToken, + }, + }); +} + export const logger = pino({ level, - ...(isDev && { - transport: { - target: "pino-pretty", - options: { - colorize: true, - ignore: "pid,hostname", - translateTime: "HH:MM:ss", - }, - }, + ...(targets.length > 0 && { + transport: { targets }, }), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f24ff1f..8e11a60d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -504,6 +504,9 @@ importers: packages/logger: dependencies: + '@axiomhq/pino': + specifier: ^1.4.0 + version: 1.4.0 pino: specifier: ^9.14.0 version: 9.14.0 @@ -891,6 +894,14 @@ packages: resolution: {integrity: sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==} engines: {node: '>=18.0.0'} + '@axiomhq/js@1.4.0': + resolution: {integrity: sha512-wC5x1ud/QJMstrjpicATkyY8+ZVWEl4WlXMtA5EZf7hkj0+b191yv4yynLxLEfr/MveXora9m6CWdJq4DsbcAg==} + engines: {node: '>=20'} + + '@axiomhq/pino@1.4.0': + resolution: {integrity: sha512-7ujZM1kqbA98BWl8ltWdTLSDE7+67nOvR30/hm8P6omISjaQV0xgtdtZtDGe/BH2VmZeQWPGfAI1FnUNYC3CtQ==} + engines: {node: '>=20'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -4338,6 +4349,10 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5557,6 +5572,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5628,6 +5647,9 @@ packages: picomatch: optional: true + fetch-retry@6.0.0: + resolution: {integrity: sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==} + fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} @@ -7206,6 +7228,9 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -7397,6 +7422,10 @@ packages: 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'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -7600,6 +7629,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.5.0: resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} engines: {node: '>=8.10.0'} @@ -9384,6 +9417,15 @@ snapshots: '@smithy/types': 4.3.2 tslib: 2.8.1 + '@axiomhq/js@1.4.0': + dependencies: + fetch-retry: 6.0.0 + + '@axiomhq/pino@1.4.0': + dependencies: + '@axiomhq/js': 1.4.0 + pino-abstract-transport: 1.2.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -12910,6 +12952,10 @@ snapshots: '@xtuc/long@4.2.2': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -14331,6 +14377,8 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + events@3.3.0: {} execa@5.1.1: @@ -14403,6 +14451,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-retry@6.0.0: {} + fflate@0.4.8: {} figures@3.2.0: @@ -16445,6 +16495,11 @@ snapshots: pify@2.3.0: {} + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.7.0 + split2: 4.2.0 + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -16594,6 +16649,8 @@ snapshots: process-warning@5.0.0: {} + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -16889,6 +16946,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@3.5.0: dependencies: picomatch: 2.3.1 diff --git a/turbo.json b/turbo.json index d3f74541..204d6b33 100644 --- a/turbo.json +++ b/turbo.json @@ -130,7 +130,9 @@ "NOVU_API_KEY", "EMAIL_UNSUBSCRIBE_SECRET", "REDIS_URL", - "LOG_LEVEL" + "LOG_LEVEL", + "AXIOM_TOKEN", + "AXIOM_DATASET" ], "globalPassThroughEnv": [ "NODE_ENV",