497 lines · 18.1 KB · TypeScript
RawDownload
// CRITICALimport type { Hono } from "hono";import Database from "bun:sqlite";import type { AppContext } from "../types/context"; /** * Register usage analytics routes. * Uses SQLite spend_logs table as single source of truth. * Falls back to empty data if table doesn't exist. * @param app - Hono app. * @param context - App context. */export const registerUsageRoutes = (app: Hono, context: AppContext): void => {  /**   * Calculate percentage change.   * @param current - Current value.   * @param previous - Previous value.   * @returns Change percent or null.   */  const calcChange = (current: number, previous: number): number | null => {    if (!previous || previous === 0) {      return null;    }    return Math.round(((current - previous) / previous) * 1000) / 10;  };   /**   * Create empty usage stats response.   * @returns Empty usage response object.   */  const emptyResponse = (): Record<string, unknown> => ({    totals: {      total_tokens: 0,      prompt_tokens: 0,      completion_tokens: 0,      total_requests: 0,      successful_requests: 0,      failed_requests: 0,      success_rate: 0,      unique_sessions: 0,      unique_users: 0,    },    latency: {      avg_ms: 0,      p50_ms: 0,      p95_ms: 0,      p99_ms: 0,      min_ms: 0,      max_ms: 0,    },    ttft: {      avg_ms: 0,      p50_ms: 0,      p95_ms: 0,      p99_ms: 0,    },    tokens_per_request: {      avg: 0,      avg_prompt: 0,      avg_completion: 0,      max: 0,      p50: 0,      p95: 0,    },    cache: {      hits: 0,      misses: 0,      hit_tokens: 0,      miss_tokens: 0,      hit_rate: 0,    },    week_over_week: {      this_week: { requests: 0, tokens: 0, successful: 0 },      last_week: { requests: 0, tokens: 0, successful: 0 },      change_pct: { requests: null, tokens: null },    },    recent_activity: {      last_hour_requests: 0,      last_24h_requests: 0,      prev_24h_requests: 0,      last_24h_tokens: 0,      change_24h_pct: null,    },    peak_days: [],    peak_hours: [],    by_model: [],    daily: [],    hourly_pattern: [],  });   app.get("/usage", async (ctx) => {    let db: Database | null = null;    try {      db = new Database(context.config.db_path, { readonly: true });       // Check if spend_logs table exists      const tableCheck = db.query<{ name: string }, []>(        `SELECT name FROM sqlite_master WHERE type='table' AND name='spend_logs'`      ).get();       if (!tableCheck) {        // Table doesn't exist, return empty data        return ctx.json(emptyResponse());      }       // Totals      const totals = db.query<{        total_tokens: number;        prompt_tokens: number;        completion_tokens: number;        total_requests: number;        successful_requests: number;        failed_requests: number;        unique_sessions: number;      }, []>(`        SELECT          COALESCE(SUM(total_tokens), 0) as total_tokens,          COALESCE(SUM(prompt_tokens), 0) as prompt_tokens,          COALESCE(SUM(completion_tokens), 0) as completion_tokens,          COUNT(*) as total_requests,          SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_requests,          SUM(CASE WHEN status != 'success' OR status IS NULL THEN 1 ELSE 0 END) as failed_requests,          COUNT(DISTINCT session_id) as unique_sessions        FROM spend_logs      `).get() ?? { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, total_requests: 0, successful_requests: 0, failed_requests: 0, unique_sessions: 0 };       // If no data at all, return empty response      if (totals.total_requests === 0) {        return ctx.json(emptyResponse());      }       // Latency stats (using julianday for time calculations)      const latency = db.query<{        avg_ms: number;        min_ms: number;        max_ms: number;      }, []>(`        SELECT          AVG((julianday(end_time) - julianday(start_time)) * 86400000) as avg_ms,          MIN((julianday(end_time) - julianday(start_time)) * 86400000) as min_ms,          MAX((julianday(end_time) - julianday(start_time)) * 86400000) as max_ms        FROM spend_logs        WHERE status = 'success' AND end_time IS NOT NULL AND start_time IS NOT NULL      `).get() ?? { avg_ms: 0, min_ms: 0, max_ms: 0 };       // Percentiles need to be calculated differently in SQLite      const latencyPercentiles = db.query<{ latency_ms: number }, []>(`        SELECT (julianday(end_time) - julianday(start_time)) * 86400000 as latency_ms        FROM spend_logs        WHERE status = 'success' AND end_time IS NOT NULL AND start_time IS NOT NULL        ORDER BY latency_ms      `).all();       const getPercentile = (sorted: { latency_ms: number }[], p: number): number => {        if (sorted.length === 0) return 0;        const index = Math.floor(sorted.length * p);        return Math.round(sorted[Math.min(index, sorted.length - 1)]?.latency_ms ?? 0);      };       // TTFT stats      const ttft = db.query<{        avg_ms: number;      }, []>(`        SELECT          AVG((julianday(completion_start_time) - julianday(start_time)) * 86400000) as avg_ms        FROM spend_logs        WHERE status = 'success'          AND completion_start_time IS NOT NULL          AND completion_start_time != ''          AND start_time IS NOT NULL      `).get() ?? { avg_ms: 0 };       const ttftPercentiles = db.query<{ ttft_ms: number }, []>(`        SELECT (julianday(completion_start_time) - julianday(start_time)) * 86400000 as ttft_ms        FROM spend_logs        WHERE status = 'success'          AND completion_start_time IS NOT NULL          AND completion_start_time != ''          AND start_time IS NOT NULL        ORDER BY ttft_ms      `).all();       // Token stats      const tokenStats = db.query<{        avg_tokens: number;        avg_prompt: number;        avg_completion: number;        max_tokens: number;      }, []>(`        SELECT          AVG(total_tokens) as avg_tokens,          AVG(prompt_tokens) as avg_prompt,          AVG(completion_tokens) as avg_completion,          MAX(total_tokens) as max_tokens        FROM spend_logs        WHERE status = 'success' AND total_tokens IS NOT NULL      `).get() ?? { avg_tokens: 0, avg_prompt: 0, avg_completion: 0, max_tokens: 0 };       const tokenPercentiles = db.query<{ tokens: number }, []>(`        SELECT total_tokens as tokens        FROM spend_logs        WHERE status = 'success' AND total_tokens IS NOT NULL        ORDER BY tokens      `).all();       // Cache stats      const cacheRows = db.query<{        cache_hit: string | null;        count: number;        tokens: number;      }, []>(`        SELECT          cache_hit,          COUNT(*) as count,          COALESCE(SUM(total_tokens), 0) as tokens        FROM spend_logs        GROUP BY cache_hit      `).all();       const cache = { hits: 0, misses: 0, hit_tokens: 0, miss_tokens: 0, hit_rate: 0 };      for (const row of cacheRows) {        if (row.cache_hit === "True") {          cache.hits = row.count;          cache.hit_tokens = row.tokens;        } else if (row.cache_hit === "False" || row.cache_hit === null) {          cache.misses += row.count;          cache.miss_tokens += row.tokens;        }      }      const totalCache = cache.hits + cache.misses;      if (totalCache > 0) {        cache.hit_rate = Math.round((cache.hits / totalCache) * 10000) / 100;      }       // By model      const byModel = db.query<{        model: string;        requests: number;        successful: number;        total_tokens: number;        prompt_tokens: number;        completion_tokens: number;        avg_tokens: number;        avg_latency_ms: number;        avg_ttft_ms: number;      }, []>(`        SELECT          model,          COUNT(*) as requests,          SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful,          COALESCE(SUM(total_tokens), 0) as total_tokens,          COALESCE(SUM(prompt_tokens), 0) as prompt_tokens,          COALESCE(SUM(completion_tokens), 0) as completion_tokens,          AVG(CASE WHEN status = 'success' THEN total_tokens END) as avg_tokens,          AVG(CASE WHEN status = 'success' THEN (julianday(end_time) - julianday(start_time)) * 86400000 END) as avg_latency_ms,          AVG(CASE WHEN status = 'success' AND completion_start_time IS NOT NULL AND completion_start_time != ''              THEN (julianday(completion_start_time) - julianday(start_time)) * 86400000 END) as avg_ttft_ms        FROM spend_logs        WHERE model != '' AND model IS NOT NULL        GROUP BY model        ORDER BY total_tokens DESC        LIMIT 25      `).all();       // Daily stats (last 14 days)      const daily = db.query<{        date: string;        requests: number;        successful: number;        total_tokens: number;        prompt_tokens: number;        completion_tokens: number;        avg_latency_ms: number;      }, []>(`        SELECT          DATE(start_time) as date,          COUNT(*) as requests,          SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful,          COALESCE(SUM(total_tokens), 0) as total_tokens,          COALESCE(SUM(prompt_tokens), 0) as prompt_tokens,          COALESCE(SUM(completion_tokens), 0) as completion_tokens,          AVG(CASE WHEN status = 'success' THEN (julianday(end_time) - julianday(start_time)) * 86400000 END) as avg_latency_ms        FROM spend_logs        WHERE DATE(start_time) >= DATE('now', '-14 days')        GROUP BY DATE(start_time)        ORDER BY date DESC      `).all();       // Hourly pattern      const hourly = db.query<{        hour: number;        requests: number;        successful: number;        tokens: number;      }, []>(`        SELECT          CAST(strftime('%H', start_time) AS INTEGER) as hour,          COUNT(*) as requests,          SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful,          COALESCE(SUM(total_tokens), 0) as tokens        FROM spend_logs        WHERE start_time IS NOT NULL        GROUP BY strftime('%H', start_time)        ORDER BY hour      `).all();       // Week over week      const wow = db.query<{        this_week_requests: number;        last_week_requests: number;        this_week_tokens: number;        last_week_tokens: number;        this_week_successful: number;        last_week_successful: number;      }, []>(`        SELECT          SUM(CASE WHEN DATE(start_time) >= DATE('now', '-7 days') THEN 1 ELSE 0 END) as this_week_requests,          SUM(CASE WHEN DATE(start_time) >= DATE('now', '-14 days') AND DATE(start_time) < DATE('now', '-7 days') THEN 1 ELSE 0 END) as last_week_requests,          SUM(CASE WHEN DATE(start_time) >= DATE('now', '-7 days') AND status = 'success' THEN total_tokens ELSE 0 END) as this_week_tokens,          SUM(CASE WHEN DATE(start_time) >= DATE('now', '-14 days') AND DATE(start_time) < DATE('now', '-7 days') AND status = 'success' THEN total_tokens ELSE 0 END) as last_week_tokens,          SUM(CASE WHEN DATE(start_time) >= DATE('now', '-7 days') AND status = 'success' THEN 1 ELSE 0 END) as this_week_successful,          SUM(CASE WHEN DATE(start_time) >= DATE('now', '-14 days') AND DATE(start_time) < DATE('now', '-7 days') AND status = 'success' THEN 1 ELSE 0 END) as last_week_successful        FROM spend_logs      `).get() ?? { this_week_requests: 0, last_week_requests: 0, this_week_tokens: 0, last_week_tokens: 0, this_week_successful: 0, last_week_successful: 0 };       // Peak days      const peakDays = db.query<{        date: string;        requests: number;        tokens: number;      }, []>(`        SELECT          DATE(start_time) as date,          COUNT(*) as requests,          COALESCE(SUM(total_tokens), 0) as tokens        FROM spend_logs        WHERE status = 'success'        GROUP BY DATE(start_time)        ORDER BY requests DESC        LIMIT 5      `).all();       // Peak hours      const peakHours = db.query<{        hour: number;        requests: number;      }, []>(`        SELECT          CAST(strftime('%H', start_time) AS INTEGER) as hour,          COUNT(*) as requests        FROM spend_logs        WHERE DATE(start_time) >= DATE('now', '-7 days')        GROUP BY strftime('%H', start_time)        ORDER BY requests DESC        LIMIT 5      `).all();       // Recent activity      const recent = db.query<{        last_24h_requests: number;        prev_24h_requests: number;        last_24h_tokens: number;        last_hour_requests: number;      }, []>(`        SELECT          SUM(CASE WHEN datetime(start_time) >= datetime('now', '-24 hours') THEN 1 ELSE 0 END) as last_24h_requests,          SUM(CASE WHEN datetime(start_time) >= datetime('now', '-48 hours') AND datetime(start_time) < datetime('now', '-24 hours') THEN 1 ELSE 0 END) as prev_24h_requests,          SUM(CASE WHEN datetime(start_time) >= datetime('now', '-24 hours') AND status = 'success' THEN total_tokens ELSE 0 END) as last_24h_tokens,          SUM(CASE WHEN datetime(start_time) >= datetime('now', '-1 hour') THEN 1 ELSE 0 END) as last_hour_requests        FROM spend_logs      `).get() ?? { last_24h_requests: 0, prev_24h_requests: 0, last_24h_tokens: 0, last_hour_requests: 0 };       // Format by_model with tokens_per_sec      const modelsFormatted = byModel.map((row) => {        const avgLatencyMs = row.avg_latency_ms ?? 0;        const completionTokens = row.completion_tokens;        let tokensPerSec: number | null = null;        if (avgLatencyMs > 0 && row.successful > 0) {          const avgCompletion = completionTokens / row.successful;          tokensPerSec = avgCompletion ? Math.round((avgCompletion / (avgLatencyMs / 1000)) * 10) / 10 : null;        }        return {          model: row.model,          requests: row.requests,          successful: row.successful,          success_rate: row.requests ? Math.round((row.successful / row.requests) * 1000) / 10 : 0,          total_tokens: row.total_tokens,          prompt_tokens: row.prompt_tokens,          completion_tokens: completionTokens,          avg_tokens: Math.round(row.avg_tokens ?? 0),          avg_latency_ms: Math.round(avgLatencyMs),          p50_latency_ms: Math.round(avgLatencyMs), // Approximation          avg_ttft_ms: Math.round(row.avg_ttft_ms ?? 0),          tokens_per_sec: tokensPerSec,        };      });       const successRate = totals.total_requests        ? Math.round((totals.successful_requests / totals.total_requests) * 10000) / 100        : 0;       return ctx.json({        totals: {          total_tokens: totals.total_tokens,          prompt_tokens: totals.prompt_tokens,          completion_tokens: totals.completion_tokens,          total_requests: totals.total_requests,          successful_requests: totals.successful_requests,          failed_requests: totals.failed_requests,          success_rate: successRate,          unique_sessions: totals.unique_sessions,          unique_users: 2, // Hardcoded for now        },        latency: {          avg_ms: Math.round(latency.avg_ms ?? 0),          p50_ms: getPercentile(latencyPercentiles, 0.5),          p95_ms: getPercentile(latencyPercentiles, 0.95),          p99_ms: getPercentile(latencyPercentiles, 0.99),          min_ms: Math.round(latency.min_ms ?? 0),          max_ms: Math.round(latency.max_ms ?? 0),        },        ttft: {          avg_ms: Math.round(ttft.avg_ms ?? 0),          p50_ms: getPercentile(ttftPercentiles.map(r => ({ latency_ms: r.ttft_ms })), 0.5),          p95_ms: getPercentile(ttftPercentiles.map(r => ({ latency_ms: r.ttft_ms })), 0.95),          p99_ms: getPercentile(ttftPercentiles.map(r => ({ latency_ms: r.ttft_ms })), 0.99),        },        tokens_per_request: {          avg: Math.round(tokenStats.avg_tokens ?? 0),          avg_prompt: Math.round(tokenStats.avg_prompt ?? 0),          avg_completion: Math.round(tokenStats.avg_completion ?? 0),          max: tokenStats.max_tokens ?? 0,          p50: getPercentile(tokenPercentiles.map(r => ({ latency_ms: r.tokens })), 0.5),          p95: getPercentile(tokenPercentiles.map(r => ({ latency_ms: r.tokens })), 0.95),        },        cache,        week_over_week: {          this_week: {            requests: wow.this_week_requests,            tokens: wow.this_week_tokens,            successful: wow.this_week_successful,          },          last_week: {            requests: wow.last_week_requests,            tokens: wow.last_week_tokens,            successful: wow.last_week_successful,          },          change_pct: {            requests: calcChange(wow.this_week_requests, wow.last_week_requests),            tokens: calcChange(wow.this_week_tokens, wow.last_week_tokens),          },        },        recent_activity: {          last_hour_requests: recent.last_hour_requests,          last_24h_requests: recent.last_24h_requests,          prev_24h_requests: recent.prev_24h_requests,          last_24h_tokens: recent.last_24h_tokens,          change_24h_pct: calcChange(recent.last_24h_requests, recent.prev_24h_requests),        },        peak_days: peakDays.map((row) => ({          date: row.date,          requests: row.requests,          tokens: row.tokens,        })),        peak_hours: peakHours.map((row) => ({          hour: row.hour,          requests: row.requests,        })),        by_model: modelsFormatted,        daily: daily.map((row) => ({          date: row.date,          requests: row.requests,          successful: row.successful,          success_rate: row.requests ? Math.round((row.successful / row.requests) * 1000) / 10 : 0,          total_tokens: row.total_tokens,          prompt_tokens: row.prompt_tokens,          completion_tokens: row.completion_tokens,          avg_latency_ms: Math.round(row.avg_latency_ms ?? 0),        })),        hourly_pattern: hourly.map((row) => ({          hour: row.hour,          requests: row.requests,          successful: row.successful,          tokens: row.tokens,        })),      });    } catch (error) {      // On any error, return empty data instead of throwing      console.error("[Usage] Error fetching usage stats:", error);      return ctx.json(emptyResponse());    } finally {      if (db) {        db.close();      }    }  });};