Je m'appelle Marc Dubois, développeur senior backend chez HolySheep AI, et aujourd'hui je vais partager avec vous une expérience douloureuse mais formative : il y a six mois, notre équipe a rencontré un incident de production qui a paralysé notre passerelle API pendant 47 minutes. Le diagnostic initial pointait vers une surcharge du serveur, mais en creusant dans les logs avec OpenTelemetry et ClickHouse, nous avons découvert un problème de latence réseau beaucoup plus subtil. Cette mésaventure m'a convaincu de l'importance cruciale d'une architecture de logging robuste pour les passerelles API IA.

Le scénario d'erreur qui a tout changé

Tout a commencé par cette erreur sur notre tableau de bord de monitoring à 14h32 un mardi après-midi :

ERROR - ConnectionError: timeout after 30000ms
	at RequestHandler.processRequest (/app/handlers/request.ts:127)
	at async APIRouter.route (/app/router/core.ts:89)
	correlation_id: "req_a8f3b2c1d4e5f6g7"
	timestamp: "2026-01-15T14:32:17.823Z"
	gateway: "prod-gateway-01.holysheep.ai"

En moins de 5 minutes, nous avions 847 requêtes en timeout. Le chaos. Cette erreur ConnectionError: timeout after 30000ms survenait exactement au moment où nous atteignions 50 000 requêtes par minute, notre pic de charge habituel. Mais la vraie cause ? Un buffer overflow dans notre système de logs qui fuyait vers le système principal. Sans une architecture de logging distribuée appropriée, nous aurions mis des heures à diagnostiquer le problème.

Architecture de référence : OpenTelemetry + ClickHouse

Notre solution moderne utilise OpenTelemetry pour la collecte des traces et métriques, avec ClickHouse comme datastore haute performance pour l'analyse de logs en temps réel. Cette combinaison offre des performances de requête exceptionnelles : moins de 50ms de latence sur des tables de plusieurs milliards de lignes.

Installation et configuration initiale

Prérequis système

Avant de commencer, installez les dépendances nécessaires. Notre environnement de production fonctionne sur Ubuntu 22.04 LTS avec Node.js 20 LTS.

# Installation de OpenTelemetry SDK et collecteur
npm install @opentelemetry/sdk-node \
    @opentelemetry/auto-instrumentations-node \
    @opentelemetry/exporter-trace-otlp-http \
    @opentelemetry/exporter-metrics-otlp-http \
    @opentelemetry/instrumentation-http \
    @opentelemetry/instrumentation-express

Installation du client ClickHouse pour Node.js

npm install @clickhouse/client

Installation de dotenv pour la gestion des variables

npm install dotenv

Installation de pino pour le logging structuré

npm install pino pino-pretty

Configuration OpenTelemetry pour la passerelle HolySheep

Voici la configuration complète que nous utilisons en production pour instrumenter notre passerelle API HolySheep :

// opentelemetry-setup.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { trace, metrics } = require('@opentelemetry/api');

// Configuration du point de terminaison ClickHouse viaOTLP
const collectorEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318';

const resource = new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'holysheep-api-gateway',
    [SemanticResourceAttributes.SERVICE_VERSION]: '2.4.1',
    'gateway.provider': 'holysheep',
    'gateway.region': 'cn-beijing'
});

const sdk = new NodeSDK({
    resource,
    traceExporter: new OTLPTraceExporter({
        url: ${collectorEndpoint}/v1/traces
    }),
    metricReader: new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter({
            url: ${collectorEndpoint}/v1/metrics
        }),
        exportIntervalMillis: 10000
    }),
    instrumentations: [
        getNodeAutoInstrumentations({
            '@opentelemetry/instrumentation-http': {
                ignoreIncomingPaths: ['/health', '/metrics']
            }
        })
    ]
});

sdk.start();
console.log('OpenTelemetry SDK initialisé pour HolySheep Gateway');

