Compare commits

..

1 Commits

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

View File

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

View File

@@ -34,10 +34,6 @@
"@lingui/macro": "^5.3.2",
"@lingui/react": "^5.3.2",
"@novu/api": "^3.11.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
"@opentelemetry/sdk-node": "^0.213.0",
"@t3-oss/env-nextjs": "^0.11.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "catalog:",

View File

@@ -1,32 +0,0 @@
export async function register() {
if (
process.env.NEXT_RUNTIME !== "nodejs" ||
process.env.NEXT_PUBLIC_KAN_ENV !== "cloud" ||
!process.env.AXIOM_TOKEN ||
!process.env.AXIOM_DATASET
) {
return;
}
const { NodeSDK } = await import("@opentelemetry/sdk-node");
const { OTLPTraceExporter } = await import("@opentelemetry/exporter-trace-otlp-http");
const { getNodeAutoInstrumentations } = await import("@opentelemetry/auto-instrumentations-node");
const sdk = new NodeSDK({
serviceName: "kan",
traceExporter: new OTLPTraceExporter({
url: "https://api.axiom.co/v1/traces",
headers: {
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
"X-Axiom-Dataset": process.env.AXIOM_DATASET,
},
}),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-fs": { enabled: false },
}),
],
});
sdk.start();
}

View File

@@ -227,7 +227,7 @@ export const boardRouter = createTRPCRouter({
boardSlug: z
.string()
.min(3)
.max(60)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
members: 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
.string()
.min(3)
.max(60)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
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 { 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";
@@ -14,27 +15,6 @@ import { createLogger } from "@kan/logger";
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 {
id: string;
name: string;
@@ -130,7 +110,8 @@ export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
try {
session = await auth.api.getSession();
} catch (error) {
log.warn({ err: error }, "Failed to get session, treating as unauthenticated");
log.error({ err: error }, "Error getting session");
throw error;
}
return createInnerTRPCContext({
@@ -189,7 +170,7 @@ const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => {
if (result.ok) {
log.info({ ...meta, status: 200 }, `${label} OK`);
} else {
const status = TRPC_STATUS_MAP[result.error.code] ?? 500;
const status = getHTTPStatusCodeFromError(result.error);
const errorCode = result.error.code;
log.error(
{ ...meta, status, errorCode, err: result.error },

View File

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

View File

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

1571
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff