From 0f10f81ead5bf7579ec7c1e57db8927ad58c8e84 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 14:42:53 +0530 Subject: [PATCH 1/5] feat: add telemetry config schema Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/types/config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/chronicle/src/types/config.ts b/packages/chronicle/src/types/config.ts index 825b381b..59597221 100644 --- a/packages/chronicle/src/types/config.ts +++ b/packages/chronicle/src/types/config.ts @@ -67,6 +67,11 @@ const analyticsSchema = z.object({ googleAnalytics: googleAnalyticsSchema.optional(), }) +const telemetrySchema = z.object({ + enabled: z.boolean().optional(), + serviceName: z.string().optional(), +}) + export const chronicleConfigSchema = z.object({ title: z.string(), description: z.string().optional(), @@ -81,6 +86,7 @@ export const chronicleConfigSchema = z.object({ api: z.array(apiSchema).optional(), llms: llmsSchema.optional(), analytics: analyticsSchema.optional(), + telemetry: telemetrySchema.optional(), }) export type ChronicleConfig = z.infer @@ -97,3 +103,4 @@ export type FooterConfig = z.infer export type LlmsConfig = z.infer export type AnalyticsConfig = z.infer export type GoogleAnalyticsConfig = z.infer +export type TelemetryConfig = z.infer From 4c351944dc17c01886dfe6d45f1de7edc52db809 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 14:42:58 +0530 Subject: [PATCH 2/5] feat: add OTel telemetry module with Prometheus exporter Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 17 ++++++++ packages/chronicle/package.json | 5 +++ packages/chronicle/src/server/telemetry.ts | 49 ++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 packages/chronicle/src/server/telemetry.ts diff --git a/bun.lock b/bun.lock index c7ce42f5..d0cf6f16 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,11 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.14", "@heroicons/react": "^2.2.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-prometheus": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-metrics": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", "@raystack/apsara": "0.55.1", "@shikijs/rehype": "^4.0.2", "@vitejs/plugin-react": "^6.0.1", @@ -285,6 +290,18 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4TGYoZKebUWVuYkV6r5wS2dUF4zH7EbWFw/Uqz1ZM1tGHQeFT9wzHGXq3iSIXMUrwu5jRdxjfMaXrYejPu2kpQ=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], "@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], diff --git a/packages/chronicle/package.json b/packages/chronicle/package.json index ac13d7c4..b4d14465 100644 --- a/packages/chronicle/package.json +++ b/packages/chronicle/package.json @@ -36,6 +36,11 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.39.14", "@heroicons/react": "^2.2.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-prometheus": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-metrics": "^2.6.1", + "@opentelemetry/semantic-conventions": "^1.40.0", "@raystack/apsara": "0.55.1", "@shikijs/rehype": "^4.0.2", "@vitejs/plugin-react": "^6.0.1", diff --git a/packages/chronicle/src/server/telemetry.ts b/packages/chronicle/src/server/telemetry.ts new file mode 100644 index 00000000..77c45635 --- /dev/null +++ b/packages/chronicle/src/server/telemetry.ts @@ -0,0 +1,49 @@ +import type { Counter, Histogram } from '@opentelemetry/api' +import sdkMetrics from '@opentelemetry/sdk-metrics' +import prometheusExporter from '@opentelemetry/exporter-prometheus' +import resources from '@opentelemetry/resources' +import semconv from '@opentelemetry/semantic-conventions' +import type { ChronicleConfig } from '@/types/config' + +const { MeterProvider } = sdkMetrics +const { PrometheusExporter } = prometheusExporter +const { resourceFromAttributes } = resources +const { ATTR_SERVICE_NAME } = semconv + +let exporter: PrometheusExporter +let requestCounter: Counter +let requestDuration: Histogram +let ssrRenderDuration: Histogram + +export function initTelemetry(config: ChronicleConfig) { + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle', + }) + + exporter = new PrometheusExporter({ preventServerStart: true }) + const provider = new MeterProvider({ resource, readers: [exporter] }) + const meter = provider.getMeter('chronicle') + + requestCounter = meter.createCounter('http_server_request_total', { + description: 'Total HTTP requests', + }) + requestDuration = meter.createHistogram('http_server_request_duration_ms', { + description: 'HTTP request duration in ms', + }) + ssrRenderDuration = meter.createHistogram('http_server_ssr_render_duration_ms', { + description: 'SSR render duration in ms', + }) +} + +export function getExporter() { + return exporter +} + +export function recordRequest(method: string, route: string, status: number, durationMs: number) { + requestCounter?.add(1, { method, route, status }) + requestDuration?.record(durationMs, { method, route, status }) +} + +export function recordSSRRender(route: string, status: number, durationMs: number) { + ssrRenderDuration?.record(durationMs, { route, status }) +} From 1a406d211f5614bd760b57dbf2fb4fa7fb123a37 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 14:43:01 +0530 Subject: [PATCH 3/5] feat: add /api/metrics Prometheus endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/metrics.ts | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/chronicle/src/server/api/metrics.ts diff --git a/packages/chronicle/src/server/api/metrics.ts b/packages/chronicle/src/server/api/metrics.ts new file mode 100644 index 00000000..57b1d789 --- /dev/null +++ b/packages/chronicle/src/server/api/metrics.ts @@ -0,0 +1,23 @@ +import type { IncomingMessage, ServerResponse } from 'node:http' +import { defineHandler } from 'nitro' +import { getExporter } from '../telemetry' + +export default defineHandler(async () => { + const exporter = getExporter() + if (!exporter) { + return new Response('Telemetry not enabled', { status: 404 }) + } + + const metricsString = await new Promise((resolve) => { + const mockRes = { + setHeader: () => mockRes, + end: (data: string) => resolve(data), + } as unknown as ServerResponse + + exporter.getMetricsRequestHandler({} as unknown as IncomingMessage, mockRes) + }) + + return new Response(metricsString, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }) +}) From 69399e20c30464230b40827e905a07c53c91923b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 14:43:07 +0530 Subject: [PATCH 4/5] feat: add Nitro telemetry plugin for HTTP metrics Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/server/plugins/telemetry.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/chronicle/src/server/plugins/telemetry.ts diff --git a/packages/chronicle/src/server/plugins/telemetry.ts b/packages/chronicle/src/server/plugins/telemetry.ts new file mode 100644 index 00000000..e5189f27 --- /dev/null +++ b/packages/chronicle/src/server/plugins/telemetry.ts @@ -0,0 +1,21 @@ +import { definePlugin } from 'nitro' +import { loadConfig } from '@/lib/config' +import { initTelemetry, recordRequest } from '../telemetry' + +export default definePlugin((nitroApp) => { + const config = loadConfig() + if (!config.telemetry?.enabled) return + + initTelemetry(config) + + nitroApp.hooks.hook('request', (event) => { + if (event.path === '/api/metrics') return + event.context._requestStart = performance.now() + }) + + nitroApp.hooks.hook('response', (res, event) => { + if (!event.context._requestStart) return + const duration = performance.now() - event.context._requestStart + recordRequest(event.method, event.path, res.status, duration) + }) +}) From 26b873422e066470c0851569e54be6f22171ab5e Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 14:43:10 +0530 Subject: [PATCH 5/5] feat: instrument SSR render duration Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/entry-server.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/chronicle/src/server/entry-server.tsx b/packages/chronicle/src/server/entry-server.tsx index 30e2c823..0ccd49bd 100644 --- a/packages/chronicle/src/server/entry-server.tsx +++ b/packages/chronicle/src/server/entry-server.tsx @@ -10,6 +10,7 @@ import { loadApiSpecs } from '@/lib/openapi'; import { PageProvider } from '@/lib/page-context'; import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; import { App } from './App'; +import { recordSSRRender } from './telemetry'; import clientAssets from './entry-client?assets=client'; import serverAssets from './entry-server?assets=ssr'; @@ -57,6 +58,7 @@ export default { const assets = clientAssets.merge(serverAssets); + const renderStart = performance.now(); const stream = await renderToReadableStream( @@ -91,9 +93,13 @@ export default { , ); + const renderDuration = performance.now() - renderStart; + const isApiRoute = pathname.startsWith('/apis'); const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200; + recordSSRRender(pathname, status, renderDuration); + return new Response(stream, { status, headers: { 'Content-Type': 'text/html;charset=utf-8' },