All files / src/lib/observability metrics-recorder.ts

0% Statements 0/164
100% Branches 0/0
0% Functions 0/1
0% Lines 0/164

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165                                                                                                                                                                                                                                                                                                                                         
/**
 * Metrics Recorder
 *
 * Records HTTP metrics and traces to the database for monitoring.
 * Used by API routes to track performance and errors.
 */

import { prisma } from '@/lib/prisma';
import { logger } from './logger';

/**
 * Record an HTTP metric to the database
 */
export async function recordHttpMetric(data: {
  method: string;
  path: string;
  statusCode: number;
  duration: number;
  userId?: number;
  requestId?: string;
}): Promise<void> {
  try {
    await prisma.httpMetric.create({
      data: {
        method: data.method.substring(0, 10),
        path: data.path.substring(0, 500),
        statusCode: data.statusCode,
        duration: data.duration,
        userId: data.userId,
        requestId: data.requestId?.substring(0, 36),
        timestamp: new Date(),
      },
    });
  } catch (error) {
    // Don't let metric recording failures break the app
    logger.error('Failed to record HTTP metric', error as Error, { path: data.path });
  }
}

/**
 * Generate a trace ID (32 hex chars)
 */
export function generateTraceId(): string {
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

/**
 * Generate a span ID (16 hex chars)
 */
export function generateSpanId(): string {
  const bytes = new Uint8Array(8);
  crypto.getRandomValues(bytes);
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

/**
 * Create a trace with its root span
 */
export async function createTrace(data: {
  name: string;
  serviceName?: string;
  duration: number;
  status: 'ok' | 'error';
  userId?: number;
  requestId?: string;
  attributes?: Record<string, string | number | boolean>;
}): Promise<string | null> {
  try {
    const traceId = generateTraceId();
    const spanId = generateSpanId();
    const now = new Date();
    const startTime = new Date(now.getTime() - data.duration);

    // Create trace and root span in transaction
    await prisma.$transaction([
      prisma.trace.create({
        data: {
          id: traceId,
          name: data.name.substring(0, 200),
          serviceName: (data.serviceName || 'elite-events').substring(0, 100),
          startTime,
          endTime: now,
          duration: data.duration,
          status: data.status,
          userId: data.userId,
          requestId: data.requestId?.substring(0, 36),
        },
      }),
      prisma.span.create({
        data: {
          id: spanId,
          traceId,
          name: data.name.substring(0, 200),
          kind: 'server',
          startTime,
          endTime: now,
          duration: data.duration,
          status: data.status,
          attributes: data.attributes ? JSON.stringify(data.attributes) : undefined,
        },
      }),
    ]);

    return traceId;
  } catch (error) {
    logger.error('Failed to create trace', error as Error, { name: data.name });
    return null;
  }
}

/**
 * Higher-order function to wrap API handlers with metric recording
 */
export function withMetrics(
  handler: (request: Request) => Promise<Response>
): (request: Request) => Promise<Response> {
  return async (request: Request): Promise<Response> => {
    const startTime = Date.now();
    const url = new URL(request.url);

    let response: Response;

    try {
      response = await handler(request);
    } catch (error) {
      throw error;
    } finally {
      const duration = Date.now() - startTime;
      const statusCode = response! ? response.status : 500;

      // Record metric asynchronously (don't await)
      recordHttpMetric({
        method: request.method,
        path: url.pathname,
        statusCode,
        duration,
        requestId: request.headers.get('x-request-id') || undefined,
      }).catch(() => {
        // Ignore errors
      });

      // Create trace for slower requests or errors
      if (duration > 100 || statusCode >= 400) {
        createTrace({
          name: `${request.method} ${url.pathname}`,
          duration,
          status: statusCode >= 400 ? 'error' : 'ok',
          requestId: request.headers.get('x-request-id') || undefined,
          attributes: {
            'http.method': request.method,
            'http.url': url.pathname,
            'http.status_code': statusCode,
          },
        }).catch(() => {
          // Ignore errors
        });
      }
    }

    return response!;
  };
}