UNPKG

lightweight-browser-load-tester

Version:

A lightweight load testing tool using real browsers for streaming applications with DRM support

420 lines (419 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrometheusExporter = void 0; /** * Exports metrics to Prometheus RemoteWrite endpoint in real-time */ class PrometheusExporter { constructor(config) { this.metricBuffer = []; this.isShuttingDown = false; this.config = { batchSize: 100, flushInterval: 10, timeout: 5000, retryAttempts: 3, retryDelay: 1000, ...config }; if (this.config.enabled) { this.startFlushTimer(); } } /** * Export browser metrics to Prometheus */ async exportBrowserMetrics(metrics) { if (!this.config.enabled) return; const timestamp = Date.now(); const baseLabels = { instance_id: metrics.instanceId, job: 'browser_load_test' }; const prometheusMetrics = [ { name: 'browser_memory_usage_mb', samples: [{ labels: baseLabels, value: metrics.memoryUsage, timestamp }] }, { name: 'browser_cpu_usage_percent', samples: [{ labels: baseLabels, value: metrics.cpuUsage, timestamp }] }, { name: 'browser_request_count_total', samples: [{ labels: baseLabels, value: metrics.requestCount, timestamp }] }, { name: 'browser_error_count_total', samples: [{ labels: baseLabels, value: metrics.errorCount, timestamp }] }, { name: 'browser_uptime_seconds', samples: [{ labels: baseLabels, value: metrics.uptime, timestamp }] } ]; this.addMetricsToBuffer(prometheusMetrics); } /** * Export DRM metrics to Prometheus */ async exportDRMMetrics(metrics) { if (!this.config.enabled) return; const timestamp = Date.now(); const baseLabels = { drm_type: metrics.drmType, job: 'browser_load_test' }; const prometheusMetrics = [ { name: 'drm_license_request_count_total', samples: [{ labels: baseLabels, value: metrics.licenseRequestCount, timestamp }] }, { name: 'drm_average_license_time_ms', samples: [{ labels: baseLabels, value: metrics.averageLicenseTime, timestamp }] }, { name: 'drm_license_success_rate', samples: [{ labels: baseLabels, value: metrics.licenseSuccessRate, timestamp }] }, { name: 'drm_error_count_total', samples: [{ labels: baseLabels, value: metrics.errors.length, timestamp }] } ]; this.addMetricsToBuffer(prometheusMetrics); } /** * Export network metrics to Prometheus */ async exportNetworkMetrics(metrics) { if (!this.config.enabled || metrics.length === 0) return; const prometheusMetrics = []; for (const metric of metrics) { const timestamp = metric.timestamp.getTime(); const baseLabels = { method: metric.method, status_code: metric.statusCode.toString(), streaming_related: (metric.isStreamingRelated || false).toString(), streaming_type: metric.streamingType || 'unknown', job: 'browser_load_test' }; prometheusMetrics.push({ name: 'http_request_duration_ms', samples: [{ labels: baseLabels, value: metric.responseTime, timestamp }] }, { name: 'http_request_size_bytes', samples: [{ labels: baseLabels, value: metric.requestSize, timestamp }] }, { name: 'http_response_size_bytes', samples: [{ labels: baseLabels, value: metric.responseSize, timestamp }] }); } this.addMetricsToBuffer(prometheusMetrics); } /** * Export test summary metrics to Prometheus */ async exportTestSummary(summary) { if (!this.config.enabled) return; const timestamp = Date.now(); const baseLabels = { job: 'browser_load_test' }; const prometheusMetrics = [ { name: 'test_total_requests', samples: [{ labels: baseLabels, value: summary.totalRequests, timestamp }] }, { name: 'test_successful_requests', samples: [{ labels: baseLabels, value: summary.successfulRequests, timestamp }] }, { name: 'test_failed_requests', samples: [{ labels: baseLabels, value: summary.failedRequests, timestamp }] }, { name: 'test_average_response_time_ms', samples: [{ labels: baseLabels, value: summary.averageResponseTime, timestamp }] }, { name: 'test_peak_concurrent_users', samples: [{ labels: baseLabels, value: summary.peakConcurrentUsers, timestamp }] }, { name: 'test_duration_seconds', samples: [{ labels: baseLabels, value: summary.testDuration, timestamp }] } ]; this.addMetricsToBuffer(prometheusMetrics); } /** * Export error metrics to Prometheus */ async exportErrorMetrics(errors) { if (!this.config.enabled || errors.length === 0) return; const timestamp = Date.now(); // Count errors by level const errorCounts = errors.reduce((acc, error) => { acc[error.level] = (acc[error.level] || 0) + 1; return acc; }, {}); const prometheusMetrics = []; for (const [level, count] of Object.entries(errorCounts)) { prometheusMetrics.push({ name: 'test_error_count_total', samples: [{ labels: { level, job: 'browser_load_test' }, value: count, timestamp }] }); } this.addMetricsToBuffer(prometheusMetrics); } /** * Manually flush all buffered metrics to Prometheus */ async flush() { if (!this.config.enabled || this.metricBuffer.length === 0) return; const metricsToSend = [...this.metricBuffer]; this.metricBuffer = []; await this.sendMetricsToPrometheus(metricsToSend); } /** * Shutdown the exporter and flush remaining metrics */ async shutdown() { this.isShuttingDown = true; if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = undefined; } await this.flush(); } /** * Add metrics to the buffer and trigger flush if batch size is reached */ addMetricsToBuffer(metrics) { this.metricBuffer.push(...metrics); if (this.metricBuffer.length >= (this.config.batchSize || 100)) { // Don't await to avoid blocking this.flush().catch(error => { console.error('Failed to flush metrics to Prometheus:', error); }); } } /** * Start the periodic flush timer */ startFlushTimer() { if (this.flushTimer) { clearInterval(this.flushTimer); } const intervalMs = (this.config.flushInterval || 10) * 1000; this.flushTimer = setInterval(() => { if (!this.isShuttingDown) { this.flush().catch(error => { console.error('Failed to flush metrics to Prometheus:', error); }); } }, intervalMs); } /** * Send metrics to Prometheus RemoteWrite endpoint */ async sendMetricsToPrometheus(metrics) { if (metrics.length === 0) return; const writeRequest = this.convertToPrometheusFormat(metrics); const payload = this.encodePrometheusPayload(writeRequest); let lastError = null; const maxRetries = this.config.retryAttempts || 3; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { await this.makeHttpRequest(payload); return; // Success, exit retry loop } catch (error) { lastError = error; if (attempt < maxRetries) { const delay = (this.config.retryDelay || 1000) * Math.pow(2, attempt); await this.sleep(delay); } } } throw new Error(`Failed to send metrics to Prometheus after ${maxRetries + 1} attempts: ${lastError?.message}`); } /** * Convert internal metrics format to Prometheus RemoteWrite format */ convertToPrometheusFormat(metrics) { const timeseries = []; for (const metric of metrics) { for (const sample of metric.samples) { // Add metric name as __name__ label const labels = [ { name: '__name__', value: metric.name }, ...Object.entries(sample.labels).map(([name, value]) => ({ name, value })) ]; timeseries.push({ labels, samples: [{ value: sample.value, timestamp: sample.timestamp }] }); } } return { timeseries }; } /** * Encode Prometheus payload using Protocol Buffers (simplified JSON for now) * In production, you might want to use actual protobuf encoding for better performance */ encodePrometheusPayload(writeRequest) { // For simplicity, we're using JSON encoding // In production, consider using protobuf for better compression and performance const jsonPayload = JSON.stringify(writeRequest); return Buffer.from(jsonPayload, 'utf-8'); } /** * Make HTTP request to Prometheus RemoteWrite endpoint */ async makeHttpRequest(payload) { const _url = new URL(this.config.remoteWriteUrl); const headers = { 'Content-Type': 'application/json', // Use application/x-protobuf for real protobuf 'Content-Length': payload.length.toString(), 'User-Agent': 'lightweight-browser-load-tester/1.0.0', ...this.config.headers }; // Add authentication if provided if (this.config.username && this.config.password) { const auth = Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64'); headers['Authorization'] = `Basic ${auth}`; } const requestOptions = { method: 'POST', headers, body: payload, signal: AbortSignal.timeout(this.config.timeout || 5000) }; try { const response = await fetch(this.config.remoteWriteUrl, requestOptions); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`HTTP ${response.status}: ${errorText}`); } } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error(`Request timeout after ${this.config.timeout}ms`); } throw error; } throw new Error('Unknown error occurred during HTTP request'); } } /** * Sleep for specified milliseconds */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get current buffer size for monitoring */ getBufferSize() { return this.metricBuffer.length; } /** * Get configuration for debugging */ getConfig() { return { ...this.config }; } } exports.PrometheusExporter = PrometheusExporter; //# sourceMappingURL=prometheus-exporter.js.map