process.on('SIGTERM', () => {
    sdk.shutdown()
        .then(() => console.log('SDK OpenTelemetry éteint'))
        .catch((error) => console.error('Erreur lors de l\'arrêt du SDK:', error))
        .finally(() => process.exit(0));
});

module.exports = { trace, metrics };

Intégration avec l'API HolySheep

Pour les appels à l'API HolySheep, notre configuration utilise le endpoint centralisé avec une clé API sécurisée :

// holysheep-client.js
const { trace, SpanKind, SpanStatusCode } = require('@opentelemetry/api');

class HolySheepAIClient {
    constructor(apiKey) {
        this.baseURL = 'https://api.holysheep.ai/v1';
        this.apiKey = apiKey;
        this.tracer = trace.getTracer('holysheep-client', '1.0.0');
    }

    async chatCompletion(messages, options = {}) {
        const span = this.tracer.startSpan('holysheep.chat.completion', {
            kind: SpanKind.CLIENT,
            attributes: {
                'ai.provider': 'holysheep',
                'ai.model': options.model || 'gpt-4.1',
                'ai.max_tokens': options.max_tokens || 2048,
                'message.count': messages.length
            }
        });

        try {
            const response = await fetch(${this.baseURL}/chat/completions, {
                method: 'POST',
                headers: {
                    'Authorization': Bearer ${this.apiKey},
                    'Content-Type': 'application/json',
                    'X-Request-ID': span.spanContext().traceId
                },
                body: JSON.stringify({
                    model: options.model || 'gpt-4.1',
                    messages: messages,
                    max_tokens: options.max_tokens || 2048,
                    temperature: options.temperature || 0.7
                })
            });

            if (!response.ok) {
                const errorBody = await response.text();
                span.setStatus({
                    code: SpanStatusCode.ERROR,
                    message: HTTP ${response.status}: ${errorBody}
                });
                span.recordException(new Error(HolySheep API Error: ${response.status}));
                throw new Error(HolySheep API Error: ${response.status} - ${errorBody});
            }

            const data = await response.json();
            span.setAttribute('ai.response.usage.total_tokens', data.usage?.total_tokens || 0);
            span.setAttribute('ai.response.id', data.id);
            
            return data;
        } catch (error) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: error.message
            });
            span.recordException(error);
            throw error;
        } finally {
            span.end();
        }
    }

    // Wrapper pour différents modèles avec tarification
    async complete(prompt, model = 'gpt-4.1') {
        const pricing = {
            'gpt-4.1': 8.00,        // $8.00 / 1M tokens
            'claude-sonnet-4.5': 15.00,  // $15.00 / 1M tokens
            'gemini-2.5-flash': 2.50,    // $2.50 / 1M tokens
            'deepseek-v3.2': 0.42        // $0.42 / 1M tokens
        };

        return this.chatCompletion(
            [{ role: 'user', content: prompt }],
            { model, max_tokens: 2048 }
        );
    }
}

module.exports = HolySheepAIClient;

Configuration ClickHouse pour le stockage des logs

ClickHouse est le cœur de notre système d'analyse de logs grâce à sa capacité à ingérer des millions de lignes par seconde avec des temps de requête inférieurs à 50ms. Notre table principale stocke tous les événements de notre passerelle API HolySheep avec un schema optimisé pour les requêtes analytiques.

-- Création de la base de données pour les logs de la passerelle
CREATE DATABASE IF NOT EXISTS holysheep_logs ON CLUSTER 'cluster-holysheep';

