@splunk/otel
Version:
The Splunk distribution of OpenTelemetry Node Instrumentation provides a Node agent that automatically instruments your Node application to capture and report distributed traces to Splunk APM.
517 lines • 21.9 kB
JavaScript
"use strict";
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.allowedMetricsOptions = void 0;
exports.createOtlpExporter = createOtlpExporter;
exports.defaultMetricReaderFactory = defaultMetricReaderFactory;
exports.startMetrics = startMetrics;
exports._setDefaultOptions = _setDefaultOptions;
const api_1 = require("@opentelemetry/api");
const resources_1 = require("@opentelemetry/resources");
const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
const exporter_metrics_otlp_proto_1 = require("@opentelemetry/exporter-metrics-otlp-proto");
const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
const utils_1 = require("../utils");
const debug_metrics_1 = require("./debug_metrics");
const util = require("util");
const resource_1 = require("../resource");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const ConsoleMetricExporter_1 = require("./ConsoleMetricExporter");
const utils_2 = require("../utils");
const configuration_1 = require("../configuration");
const convert_1 = require("../configuration/convert");
const typedKeys = (obj) => Object.keys(obj);
function _loadExtension() {
let extension;
try {
extension = require('../native_ext');
}
catch (e) {
api_1.diag.error('Unable to load native metrics extension. Event loop and GC metrics will not be reported', e);
}
return extension === null || extension === void 0 ? void 0 : extension.metrics;
}
function recordGcSumMetric(counter, counters, field) {
for (const type of typedKeys(counters.gc)) {
counter.add(counters.gc[type][field].sum, {
'gc.type': type,
});
}
}
function recordGcCountMetric(counter, counters) {
for (const type of typedKeys(counters.gc)) {
counter.add(counters.gc[type].collected.count, {
'gc.type': type,
});
}
}
const SUPPORTED_EXPORTER_TYPES = ['console', 'otlp', 'none'];
function areValidExporterTypes(types) {
return types.every((t) => SUPPORTED_EXPORTER_TYPES.includes(t));
}
function createOtlpExporter(options) {
let protocol = (0, utils_2.getEnvValueByPrecedence)([
'OTEL_EXPORTER_OTLP_METRICS_PROTOCOL',
'OTEL_EXPORTER_OTLP_PROTOCOL',
]);
let endpoint = options.endpoint;
if (options.realm) {
if (protocol !== undefined && protocol !== 'http/protobuf') {
api_1.diag.warn(`OTLP metric exporter: defaulting protocol to 'http/protobuf' instead of '${protocol}' due to realm being defined.`);
}
const envEndpoint = (0, utils_2.getEnvValueByPrecedence)([
'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT',
'OTEL_EXPORTER_OTLP_ENDPOINT',
]);
if (endpoint === undefined && envEndpoint === undefined) {
endpoint = `https://ingest.${options.realm}.signalfx.com/v2/datapoint/otlp`;
protocol = 'http/protobuf';
}
else {
api_1.diag.warn('OTLP metric exporter factory: Realm value ignored (full endpoint URL has been specified).');
}
}
protocol = protocol !== null && protocol !== void 0 ? protocol : 'http/protobuf';
switch (protocol) {
case 'grpc': {
const grpcModule = require('@grpc/grpc-js');
const otlpGrpc = require('@opentelemetry/exporter-metrics-otlp-grpc');
const metadata = new grpcModule.Metadata();
if (options.accessToken) {
metadata.set('X-SF-TOKEN', options.accessToken);
}
return new otlpGrpc.OTLPMetricExporter({
url: endpoint,
metadata,
});
}
case 'http/protobuf': {
const headers = options.accessToken
? {
'X-SF-TOKEN': options.accessToken,
}
: undefined;
const url = (0, utils_1.ensureResourcePath)(endpoint, '/v1/metrics');
return new exporter_metrics_otlp_proto_1.OTLPMetricExporter({
url,
headers,
});
}
default:
throw new Error(`Metrics: expected OTLP protocol to be either grpc or http/protobuf, got ${protocol}.`);
}
}
function createExporters(options) {
const metricExporters = (0, utils_2.getEnvArray)('OTEL_METRICS_EXPORTER') || [
'otlp',
];
if (!areValidExporterTypes(metricExporters)) {
throw new Error(`Invalid value for OTEL_METRICS_EXPORTER env variable: ${util.inspect((0, utils_2.getNonEmptyEnvVar)('OTEL_METRICS_EXPORTER'))}. Choose from ${util.inspect(SUPPORTED_EXPORTER_TYPES, {
compact: true,
})} or leave undefined.`);
}
return metricExporters.flatMap((type) => {
switch (type) {
case 'otlp':
return createOtlpExporter(options);
case 'console':
return new ConsoleMetricExporter_1.ConsoleMetricExporter();
default:
return [];
}
});
}
function toAggregationTemporalityPreference(preference) {
if (preference === 'cumulative')
return exporter_metrics_otlp_http_1.AggregationTemporalityPreference.CUMULATIVE;
if (preference === 'delta')
return exporter_metrics_otlp_http_1.AggregationTemporalityPreference.DELTA;
if (preference === 'low_memory')
return exporter_metrics_otlp_http_1.AggregationTemporalityPreference.LOWMEMORY;
return undefined;
}
function toMetricExporter(configExporter) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
if (configExporter.otlp_http !== undefined) {
const otlpHttp = configExporter.otlp_http;
const configHeaders = otlpHttp.headers || [];
const headers = {};
for (const header of configHeaders) {
if (header.value !== null) {
headers[header.name] = header.value;
}
}
const url = (0, utils_1.ensureResourcePath)(otlpHttp.endpoint, '/v1/metrics');
return new exporter_metrics_otlp_proto_1.OTLPMetricExporter({
url,
headers,
timeoutMillis: (_a = otlpHttp.timeout) !== null && _a !== void 0 ? _a : undefined,
compression: (0, convert_1.toCompression)(otlpHttp.compression),
httpAgentOptions: {
ca: (0, utils_1.readFileContent)((_b = otlpHttp.tls) === null || _b === void 0 ? void 0 : _b.ca_file),
cert: (0, utils_1.readFileContent)((_c = otlpHttp.tls) === null || _c === void 0 ? void 0 : _c.cert_file),
key: (0, utils_1.readFileContent)((_d = otlpHttp.tls) === null || _d === void 0 ? void 0 : _d.key_file),
},
temporalityPreference: toAggregationTemporalityPreference(otlpHttp.temporality_preference),
});
}
else if (configExporter.otlp_grpc !== undefined) {
const grpcModule = require('@grpc/grpc-js');
const otlpGrpc = require('@opentelemetry/exporter-metrics-otlp-grpc');
const cfgGrpc = configExporter.otlp_grpc;
const metadata = new grpcModule.Metadata();
for (const header of cfgGrpc.headers || []) {
if (header.value !== null) {
metadata.set(header.name, header.value);
}
}
const credentials = cfgGrpc.tls === undefined
? undefined
: grpcModule.credentials.createSsl((0, utils_1.readFileContent)((_e = cfgGrpc.tls) === null || _e === void 0 ? void 0 : _e.cert_file), (0, utils_1.readFileContent)((_f = cfgGrpc.tls) === null || _f === void 0 ? void 0 : _f.key_file), (0, utils_1.readFileContent)((_g = cfgGrpc.tls) === null || _g === void 0 ? void 0 : _g.ca_file));
return new otlpGrpc.OTLPMetricExporter({
url: (_h = cfgGrpc.endpoint) !== null && _h !== void 0 ? _h : undefined,
metadata,
timeoutMillis: (_j = cfgGrpc.timeout) !== null && _j !== void 0 ? _j : undefined,
compression: (0, convert_1.toCompression)(cfgGrpc.compression),
credentials,
temporalityPreference: toAggregationTemporalityPreference(cfgGrpc.temporality_preference),
});
}
else if (configExporter.console !== undefined) {
return new ConsoleMetricExporter_1.ConsoleMetricExporter();
}
else if (configExporter['otlp_file/development'] !== undefined) {
api_1.diag.warn('metric exporter "otlp_file/development" is not supported');
}
return undefined;
}
function toViewAggregationOption(aggregation) {
var _a, _b, _c;
if (aggregation === undefined) {
return undefined;
}
if (aggregation.sum !== undefined) {
return { type: sdk_metrics_1.AggregationType.SUM };
}
else if (aggregation.drop !== undefined) {
return { type: sdk_metrics_1.AggregationType.DROP };
}
else if (aggregation.default !== undefined) {
return { type: sdk_metrics_1.AggregationType.DEFAULT };
}
else if (aggregation.last_value !== undefined) {
return { type: sdk_metrics_1.AggregationType.LAST_VALUE };
}
else if (aggregation.explicit_bucket_histogram !== undefined) {
const hist = aggregation.explicit_bucket_histogram;
return {
type: sdk_metrics_1.AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
options: {
recordMinMax: (_a = hist.record_min_max) !== null && _a !== void 0 ? _a : undefined,
boundaries: hist.boundaries || [],
},
};
}
else if (aggregation.base2_exponential_bucket_histogram !== undefined) {
const hist = aggregation.base2_exponential_bucket_histogram;
return {
type: sdk_metrics_1.AggregationType.EXPONENTIAL_HISTOGRAM,
options: {
recordMinMax: (_b = hist.record_min_max) !== null && _b !== void 0 ? _b : undefined,
maxSize: (_c = hist.max_size) !== null && _c !== void 0 ? _c : undefined,
},
};
}
return undefined;
}
function toAttributesProcessor(includeExclude) {
if (includeExclude === undefined) {
return undefined;
}
const included = includeExclude.included || [];
const excluded = includeExclude.excluded || [];
// TODO: Wildcard support
return {
process: (incoming) => {
const newAttributes = {};
for (const include of included) {
const value = incoming[include];
if (value !== undefined) {
newAttributes[include] = value;
}
}
for (const exclude of excluded) {
const value = incoming[exclude];
if (value !== undefined) {
delete newAttributes[exclude];
}
}
return newAttributes;
},
};
}
function toInstrumentType(type) {
if (type === undefined || type === null) {
return undefined;
}
switch (type) {
case 'counter':
return sdk_metrics_1.InstrumentType.COUNTER;
case 'gauge':
return sdk_metrics_1.InstrumentType.GAUGE;
case 'histogram':
return sdk_metrics_1.InstrumentType.HISTOGRAM;
case 'observable_counter':
return sdk_metrics_1.InstrumentType.OBSERVABLE_COUNTER;
case 'observable_gauge':
return sdk_metrics_1.InstrumentType.OBSERVABLE_GAUGE;
case 'observable_up_down_counter':
return sdk_metrics_1.InstrumentType.OBSERVABLE_UP_DOWN_COUNTER;
case 'up_down_counter':
return sdk_metrics_1.InstrumentType.UP_DOWN_COUNTER;
default: {
api_1.diag.warn(`unknown instrument type '${type}'`);
return undefined;
}
}
}
function toViewOptions(configView) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const attributeProcessor = toAttributesProcessor(configView.stream.attribute_keys);
return {
name: (_a = configView.stream.name) !== null && _a !== void 0 ? _a : undefined,
description: (_b = configView.stream.description) !== null && _b !== void 0 ? _b : undefined,
attributesProcessors: attributeProcessor ? [attributeProcessor] : undefined,
aggregation: toViewAggregationOption(configView.stream.aggregation),
aggregationCardinalityLimit: (_c = configView.stream.aggregation_cardinality_limit) !== null && _c !== void 0 ? _c : undefined,
instrumentType: toInstrumentType(configView.selector.instrument_type),
instrumentName: (_d = configView.selector.instrument_name) !== null && _d !== void 0 ? _d : undefined,
instrumentUnit: (_e = configView.selector.unit) !== null && _e !== void 0 ? _e : undefined,
meterName: (_f = configView.selector.meter_name) !== null && _f !== void 0 ? _f : undefined,
meterVersion: (_g = configView.selector.meter_version) !== null && _g !== void 0 ? _g : undefined,
meterSchemaUrl: (_h = configView.selector.meter_schema_url) !== null && _h !== void 0 ? _h : undefined,
};
}
function defaultMetricReaderFactory(options) {
var _a, _b;
const cfgMeterProvider = (0, configuration_1.getConfigMeterProvider)();
if (cfgMeterProvider === undefined) {
return createExporters(options).map((exporter) => {
return new sdk_metrics_1.PeriodicExportingMetricReader({
exportIntervalMillis: options.exportIntervalMillis,
exporter,
});
});
}
const readers = [];
if (cfgMeterProvider === null) {
return readers;
}
for (const reader of cfgMeterProvider.readers) {
if (reader.pull !== undefined) {
api_1.diag.warn('pull metric reader not supported');
}
else if (reader.periodic !== undefined) {
const periodicReader = reader.periodic;
const exporter = toMetricExporter(periodicReader.exporter);
if (exporter !== undefined) {
// TODO: Cardinality limits when OTel supports them.
readers.push(new sdk_metrics_1.PeriodicExportingMetricReader({
exporter,
exportIntervalMillis: (_a = periodicReader.interval) !== null && _a !== void 0 ? _a : undefined,
exportTimeoutMillis: (_b = periodicReader.timeout) !== null && _b !== void 0 ? _b : undefined,
}));
}
}
}
return readers;
}
function buildMeterProvider(options) {
const debugMetricsViews = options.debugMetricsEnabled
? (0, debug_metrics_1.getDebugMetricsViews)()
: [];
const readers = options.metricReaderFactory(options);
const configMeterProvider = (0, configuration_1.getConfigMeterProvider)();
if (configMeterProvider === undefined) {
return new sdk_metrics_1.MeterProvider({
resource: options.resource,
views: [...(options.views || []), ...debugMetricsViews],
readers,
});
}
const cfgViews = configMeterProvider.views || [];
let views = [];
if (options.views === undefined) {
for (const cfgView of cfgViews) {
views.push(toViewOptions(cfgView));
}
}
else {
views = options.views;
}
return new sdk_metrics_1.MeterProvider({
resource: options.resource,
views: [...views, ...debugMetricsViews],
readers,
});
}
exports.allowedMetricsOptions = [
'accessToken',
'realm',
'endpoint',
'exportIntervalMillis',
'metricReaderFactory',
'views',
'resourceFactory',
'runtimeMetricsEnabled',
'runtimeMetricsCollectionIntervalMillis',
'serviceName',
'debugMetricsEnabled',
];
function startMetrics(options) {
const provider = buildMeterProvider(options);
api_1.metrics.setGlobalMeterProvider(provider);
async function stopGlobalMetrics() {
api_1.metrics.disable();
if (provider !== undefined) {
await provider.forceFlush();
await provider.shutdown();
}
}
if (options.debugMetricsEnabled) {
(0, debug_metrics_1.enableDebugMetrics)();
}
if (!options.runtimeMetricsEnabled) {
return {
stop: stopGlobalMetrics,
};
}
const meter = api_1.metrics.getMeter('splunk-otel-js-runtime-metrics');
meter
.createObservableGauge('process.runtime.nodejs.memory.heap.total', {
unit: 'By',
valueType: api_1.ValueType.INT,
})
.addCallback((result) => {
result.observe(process.memoryUsage().heapTotal);
});
meter
.createObservableGauge('process.runtime.nodejs.memory.heap.used', {
unit: 'By',
valueType: api_1.ValueType.INT,
})
.addCallback((result) => {
result.observe(process.memoryUsage().heapUsed);
});
meter
.createObservableGauge('process.runtime.nodejs.memory.rss', {
unit: 'By',
valueType: api_1.ValueType.INT,
})
.addCallback((result) => {
result.observe(process.memoryUsage().rss);
});
const extension = _loadExtension();
if (extension === undefined) {
return {
stop: stopGlobalMetrics,
};
}
extension.start();
let runtimeCounters = extension.collect();
meter
.createObservableGauge('process.runtime.nodejs.event_loop.lag.max', {
unit: 'ns',
valueType: api_1.ValueType.INT,
})
.addCallback((result) => {
result.observe(runtimeCounters.eventLoopLag.max);
});
meter
.createObservableGauge('process.runtime.nodejs.event_loop.lag.min', {
unit: 'ns',
valueType: api_1.ValueType.INT,
})
.addCallback((result) => {
result.observe(runtimeCounters.eventLoopLag.min);
});
const gcSizeCounter = meter.createCounter('process.runtime.nodejs.memory.gc.size', {
unit: 'By',
valueType: api_1.ValueType.INT,
});
const gcPauseCounter = meter.createCounter('process.runtime.nodejs.memory.gc.pause', {
unit: 'By',
valueType: api_1.ValueType.INT,
});
const gcCountCounter = meter.createCounter('process.runtime.nodejs.memory.gc.count', {
unit: '1',
valueType: api_1.ValueType.INT,
});
const interval = setInterval(() => {
runtimeCounters = extension.collect();
extension.reset();
recordGcSumMetric(gcSizeCounter, runtimeCounters, 'collected');
recordGcSumMetric(gcPauseCounter, runtimeCounters, 'duration');
recordGcCountMetric(gcCountCounter, runtimeCounters);
}, options.runtimeMetricsCollectionIntervalMillis);
interval.unref();
return {
stop: async () => {
clearInterval(interval);
await stopGlobalMetrics();
},
};
}
function _setDefaultOptions(options = {}) {
var _a, _b, _c, _d;
const accessToken = options.accessToken || (0, configuration_1.getNonEmptyConfigVar)('SPLUNK_ACCESS_TOKEN') || '';
const realm = options.realm || (0, configuration_1.getNonEmptyConfigVar)('SPLUNK_REALM') || '';
if (realm) {
if (!accessToken) {
throw new Error('Splunk realm is set, but access token is unset. To send metrics to the Observability Cloud, both need to be set');
}
if (options.metricReaderFactory) {
api_1.diag.warn('Splunk realm is set with a custom metric reader. Make sure to use OTLP metrics proto HTTP exporter.');
}
}
const envResource = (0, resource_1.getDetectedResource)();
const serviceName = String(options.serviceName ||
(0, configuration_1.getNonEmptyConfigVar)('OTEL_SERVICE_NAME') ||
((_a = envResource.attributes) === null || _a === void 0 ? void 0 : _a[semantic_conventions_1.ATTR_SERVICE_NAME]) ||
(0, utils_1.defaultServiceName)());
const resourceFactory = options.resourceFactory || ((resource) => resource);
let resource = resourceFactory((0, resources_1.resourceFromAttributes)(envResource.attributes || {}).merge((0, configuration_1.configGetResource)()));
resource = resource.merge((0, resources_1.resourceFromAttributes)({
[semantic_conventions_1.ATTR_SERVICE_NAME]: serviceName,
}));
return {
serviceName,
accessToken,
realm,
resource,
endpoint: options.endpoint,
views: options.views,
metricReaderFactory: (_b = options.metricReaderFactory) !== null && _b !== void 0 ? _b : defaultMetricReaderFactory,
exportIntervalMillis: options.exportIntervalMillis ||
(0, configuration_1.getConfigNumber)('OTEL_METRIC_EXPORT_INTERVAL', 30000),
debugMetricsEnabled: (_c = options.debugMetricsEnabled) !== null && _c !== void 0 ? _c : (0, configuration_1.getConfigBoolean)('SPLUNK_DEBUG_METRICS_ENABLED', false),
runtimeMetricsEnabled: (_d = options.runtimeMetricsEnabled) !== null && _d !== void 0 ? _d : (0, configuration_1.getConfigBoolean)('SPLUNK_RUNTIME_METRICS_ENABLED', true),
runtimeMetricsCollectionIntervalMillis: options.runtimeMetricsCollectionIntervalMillis ||
(0, configuration_1.getConfigNumber)('SPLUNK_RUNTIME_METRICS_COLLECTION_INTERVAL', 5000),
};
}
//# sourceMappingURL=index.js.map