Skip to content

Commit ff7cfe4

Browse files
committed
Add support for backend telemetry (tracing & metrics)
This uses the opentelemetry SDK to provide support for traces and two simple metrics (a conversations count and a messages count). The telemetry entrypoint is built separately (unfortunately it does not seem like there is a method for doing this in one build as sveltekit overrides rollupOptions). As written the metrics may be too high cardinality for some use cases (as they are keyed per user), but these attributes can always be dropped in a collector.
1 parent dff58d2 commit ff7cfe4

File tree

6 files changed

+125
-3
lines changed

6 files changed

+125
-3
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ RUN --mount=type=cache,target=/app/.npm \
1919
COPY --link --chown=1000 . .
2020

2121
RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local \
22-
npm run build
22+
npm run build && npm run build -- --config vite.telemetry.config.ts
2323

2424
FROM node:20-slim
2525

@@ -29,4 +29,4 @@ COPY --from=builder-production /app/node_modules /app/node_modules
2929
COPY --link --chown=1000 package.json /app/package.json
3030
COPY --from=builder /app/build /app/build
3131

32-
CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon
32+
CMD pm2 start /app/build/index.js --node-args="--require /app/build/telemetry.cjs" -i $CPU_CORES --no-daemon

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"packageManager": "npm@9.5.0",
66
"scripts": {
77
"dev": "vite dev",
8-
"build": "vite build",
8+
"build": "bash scripts/clean-opentelemetry.sh && vite build",
99
"preview": "vite preview",
1010
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1111
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -53,6 +53,12 @@
5353
"@huggingface/hub": "^0.5.1",
5454
"@huggingface/inference": "^2.6.3",
5555
"@iconify-json/bi": "^1.1.21",
56+
"@opentelemetry/api": "^1.8.0",
57+
"@opentelemetry/auto-instrumentations-node": "^0.44.0",
58+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.50.0",
59+
"@opentelemetry/sdk-metrics": "^1.23.0",
60+
"@opentelemetry/sdk-node": "^0.50.0",
61+
"@opentelemetry/sdk-trace-node": "^1.23.0",
5662
"@resvg/resvg-js": "^2.6.0",
5763
"@xenova/transformers": "^2.16.1",
5864
"autoprefixer": "^10.4.14",

src/routes/conversation/+server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { defaultEmbeddingModel } from "$lib/server/embeddingModels";
1010
import { v4 } from "uuid";
1111
import { authCondition } from "$lib/server/auth";
1212
import { usageLimits } from "$lib/server/usageLimits";
13+
import { metrics } from "@opentelemetry/api";
1314

1415
export const POST: RequestHandler = async ({ locals, request }) => {
1516
const body = await request.text();
@@ -111,6 +112,15 @@ export const POST: RequestHandler = async ({ locals, request }) => {
111112
...(values.fromShare ? { meta: { fromShareId: values.fromShare } } : {}),
112113
});
113114

115+
const meter = metrics.getMeter("chat-ui");
116+
const counter = meter.createCounter("chat-ui.conversations.count", {
117+
description: "The number of conversations created",
118+
});
119+
counter.add(1, {
120+
"chat-ui.model": values.model,
121+
"user.email": locals.user?.email || undefined,
122+
});
123+
114124
return new Response(
115125
JSON.stringify({
116126
conversationId: res.insertedId.toString(),

src/routes/conversation/[id]/+server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { addSibling } from "$lib/utils/tree/addSibling.js";
2323
import { preprocessMessages } from "$lib/server/preprocessMessages.js";
2424
import { usageLimits } from "$lib/server/usageLimits";
2525
import { isURLLocal } from "$lib/server/isURLLocal.js";
26+
import { metrics } from "@opentelemetry/api";
2627

2728
export async function POST({ request, locals, params, getClientAddress }) {
2829
const id = z.string().parse(params.id);
@@ -243,6 +244,15 @@ export async function POST({ request, locals, params, getClientAddress }) {
243244
messageId
244245
);
245246

247+
const meter = metrics.getMeter("chat-ui");
248+
const counter = meter.createCounter("chat-ui.conversations.messages.count", {
249+
description: "The number of user messages created",
250+
});
251+
counter.add(1, {
252+
"chat-ui.model": "values.model",
253+
"user.email": locals.user?.email || undefined,
254+
});
255+
246256
messageToWriteToId = addChildren(
247257
conv,
248258
{

src/telemetry.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This file is built outside of sveltekit and cannot import from the rest of the application
2+
// or special imports like $env/dynamic/private.
3+
import { NodeSDK } from "@opentelemetry/sdk-node";
4+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
5+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
6+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
7+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
8+
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
9+
import { AlwaysOnSampler } from "@opentelemetry/sdk-trace-base";
10+
import { Resource } from "@opentelemetry/resources";
11+
12+
const TRACE_URL =
13+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/traces" || "http://localhost:4318/v1/traces";
14+
const METRICS_URL =
15+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/metrics" || "http://localhost:4318/v1/metrics";
16+
const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || "huggingface/chat-ui";
17+
18+
const exporter = new OTLPTraceExporter({
19+
url: TRACE_URL,
20+
headers: {},
21+
});
22+
23+
const otelNodeSdk = new NodeSDK({
24+
autoDetectResources: true,
25+
serviceName: SERVICE_NAME,
26+
traceExporter: exporter,
27+
metricReader: new PeriodicExportingMetricReader({
28+
exporter: new OTLPMetricExporter({
29+
url: METRICS_URL,
30+
headers: {},
31+
}),
32+
}),
33+
sampler: new AlwaysOnSampler(),
34+
resource: new Resource({
35+
[SEMRESATTRS_SERVICE_NAME]: SERVICE_NAME,
36+
}),
37+
instrumentations: [
38+
getNodeAutoInstrumentations({
39+
"@opentelemetry/instrumentation-http": {
40+
ignoreIncomingRequestHook: (request) => {
41+
// Don't trace static asset requests
42+
if (
43+
request.url?.endsWith(".js") ||
44+
request.url?.endsWith(".svg") ||
45+
request.url?.endsWith(".css")
46+
) {
47+
return false;
48+
}
49+
return true;
50+
},
51+
},
52+
}),
53+
],
54+
});
55+
56+
export class Telemetry {
57+
private static instance: Telemetry;
58+
private initialized = false;
59+
60+
private constructor() {}
61+
62+
public static getInstance(): Telemetry {
63+
if (!Telemetry.instance) {
64+
Telemetry.instance = new Telemetry();
65+
}
66+
return Telemetry.instance;
67+
}
68+
69+
public start() {
70+
if (!this.initialized) {
71+
this.initialized = true;
72+
otelNodeSdk.start();
73+
}
74+
}
75+
}
76+
77+
Telemetry.getInstance().start();

vite.telemetry.config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { defineConfig } from "vite";
2+
3+
export default defineConfig({
4+
build: {
5+
emptyOutDir: false,
6+
ssr: true,
7+
target: "node18",
8+
outDir: "build",
9+
rollupOptions: {
10+
input: {
11+
telemetry: "src/telemetry.ts",
12+
},
13+
},
14+
lib: {
15+
formats: ["cjs"],
16+
entry: "src/telemetry.ts",
17+
},
18+
},
19+
});

0 commit comments

Comments
 (0)