-- Table principale des requêtes API avec MergeTree engine
CREATE TABLE IF NOT EXISTS holysheep_logs.api_requests
(
    -- Identifiants
    trace_id String DEFAULT substring(hex(generateUUIDv4()), 1, 32),
    span_id String,
    
    -- Métadonnées de la requête
    timestamp DateTime64(3) DEFAULT now64(3),
    request_id String,
    correlation_id String,
    
    -- Informations client
    client_ip String,
    client_region String DEFAULT 'unknown',
    user_agent String,
    
    -- Détails de la requête
    method String,
    endpoint String,
    path String,
    query_params String,
    
    -- Authentification
    api_key_hash String,
    user_id String,
    
    -- Réponse
    status_code UInt16,
    response_time_ms Float32,
    error_message String,
    
    -- Métriques AI
    ai_provider String DEFAULT 'holysheep',
    ai_model String,
    tokens_used UInt32,
    tokens_prompt UInt32,
    tokens_completion UInt32,
    cost_usd Float32,
    
    -- Headers
    request_headers String,
    response_headers String,
    
    -- Body (sanitized)
    request_body String,
    response_body String,
    
    -- Géolocalisation
    geo_country String,
    geo_city String,
    geo_latitude Float32,
    geo_longitude Float32
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp DESC, trace_id)
TTL timestamp + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;

-- Table pour les métriques agrégées (Materialized View)
CREATE MATERIALIZED VIEW holysheep_logs.request_metrics_mv
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp, endpoint, status_code)
AS SELECT
    toStartOfMinute(timestamp) AS timestamp,
    endpoint,
    status_code,
    count() AS request_count,
    sum(response_time_ms) AS total_response_time,
    avg(response_time_ms) AS avg_response_time,
    quantile(0.95)(response_time_ms) AS p95_response_time,
    quantile(0.99)(response_time_ms) AS p99_response_time,
    sum(tokens_used) AS total_tokens,
    sum(cost_usd) AS total_cost
FROM holysheep_logs.api_requests
GROUP BY timestamp, endpoint, status_code;

-- Table pour les logs d'erreurs avec index optimisé
CREATE TABLE IF NOT EXISTS holysheep_logs.api_errors
(
    timestamp DateTime64(3) DEFAULT now64(3),
    trace_id String,
    request_id String,
    error_type String,
    error_code String,
    error_message String,
    stack_trace String,
    endpoint String,
    client_ip String,
    ai_model String
)
ENGINE = ReplacingMergeTree(timestamp)
ORDER BY (timestamp DESC, error_type, trace_id)
SETTINGS index_granularity = 1024;

Collecteur OpenTelemetry vers ClickHouse

Nous utilisons un collecteur OpenTelemetry personnalisé qui relaie les données directement vers ClickHouse via le protocole HTTP native de ClickHouse. Cette architecture nous permet d'atteindre un throughput de 100 000 spans par seconde.

// clickhouse-exporter.js
const { ClickHouse } = require('@clickhouse/client');

class ClickHouseExporter {
    constructor(options = {}) {
        this.clickhouse = new ClickHouse({
            host: options.host || 'http://localhost:8123',
            database: options.database || 'holysheep_logs',
            username: options.username || 'default',
            password: options.password || '',
            maxQueueSize: 100000,
            maxExecutionTime: 30000
        });
        
        this.buffer = [];
        this.bufferSize = options.bufferSize || 1000;
        this.flushInterval = options.flushInterval || 5000;
        
        this.startFlushTimer();
    }

    async export(traces, metrics) {
        for (const span of traces) {
            const record = this.transformSpan(span);
            this.buffer.push(record);
            
            if (this.buffer.length >= this.bufferSize) {
                await this.flush();
            }
        }
    }

