Skip to content

Commit

Permalink
feat: add getInstrumentations method (#82)
Browse files Browse the repository at this point in the history
This adds an export (with an API very similar to the getInstrumentations
from `@opentelemetry/auto-instrumentations-node`) that users of this distro
can use to cleanly customize the default set of provided instrumentations.
  • Loading branch information
david-luna authored Mar 14, 2024
1 parent 5f3b9b8 commit 9002961
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 22 deletions.
33 changes: 29 additions & 4 deletions examples/start-elastic-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,44 @@

const path = require('path');

const {ElasticNodeSDK} = require('@elastic/opentelemetry-node');
const {
ElasticNodeSDK,
getInstrumentations,
} = require('@elastic/opentelemetry-node');

// TODO: remove BunyanInstrumentation manual usage when the distro includes it by default
const {
BunyanInstrumentation,
} = require('@opentelemetry/instrumentation-bunyan');

const {
ExpressInstrumentation,
} = require('@opentelemetry/instrumentation-express');

const sdk = new ElasticNodeSDK({
serviceName: path.parse(process.argv[1]).name,
// One can **override** completelly the instrumentations provided by ElasticNodeSDK
// by specifying `instrumentations`.
instrumentations: [
// One can **override** the instrumentations provided by ElasticNodeSDK
// by specifying `instrumentations`. How to **extended** the set of
// instrumentations provided by ElasticNodeSDK is to be determined (TODO).
// Users can have the default instrumentations by calling `getInstrumentations`
// method. The options param is a Record<string, Object | Function> where the key
// is the name of the instrumentation.
getInstrumentations({
// It's possible to pass a configuration object to the instrumentation
'@opentelemetry/instrumentation-http': {
serverName: 'test',
},
// But also a function could be used to handle more complex scenarios
'@opentelemetry/instrumentation-express': () => {
// User can return `undefined` if he/she wants to disable it
if (process.env.ETEL_DISABLE_EXPRESS) {
return undefined;
}
// Or return a new instrumentation to replace the default one
return new ExpressInstrumentation();
},
}),
// Users can apend their own instrumentations as they would do with the vanilla SDK.
new BunyanInstrumentation(),
],
});
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-node/lib/detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const ELASTIC_SDK_VERSION = require('../package.json').version;
class ElasticDistroDetector {
detect() {
// TODO: change to semconv resource attribs when
// `@opentelemetry/semantic-conventions`get updated with the attribs used
// `@opentelemetry/semantic-conventions` gets updated with the attribs used
// https://github.com/open-telemetry/opentelemetry-js/issues/4235
return new Resource({
'telemetry.distro.name': 'elastic',
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-node/lib/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const envToRestore = {};

/**
* Reads a string in the format `key-1=value,key2=value2` and parses
* it into an object. This is the format specified for ley value pairs
* it into an object. This is the format specified for key value pairs
* for OTEL environment vars. Example:
* https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_headers
*
Expand Down
2 changes: 2 additions & 0 deletions packages/opentelemetry-node/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const {ElasticNodeSDK} = require('./sdk');
const {getInstrumentations} = require('./instrumentations');

// TODO: this should reexport things from @otel/sdk-node (like 'api', 'core', etc.)

module.exports = {
ElasticNodeSDK,
getInstrumentations,
};
66 changes: 66 additions & 0 deletions packages/opentelemetry-node/lib/instrumentations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @typedef {import('@opentelemetry/instrumentation').Instrumentation} Instrumentation
*
* @callback InstrumentationFactory
* @returns {Instrumentation}
*
* @typedef {{
* "@opentelemetry/instrumentation-http": import('@opentelemetry/instrumentation-http').HttpInstrumentationConfig | InstrumentationFactory,
* "@opentelemetry/instrumentation-express": import('@opentelemetry/instrumentation-express').ExpressInstrumentationConfig | InstrumentationFactory
* }} InstrumentaionsMap
*/

const {HttpInstrumentation} = require('@opentelemetry/instrumentation-http');
const {
ExpressInstrumentation,
} = require('@opentelemetry/instrumentation-express');

// Instrumentations attach their Hook (for require-in-the-middle or import-in-the-middle)
// when the `enable` method is called and this happens inside their constructor
// https://github.com/open-telemetry/opentelemetry-js/blob/1b4999f386e0240b7f65350e8360ccc2930b0fe6/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts#L71
//
// The SDK cannot construct any instrumentation until it has resolved the config. So to
// do a lazy creation of instrumentations we have factory functions that can receive
// the user's config and can default to something else if needed.
/** @type {Record<keyof InstrumentaionsMap, (cfg: any) => Instrumentation>} */
const INSTRUMENTATIONS = {
'@opentelemetry/instrumentation-http': (cfg) =>
new HttpInstrumentation(cfg),
'@opentelemetry/instrumentation-express': (cfg) =>
new ExpressInstrumentation(cfg),
};

/**
* Get the list of instrumentations baed on options
* @param {Partial<InstrumentaionsMap>} [opts={}]
* @returns {Array<Instrumentation>}
*/
function getInstrumentations(opts = {}) {
/** @type {Array<Instrumentation>} */
const instrumentations = [];

Object.keys(INSTRUMENTATIONS).forEach((name) => {
const isFactory = typeof opts[name] === 'function';
const isObject = typeof opts[name] === 'object';
const instrFactory = isFactory ? opts[name] : INSTRUMENTATIONS[name];
const instrConfig = isObject ? opts[name] : undefined;

// We should instantiate a instrumentation if:
// - there is no config passed (elastic SDK will use its defaults)
// - the configuration passed is not disabling it
let instr;
if (!instrConfig || instrConfig.enabled !== false) {
instr = instrFactory(instrConfig);
}

if (instr) {
instrumentations.push(instr);
}
});

return instrumentations;
}

module.exports = {
getInstrumentations,
};
27 changes: 11 additions & 16 deletions packages/opentelemetry-node/lib/sdk.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @typedef {import('@opentelemetry/sdk-node').NodeSDKConfiguration} NodeSDKConfiguration
*/

const {
OTLPMetricExporter,
} = require('@opentelemetry/exporter-metrics-otlp-proto');
Expand All @@ -8,25 +12,17 @@ const {
hostDetectorSync,
processDetectorSync,
} = require('@opentelemetry/resources');
const {HttpInstrumentation} = require('@opentelemetry/instrumentation-http');
const {
ExpressInstrumentation,
} = require('@opentelemetry/instrumentation-express');
const {BatchLogRecordProcessor} = require('@opentelemetry/sdk-logs');

const {setupLogger} = require('./logging');
const {distroDetectorSync} = require('./detector');
const {setupEnvironment, restoreEnvironment} = require('./environment');

const {getInstrumentations} = require('./instrumentations');
const {enableHostMetrics, HOST_METRICS_VIEWS} = require('./metrics/host');

/**
* @typedef {Partial<import('@opentelemetry/sdk-node').NodeSDKConfiguration>} PartialNodeSDKConfiguration
*/

class ElasticNodeSDK extends NodeSDK {
/**
* @param {PartialNodeSDKConfiguration} opts
* @param {Partial<NodeSDKConfiguration>} opts
*/
constructor(opts = {}) {
const log = setupLogger();
Expand All @@ -38,7 +34,7 @@ class ElasticNodeSDK extends NodeSDK {

// - NodeSDK defaults to `TracerProviderWithEnvExporters` if neither
// `spanProcessor` nor `traceExporter` are passed in.
/** @type {PartialNodeSDKConfiguration} */
/** @type {Partial<NodeSDKConfiguration>} */
const defaultConfig = {
resourceDetectors: [
// Elastic's own detector to add some metadata
Expand All @@ -49,13 +45,12 @@ class ElasticNodeSDK extends NodeSDK {
hostDetectorSync,
// TODO cloud/container detectors by default
],
instrumentations: [
// TODO All the instrumentations. Perf. Config support. Custom given instrs.
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
};

// Use user's instrumetations or get the default ones
defaultConfig.instrumentations =
opts.instrumentations || getInstrumentations();

// Default logs exporter.
// TODO: handle other protocols per OTEL_ exporter envvars (or get core NodeSDK to do it). Currently hardcoding to http/proto
defaultConfig.logRecordProcessor = new BatchLogRecordProcessor(
Expand Down
3 changes: 3 additions & 0 deletions packages/opentelemetry-node/lib/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// TODO: check if we need to reexport types from instrumentations
export {getInstrumentations} from './instrumentations';
export {ElasticNodeSDK} from './sdk';
1 change: 1 addition & 0 deletions packages/opentelemetry-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"test": "tape test/**/*.test.js"
},
"main": "lib/index.js",
"types": "lib/types.d.ts",
"dependencies": {
"@opentelemetry/exporter-logs-otlp-proto": "^0.48.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.48.0",
Expand Down

0 comments on commit 9002961

Please sign in to comment.