Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
c28fbf2668 feat: add log drain 2026-03-19 21:14:19 +00:00
7 changed files with 86 additions and 62 deletions

View File

@@ -28,7 +28,6 @@ const config = {
], ],
}, },
/** Enables hot reloading for local packages without a build step */ /** Enables hot reloading for local packages without a build step */
transpilePackages: [ transpilePackages: [
"@kan/api", "@kan/api",
@@ -66,8 +65,6 @@ const config = {
}, },
}, },
}, },
serverExternalPackages: ["pino"],
experimental: { experimental: {
// instrumentationHook: true, // instrumentationHook: true,
swcPlugins: [["@lingui/swc-plugin", {}]], swcPlugins: [["@lingui/swc-plugin", {}]],

View File

@@ -39,7 +39,7 @@ export function LabelForm({
labelPublicId: entityId, labelPublicId: entityId,
}, },
{ {
enabled: !!isEdit && entityId.length >= 12, enabled: isEdit && !!entityId,
}, },
); );

View File

@@ -227,7 +227,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z boardSlug: z
.string() .string()
.min(3) .min(3)
.max(60) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
members: z.array(z.string().min(12)).optional(), members: z.array(z.string().min(12)).optional(),
labels: z.array(z.string().min(12)).optional(), labels: z.array(z.string().min(12)).optional(),
@@ -657,7 +657,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z boardSlug: z
.string() .string()
.min(3) .min(3)
.max(60) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
boardPublicId: z.string().min(12), boardPublicId: z.string().min(12),
}), }),

View File