    transformSpan(span) {
        const attributes = this.extractAttributes(span);
        
        return {
            trace_id: span.traceId,
            span_id: span.spanId,
            timestamp: new Date(span.startTime / 1000000).toISOString(),
            request_id: attributes['http.request_id'] || attributes['request.id'],
            correlation_id: attributes['correlation.id'],
            method: attributes['http.method'],
            endpoint: attributes['http.url'] || attributes['endpoint'],
            path: attributes['http.route'] || attributes['http.target'],
            status_code: attributes['http.status_code'] || 200,
            response_time_ms: (span.endTime - span.startTime) / 1000000,
            error_message: span.status?.message || '',
            ai_model: attributes['ai.model'] || attributes['model'],
            tokens_used: attributes['ai.tokens.total'] || 0,
            cost_usd: attributes['ai.cost.usd'] || 0,
            request_body: attributes['http.request.body'],
            response_body: attributes['http.response.body'],
            client_ip: attributes['http.client_ip'],
            user_agent: attributes['http.user_agent']
        };
    }

    extractAttributes(span) {
        const attrs = {};
        if (span.attributes) {
            for (const attr of span.attributes) {
                attrs[attr.key] = attr.value;
            }
        }
        return attrs;
    }

    async flush() {
        if (this.buffer.length === 0) return;
        
        const records = this.buffer.splice(0, this.buffer.length);
        
        try {
            await this.clickhouse.insert({
                table: 'api_requests',
                values: records,
                format: 'JSONEachRow'
            });
            
            console.log([ClickHouseExporter] Flush réussi: ${records.length} enregistrements);
        } catch (error) {
            console.error('[ClickHouseExporter] Erreur lors du flush:', error.message);
            // Retry avec backoff exponentiel
            await this.retryFlush(records, 3);
        }
    }

    async retryFlush(records, attempts) {
        for (let i = 0; i < attempts; i++) {
            try {
                await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
                await this.clickhouse.insert({
                    table: 'api_requests',
                    values: records,
                    format: 'JSONEachRow'
                });
                return;
            } catch (error) {
                console.error([ClickHouseExporter] Retry ${i + 1} échoué:, error.message);
            }
        }
        console.error([ClickHouseExporter] Échec permanent après ${attempts} tentatives);
    }

    startFlushTimer() {
        setInterval(() => this.flush(), this.flushInterval);
    }
}

module.exports = ClickHouseExporter;

Requêtes analytiques pour le debugging

Avec cette infrastructure en place, le debugging devient un plaisir plutôt qu'une corvée. Voici quelques requêtes SQL ClickHouse que nous utilisons quotidiennement pour diagnostiquer les problèmes de performance sur notre passerelle HolySheep.

