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
JavaScript
"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