UNPKG

artillery-plugin-publish-metrics

Version:

Publish metrics from your Artillery.io tests to external monitoring & observability systems

243 lines (210 loc) 7.39 kB
'use strict'; const { vendorTranslators } = require('./translators/vendor-otel'); const { diag, DiagConsoleLogger, DiagLogLevel, context } = require('@opentelemetry/api'); const { Resource } = require('@opentelemetry/resources'); const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); const { AsyncLocalStorageContextManager } = require('@opentelemetry/context-async-hooks'); // Setting the contextManager here as it needs to be set globally on the context before anything else const contextManager = new AsyncLocalStorageContextManager(); contextManager.enable(); context.setGlobalContextManager(contextManager); // DEBUGGING SETUP - setting the OpenTelemetry's internal diagnostic handler here to run when debug is enabled if ( process.env.DEBUG && process.env.DEBUG.includes('plugin:publish-metrics:') ) { diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR); } class OTelReporter { constructor(configList, events, script) { this.script = script; this.events = events; this.engines = new Set(); this.translatedConfigsList = configList.map((config) => { // Translate the vendor-specific config to OpenTelemetry config const translatedConfig = this.translateToOtel(config); // Setting serviceName to first serviceName configured if (!this.serviceName && translatedConfig.serviceName) { this.serviceName = translatedConfig.serviceName; } if (translatedConfig.resourceAttributes) { this.resourceAttributes = translatedConfig.resourceAttributes; } else { this.resourceAttributes = {}; } // Setting traces to first traces configured if (translatedConfig.traces && !this.tracesConfig) { this.tracesConfig = translatedConfig.traces; // Setting debug for traces this.traceDebug = require('debug')( `plugin:publish-metrics:${this.tracesConfig.type}` ); } // Setting metrics to first metrics configured if (translatedConfig.metrics && !this.metricsConfig) { this.metricsConfig = translatedConfig.metrics; // Setting debug for metrics this.metricDebug = require('debug')( `plugin:publish-metrics:${this.metricsConfig.type}` ); } return translatedConfig; }); if (!this.metricsConfig && !this.tracesConfig) { return this; } // Warn if traces are configured in multiple reporters this.warnIfDuplicateTracesConfigured(this.translatedConfigsList); // Create set of all engines used in test -> even though we only support Playwright and HTTP engine for now this is future compatible this.getEngines(this.script.scenarios || []); // Setting resources here as they are used by both metrics and traces and need to be set in a central place where OTel setup is initialised and before any data is generated this.resource = Resource.default().merge( new Resource( Object.assign( {}, { [SemanticResourceAttributes.SERVICE_NAME]: this.serviceName || 'Artillery-test' }, this.resourceAttributes ) ) ); // HANDLING METRICS if (this.metricsConfig) { const { OTelMetricsReporter } = require('./metrics'); this.metricReporter = new OTelMetricsReporter( this.metricsConfig, this.events, this.resource ); } // HANDLING TRACES if (this.tracesConfig) { global.artillery.OTEL_TRACING_ENABLED = true; // Handling telemetry for traces events.on('done', async (report) => { const spanCount = report?.counters?.['plugins.publish-metrics.spans.exported']; await this.sendTraceTelemetry(spanCount, this.tracesConfig.type); }); // OpenTelemetry trace setup that is shared between engines - it is set in a separate class so it doesn't get duplicated in case both engines are used in a test const { OTelTraceConfig } = require('./tracing/base'); this.trace = new OTelTraceConfig(this.tracesConfig, this.resource); this.trace.configure(); // Run HTTP engine tracing if (this.engines.has('http')) { const { OTelHTTPTraceReporter } = require('./tracing/http'); this.httpReporter = new OTelHTTPTraceReporter( this.tracesConfig, script ); this.httpReporter.run(); } // Run Playwright tracing if (this.engines.has('playwright')) { const { OTelPlaywrightTraceReporter } = require('./tracing/playwright'); this.playwrightReporter = new OTelPlaywrightTraceReporter( this.tracesConfig, script ); this.playwrightReporter.run(); } } } debug(msg) { if (this.traceDebug) { this.traceDebug(msg); } if (this.metricDebug) { this.metricDebug(msg); } } warnIfDuplicateTracesConfigured(configList) { const tracesConfigs = configList.filter((config) => config.traces); if (tracesConfigs.length > 1) { console.warn( 'WARNING: Multiple reporters configured for traces. Currently, you can only use one reporter at a time for reporting traces. Only the first reporter will be used.' ); } } translateToOtel(config) { return vendorTranslators[config.type](config); } getEngines(scenarios) { scenarios.forEach((scenario) => { scenario.engine ? this.engines.add(scenario.engine) : this.engines.add('http'); }); } async sendTraceTelemetry(spanCount, reporterType) { if (process.env.ARTILLERY_DISABLE_TELEMETRY) { return; } const popularDestinations = { 'nr-data.net': 'new-relic', lightstep: 'lightstep', honeycomb: 'honeycomb', dynatrace: 'dynatrace', grafana: 'grafana' }; let destination; if (reporterType !== 'open-telemetry') { destination = reporterType; } else { const destinationFromEndpoint = Object.keys(popularDestinations).find( (key) => this.tracesConfig?.endpoint?.includes(key) ); destination = destinationFromEndpoint ? popularDestinations[destinationFromEndpoint] : 'custom'; } const telemetry = global.artillery?.telemetry; if (telemetry) { await telemetry.capture('otel-span-count', { spansExported: spanCount, destination }); return true; } } async cleanup(done) { this.debug('Cleaning up'); if (!this.metricsConfig && !this.tracesConfig) { return done(); } // Waiting for flush period to complete here rather than in trace/metric reporters this.debug('Waiting for flush period to end'); await new Promise((resolve) => setTimeout(resolve, 10000)); if (this.metricReporter) { await this.metricReporter.cleanup(); } if (!this.httpReporter && !this.playwrightReporter) { return done(); } if (this.httpReporter) { await this.httpReporter.cleanup('http'); } if (this.playwrightReporter) { await this.playwrightReporter.cleanup('playwright'); } await this.trace.shutDown(); return done(); } } function createOTelReporter(config, events, script) { return new OTelReporter(config, events, script); } module.exports = { createOTelReporter };