-- Diagnostic des erreurs 401/403 (problèmes d'authentification)
SELECT 
    toStartOfMinute(timestamp) AS minute,
    status_code,
    error_message,
    count() AS error_count,
    uniqExact(client_ip) AS unique_clients,
    any(endpoint) AS example_endpoint
FROM holysheep_logs.api_requests
WHERE timestamp >= now() - INTERVAL 1 HOUR
    AND status_code >= 400
GROUP BY minute, status_code, error_message
ORDER BY minute DESC, error_count DESC
LIMIT 50;

-- Analyse de la latence par modèle AI (détection des modèles lents)
SELECT 
    ai_model,
    count() AS total_requests,
    avg(response_time_ms) AS avg_latency_ms,
    quantile(0.5)(response_time_ms) AS median_latency_ms,
    quantile(0.95)(response_time_ms) AS p95_latency_ms,
    quantile(0.99)(response_time_ms) AS p99_latency_ms,
    max(response_time_ms) AS max_latency_ms,
    sum(tokens_used) AS total_tokens,
    sum(cost_usd) AS total_cost_usd
FROM holysheep_logs.api_requests
WHERE timestamp >= now() - INTERVAL 24 HOUR
    AND status_code = 200
GROUP BY ai_model
ORDER BY avg_latency_ms DESC;

-- Corrélation entre charge et latence (pour identifier les goulots d'étranglement)
SELECT 
    toStartOfMinute(timestamp) AS minute,
    count() AS requests_per_minute,
    avg(response_time_ms) AS avg_latency,
    max(response_time_ms) AS max_latency,
    quantile(0.99)(response_time_ms) AS p99_latency,
    countIf(status_code >= 500) AS error_5xx_count
FROM holysheep_logs.api_requests
WHERE timestamp >= now() - INTERVAL 2 HOUR
GROUP BY minute
ORDER BY minute;

Détection d'anomalies en temps réel

Notre système de monitoring utilise des requêtes continues ClickHouse pour détecter les anomalies de performance. Avec la latence inférieure à 50ms de HolySheep, nous pouvons réagir en temps réel aux pics de charge.

// anomaly-detector.js
const { ClickHouse } = require('@clickhouse/client');

class AnomalyDetector {
    constructor() {
        this.clickhouse = new ClickHouse({
            host: 'http://localhost:8123',
            database: 'holysheep_logs'
        });
        
        this.thresholds = {
            p99Latency: 5000,        // 5 secondes
            errorRate: 0.05,         // 5% d'erreurs
            tokensPerMinute: 100000, // Limite de rate
            costPerHour: 1000        // Alerte si > $1000/heure
        };
    }

    async checkForAnomalies() {
        const [
            latencyAnomalies,
            errorAnomalies,
            costAnomalies
        ] = await Promise.all([
            this.checkLatencyAnomalies(),
            this.checkErrorAnomalies(),
            this.checkCostAnomalies()
        ]);

        return {
            timestamp: new Date().toISOString(),
            anomalies: [...latencyAnomalies, ...errorAnomalies, ...costAnomalies],
            healthy: latencyAnomalies.length === 0 && errorAnomalies.length === 0
        };
    }

    async checkLatencyAnomalies() {
        const result = await this.clickhouse.query({
            query: `
                SELECT 
                    'high_latency' AS anomaly_type,
                    ai_model,
                    count() AS request_count,
                    quantile(0.99)(response_time_ms) AS p99_latency
                FROM holysheep_logs.api_requests
                WHERE timestamp >= now() - INTERVAL 5 MINUTE
                GROUP BY ai_model
                HAVING p99_latency > ${this.thresholds.p99Latency}
            `,
            format: 'JSON'
        }).then(r => r.json());

        return result.data.map(row => ({
            type: 'latency',
            severity: row.p99_latency > 10000 ? 'critical' : 'warning',
            message: Latence P99 élevée pour ${row.ai_model}: ${row.p99_latency.toFixed(0)}ms,
            details: row
        }));
    }

    async checkErrorAnomalies() {
        const result = await this.clickhouse.query({
            query: `
                SELECT 
                    endpoint,
                    status_code,
                    count() AS error_count,
                    count() / (SELECT count() FROM holysheep_logs.api_requests WHERE timestamp >= now() - INTERVAL 5 MINUTE) AS error_rate
                FROM holysheep_logs.api_requests
                WHERE timestamp >= now() - INTERVAL 5 MINUTE
                    AND status_code >= 400
                GROUP BY endpoint, status_code
                HAVING error_rate > ${this.thresholds.errorRate}
            `,
            format: 'JSON'
        }).then(r => r.json());

        return result.data.map(row => ({
            type: 'error_rate',
            severity: row.error_rate > 0.1 ? 'critical' : 'warning',
            message: Taux d'erreur élevé sur ${row.endpoint}: ${(row.error_rate * 100).toFixed(1)}%,
            details: row
        }));
    }

    async checkCostAnomalies() {
        const result = await this.clickhouse.query({
            query: `
                SELECT 
                    toStartOfHour(timestamp) AS hour,
                    sum(cost_usd) AS total_cost,
                    sum(tokens_used) AS total_tokens
                FROM holysheep_logs.api_requests
                WHERE timestamp >= now() - INTERVAL 2 HOUR
                GROUP BY hour
                HAVING total_cost > ${this.thresholds.costPerHour}
            `,
            format: 'JSON'
        }).then(r => r.json());

        return result.data.map(row => ({
            type: 'cost',
            severity: row.total_cost > 500