From 42fddd85fdc748ac7fd99e2e77a4268aacb2133d Mon Sep 17 00:00:00 2001 From: Gianpaolo Sanseverino Date: Wed, 20 May 2026 18:11:15 +0200 Subject: [PATCH] observability --- .qualops/.qualopsrc.json | 1 - Dockerfile | 4 +-- docker-compose.yml | 51 ++++++++++++++++++++++++++++++++ docker/grafana-datasources.yaml | 20 +++++++++++++ docker/grafana-provisioning.yaml | 11 +++++++ docker/tempo.yaml | 26 ++++++++++++++++ package.json | 2 ++ pnpm-lock.yaml | 14 +++++++++ src/api/server.ts | 3 ++ src/modes/serve.ts | 2 +- src/observability/tracing.ts | 8 ++++- 11 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 docker-compose.yml create mode 100644 docker/grafana-datasources.yaml create mode 100644 docker/grafana-provisioning.yaml create mode 100644 docker/tempo.yaml diff --git a/.qualops/.qualopsrc.json b/.qualops/.qualopsrc.json index 04ff103..7c6e95b 100644 --- a/.qualops/.qualopsrc.json +++ b/.qualops/.qualopsrc.json @@ -23,4 +23,3 @@ "maxInlineComments": 50 } } - diff --git a/Dockerfile b/Dockerfile index b5d810a..c8abdbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,9 +24,7 @@ RUN pnpm run build FROM node:22-bookworm-slim AS runtime WORKDIR /app -ENV NODE_ENV=production \ - PORT=3000 \ - CONFIG_PATH=/etc/configurable-agent/config.yaml +ENV NODE_ENV=production PORT=3000 RUN corepack enable && corepack prepare pnpm@10.30.2 --activate COPY package.json pnpm-lock.yaml ./ COPY pnpm-workspace.yaml* ./ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..97bd3cf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +services: + agent: + build: + context: . + dockerfile: ./Dockerfile + ports: + - "3000:3000" + environment: + - CONFIG_PATH=/app/example.config.yaml + - OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 + - OTEL_SERVICE_NAME=configurable-agent + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + volumes: + - ./example.config.yaml:/app/example.config.yaml:ro + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + depends_on: + - tempo + + tempo: + image: grafana/tempo:2.6.1 + command: ["-config.file=/etc/tempo.yaml"] + volumes: + - ./docker/tempo.yaml:/etc/tempo.yaml + - tempo_data:/var/tempo + ports: + - "3200:3200" # HTTP API + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + + grafana: + image: grafana/grafana:latest + volumes: + - ./docker/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - ./docker/grafana-provisioning.yaml:/etc/grafana/provisioning/dashboards/dashboard.yaml + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor + ports: + - "3001:3000" + depends_on: + - tempo + +volumes: + tempo_data: diff --git a/docker/grafana-datasources.yaml b/docker/grafana-datasources.yaml new file mode 100644 index 0000000..711dcdd --- /dev/null +++ b/docker/grafana-datasources.yaml @@ -0,0 +1,20 @@ +apiVersion: 1 + +datasources: + - name: Tempo + type: tempo + access: proxy + orgId: 1 + url: http://tempo:3200 + basicAuth: false + isDefault: true + version: 1 + editable: false + apiVersion: 1 + uid: tempo + jsonData: + httpMethod: GET + search: + hide: false + nodeGraph: + enabled: true diff --git a/docker/grafana-provisioning.yaml b/docker/grafana-provisioning.yaml new file mode 100644 index 0000000..57bc72e --- /dev/null +++ b/docker/grafana-provisioning.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "default" + orgId: 1 + folder: "" + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /var/lib/grafana/dashboards diff --git a/docker/tempo.yaml b/docker/tempo.yaml new file mode 100644 index 0000000..a3d7425 --- /dev/null +++ b/docker/tempo.yaml @@ -0,0 +1,26 @@ +server: + http_listen_port: 3200 + +distributor: + receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + grpc: + endpoint: 0.0.0.0:4317 + +ingester: + max_block_duration: 5m + +compactor: + compaction: + block_retention: 1h + +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + local: + path: /var/tempo/blocks diff --git a/package.json b/package.json index 0a51b9d..0304302 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "engines": { "node": ">=22" }, + "packageManager": "pnpm@10.28.2", "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc -p tsconfig.build.json && chmod +x dist/index.js", @@ -28,6 +29,7 @@ "@ai-sdk/openai": "^2.0.0", "@ai-sdk/openai-compatible": "^1.0.36", "@hono/node-server": "^1.14.0", + "@hono/otel": "1.1.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.54.0", "@opentelemetry/exporter-trace-otlp-http": "^0.55.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2b9320..3e080e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@hono/node-server': specifier: ^1.14.0 version: 1.19.14(hono@4.12.14) + '@hono/otel': + specifier: 1.1.2 + version: 1.1.2(hono@4.12.14) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.1 @@ -382,6 +385,11 @@ packages: peerDependencies: hono: ^4 + '@hono/otel@1.1.2': + resolution: {integrity: sha512-UaBMKPGaQTj4sjvpGqQ+57eolTwMI2znzxV/QBUF99XxhcmqtqaZX95flXpGgWb+lnlinlCy23Y1sZ8+TzzM9A==} + peerDependencies: + hono: '>=4.0.0' + '@inquirer/ansi@2.0.5': resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} @@ -2101,6 +2109,12 @@ snapshots: dependencies: hono: 4.12.14 + '@hono/otel@1.1.2(hono@4.12.14)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + hono: 4.12.14 + '@inquirer/ansi@2.0.5': {} '@inquirer/confirm@6.0.12(@types/node@22.19.17)': diff --git a/src/api/server.ts b/src/api/server.ts index ae26a55..b5e9b4a 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -1,3 +1,4 @@ +import { httpInstrumentationMiddleware } from '@hono/otel'; import type { ModelMessage, ToolSet } from 'ai'; import { Hono } from 'hono'; import { streamSSE } from 'hono/streaming'; @@ -20,6 +21,8 @@ export function buildServer(config: AgentConfig, options: BuildServerOptions) { const app = new Hono(); const { tools } = options; + app.use('*', httpInstrumentationMiddleware()); + app.get('/health', (c) => c.json({ status: 'ok' })); app.get('/ready', (c) => { diff --git a/src/modes/serve.ts b/src/modes/serve.ts index 20bd653..2489f99 100644 --- a/src/modes/serve.ts +++ b/src/modes/serve.ts @@ -23,7 +23,7 @@ export async function runServe(): Promise { let registry: Awaited>; try { registry = await buildMcpRegistry(config); - + logger.info( { tools: Object.keys(registry.tools).length, servers: config.mcpTools.length }, 'mcp registry ready', diff --git a/src/observability/tracing.ts b/src/observability/tracing.ts index f85fd79..8c0c444 100644 --- a/src/observability/tracing.ts +++ b/src/observability/tracing.ts @@ -17,7 +17,13 @@ export function startTracing(): void { [SemanticResourceAttributes.SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION ?? '0.1.0', }), traceExporter: new OTLPTraceExporter(), - instrumentations: [getNodeAutoInstrumentations()], + instrumentations: [ + getNodeAutoInstrumentations({ + '@opentelemetry/instrumentation-http': { + requireParentforIncomingSpans: true, + }, + }), + ], }); sdk.start(); }