UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

553 lines (552 loc) 19.6 kB
import { TokenTracker } from "./tokenTracker.js"; /** * Metrics Aggregator for comprehensive telemetry analysis * Provides latency percentiles, token aggregation, and cost tracking */ export class MetricsAggregator { spans = []; latencyValues = []; tokenTracker; timeWindows = new Map(); config; spansByType = new Map(); costByProvider = new Map(); costByModel = new Map(); successCount = 0; failureCount = 0; firstSpanTime; lastSpanTime; constructor(config = {}) { this.config = { maxSpansRetained: config.maxSpansRetained ?? 10000, enableTimeWindows: config.enableTimeWindows ?? true, timeWindowMs: config.timeWindowMs ?? 60000, // 1 minute default maxTimeWindows: config.maxTimeWindows ?? 60, // 1 hour of 1-minute windows }; this.tokenTracker = new TokenTracker(); } /** * Record a span for metrics aggregation */ recordSpan(span) { // Enforce maximum spans limit if (this.spans.length >= this.config.maxSpansRetained) { const evicted = this.spans.shift(); // Remove oldest span // Only trim latencyValues when the evicted span had a duration recorded if (evicted?.durationMs !== undefined) { this.latencyValues.shift(); } // Note: We keep aggregated metrics, only raw spans and latency values are trimmed } this.spans.push(span); // Update timestamps const spanTime = new Date(span.startTime); if (!this.firstSpanTime || spanTime < this.firstSpanTime) { this.firstSpanTime = spanTime; } if (!this.lastSpanTime || spanTime > this.lastSpanTime) { this.lastSpanTime = spanTime; } // Track latency if duration is available if (span.durationMs !== undefined) { this.latencyValues.push(span.durationMs); } // Track success/failure if (span.status === 2) { // SpanStatus.ERROR = 2 this.failureCount++; } else { this.successCount++; } // Track by span type const currentCount = this.spansByType.get(span.type) ?? 0; this.spansByType.set(span.type, currentCount + 1); // Track tokens via TokenTracker this.tokenTracker.trackSpan(span); // Track cost by provider and model this.trackCosts(span); // Update time window if enabled if (this.config.enableTimeWindows) { this.updateTimeWindow(span); } } /** * Track cost aggregations from a span */ trackCosts(span) { const attrs = span.attributes; const provider = attrs["ai.provider"]; const model = attrs["ai.model"]; const inputCost = attrs["ai.cost.input"] ?? 0; const outputCost = attrs["ai.cost.output"] ?? 0; const totalCost = attrs["ai.cost.total"] ?? inputCost + outputCost; const inputTokens = attrs["ai.tokens.input"] ?? 0; const outputTokens = attrs["ai.tokens.output"] ?? 0; // Update provider costs if (provider) { const existing = this.costByProvider.get(provider) ?? { provider, totalCost: 0, requestCount: 0, avgCostPerRequest: 0, inputCost: 0, outputCost: 0, }; existing.totalCost += totalCost; existing.requestCount += 1; existing.avgCostPerRequest = existing.totalCost / existing.requestCount; existing.inputCost += inputCost; existing.outputCost += outputCost; this.costByProvider.set(provider, existing); } // Update model costs if (model) { const existing = this.costByModel.get(model) ?? { model, provider: provider ?? "unknown", totalCost: 0, requestCount: 0, avgCostPerRequest: 0, inputTokens: 0, outputTokens: 0, inputCost: 0, outputCost: 0, }; existing.totalCost += totalCost; existing.requestCount += 1; existing.avgCostPerRequest = existing.totalCost / existing.requestCount; existing.inputTokens += inputTokens; existing.outputTokens += outputTokens; existing.inputCost += inputCost; existing.outputCost += outputCost; this.costByModel.set(model, existing); } } /** * Update time window statistics */ updateTimeWindow(span) { const spanTime = new Date(span.startTime).getTime(); const windowKey = Math.floor(spanTime / this.config.timeWindowMs) * this.config.timeWindowMs; // Enforce maximum time windows if (this.timeWindows.size >= this.config.maxTimeWindows && !this.timeWindows.has(windowKey)) { // Remove oldest window const oldestKey = Math.min(...Array.from(this.timeWindows.keys())); this.timeWindows.delete(oldestKey); } let window = this.timeWindows.get(windowKey); if (!window) { window = this.createEmptyTimeWindow(windowKey); this.timeWindows.set(windowKey, window); } // Update window stats window.requestCount++; if (span.status === 2) { window.errorCount++; } window.successRate = (window.requestCount - window.errorCount) / window.requestCount; window.throughput = window.requestCount / (window.windowDurationMs / 1000); // Update window end time const spanEndTime = new Date(span.endTime ?? span.startTime); if (spanEndTime > window.windowEnd) { window.windowEnd = spanEndTime; } } /** * Create an empty time window */ createEmptyTimeWindow(windowKey) { return { windowStart: new Date(windowKey), windowEnd: new Date(windowKey + this.config.timeWindowMs), windowDurationMs: this.config.timeWindowMs, requestCount: 0, errorCount: 0, successRate: 1, throughput: 0, latency: this.createEmptyLatencyStats(), tokens: { totalInputTokens: 0, totalOutputTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, reasoningTokens: 0, totalCost: 0, byProvider: new Map(), byModel: new Map(), bySpanType: new Map(), }, costByProvider: new Map(), costByModel: new Map(), }; } /** * Create empty latency stats */ createEmptyLatencyStats() { return { min: 0, max: 0, mean: 0, median: 0, p50: 0, p75: 0, p90: 0, p95: 0, p99: 0, stdDev: 0, count: 0, }; } /** * Calculate latency percentile from sorted array */ calculatePercentile(sortedValues, percentile) { if (sortedValues.length === 0) { return 0; } const index = Math.ceil((percentile / 100) * sortedValues.length) - 1; return sortedValues[Math.max(0, index)] ?? 0; } /** * Calculate standard deviation */ calculateStdDev(values, mean) { if (values.length < 2) { return 0; } const squaredDiffs = values.map((v) => (v - mean) ** 2); const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / values.length; return Math.sqrt(avgSquaredDiff); } /** * Get comprehensive latency statistics */ getLatencyStats() { if (this.latencyValues.length === 0) { return this.createEmptyLatencyStats(); } const sorted = [...this.latencyValues].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); const mean = sum / sorted.length; return { min: sorted[0] ?? 0, max: sorted[sorted.length - 1] ?? 0, mean, median: this.calculatePercentile(sorted, 50), p50: this.calculatePercentile(sorted, 50), p75: this.calculatePercentile(sorted, 75), p90: this.calculatePercentile(sorted, 90), p95: this.calculatePercentile(sorted, 95), p99: this.calculatePercentile(sorted, 99), stdDev: this.calculateStdDev(sorted, mean), count: sorted.length, }; } /** * Get token usage statistics */ getTokenStats() { return this.tokenTracker.getStats(); } /** * Get cost breakdown by provider */ getCostByProvider() { return Array.from(this.costByProvider.values()); } /** * Get cost breakdown by model */ getCostByModel() { return Array.from(this.costByModel.values()); } /** * Get total cost across all providers */ getTotalCost() { let total = 0; const providerStatsArray = Array.from(this.costByProvider.values()); for (const stats of providerStatsArray) { total += stats.totalCost; } return total; } /** * Get time window statistics */ getTimeWindows() { return Array.from(this.timeWindows.values()).sort((a, b) => a.windowStart.getTime() - b.windowStart.getTime()); } /** * Get statistics for a specific time range */ getStatsForTimeRange(startTime, endTime) { const relevantSpans = this.spans.filter((span) => { const spanTime = new Date(span.startTime); return spanTime >= startTime && spanTime <= endTime; }); // Create a temporary aggregator for this time range const tempAggregator = new MetricsAggregator({ enableTimeWindows: false, }); for (const span of relevantSpans) { tempAggregator.recordSpan(span); } const summary = tempAggregator.getSummary(); const durationMs = endTime.getTime() - startTime.getTime(); return { windowStart: startTime, windowEnd: endTime, windowDurationMs: durationMs, requestCount: summary.totalSpans, errorCount: summary.failedSpans, successRate: summary.successRate, throughput: summary.totalSpans / (durationMs / 1000), latency: summary.latency, tokens: summary.tokens, costByProvider: new Map(summary.costByProvider.map((p) => [p.provider, p])), costByModel: new Map(summary.costByModel.map((m) => [m.model, m])), }; } /** * Record a latency measurement for an operation * Use this for standalone latency tracking without a full span */ recordLatency(operation, latencyMs) { this.latencyValues.push(latencyMs); // Track by operation type (similar to span type) const currentCount = this.spansByType.get(operation) ?? 0; this.spansByType.set(operation, currentCount + 1); // Update timestamps const now = new Date(); if (!this.firstSpanTime) { this.firstSpanTime = now; } this.lastSpanTime = now; // Increment success count (standalone latency records are assumed successful) this.successCount++; } /** * Get comprehensive metrics summary (alias for getSummary) */ getMetrics() { return this.getSummary(); } /** * Get comprehensive metrics summary */ getSummary() { const totalSpans = this.successCount + this.failureCount; return { totalSpans, successfulSpans: this.successCount, failedSpans: this.failureCount, successRate: totalSpans > 0 ? this.successCount / totalSpans : 1, latency: this.getLatencyStats(), tokens: this.getTokenStats(), costByProvider: this.getCostByProvider(), costByModel: this.getCostByModel(), totalCost: this.getTotalCost(), spansByType: Object.fromEntries(this.spansByType), firstSpanTime: this.firstSpanTime, lastSpanTime: this.lastSpanTime, trackingDurationMs: this.firstSpanTime && this.lastSpanTime ? this.lastSpanTime.getTime() - this.firstSpanTime.getTime() : undefined, }; } /** * Get all recorded spans (returns a copy) */ getSpans() { return [...this.spans]; } /** * Get spans grouped by traceId as hierarchical trace views */ getTraces() { const traceMap = new Map(); for (const span of this.spans) { const existing = traceMap.get(span.traceId) || []; existing.push(span); traceMap.set(span.traceId, existing); } const traces = []; for (const [traceId, spans] of traceMap) { // Root span is the one with no parentSpanId, or first span if all have parents const rootSpan = spans.find((s) => !s.parentSpanId) || spans[0]; const childSpans = spans.filter((s) => s !== rootSpan); // Calculate total duration let totalDurationMs = 0; for (const s of spans) { if (s.durationMs) { totalDurationMs = Math.max(totalDurationMs, s.durationMs); } } // Determine status const hasError = spans.some((s) => s.status === 2); // SpanStatus.ERROR const allOk = spans.every((s) => s.status === 1); // SpanStatus.OK const status = hasError ? "error" : allOk ? "ok" : "partial"; traces.push({ traceId, rootSpan, childSpans, totalDurationMs, spanCount: spans.length, status, }); } return traces; } /** * Get the underlying token tracker for custom pricing configuration */ getTokenTracker() { return this.tokenTracker; } /** * Reset all metrics */ reset() { this.spans = []; this.latencyValues = []; this.tokenTracker.reset(); this.timeWindows.clear(); this.spansByType.clear(); this.costByProvider.clear(); this.costByModel.clear(); this.successCount = 0; this.failureCount = 0; this.firstSpanTime = undefined; this.lastSpanTime = undefined; } /** * Export metrics as JSON */ toJSON() { const summary = this.getSummary(); return { totalSpans: summary.totalSpans, successfulSpans: summary.successfulSpans, failedSpans: summary.failedSpans, successRate: summary.successRate, latency: summary.latency, tokens: this.tokenTracker.toJSON(), costByProvider: summary.costByProvider, costByModel: summary.costByModel, totalCost: summary.totalCost, spansByType: summary.spansByType, firstSpanTime: summary.firstSpanTime?.toISOString(), lastSpanTime: summary.lastSpanTime?.toISOString(), trackingDurationMs: summary.trackingDurationMs, timeWindows: this.config.enableTimeWindows ? this.getTimeWindows().map((w) => ({ windowStart: w.windowStart.toISOString(), windowEnd: w.windowEnd.toISOString(), requestCount: w.requestCount, errorCount: w.errorCount, successRate: w.successRate, throughput: w.throughput, })) : undefined, }; } /** * Format cost as currency string */ formatCost(cost, currency = "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: 4, }).format(cost); } /** * Get a formatted summary string */ getFormattedSummary() { const summary = this.getSummary(); const latency = summary.latency; const lines = [ "=== Metrics Summary ===", "", "Request Statistics:", ` Total requests: ${summary.totalSpans.toLocaleString()}`, ` Successful: ${summary.successfulSpans.toLocaleString()}`, ` Failed: ${summary.failedSpans.toLocaleString()}`, ` Success rate: ${(summary.successRate * 100).toFixed(2)}%`, "", "Latency (ms):", ` Min: ${latency.min.toFixed(2)}`, ` Max: ${latency.max.toFixed(2)}`, ` Mean: ${latency.mean.toFixed(2)}`, ` P50: ${latency.p50.toFixed(2)}`, ` P95: ${latency.p95.toFixed(2)}`, ` P99: ${latency.p99.toFixed(2)}`, "", "Token Usage:", ` Input tokens: ${summary.tokens.totalInputTokens.toLocaleString()}`, ` Output tokens: ${summary.tokens.totalOutputTokens.toLocaleString()}`, ` Total tokens: ${summary.tokens.totalTokens.toLocaleString()}`, "", "Cost:", ` Total: ${this.formatCost(summary.totalCost)}`, ]; // Add cost by provider if (summary.costByProvider.length > 0) { lines.push(""); lines.push("Cost by Provider:"); for (const providerCost of summary.costByProvider) { lines.push(` ${providerCost.provider}: ${this.formatCost(providerCost.totalCost)} (${providerCost.requestCount} requests)`); } } // Add cost by model if (summary.costByModel.length > 0) { lines.push(""); lines.push("Cost by Model:"); for (const modelCost of summary.costByModel) { lines.push(` ${modelCost.model}: ${this.formatCost(modelCost.totalCost)} (${modelCost.requestCount} requests)`); } } // Add tracking duration if (summary.trackingDurationMs) { const durationSec = summary.trackingDurationMs / 1000; const throughput = summary.totalSpans / durationSec; lines.push(""); lines.push(`Tracking duration: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`); } return lines.join("\n"); } } /** * Global metrics aggregator instance (singleton pattern from main) */ let globalMetricsAggregator = null; /** * Get the global metrics aggregator instance */ export function getMetricsAggregator() { if (!globalMetricsAggregator) { globalMetricsAggregator = new MetricsAggregator(); } return globalMetricsAggregator; } /** * Reset the global metrics aggregator (for testing) */ export function resetMetricsAggregator() { if (globalMetricsAggregator) { globalMetricsAggregator.reset(); } globalMetricsAggregator = null; }