All files / src/lib/observability metrics.ts

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

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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
/**
 * Metrics Service
 *
 * Application metrics collection using OpenTelemetry.
 * Use these utilities to record business and performance metrics.
 */

import { metrics } from '@opentelemetry/api';

// Get the meter for this service
const meter = metrics.getMeter('elite-events');

// ============================================================================
// HTTP Request Metrics
// ============================================================================

/**
 * Counter for total HTTP requests
 */
export const httpRequestCounter = meter.createCounter('http_requests_total', {
  description: 'Total number of HTTP requests',
  unit: '1',
});

/**
 * Histogram for HTTP request duration
 */
export const httpRequestDuration = meter.createHistogram('http_request_duration_ms', {
  description: 'HTTP request duration in milliseconds',
  unit: 'ms',
});

/**
 * Histogram for HTTP request size
 */
export const httpRequestSize = meter.createHistogram('http_request_size_bytes', {
  description: 'HTTP request body size in bytes',
  unit: 'bytes',
});

/**
 * Histogram for HTTP response size
 */
export const httpResponseSize = meter.createHistogram('http_response_size_bytes', {
  description: 'HTTP response body size in bytes',
  unit: 'bytes',
});

// ============================================================================
// Business Metrics
// ============================================================================

/**
 * Counter for total orders
 */
export const ordersCounter = meter.createCounter('orders_total', {
  description: 'Total number of orders',
  unit: '1',
});

/**
 * Histogram for order values
 */
export const orderValueHistogram = meter.createHistogram('order_value_cents', {
  description: 'Order value distribution in cents',
  unit: 'cents',
});

/**
 * Counter for cart abandonment
 */
export const cartAbandonmentCounter = meter.createCounter('cart_abandonment_total', {
  description: 'Number of abandoned carts',
  unit: '1',
});

/**
 * Counter for user registrations
 */
export const userRegistrationCounter = meter.createCounter('user_registrations_total', {
  description: 'Total number of user registrations',
  unit: '1',
});

/**
 * Counter for successful logins
 */
export const loginCounter = meter.createCounter('logins_total', {
  description: 'Total number of login attempts',
  unit: '1',
});

/**
 * Counter for product views
 */
export const productViewCounter = meter.createCounter('product_views_total', {
  description: 'Total number of product views',
  unit: '1',
});

/**
 * Counter for add to cart actions
 */
export const addToCartCounter = meter.createCounter('add_to_cart_total', {
  description: 'Total number of add to cart actions',
  unit: '1',
});

// ============================================================================
// Performance Metrics
// ============================================================================

/**
 * Histogram for database query duration
 */
export const databaseQueryDuration = meter.createHistogram('db_query_duration_ms', {
  description: 'Database query duration in milliseconds',
  unit: 'ms',
});

/**
 * Counter for database errors
 */
export const databaseErrorCounter = meter.createCounter('db_errors_total', {
  description: 'Total number of database errors',
  unit: '1',
});

/**
 * Histogram for cache operations
 */
export const cacheOperationDuration = meter.createHistogram('cache_operation_duration_ms', {
  description: 'Cache operation duration in milliseconds',
  unit: 'ms',
});

/**
 * Counter for cache hits/misses
 */
export const cacheHitCounter = meter.createCounter('cache_hits_total', {
  description: 'Total number of cache hits',
  unit: '1',
});

export const cacheMissCounter = meter.createCounter('cache_misses_total', {
  description: 'Total number of cache misses',
  unit: '1',
});

/**
 * Histogram for external API call duration
 */
export const externalApiDuration = meter.createHistogram('external_api_duration_ms', {
  description: 'External API call duration in milliseconds',
  unit: 'ms',
});

// ============================================================================
// System Metrics
// ============================================================================

/**
 * UpDownCounter for active connections
 */
export const activeConnections = meter.createUpDownCounter('active_connections', {
  description: 'Number of active connections',
  unit: '1',
});

/**
 * UpDownCounter for WebSocket connections
 */
export const websocketConnections = meter.createUpDownCounter('websocket_connections', {
  description: 'Number of active WebSocket connections',
  unit: '1',
});

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Record an HTTP request with all relevant metrics
 */
