Reciprok Docs

Monitoring & observabilité

Stack self-host, Signoz (OpenTelemetry + ClickHouse), pino pour les logs structurés

Tout self-host, open source, déployé via Dokploy à côté de l'app. Signoz remplace l'ancienne stack Grafana/Loki/Prometheus/GlitchTip dans un seul paquet cohérent. On contrôle nos données et notre budget.

Vue d'ensemble

ComposantRôleImage Docker
OTel CollectorReçoit les exports OTLP (traces + metrics + logs), route vers ClickHousesignoz/signoz-otel-collector:v0.144.2
ClickHouseStorage columnar des traces, metrics, logsclickhouse/clickhouse-server:25.5.6
ZookeeperCoordination ClickHouse (cluster mode)signoz/zookeeper:3.7.1
Signoz UIDashboard, explorer, alertes, http://localhost:8080signoz/signoz:v0.119.0

Stack embarquée dans le docker-compose.dev.yml derrière le profile observability et dans docker-compose.prod.yml (always on). bun run docker:up lance tout d'un coup.

SDK côté app

Backend (apps/server)

Le SDK OTel est initialisé avant tous les autres imports dans src/index.ts :

apps/server/src/index.ts
import { shutdownOtel, startOtel } from "@/lib/otel";

startOtel(); // ← tout premier call, capture les spans dès le boot

import { app } from "@/app";
// …

lib/otel.ts configure NodeSDK avec :

  • OTLPTraceExporter gRPC vers OTEL_EXPORTER_OTLP_ENDPOINT (default http://localhost:4317)
  • OTLPMetricExporter gRPC, export periodique toutes les 30 s
  • PgInstrumentation, instrumente automatiquement toutes les queries Drizzle/pg
  • PinoInstrumentation, injecte trace_id et span_id dans chaque log pino, permet la corrélation logs ↔ traces dans Signoz

Un plugin Elysia (lib/otel-plugin.ts) crée un span HTTP par requête, nom <METHOD> <path>, attrs http.method, http.route, http.status_code, http.duration_ms. Instrumentation manuelle car Bun ne hook pas le module http de Node comme l'auto-instrumentation classique.

Frontend (apps/web)

Next.js 16 a un support natif via src/instrumentation.ts. On utilise @vercel/otel qui wire le SDK en une ligne :

apps/web/src/instrumentation.ts
import { registerOTel } from "@vercel/otel";

export function register() {
  if (process.env.OTEL_ENABLED === "false") return;
  registerOTel({
    serviceName: process.env.OTEL_SERVICE_NAME ?? "reciprok-web",
  });
}

Exporte automatiquement : Server Actions, Route Handlers, fetch calls.

Métriques custom

Les métriques métier sont émises via le SDK OTel (meter reciprok.ai dans apps/server/src/modules/ai/ai.metrics.ts) :

MétriqueTypeAttributsPourquoi
reciprok.ai.tokenscountermodel, task, user_id, kind (input/output/cache_read/cache_write)Suivi consommation tokens Claude par user/modèle
reciprok.ai.cost_eurhistogrammodel, task, user_idDistribution p50/p95/p99 du coût par appel
reciprok.ai.tool_callscountertool, modelQuels tools Claude invoque le plus

Les spans HTTP et DB sont ajoutés automatiquement par les plugins, pas besoin de metrics custom pour reciprok_http_requests_total ou reciprok_db_query_duration_seconds (déjà dans les traces).

Logs

Logger pino

apps/server/src/lib/logger.ts, pino JSON en prod (stdout), pretty-print coloré en dev, silent en test.

import { logger } from "@/lib/logger";

logger.info({ userId, requestId }, "request created");
logger.error({ err, payload }, "failed to parse webhook");

Jamais de console.* dans apps/server/src/. Le logger est structuré, Signoz peut filtrer sur les clés.

Corrélation traces ↔ logs

PinoInstrumentation injecte automatiquement dans chaque ligne :

{
  "level": 30,
  "time": 1713345678,
  "msg": "request created",
  "userId": "...",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7"
}

Dans Signoz, cliquer sur un span ouvre les logs correspondants (même trace_id). Inverse : cliquer sur un log affiche la trace complète.

Alertes (configurées dans Signoz UI)

ConditionSévérité
Taux d'erreur HTTP 5xx > 5 % sur 5 minCritical
Latence p95 sur /api/requests/* > 2 s sur 10 minWarning
reciprok.ai.cost_eur cumulé > 200 €/moisWarning
reciprok.ai.cost_eur cumulé > 400 €/moisCritical
Postgres connections > 80 % du maxWarning
Healthcheck GET / KO sur 2 minCritical

Canaux : Discord équipe (Critical), email admin (Warning).

Rétention

  • Traces : 15 jours
  • Metrics : 30 jours
  • Logs : 15 jours

Stockage ClickHouse local (disque VPS Hostinger, ~10 GB estimés).

Costs

Stack self-host sur le même VPS que l'app. Coût marginal : 0 €. ClickHouse est très efficace en compression, on tient facilement 30 j de trafic sur 10 GB.

Debug local

  1. bun run docker:up lance toute la stack
  2. UI Signoz sur http://localhost:8080 (compte admin créé au premier login)
  3. Les services reciprok-server et reciprok-web apparaissent dans Services après quelques requêtes
  4. Explorer les traces : Traces → filter by service.name
  5. Métriques AI : Metrics → chercher reciprok.ai.*

Voir aussi

  • operations/environments, où sont déployés les services
  • operations/costs, budget global et plafonds IA
  • decisions/ai-stack, model routing Sonnet/Haiku et prompt caching

On this page