En tant qu'ingénieur qui a déployé des dizaines de pipelines IA en production, je sais que la surveillance des appels modèle n'est pas une option — c'est une nécessité. Quand votre latence P99 dépasse 2 secondes sans observabilité, vous partez en aveugle. Aujourd'hui, je vous montre comment construire un système de monitoring complet avec OpenTelemetry, en intégrant naturellement HolySheep AI comme fournisseur d'API optimisé.
为什么 AI 应用需要 OpenTelemetry
Les applications IA présentent des défis uniques pour l'observabilité. Contrairement aux API REST classiques, les appels aux modèles de langage impliquent :
- Latence variable selon la complexité du prompt
- Coûts variables par token (输入 vs 输出)
- Taux d'erreur non triviaux (rate limits, timeouts, context overflow)
- Nécessité de corréler les traces avec les métadonnées de session
OpenTelemetry offre le标准的 trifecta : traces réparties, métriques custom, et logs structurés — le tout avec un SDK unifié pour Node.js, Python, Go, et Java. Pour une plateforme comme HolySheep AI qui promet moins de 50ms de latence, le monitoring devient critique pour maintenir ces SLA.
Architecture de surveillance IA avec OpenTelemetry
L'architecture que je recommande pour monitorer les applications IA en production se compose de trois couches :
- Instrumentation automatique : captures les appels HTTP sortants et les spans OpenAI Compatibility API
- Instrumentation manuelle : ajoute le contexte métier (user_id, session_id, prompt_tokens, completion_tokens)
- Backend d'observabilité : stocke les données (Jaeger, Grafana, ou services managés)
集成代码:TypeScript + OpenTelemetry
Commençons par l'implémentation complète. Je vais utiliser TypeScript avec le SDK OpenTelemetry, instrumentant automatiquement les appels HTTP et enrichissant manuellement les spans avec les métadonnées spécifiques IA.
//ai-monitoring/src/instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { trace, metrics, SpanStatusCode, Span } from '@opentelemetry/api';
import { AI_METADATA_ATTRIBUTES } from './semantic-conventions';
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'ai-application',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
environment: process.env.OTEL_ENVIRONMENT || 'production',
});
// Configuration du trace exporter
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
});
// Configuration du metric exporter
const metricExporter = new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/metrics',
});
const metricReader = new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 10000,
});
const sdk = new NodeSDK({
resource,
traceExporter,
metricReader,
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
enabled: true,
ignoreIncomingRequestHook: (req) => {
return req.url === '/health';
},
},
'@opentelemetry/instrumentation-express': {
enabled: true,
},
}),
],
});
sdk.start();
// Hook pour arrêter proprement
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('OpenTelemetry SDK shut down successfully'))
.catch((error) => console.error('Error shutting down OpenTelemetry SDK', error))
.finally(() => process.exit(0));
});
export { sdk, trace, metrics, SpanStatusCode, Span };
//ai-monitoring/src/semantic-conventions.ts
import { Span, context, SpanKind, SpanStatusCode } from '@opentelemetry/api';
// Conventions sémantiques pour les attributs AI (suivant OpenTelemetry semantic conventions)
export const AI_METADATA_ATTRIBUTES = {
// Identifiants
'ai.request.id': 'string',
'ai.session.id': 'string',
'ai.user.id': 'string',
// Métadonnées du modèle
'ai.model.name': 'string',
'ai.model.provider': 'string', // 'holysheep', 'openai', 'anthropic'
'ai.model.version': 'string',
// Tokens (essentiels pour le coût)
'ai.tokens.prompt': 'int64',
'ai.tokens.completion': 'int64',
'ai.tokens.total': 'int64',
// Latence détaillée
'ai.latency.first_token_ms': 'int64',
'ai.latency.total_ms': 'int64',
// Coût
'ai.cost.usd': 'double',
'ai.cost.currency': 'string',
// Qualité de réponse
'ai.response.finish_reason': 'string', // 'stop', 'length', 'content_filter'
'ai.usage.prompt_tokens': 'int64',
'ai.usage.completion_tokens': 'int64',
} as const;
// Interface pour les métadonnées de requête IA
export interface AIMetadata {
sessionId: string;
userId?: string;
model: string;
provider: string;
promptTokens: number;
completionTokens: number;
latencyMs: number;
costUsd: number;
finishReason: string;
}
// Fonction helper pour créer un span IA
export function createAISpan(
name: string,
metadata: AIMetadata,
callback: (span: Span) => Promise<any>
): Promise<any> {
const tracer = trace.getTracer('ai-monitoring');
return tracer.startActiveSpan(
ai.${name},
{
kind: SpanKind.CLIENT,
attributes: {
'ai.request.id': generateRequestId(),
'ai.session.id': metadata.sessionId,
'ai.user.id': metadata.userId || 'anonymous',
'ai.model.name': metadata.model,
'ai.model.provider': metadata.provider,
'ai.tokens.prompt': metadata.promptTokens,
'ai.tokens.completion': metadata.completionTokens,
'ai.tokens.total': metadata.promptTokens + metadata.completionTokens,
'ai.latency.total