@j03fr0st/pubg-ts
Version:
A comprehensive TypeScript wrapper for the PUBG API
436 lines (377 loc) • 12.2 kB
text/typescript
import { performance } from 'node:perf_hooks';
import client, { Counter, Histogram, Gauge, Registry } from 'prom-client';
import { trace, context, SpanKind, SpanStatusCode } from '@opentelemetry/api';
import { logger } from './logger';
export interface MonitoringConfig {
enabled: boolean;
metricsEnabled: boolean;
tracingEnabled: boolean;
healthCheckEnabled: boolean;
collectDefaultMetrics: boolean;
prefix: string;
}
export interface RequestMetrics {
duration: number;
statusCode?: number;
endpoint?: string;
method?: string;
error?: boolean;
}
export interface SystemHealth {
status: 'healthy' | 'degraded' | 'unhealthy';
timestamp: number;
uptime: number;
memory: {
used: number;
total: number;
percentage: number;
};
api: {
status: 'healthy' | 'degraded' | 'unhealthy';
responseTime: number;
errorRate: number;
};
cache: {
hitRate: number;
size: number;
maxSize: number;
};
rateLimit: {
remaining: number;
limit: number;
resetTime: number;
};
}
/**
* Production-ready monitoring and observability system
*
* Provides comprehensive monitoring capabilities including:
* - Prometheus metrics collection
* - OpenTelemetry distributed tracing
* - Health checks and system monitoring
* - Performance tracking and alerting
*
* @example
* ```typescript
* const monitor = new MonitoringSystem({
* enabled: true,
* metricsEnabled: true,
* tracingEnabled: true,
* prefix: 'pubg_sdk'
* });
*
* // Track API requests
* const span = monitor.startSpan('api_request', { endpoint: '/players' });
* try {
* const result = await apiCall();
* monitor.recordRequestMetrics({
* duration: 150,
* statusCode: 200,
* endpoint: '/players'
* });
* span.setStatus({ code: SpanStatusCode.OK });
* } catch (error) {
* monitor.recordError(error);
* span.recordException(error as Error);
* span.setStatus({ code: SpanStatusCode.ERROR });
* } finally {
* span.end();
* }
* ```
*/
export class MonitoringSystem {
private config: MonitoringConfig;
private registry: Registry;
private tracer: any;
// Prometheus metrics
private requestCounter!: Counter<string>;
private requestDuration!: Histogram<string>;
private errorCounter!: Counter<string>;
private cacheHitRate!: Gauge<string>;
private activeConnections!: Gauge<string>;
private rateLimitRemaining!: Gauge<string>;
private memoryUsage!: Gauge<string>;
// Health tracking
private healthStatus: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
private startTime: number;
private lastHealthCheck: number = 0;
constructor(config: Partial<MonitoringConfig> = {}) {
this.config = {
enabled: true,
metricsEnabled: true,
tracingEnabled: true,
healthCheckEnabled: true,
collectDefaultMetrics: true,
prefix: 'pubg_sdk',
...config
};
this.startTime = Date.now();
this.registry = new Registry();
if (this.config.enabled) {
this.initializeMetrics();
this.initializeTracing();
this.startHealthChecking();
if (this.config.collectDefaultMetrics) {
client.collectDefaultMetrics({
register: this.registry,
prefix: `${this.config.prefix}_`
});
}
}
}
private initializeMetrics(): void {
if (!this.config.metricsEnabled) return;
const prefix = this.config.prefix;
// Request metrics
this.requestCounter = new Counter({
name: `${prefix}_requests_total`,
help: 'Total number of API requests',
labelNames: ['method', 'endpoint', 'status_code', 'status_class'],
registers: [this.registry]
});
this.requestDuration = new Histogram({
name: `${prefix}_request_duration_seconds`,
help: 'Request duration in seconds',
labelNames: ['method', 'endpoint', 'status_code'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
registers: [this.registry]
});
this.errorCounter = new Counter({
name: `${prefix}_errors_total`,
help: 'Total number of errors',
labelNames: ['type', 'endpoint', 'error_code'],
registers: [this.registry]
});
// System metrics
this.cacheHitRate = new Gauge({
name: `${prefix}_cache_hit_rate`,
help: 'Cache hit rate percentage',
registers: [this.registry]
});
this.activeConnections = new Gauge({
name: `${prefix}_active_connections`,
help: 'Number of active connections',
registers: [this.registry]
});
this.rateLimitRemaining = new Gauge({
name: `${prefix}_rate_limit_remaining`,
help: 'Remaining rate limit quota',
registers: [this.registry]
});
this.memoryUsage = new Gauge({
name: `${prefix}_memory_usage_bytes`,
help: 'Memory usage in bytes',
labelNames: ['type'],
registers: [this.registry]
});
logger.client('Prometheus metrics initialized');
}
private initializeTracing(): void {
if (!this.config.tracingEnabled) return;
this.tracer = trace.getTracer('pubg-ts-sdk', '1.0.0');
logger.client('OpenTelemetry tracing initialized');
}
private startHealthChecking(): void {
if (!this.config.healthCheckEnabled) return;
// Run health checks every 30 seconds
setInterval(() => {
this.performHealthCheck();
}, 30000);
// Initial health check
this.performHealthCheck();
}
/**
* Start a new tracing span
*/
public startSpan(name: string, attributes: Record<string, any> = {}): any {
if (!this.config.tracingEnabled || !this.tracer) {
return { end: () => {}, setStatus: () => {}, recordException: () => {} };
}
return this.tracer.startSpan(name, {
kind: SpanKind.CLIENT,
attributes
});
}
/**
* Record request metrics
*/
public recordRequestMetrics(metrics: RequestMetrics): void {
if (!this.config.metricsEnabled) return;
const { duration, statusCode = 0, endpoint = 'unknown', method = 'unknown', error = false } = metrics;
const statusClass = this.getStatusClass(statusCode);
// Increment request counter
this.requestCounter.inc({
method,
endpoint,
status_code: statusCode.toString(),
status_class: statusClass
});
// Record request duration
this.requestDuration.observe(
{ method, endpoint, status_code: statusCode.toString() },
duration / 1000 // Convert to seconds
);
// Record errors
if (error || statusCode >= 400) {
this.errorCounter.inc({
type: error ? 'client_error' : 'http_error',
endpoint,
error_code: statusCode.toString()
});
}
}
/**
* Record error occurrences
*/
public recordError(error: Error, context: Record<string, any> = {}): void {
if (!this.config.metricsEnabled) return;
this.errorCounter.inc({
type: error.constructor.name,
endpoint: context.endpoint || 'unknown',
error_code: context.code || 'unknown'
});
logger.error('Error recorded in monitoring', { error: error.message, context });
}
/**
* Update cache metrics
*/
public updateCacheMetrics(hitRate: number): void {
if (!this.config.metricsEnabled) return;
this.cacheHitRate.set(hitRate);
}
/**
* Update rate limit metrics
*/
public updateRateLimitMetrics(remaining: number): void {
if (!this.config.metricsEnabled) return;
this.rateLimitRemaining.set(remaining);
}
/**
* Update connection metrics
*/
public updateConnectionMetrics(active: number): void {
if (!this.config.metricsEnabled) return;
this.activeConnections.set(active);
}
/**
* Get current system health
*/
public async getHealth(): Promise<SystemHealth> {
const now = Date.now();
const memStats = process.memoryUsage();
return {
status: this.healthStatus,
timestamp: now,
uptime: now - this.startTime,
memory: {
used: memStats.heapUsed,
total: memStats.heapTotal,
percentage: (memStats.heapUsed / memStats.heapTotal) * 100
},
api: {
status: this.healthStatus,
responseTime: await this.measureApiResponseTime(),
errorRate: this.calculateErrorRate()
},
cache: {
hitRate: this.getCacheHitRate(),
size: 0, // Would be set by cache implementation
maxSize: 100 // Would be set by cache implementation
},
rateLimit: {
remaining: this.getRateLimitRemaining(),
limit: 10, // Would be set by rate limiter
resetTime: Date.now() + 60000
}
};
}
/**
* Get Prometheus metrics
*/
public async getMetrics(): Promise<string> {
if (!this.config.metricsEnabled) {
return '# Metrics disabled\n';
}
// Update memory metrics before collecting
const memStats = process.memoryUsage();
this.memoryUsage.set({ type: 'heap_used' }, memStats.heapUsed);
this.memoryUsage.set({ type: 'heap_total' }, memStats.heapTotal);
this.memoryUsage.set({ type: 'external' }, memStats.external);
this.memoryUsage.set({ type: 'rss' }, memStats.rss);
return await this.registry.metrics();
}
/**
* Perform comprehensive health check
*/
private async performHealthCheck(): Promise<void> {
const startTime = performance.now();
try {
// Check memory usage
const memStats = process.memoryUsage();
const memoryPercentage = (memStats.heapUsed / memStats.heapTotal) * 100;
// Check API response time
const apiResponseTime = await this.measureApiResponseTime();
// Calculate error rate
const errorRate = this.calculateErrorRate();
// Determine overall health status
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
if (memoryPercentage > 90 || apiResponseTime > 5000 || errorRate > 0.1) {
status = 'unhealthy';
} else if (memoryPercentage > 70 || apiResponseTime > 2000 || errorRate > 0.05) {
status = 'degraded';
}
this.healthStatus = status;
this.lastHealthCheck = Date.now();
const duration = performance.now() - startTime;
logger.client('Health check completed', {
status,
duration: `${duration.toFixed(2)}ms`,
memory: `${memoryPercentage.toFixed(2)}%`,
apiResponseTime: `${apiResponseTime}ms`,
errorRate: `${(errorRate * 100).toFixed(2)}%`
});
} catch (error) {
this.healthStatus = 'unhealthy';
logger.error('Health check failed', { error });
}
}
private async measureApiResponseTime(): Promise<number> {
// In a real implementation, this would make a test API call
// For now, return a simulated response time
return Math.random() * 500 + 100; // 100-600ms
}
private calculateErrorRate(): number {
// In a real implementation, this would calculate from actual metrics
// For now, return a simulated error rate
return Math.random() * 0.01; // 0-1%
}
private getCacheHitRate(): number {
// Would be calculated from actual cache statistics
return 0.85; // 85% hit rate
}
private getRateLimitRemaining(): number {
// Would be retrieved from actual rate limiter
return 8; // 8 requests remaining
}
private getStatusClass(statusCode: number): string {
if (statusCode >= 200 && statusCode < 300) return '2xx';
if (statusCode >= 300 && statusCode < 400) return '3xx';
if (statusCode >= 400 && statusCode < 500) return '4xx';
if (statusCode >= 500) return '5xx';
return 'unknown';
}
/**
* Shutdown monitoring system gracefully
*/
public shutdown(): void {
logger.client('Shutting down monitoring system');
this.registry.clear();
}
}
// Export singleton instance
export const monitoringSystem = new MonitoringSystem({
enabled: process.env.MONITORING_ENABLED !== 'false',
metricsEnabled: process.env.METRICS_ENABLED !== 'false',
tracingEnabled: process.env.TRACING_ENABLED !== 'false',
prefix: process.env.METRICS_PREFIX || 'pubg_sdk'
});