UNPKG

lightweight-browser-load-tester

Version:

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

491 lines 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenTelemetryExporter = void 0; /** * Exports metrics to OpenTelemetry OTLP endpoint in real-time * This is a simplified implementation that demonstrates OpenTelemetry integration */ class OpenTelemetryExporter { constructor(config) { this.metricBuffer = []; this.isInitialized = false; this.isShuttingDown = false; this.config = { serviceName: 'lightweight-browser-load-tester', serviceVersion: '1.0.0', timeout: 5000, compression: 'gzip', batchTimeout: 5000, maxExportBatchSize: 512, maxQueueSize: 2048, exportTimeout: 30000, ...config }; if (this.config.enabled) { this.startFlushTimer(); } } /** * Initialize OpenTelemetry exporter (simplified implementation) */ async initialize() { if (!this.config.enabled || this.isInitialized) { return; } try { // Simplified initialization - in a real implementation, this would set up the OpenTelemetry SDK this.isInitialized = true; } catch (error) { throw new Error(`Failed to initialize OpenTelemetry exporter: ${error instanceof Error ? error.message : String(error)}`); } } /** * Export browser metrics to OpenTelemetry */ async exportBrowserMetrics(metrics) { if (!this.config.enabled || !this.isInitialized) return; const timestamp = Date.now(); const baseLabels = { instance_id: metrics.instanceId, service: this.config.serviceName }; const otelMetrics = [ { name: 'browser_memory_usage_mb', value: metrics.memoryUsage, labels: baseLabels, timestamp, type: 'gauge' }, { name: 'browser_cpu_usage_percent', value: metrics.cpuUsage, labels: baseLabels, timestamp, type: 'gauge' }, { name: 'browser_request_count_total', value: metrics.requestCount, labels: baseLabels, timestamp, type: 'counter' }, { name: 'browser_error_count_total', value: metrics.errorCount, labels: baseLabels, timestamp, type: 'counter' }, { name: 'browser_uptime_seconds', value: metrics.uptime, labels: baseLabels, timestamp, type: 'gauge' } ]; this.addMetricsToBuffer(otelMetrics); } /** * Export DRM metrics to OpenTelemetry */ async exportDRMMetrics(metrics) { if (!this.config.enabled || !this.isInitialized) return; const timestamp = Date.now(); const baseLabels = { drm_type: metrics.drmType, service: this.config.serviceName }; const otelMetrics = [ { name: 'drm_license_request_count_total', value: metrics.licenseRequestCount, labels: baseLabels, timestamp, type: 'counter' }, { name: 'drm_average_license_time_ms', value: metrics.averageLicenseTime, labels: baseLabels, timestamp, type: 'histogram' }, { name: 'drm_license_success_rate', value: metrics.licenseSuccessRate, labels: baseLabels, timestamp, type: 'gauge' }, { name: 'drm_error_count_total', value: metrics.errors.length, labels: baseLabels, timestamp, type: 'counter' } ]; this.addMetricsToBuffer(otelMetrics); } /** * Export network metrics to OpenTelemetry */ async exportNetworkMetrics(metrics) { if (!this.config.enabled || !this.isInitialized || metrics.length === 0) return; const otelMetrics = []; 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', service: this.config.serviceName }; otelMetrics.push({ name: 'http_request_duration_ms', value: metric.responseTime, labels: baseLabels, timestamp, type: 'histogram' }, { name: 'http_request_size_bytes', value: metric.requestSize, labels: baseLabels, timestamp, type: 'histogram' }, { name: 'http_response_size_bytes', value: metric.responseSize, labels: baseLabels, timestamp, type: 'histogram' }); } this.addMetricsToBuffer(otelMetrics); } /** * Export test summary metrics to OpenTelemetry */ async exportTestSummary(summary) { if (!this.config.enabled || !this.isInitialized) return; const timestamp = Date.now(); const baseLabels = { service: this.config.serviceName }; const otelMetrics = [ { name: 'test_total_requests', value: summary.totalRequests, labels: baseLabels, timestamp, type: 'counter' }, { name: 'test_successful_requests', value: summary.successfulRequests, labels: baseLabels, timestamp, type: 'counter' }, { name: 'test_failed_requests', value: summary.failedRequests, labels: baseLabels, timestamp, type: 'counter' }, { name: 'test_average_response_time_ms', value: summary.averageResponseTime, labels: baseLabels, timestamp, type: 'gauge' }, { name: 'test_peak_concurrent_users', value: summary.peakConcurrentUsers, labels: baseLabels, timestamp, type: 'gauge' }, { name: 'test_duration_seconds', value: summary.testDuration, labels: baseLabels, timestamp, type: 'gauge' } ]; this.addMetricsToBuffer(otelMetrics); } /** * Export error metrics to OpenTelemetry */ async exportErrorMetrics(errors) { if (!this.config.enabled || !this.isInitialized || 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 otelMetrics = []; for (const [level, count] of Object.entries(errorCounts)) { otelMetrics.push({ name: 'test_error_count_total', value: count, labels: { level, service: this.config.serviceName }, timestamp, type: 'counter' }); } this.addMetricsToBuffer(otelMetrics); } /** * 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.maxExportBatchSize || 512)) { // Don't await to avoid blocking this.flush().catch(error => { console.error('Failed to flush metrics to OpenTelemetry:', error); }); } } /** * Start the periodic flush timer */ startFlushTimer() { if (this.flushTimer) { clearInterval(this.flushTimer); } const intervalMs = this.config.batchTimeout || 5000; this.flushTimer = setInterval(() => { if (!this.isShuttingDown) { this.flush().catch(error => { console.error('Failed to flush metrics to OpenTelemetry:', error); }); } }, intervalMs); } /** * Manually flush all buffered metrics to OpenTelemetry */ async flush() { if (!this.config.enabled || this.metricBuffer.length === 0) return; const metricsToSend = [...this.metricBuffer]; this.metricBuffer = []; await this.sendMetricsToOTLP(metricsToSend); } /** * Send metrics to OpenTelemetry OTLP endpoint */ async sendMetricsToOTLP(metrics) { if (metrics.length === 0) return; const otlpRequest = this.convertToOTLPFormat(metrics); const payload = JSON.stringify(otlpRequest); let lastError = null; const maxRetries = 3; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { await this.makeHttpRequest(Buffer.from(payload, 'utf-8')); return; // Success, exit retry loop } catch (error) { lastError = error; if (attempt < maxRetries) { const delay = 1000 * Math.pow(2, attempt); await this.sleep(delay); } } } console.error(`Failed to send metrics to OpenTelemetry after ${maxRetries + 1} attempts: ${lastError?.message}`); } /** * Convert internal metrics format to OTLP format */ convertToOTLPFormat(metrics) { const resourceMetrics = [{ resource: { attributes: [ { key: 'service.name', value: { stringValue: this.config.serviceName } }, { key: 'service.version', value: { stringValue: this.config.serviceVersion } } ] }, scopeMetrics: [{ scope: { name: this.config.serviceName, version: this.config.serviceVersion }, metrics: metrics.map(metric => this.convertMetricToOTLP(metric)) }] }]; return { resourceMetrics }; } /** * Convert a single metric to OTLP format */ convertMetricToOTLP(metric) { const attributes = Object.entries(metric.labels).map(([key, value]) => ({ key, value: { stringValue: value } })); const timeUnixNano = (metric.timestamp * 1000000).toString(); // Convert to nanoseconds const baseMetric = { name: metric.name, description: `${metric.name} metric`, unit: this.getMetricUnit(metric.name) }; switch (metric.type) { case 'gauge': return { ...baseMetric, gauge: { dataPoints: [{ attributes, timeUnixNano, asDouble: metric.value }] } }; case 'counter': return { ...baseMetric, sum: { dataPoints: [{ attributes, timeUnixNano, asDouble: metric.value }], aggregationTemporality: 2, // CUMULATIVE isMonotonic: true } }; case 'histogram': return { ...baseMetric, histogram: { dataPoints: [{ attributes, timeUnixNano, count: '1', sum: metric.value, bucketCounts: ['0', '1'], explicitBounds: [metric.value] }], aggregationTemporality: 2 // CUMULATIVE } }; default: return baseMetric; } } /** * Get appropriate unit for metric name */ getMetricUnit(metricName) { if (metricName.includes('_mb')) return 'MB'; if (metricName.includes('_percent')) return '%'; if (metricName.includes('_seconds')) return 's'; if (metricName.includes('_ms')) return 'ms'; if (metricName.includes('_bytes')) return 'bytes'; return '1'; } /** * Make HTTP request to OpenTelemetry OTLP endpoint */ async makeHttpRequest(payload) { const headers = { 'Content-Type': 'application/json', 'Content-Length': payload.length.toString(), 'User-Agent': 'lightweight-browser-load-tester/1.0.0', ...this.config.headers }; const requestOptions = { method: 'POST', headers, body: payload, signal: AbortSignal.timeout(this.config.timeout || 5000) }; try { const response = await fetch(this.config.endpoint, 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)); } /** * Shutdown the exporter and flush remaining metrics */ async shutdown() { if (!this.isInitialized || this.isShuttingDown) return; this.isShuttingDown = true; try { // Clear flush timer if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = undefined; } // Flush remaining metrics await this.flush(); } catch (error) { console.error('Error during OpenTelemetry shutdown:', error); } finally { this.isInitialized = false; } } /** * Get configuration for debugging */ getConfig() { return { ...this.config }; } /** * Check if the exporter is initialized */ isReady() { return this.isInitialized && !this.isShuttingDown; } } exports.OpenTelemetryExporter = OpenTelemetryExporter; //# sourceMappingURL=opentelemetry-exporter.js.map