export function recordHttpRequest(
  method: string,
  path: string,
  statusCode: number,
  durationMs: number,
  options?: {
    requestSizeBytes?: number;
    responseSizeBytes?: number;
  }
): void {
  const attributes = {
    method,
    path: normalizePath(path),
    status_code: statusCode,
    status_class: `${Math.floor(statusCode / 100)}xx`,
  };

  httpRequestCounter.add(1, attributes);
  httpRequestDuration.record(durationMs, attributes);

  if (options?.requestSizeBytes !== undefined) {
    httpRequestSize.record(options.requestSizeBytes, attributes);
  }

  if (options?.responseSizeBytes !== undefined) {
    httpResponseSize.record(options.responseSizeBytes, attributes);
  }
}

/**
 * Record a database query
 */
export function recordDatabaseQuery(
  operation: string,
  table: string,
  durationMs: number,
  success: boolean = true
): void {
  const attributes = {
    operation,
    table,
    success: String(success),
  };

  databaseQueryDuration.record(durationMs, attributes);

  if (!success) {
    databaseErrorCounter.add(1, attributes);
  }
}

/**
 * Record a cache operation
 */
export function recordCacheOperation(
  operation: 'get' | 'set' | 'delete',
  hit: boolean,
  durationMs: number
): void {
  const attributes = { operation };

  cacheOperationDuration.record(durationMs, attributes);

  if (operation === 'get') {
    if (hit) {
      cacheHitCounter.add(1, attributes);
    } else {
      cacheMissCounter.add(1, attributes);
    }
  }
}

/**
 * Record an external API call
 */
export function recordExternalApiCall(
  service: string,
  operation: string,
  durationMs: number,
  success: boolean
): void {
  externalApiDuration.record(durationMs, {
    service,
    operation,
    success: String(success),
  });
}

/**
 * Record an order
 */
export function recordOrder(
  valueInCents: number,
  status: 'created' | 'paid' | 'cancelled' | 'refunded'
): void {
  ordersCounter.add(1, { status });
  orderValueHistogram.record(valueInCents, { status });
}

/**
 * Record a user login
 */
export function recordLogin(success: boolean, method: string = 'credentials'): void {
  loginCounter.add(1, {
    success: String(success),
    method,
  });
}

/**
 * Record a product view
 */
export function recordProductView(productId: string, categoryId?: string): void {
  productViewCounter.add(1, {
    product_id: productId,
    ...(categoryId && { category_id: categoryId }),
  });
}

/**
 * Record add to cart action
 */
export function recordAddToCart(productId: string, quantity: number): void {
  addToCartCounter.add(quantity, {
    product_id: productId,
  });
}

/**
 * Record cart abandonment
 */
export function recordCartAbandonment(itemCount: number, valueInCents: number): void {
  cartAbandonmentCounter.add(1, {
    item_count_bucket: getItemCountBucket(itemCount),
    value_bucket: getValueBucket(valueInCents),
  });
}

/**
 * Increment active connections
 */
export function incrementConnections(type: 'http' | 'websocket' = 'http'): void {
  if (type === 'websocket') {
    websocketConnections.add(1);
  } else {
    activeConnections.add(1);
  }
}

/**
 * Decrement active connections
 */
export function decrementConnections(type: 'http' | 'websocket' = 'http'): void {
  if (type === 'websocket') {
    websocketConnections.add(-1);
  } else {
    activeConnections.add(-1);
  }
}

// ============================================================================
// Utility Functions
// ============================================================================

/**
 * Normalize path by replacing dynamic segments with placeholders
 */
function normalizePath(path: string): string {
  return path
    // Replace UUIDs
    .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id')
    // Replace numeric IDs
    .replace(/\/\d+/g, '/:id')
    // Replace slugs (alphanumeric with hyphens)
    .replace(/\/[a-z0-9]+-[a-z0-9-]+/gi, '/:slug');
}

/**
 * Get bucket for item count
 */
function getItemCountBucket(count: number): string {
  if (count === 1) return '1';
  if (count <= 3) return '2-3';
  if (count <= 5) return '4-5';
  if (count <= 10) return '6-10';
  return '10+';
}

/**
 * Get bucket for value
 */
function getValueBucket(cents: number): string {
  const dollars = cents / 100;
  if (dollars < 25) return '<25';
  if (dollars < 50) return '25-50';
  if (dollars < 100) return '50-100';
  if (dollars < 250) return '100-250';
  return '250+';
}