Compare commits
1 Commits
feat/opent
...
fix/api-ke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3687cfb10 |
@@ -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,15 +65,8 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
serverExternalPackages: [
|
|
||||||
"pino",
|
|
||||||
"@opentelemetry/sdk-node",
|
|
||||||
"@opentelemetry/auto-instrumentations-node",
|
|
||||||
"@opentelemetry/exporter-trace-otlp-http",
|
|
||||||
],
|
|
||||||
|
|
||||||
experimental: {
|
experimental: {
|
||||||
instrumentationHook: true,
|
// instrumentationHook: true,
|
||||||
swcPlugins: [["@lingui/swc-plugin", {}]],
|
swcPlugins: [["@lingui/swc-plugin", {}]],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,6 @@
|
|||||||
"@lingui/macro": "^5.3.2",
|
"@lingui/macro": "^5.3.2",
|
||||||
"@lingui/react": "^5.3.2",
|
"@lingui/react": "^5.3.2",
|
||||||
"@novu/api": "^3.11.0",
|
"@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",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -39,8 +39,6 @@ services:
|
|||||||
|
|
||||||
# Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod)
|
# Logging (optional - debug, info, warn, error; defaults to debug in dev, info in prod)
|
||||||
- LOG_LEVEL=${LOG_LEVEL}
|
- LOG_LEVEL=${LOG_LEVEL}
|
||||||
- AXIOM_TOKEN=${AXIOM_TOKEN}
|
|
||||||
- AXIOM_DATASET=${AXIOM_DATASET}
|
|
||||||
|
|
||||||
# Stripe
|
# Stripe
|
||||||
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
||||||
|
|||||||
@@ -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),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { randomUUID } from "crypto";
|
|
||||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
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";
|
||||||
@@ -12,28 +11,7 @@ import { initAuth } from "@kan/auth/server";
|
|||||||
import { createDrizzleClient } from "@kan/db/client";
|
import { createDrizzleClient } from "@kan/db/client";
|
||||||
import { createLogger } from "@kan/logger";
|
import { createLogger } from "@kan/logger";
|
||||||
|
|
||||||
const log = createLogger("api");
|
const log = createLogger("trpc");
|
||||||
|
|
||||||
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;
|
||||||
@@ -72,7 +50,6 @@ interface CreateContextOptions {
|
|||||||
db: dbClient;
|
db: dbClient;
|
||||||
auth: ReturnType<typeof createAuthWithHeaders>;
|
auth: ReturnType<typeof createAuthWithHeaders>;
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
transport?: "trpc" | "rest";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
@@ -81,8 +58,6 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
|||||||
db: opts.db,
|
db: opts.db,
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
headers: opts.headers,
|
headers: opts.headers,
|
||||||
transport: opts.transport ?? "trpc",
|
|
||||||
requestId: randomUUID(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,13 +69,7 @@ export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
|
|||||||
|
|
||||||
const session = await auth.api.getSession();
|
const session = await auth.api.getSession();
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({ db, user: session?.user, auth, headers });
|
||||||
db,
|
|
||||||
user: session?.user,
|
|
||||||
auth,
|
|
||||||
headers,
|
|
||||||
transport: "trpc",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createNextApiContext = async (req: NextApiRequest) => {
|
export const createNextApiContext = async (req: NextApiRequest) => {
|
||||||
@@ -111,13 +80,7 @@ export const createNextApiContext = async (req: NextApiRequest) => {
|
|||||||
|
|
||||||
const session = await auth.api.getSession();
|
const session = await auth.api.getSession();
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({ db, user: session?.user, auth, headers });
|
||||||
db,
|
|
||||||
user: session?.user,
|
|
||||||
auth,
|
|
||||||
headers,
|
|
||||||
transport: "trpc",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
|
export const createRESTContext = async ({ req }: CreateNextContextOptions) => {
|
||||||
@@ -130,16 +93,11 @@ 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({ db, user: session?.user, auth, headers });
|
||||||
db,
|
|
||||||
user: session?.user,
|
|
||||||
auth,
|
|
||||||
headers,
|
|
||||||
transport: "rest",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = initTRPC
|
const t = initTRPC
|
||||||
@@ -168,33 +126,12 @@ const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => {
|
|||||||
const result = await next();
|
const result = await next();
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
const { user, transport, requestId } = ctx as {
|
const meta = { procedure: path, type, duration, userId: (ctx as { user?: { id: string } }).user?.id };
|
||||||
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) {
|
if (result.ok) {
|
||||||
log.info({ ...meta, status: 200 }, `${label} OK`);
|
log.info(meta, "tRPC OK");
|
||||||
} else {
|
} else {
|
||||||
const status = TRPC_STATUS_MAP[result.error.code] ?? 500;
|
log.error({ ...meta, err: result.error }, "tRPC error");
|
||||||
const errorCode = result.error.code;
|
|
||||||
log.error(
|
|
||||||
{ ...meta, status, errorCode, err: result.error },
|
|
||||||
`${label} error`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@axiomhq/js": "^1.4.0",
|
|
||||||
"@opentelemetry/api": "^1.9.0",
|
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"pino-pretty": "^13.1.3"
|
"pino-pretty": "^13.1.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,63 +1,20 @@
|
|||||||
import { Axiom } from "@axiomhq/js";
|
|
||||||
import { trace } from "@opentelemetry/api";
|
|
||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== "production";
|
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 level = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
|
||||||
|
|
||||||
const axiomToken = process.env.AXIOM_TOKEN;
|
export const logger = pino({
|
||||||
const axiomDataset = process.env.AXIOM_DATASET;
|
level,
|
||||||
const useAxiom = isCloud && !!axiomToken && !!axiomDataset;
|
...(isDev && {
|
||||||
|
transport: {
|
||||||
function createAxiomStream(
|
target: "pino-pretty",
|
||||||
token: string,
|
options: {
|
||||||
dataset: string,
|
colorize: true,
|
||||||
): pino.DestinationStream {
|
ignore: "pid,hostname",
|
||||||
const client = new Axiom({ token });
|
translateTime: "HH:MM:ss",
|
||||||
return {
|
},
|
||||||
write(msg: string) {
|
|
||||||
try {
|
|
||||||
client.ingest(dataset, [JSON.parse(msg) as Record<string, unknown>]);
|
|
||||||
} catch {
|
|
||||||
// ignore malformed log lines
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
}),
|
||||||
}
|
});
|
||||||
|
|
||||||
export const logger = useAxiom
|
export const createLogger = (module: string) => logger.child({ module });
|
||||||
? 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 {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createLogger = (module: string) =>
|
|
||||||
logger.child({ module, ...getTraceContext() });
|
|
||||||
|
|||||||
1536
pnpm-lock.yaml
generated
1536
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -130,9 +130,7 @@
|
|||||||
"NOVU_API_KEY",
|
"NOVU_API_KEY",
|
||||||
"EMAIL_UNSUBSCRIBE_SECRET",
|
"EMAIL_UNSUBSCRIBE_SECRET",
|
||||||
"REDIS_URL",
|
"REDIS_URL",
|
||||||
"LOG_LEVEL",
|
"LOG_LEVEL"
|
||||||
"AXIOM_TOKEN",
|
|
||||||
"AXIOM_DATASET"
|
|
||||||
],
|
],
|
||||||
"globalPassThroughEnv": [
|
"globalPassThroughEnv": [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
|
|||||||
Reference in New Issue
Block a user