@@ -3,6 +3,7 @@ import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import type { OpenApiMeta } from "trpc-to-openapi"; import type { OpenApiMeta } from "trpc-to-openapi";
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
import { env } from "next-runtime-env"; import { env } from "next-runtime-env";
import superjson from "superjson"; import superjson from "superjson";
import { ZodError } from "zod"; import { ZodError } from "zod";
@@ -14,27 +15,6 @@ import { createLogger } from "@kan/logger";
const log = createLogger("api"); const log = createLogger("api");
const TRPC_STATUS_MAP: Partial<Record<TRPCError["code"], number>> = {
PARSE_ERROR: 400,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_SUPPORTED: 405,
TIMEOUT: 408,
CONFLICT: 409,
PRECONDITION_FAILED: 412,
PAYLOAD_TOO_LARGE: 413,
UNPROCESSABLE_CONTENT: 422,
TOO_MANY_REQUESTS: 429,
CLIENT_CLOSED_REQUEST: 499,
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
};
export interface User { export interface User {
id: string; id: string;
name: string; name: string;
@@ -130,7 +110,8 @@ export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
try { try {
session = await auth.api.getSession(); session = await auth.api.getSession();
} catch (error) { } catch (error) {
log.warn({ err: error }, "Failed to get session, treating as unauthenticated"); log.error({ err: error }, "Error getting session");
throw error;
} }
return createInnerTRPCContext({ return createInnerTRPCContext({
@@ -163,9 +144,9 @@ export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory; export const createCallerFactory = t.createCallerFactory;
const loggingMiddleware = t.middleware(async ({ path, type, next, ctx, getRawInput }) => { const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => {
const start = Date.now(); const start = Date.now();
const [result, input] = await Promise.all([next(), getRawInput().catch(() => undefined)]); const result = await next();
const duration = Date.now() - start; const duration = Date.now() - start;
const { user, transport, requestId } = ctx as { const { user, transport, requestId } = ctx as {
@@ -182,7 +163,6 @@ const loggingMiddleware = t.middleware(async ({ path, type, next, ctx, getRawInp
duration, duration,
userId: user?.id, userId: user?.id,
...(isCloud && { email: user?.email }), ...(isCloud && { email: user?.email }),
input,
}; };
const label = transport === "rest" ? "REST" : "tRPC"; const label = transport === "rest" ? "REST" : "tRPC";
@@ -190,7 +170,7 @@ const loggingMiddleware = t.middleware(async ({ path, type, next, ctx, getRawInp
if (result.ok) { if (result.ok) {
log.info({ ...meta, status: 200 }, `${label} OK`); log.info({ ...meta, status: 200 }, `${label} OK`);
} else { } else {
const status = TRPC_STATUS_MAP[result.error.code] ?? 500; const status = getHTTPStatusCodeFromError(result.error);
const errorCode = result.error.code; const errorCode = result.error.code;
log.error( log.error(
{ ...meta, status, errorCode, err: result.error }, { ...meta, status, errorCode, err: result.error },

View File

@@ -19,7 +19,7 @@
"typecheck": "tsc --noEmit --emitDeclarationOnly false" "typecheck": "tsc --noEmit --emitDeclarationOnly false"
}, },
"dependencies": { "dependencies": {
"@axiomhq/js": "^1.4.0", "@axiomhq/pino": "^1.4.0",
"pino": "^9.14.0", "pino": "^9.14.0",
"pino-pretty": "^13.1.3" "pino-pretty": "^13.1.3"
}, },

View File

@@ -1,4 +1,3 @@
import { Axiom } from "@axiomhq/js";
import pino from "pino"; import pino from "pino";
const isDev = process.env.NODE_ENV !== "production"; const isDev = process.env.NODE_ENV !== "production";
@@ -9,35 +8,34 @@ const axiomToken = process.env.AXIOM_TOKEN;
const axiomDataset = process.env.AXIOM_DATASET; const axiomDataset = process.env.AXIOM_DATASET;
const useAxiom = isCloud && !!axiomToken && !!axiomDataset; const useAxiom = isCloud && !!axiomToken && !!axiomDataset;
function createAxiomStream(token: string, dataset: string): pino.DestinationStream { const targets: pino.TransportTargetOptions[] = [];
const client = new Axiom({ token });
return { if (isDev) {
write(msg: string) { targets.push({
try { target: "pino-pretty",
client.ingest(dataset, [JSON.parse(msg) as Record<string, unknown>]); options: {
} catch { colorize: true,
// ignore malformed log lines ignore: "pid,hostname",
} translateTime: "HH:MM:ss",
}, },
}; });
} }
export const logger = useAxiom if (useAxiom) {
? pino( targets.push({
{ level }, target: "@axiomhq/pino",
pino.multistream([ options: {
{ stream: process.stdout, level }, dataset: axiomDataset,
{ stream: createAxiomStream(axiomToken, axiomDataset), level }, token: axiomToken,
]), },
) });
: pino({ }
level,
...(isDev && { export const logger = pino({
transport: { level,
target: "pino-pretty", ...(targets.length > 0 && {
options: { colorize: true, ignore: "pid,hostname", translateTime: "HH:MM:ss" }, transport: { targets },
}, }),
}), });
});
export const createLogger = (module: string) => logger.child({ module }); export const createLogger = (module: string) => logger.child({ module });

51
pnpm-lock.yaml generated
View File

@@ -504,7 +504,7 @@ importers:
packages/logger: packages/logger:
dependencies: dependencies:
'@axiomhq/js': '@axiomhq/pino':
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
pino: pino:
@@ -898,6 +898,10 @@ packages:
resolution: {integrity: sha512-wC5x1ud/QJMstrjpicATkyY8+ZVWEl4WlXMtA5EZf7hkj0+b191yv4yynLxLEfr/MveXora9m6CWdJq4DsbcAg==} resolution: {integrity: sha512-wC5x1ud/QJMstrjpicATkyY8+ZVWEl4WlXMtA5EZf7hkj0+b191yv4yynLxLEfr/MveXora9m6CWdJq4DsbcAg==}
engines: {node: '>=20'} engines: {node: '>=20'}
'@axiomhq/pino@1.4.0':
resolution: {integrity: sha512-7ujZM1kqbA98BWl8ltWdTLSDE7+67nOvR30/hm8P6omISjaQV0xgtdtZtDGe/BH2VmZeQWPGfAI1FnUNYC3CtQ==}
engines: {node: '>=20'}
'@babel/code-frame@7.27.1': '@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -4345,6 +4349,10 @@ packages:
'@xtuc/long@4.2.2': '@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 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: accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -5564,6 +5572,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} 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: events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'} engines: {node: '>=0.8.x'}
@@ -7216,6 +7228,9 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
pino-abstract-transport@1.2.0:
resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
pino-abstract-transport@2.0.0: pino-abstract-transport@2.0.0:
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
@@ -7407,6 +7422,10 @@ packages:
process-warning@5.0.0: process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} 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: prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -7610,6 +7629,10 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} 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: readdirp@3.5.0:
resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
@@ -9398,6 +9421,11 @@ snapshots:
dependencies: dependencies:
fetch-retry: 6.0.0 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': '@babel/code-frame@7.27.1':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
@@ -12924,6 +12952,10 @@ snapshots:
'@xtuc/long@4.2.2': {} '@xtuc/long@4.2.2': {}
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
accepts@1.3.8: accepts@1.3.8:
dependencies: dependencies:
mime-types: 2.1.35 mime-types: 2.1.35
@@ -14345,6 +14377,8 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
event-target-shim@5.0.1: {}
events@3.3.0: {} events@3.3.0: {}
execa@5.1.1: execa@5.1.1:
@@ -16461,6 +16495,11 @@ snapshots:
pify@2.3.0: {} 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: pino-abstract-transport@2.0.0:
dependencies: dependencies:
split2: 4.2.0 split2: 4.2.0
@@ -16610,6 +16649,8 @@ snapshots:
process-warning@5.0.0: {} process-warning@5.0.0: {}
process@0.11.10: {}
prompts@2.4.2: prompts@2.4.2:
dependencies: dependencies:
kleur: 3.0.3 kleur: 3.0.3
@@ -16905,6 +16946,14 @@ snapshots:
string_decoder: 1.3.0 string_decoder: 1.3.0
util-deprecate: 1.0.2 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: readdirp@3.5.0:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1