lightweight-browser-load-tester
Version:
A lightweight load testing tool using real browsers for streaming applications with DRM support
491 lines • 16.3 kB
JavaScript
"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