Skip to content

Commit 950ef51

Browse files
committed
deps
1 parent 82c4186 commit 950ef51

File tree

7 files changed

+345
-168
lines changed

7 files changed

+345
-168
lines changed

apps/api/package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "@databuddy/api",
3+
"version": "1.0.0",
34
"module": "index.ts",
45
"type": "module",
56
"private": true,
@@ -9,7 +10,7 @@
910
"test:watch": "vitest"
1011
},
1112
"devDependencies": {
12-
"@types/bun": "^1.3.1"
13+
"@types/bun": "^1.3.2"
1314
},
1415
"peerDependencies": {
1516
"typescript": "^5.9.3"
@@ -22,18 +23,19 @@
2223
"@databuddy/sdk": "^2.2.0",
2324
"@databuddy/shared": "workspace:*",
2425
"@elysiajs/cors": "^1.4.0",
25-
"@elysiajs/opentelemetry": "^1.4.6",
26-
"@logtail/edge": "^0.5.6",
26+
"@opentelemetry/api": "^1.9.0",
2727
"@opentelemetry/exporter-trace-otlp-proto": "^0.208.0",
28+
"@opentelemetry/resources": "^2.2.0",
29+
"@opentelemetry/sdk-node": "^0.208.0",
2830
"@opentelemetry/sdk-trace-node": "^2.2.0",
29-
"@upstash/ratelimit": "^2.0.6",
31+
"@opentelemetry/semantic-conventions": "^1.38.0",
3032
"@upstash/redis": "^1.35.6",
31-
"ai": "^5.0.81",
33+
"ai": "^5.0.93",
3234
"autumn": "^1.0.2",
3335
"autumn-js": "^0.0.101",
34-
"dayjs": "^1.11.18",
36+
"dayjs": "^1.11.19",
3537
"drizzle-orm": "^0.42.0",
36-
"elysia": "^1.4.13",
38+
"elysia": "^1.4.16",
3739
"jszip": "^3.10.1",
3840
"pino": "^9.14.0",
3941
"pino-pretty": "^13.1.2",

apps/api/src/index.ts

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,47 @@
11
import "./polyfills/compression";
22
import { auth } from "@databuddy/auth";
3-
import { appRouter, createRPCContext } from "@databuddy/rpc";
3+
import {
4+
appRouter,
5+
createAbortSignalInterceptor,
6+
createRPCContext,
7+
setupUncaughtErrorHandlers,
8+
} from "@databuddy/rpc";
49
import { logger } from "@databuddy/shared/logger";
510
import cors from "@elysiajs/cors";
6-
import { opentelemetry } from "@elysiajs/opentelemetry";
7-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
8-
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
911
import { onError } from "@orpc/server";
1012
import { RPCHandler } from "@orpc/server/fetch";
1113
import { autumnHandler } from "autumn-js/elysia";
1214
import { Elysia } from "elysia";
15+
import {
16+
endRequestSpan,
17+
initTracing,
18+
shutdownTracing,
19+
startRequestSpan,
20+
} from "./lib/tracing";
1321
import { assistant } from "./routes/assistant";
1422
// import { customSQL } from './routes/custom-sql';
1523
import { exportRoute } from "./routes/export";
1624
import { health } from "./routes/health";
1725
import { publicApi } from "./routes/public";
1826
import { query } from "./routes/query";
1927

28+
initTracing();
29+
setupUncaughtErrorHandlers();
30+
2031
const rpcHandler = new RPCHandler(appRouter, {
2132
interceptors: [
33+
createAbortSignalInterceptor(),
2234
onError((error) => {
2335
logger.error(error);
2436
}),
2537
],
2638
});
2739

2840
const app = new Elysia()
29-
.use(
30-
opentelemetry({
31-
spanProcessors: [
32-
new BatchSpanProcessor(
33-
new OTLPTraceExporter({
34-
url: "https://api.axiom.co/v1/traces",
35-
headers: {
36-
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
37-
"X-Axiom-Dataset": process.env.AXIOM_DATASET ?? "api",
38-
},
39-
})
40-
),
41-
],
42-
})
43-
)
41+
.state("tracing", {
42+
span: null as ReturnType<typeof startRequestSpan> | null,
43+
startTime: 0,
44+
})
4445
.use(publicApi)
4546
.use(
4647
cors({
@@ -54,6 +55,23 @@ const app = new Elysia()
5455
})
5556
)
5657
.use(health)
58+
.onBeforeHandle(function startTrace({ request, path, store }) {
59+
const method = request.method;
60+
const startTime = Date.now();
61+
const span = startRequestSpan(method, request.url, path);
62+
63+
// Store span and start time in Elysia store
64+
store.tracing = {
65+
span,
66+
startTime,
67+
};
68+
})
69+
.onAfterHandle(function endTrace({ response, store }) {
70+
if (store.tracing?.span && store.tracing.startTime) {
71+
const statusCode = response instanceof Response ? response.status : 200;
72+
endRequestSpan(store.tracing.span, statusCode, store.tracing.startTime);
73+
}
74+
})
5775
.use(
5876
autumnHandler({
5977
identify: async ({ request }) => {
@@ -63,21 +81,15 @@ const app = new Elysia()
6381
});
6482

6583
return {
66-
customerId: session?.user.id,
84+
customerId: session?.user.id ?? undefined,
6785
customerData: {
68-
name: session?.user.name,
69-
email: session?.user.email,
86+
name: session?.user.name ?? undefined,
87+
email: session?.user.email ?? undefined,
7088
},
7189
};
7290
} catch (error) {
7391
logger.error({ error }, "Failed to get session for autumn handler");
74-
return {
75-
customerId: null,
76-
customerData: {
77-
name: null,
78-
email: null,
79-
},
80-
};
92+
return null;
8193
}
8294
},
8395
})
@@ -104,7 +116,12 @@ const app = new Elysia()
104116
parse: "none",
105117
}
106118
)
107-
.onError(function handleError({ error, code }) {
119+
.onError(function handleError({ error, code, store }) {
120+
if (store.tracing?.span && store.tracing.startTime) {
121+
const statusCode = code === "NOT_FOUND" ? 404 : 500;
122+
endRequestSpan(store.tracing.span, statusCode, store.tracing.startTime);
123+
}
124+
108125
const errorMessage = error instanceof Error ? error.message : String(error);
109126
logger.error({ error, code }, errorMessage);
110127

@@ -123,12 +140,18 @@ export default {
123140
port: 3001,
124141
};
125142

126-
process.on("SIGINT", () => {
127-
logger.info({ message: "SIGINT signal received, shutting down..." });
143+
process.on("SIGINT", async () => {
144+
logger.info("SIGINT received, shutting down gracefully...");
145+
await shutdownTracing().catch((error) =>
146+
logger.error({ error }, "Shutdown error")
147+
);
128148
process.exit(0);
129149
});
130150

131-
process.on("SIGTERM", () => {
132-
logger.info({ message: "SIGTERM signal received, shutting down..." });
151+
process.on("SIGTERM", async () => {
152+
logger.info("SIGTERM received, shutting down gracefully...");
153+
await shutdownTracing().catch((error) =>
154+
logger.error({ error }, "Shutdown error")
155+
);
133156
process.exit(0);
134157
});

apps/api/src/lib/tracing.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { createORPCInstrumentation } from "@databuddy/rpc";
2+
import { type Span, SpanStatusCode, trace } from "@opentelemetry/api";
3+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
4+
import { resourceFromAttributes } from "@opentelemetry/resources";
5+
import { NodeSDK } from "@opentelemetry/sdk-node";
6+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
7+
import {
8+
ATTR_SERVICE_NAME,
9+
ATTR_SERVICE_VERSION,
10+
} from "@opentelemetry/semantic-conventions";
11+
import pkg from "../../package.json";
12+
13+
let sdk: NodeSDK | null = null;
14+
15+
/**
16+
* Initialize OpenTelemetry
17+
*/
18+
export function initTracing(): void {
19+
if (sdk) {
20+
return;
21+
}
22+
23+
const exporter = new OTLPTraceExporter({
24+
url: "https://api.axiom.co/v1/traces",
25+
headers: {
26+
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
27+
"X-Axiom-Dataset": process.env.AXIOM_DATASET ?? "api",
28+
},
29+
});
30+
31+
sdk = new NodeSDK({
32+
resource: resourceFromAttributes({
33+
[ATTR_SERVICE_NAME]: "api",
34+
[ATTR_SERVICE_VERSION]: pkg.version,
35+
}),
36+
spanProcessor: new BatchSpanProcessor(exporter, {
37+
scheduledDelayMillis: 1000,
38+
exportTimeoutMillis: 30_000,
39+
maxExportBatchSize: 512,
40+
maxQueueSize: 2048,
41+
}),
42+
instrumentations: [createORPCInstrumentation()],
43+
});
44+
45+
sdk.start();
46+
}
47+
48+
export async function shutdownTracing(): Promise<void> {
49+
if (sdk) {
50+
await sdk.shutdown();
51+
sdk = null;
52+
}
53+
}
54+
55+
/**
56+
* Get tracer
57+
*/
58+
function getTracer() {
59+
return trace.getTracer("api");
60+
}
61+
62+
/**
63+
* Create a span - replaces @elysiajs/opentelemetry record
64+
*/
65+
export function record<T>(name: string, fn: () => Promise<T> | T): Promise<T> {
66+
const tracer = getTracer();
67+
return tracer.startActiveSpan(name, async (span: Span) => {
68+
try {
69+
const result = await fn();
70+
span.setStatus({ code: SpanStatusCode.OK });
71+
return result;
72+
} catch (error) {
73+
span.setStatus({
74+
code: SpanStatusCode.ERROR,
75+
message: error instanceof Error ? error.message : String(error),
76+
});
77+
span.recordException(
78+
error instanceof Error ? error : new Error(String(error))
79+
);
80+
throw error;
81+
} finally {
82+
span.end();
83+
}
84+
});
85+
}
86+
87+
/**
88+
* Set attributes on active span - replaces @elysiajs/opentelemetry setAttributes
89+
*/
90+
export function setAttributes(
91+
attributes: Record<string, string | number | boolean>
92+
): void {
93+
const span = trace.getActiveSpan();
94+
if (span) {
95+
for (const [key, value] of Object.entries(attributes)) {
96+
span.setAttribute(key, value);
97+
}
98+
}
99+
}
100+
101+
/**
102+
* Start HTTP request span
103+
*/
104+
export function startRequestSpan(
105+
method: string,
106+
path: string,
107+
route?: string
108+
): Span {
109+
const tracer = getTracer();
110+
return tracer.startSpan(`${method} ${route ?? path}`, {
111+
kind: 1, // SERVER
112+
attributes: {
113+
"http.method": method,
114+
"http.route": route ?? path,
115+
"http.target": path,
116+
},
117+
});
118+
}
119+
120+
/**
121+
* End HTTP request span
122+
*/
123+
export function endRequestSpan(
124+
span: Span,
125+
statusCode: number,
126+
startTime: number
127+
): void {
128+
span.setAttribute("http.status_code", statusCode);
129+
span.setAttribute("http.response.duration_ms", Date.now() - startTime);
130+
span.setStatus({
131+
code: statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.OK,
132+
message: statusCode >= 400 ? `HTTP ${statusCode}` : undefined,
133+
});
134+
span.end();
135+
}

0 commit comments

Comments
 (0)