Skip to main content
Glama
aggregator.ts9.73 kB
/** * Analytics Aggregator - SQL queries for usage analytics */ import { Pool } from 'pg'; import type { AnalyticsQuery, AnalyticsResponse, LayerMetrics, ModelMetrics, DailyMetrics, } from '../types/tracing.js'; export class AnalyticsAggregator { constructor(private db: Pool) { } async getAnalytics( query: AnalyticsQuery, ): Promise<AnalyticsResponse> { const { projectId, userId, startDate, endDate, groupBy = 'day' } = query; // Build WHERE clause const conditions: string[] = ['1=1']; const params: unknown[] = []; let paramIndex = 1; if (projectId) { params.push(projectId); conditions.push(`c.project_id = $${paramIndex++}`); } if (userId) { params.push(userId); conditions.push(`c.user_id = $${paramIndex++}`); } if (startDate) { params.push(startDate); conditions.push(`rt.created_at >= $${paramIndex++}`); } if (endDate) { params.push(endDate); conditions.push(`rt.created_at <= $${paramIndex++}`); } const whereClause = conditions.join(' AND '); // Get totals const totalsQuery = ` SELECT COUNT(*) as total_requests, COALESCE(SUM((llm_call->>'inputTokens')::int), 0) as input_tokens, COALESCE(SUM((llm_call->>'outputTokens')::int), 0) as output_tokens, COALESCE(SUM(total_cost), 0) as total_cost FROM request_traces rt LEFT JOIN conversations c ON rt.conversation_id = c.id CROSS JOIN LATERAL jsonb_array_elements(rt.llm_calls) AS llm_call WHERE ${whereClause} `; const totalsResult = await this.db.query(totalsQuery, params); const totals = totalsResult.rows[0]; // Get breakdown by layer const byLayer = await this.getLayerBreakdown(whereClause, params); // Get breakdown by model const byModel = await this.getModelBreakdown(whereClause, params); // Get daily metrics const byDay = await this.getDailyMetrics(whereClause, params, groupBy); return { timeRange: { start: startDate || 'all', end: endDate || 'all', }, totalRequests: parseInt(totals.total_requests), totalTokens: { input: parseInt(totals.input_tokens), output: parseInt(totals.output_tokens), total: parseInt(totals.input_tokens) + parseInt(totals.output_tokens), }, totalCost: parseFloat(totals.total_cost), breakdown: { byLayer, byModel, byDay, }, }; } private async getLayerBreakdown( whereClause: string, params: unknown[], ): Promise<Record<string, LayerMetrics>> { const query = ` SELECT routing_decision->>'layer' as layer, COUNT(*) as requests, COALESCE(SUM((llm_call->>'inputTokens')::int + (llm_call->>'outputTokens')::int), 0) as tokens, COALESCE(SUM(total_cost), 0) as cost, COALESCE(AVG(total_duration_ms), 0) as avg_duration_ms, (1.0 - COALESCE(SUM(CASE WHEN error_info IS NOT NULL THEN 1 ELSE 0 END)::float / NULLIF(COUNT(*), 0), 0)) as success_rate FROM request_traces rt LEFT JOIN conversations c ON rt.conversation_id = c.id CROSS JOIN LATERAL jsonb_array_elements(rt.routing_decisions) AS routing_decision LEFT JOIN LATERAL jsonb_array_elements(rt.llm_calls) AS llm_call ON true WHERE ${whereClause} GROUP BY routing_decision->>'layer' `; const result = await this.db.query(query, params); const breakdown: Record<string, LayerMetrics> = {}; for (const row of result.rows) { breakdown[row.layer] = { requests: parseInt(row.requests), tokens: parseInt(row.tokens), cost: parseFloat(row.cost), avgDurationMs: parseFloat(row.avg_duration_ms), successRate: parseFloat(row.success_rate), }; } return breakdown; } private async getModelBreakdown( whereClause: string, params: unknown[], ): Promise<Record<string, ModelMetrics>> { const query = ` SELECT llm_call->>'model' as model, COUNT(*) as requests, COALESCE(SUM((llm_call->>'inputTokens')::int), 0) as input_tokens, COALESCE(SUM((llm_call->>'outputTokens')::int), 0) as output_tokens, COALESCE(SUM((llm_call->>'cost')::float), 0) as cost, COALESCE(AVG((llm_call->>'durationMs')::int), 0) as avg_duration_ms, SUM(CASE WHEN llm_call->>'error' IS NOT NULL THEN 1 ELSE 0 END) as errors FROM request_traces rt LEFT JOIN conversations c ON rt.conversation_id = c.id CROSS JOIN LATERAL jsonb_array_elements(rt.llm_calls) AS llm_call WHERE ${whereClause} GROUP BY llm_call->>'model' `; const result = await this.db.query(query, params); const breakdown: Record<string, ModelMetrics> = {}; for (const row of result.rows) { breakdown[row.model] = { requests: parseInt(row.requests), tokens: { input: parseInt(row.input_tokens), output: parseInt(row.output_tokens), total: parseInt(row.input_tokens) + parseInt(row.output_tokens), }, cost: parseFloat(row.cost), avgDurationMs: parseFloat(row.avg_duration_ms), errors: parseInt(row.errors), }; } return breakdown; } private async getDailyMetrics( whereClause: string, params: unknown[], groupBy: 'day' | 'week' | 'month', ): Promise<DailyMetrics[]> { const dateFormat = groupBy === 'day' ? 'YYYY-MM-DD' : groupBy === 'week' ? 'YYYY-"W"IW' : 'YYYY-MM'; const query = ` SELECT TO_CHAR(rt.created_at, '${dateFormat}') as date, COUNT(*) as requests, COALESCE(SUM((llm_call->>'inputTokens')::int + (llm_call->>'outputTokens')::int), 0) as tokens, COALESCE(SUM(total_cost), 0) as cost, COALESCE(AVG(total_duration_ms), 0) as avg_duration_ms FROM request_traces rt LEFT JOIN conversations c ON rt.conversation_id = c.id LEFT JOIN LATERAL jsonb_array_elements(rt.llm_calls) AS llm_call ON true WHERE ${whereClause} GROUP BY TO_CHAR(rt.created_at, '${dateFormat}') ORDER BY date DESC LIMIT 30 `; const result = await this.db.query(query, params); return result.rows.map((row) => ({ date: row.date, requests: parseInt(row.requests), tokens: parseInt(row.tokens), cost: parseFloat(row.cost), avgDurationMs: parseFloat(row.avg_duration_ms), })); } /** * Get top N most expensive requests */ async getTopExpensiveRequests( projectId: string, limit = 10, ): Promise<Array<{ traceId: string; cost: number; durationMs: number; requestType: string; createdAt: Date; }>> { const query = ` SELECT rt.id as trace_id, rt.total_cost as cost, rt.total_duration_ms as duration_ms, rt.request_type, rt.created_at FROM request_traces rt LEFT JOIN conversations c ON rt.conversation_id = c.id WHERE c.project_id = $1 ORDER BY rt.total_cost DESC LIMIT $2 `; const result = await this.db.query(query, [projectId, limit]); return result.rows.map((row) => ({ traceId: row.trace_id, cost: parseFloat(row.cost), durationMs: row.duration_ms, requestType: row.request_type, createdAt: row.created_at, })); } /** * Get error rate by model */ async getErrorRateByModel( projectId: string, ): Promise<Array<{ model: string; totalCalls: number; errors: number; errorRate: number; }>> { const query = ` SELECT llm_call->>'model' as model, COUNT(*) as total_calls, SUM(CASE WHEN llm_call->>'error' IS NOT NULL THEN 1 ELSE 0 END) as errors, (SUM(CASE WHEN llm_call->>'error' IS NOT NULL THEN 1 ELSE 0 END)::float / COUNT(*)) as error_rate FROM request_traces rt LEFT JOIN conversations c ON rt.conversation_id = c.id CROSS JOIN LATERAL jsonb_array_elements(rt.llm_calls) AS llm_call WHERE c.project_id = $1 GROUP BY llm_call->>'model' ORDER BY error_rate DESC `; const result = await this.db.query(query, [projectId]); return result.rows.map((row) => ({ model: row.model, totalCalls: parseInt(row.total_calls), errors: parseInt(row.errors), errorRate: parseFloat(row.error_rate), })); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server