Skip to content

Refactor the OpenTelemetry package #1066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,034 changes: 676 additions & 358 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions packages/opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ An [OpenTelemetry](https://opentelemetry.io/docs/what-is-opentelemetry/) client
* [`options.authorizationHeader`](#optionsauthorizationheader)
* [`options.tracing`](#optionstracing)
* [`options.tracing.endpoint`](#optionstracingendpoint)
* [`options.tracing.authorizationHeader`](#optionstracingauthorizationheader)
* [`options.tracing.samplePercentage`](#optionstracingsamplepercentage)
* [`OTEL_` environment variables](#otel_-environment-variables)
* [Contributing](#contributing)
Expand Down Expand Up @@ -209,10 +210,7 @@ setupOpenTelemetry({

#### `options.authorizationHeader`

Set the [`Authorization` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) in requests to the OpenTelemetry collector. Defaults to `undefined`.

**Environment variable:** `OPENTELEMETRY_AUTHORIZATION_HEADER`<br/>
**Option:** `authorizationHeader` (`String`)
**Deprecated**. This will still work but has been replaced with [`options.tracing.authorizationHeader`](#optionstracingauthorizationheader), which is now the preferred way to set this option.

#### `options.tracing`

Expand All @@ -225,6 +223,13 @@ A URL to send OpenTelemetry traces to. E.g. `http://localhost:4318/v1/traces`. D
**Environment variable:** `OPENTELEMETRY_TRACING_ENDPOINT`<br/>
**Option:** `tracing.endpoint` (`String`)

#### `options.tracing.authorizationHeader`

Set the [`Authorization` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) in requests to the OpenTelemetry tracing collector. Defaults to `undefined`.

**Environment variable:** `OPENTELEMETRY_AUTHORIZATION_HEADER`<br/>
**Option:** `tracing.authorizationHeader` (`String`)

#### `options.tracing.samplePercentage`

The percentage of traces to send to the exporter. Defaults to `5` which means that 5% of traces will be exported.
Expand Down
60 changes: 60 additions & 0 deletions packages/opentelemetry/lib/config/instrumentations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const {
getNodeAutoInstrumentations
} = require('@opentelemetry/auto-instrumentations-node');
const { logRecoverableError } = require('@dotcom-reliability-kit/log-error');
const { UserInputError } = require('@dotcom-reliability-kit/errors');

// Request paths that we ignore when instrumenting HTTP requests
const IGNORED_REQUEST_PATHS = ['/__gtg', '/__health', '/favicon.ico'];

/**
* Create an instrumentations array for configuring OpenTelemetry.
*
* @returns {import('@opentelemetry/sdk-node').NodeSDKConfiguration['instrumentations']}
*/
exports.createInstrumentationConfig = function createInstrumentationConfig() {
return [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook
},
'@opentelemetry/instrumentation-fs': {
enabled: false
}
})
];
};

/**
* NOTE: this is not a filter like you know it. The name gives us a clue:
* if the hook returns `true` then the request WILL be ignored.
*
* @see https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/README.md#http-instrumentation-options
* @type {import('@opentelemetry/instrumentation-http').IgnoreIncomingRequestFunction}
*/
function ignoreIncomingRequestHook(request) {
if (request.url) {
try {
const url = new URL(request.url, `http://${request.headers.host}`);

// Don't send traces for paths that we frequently poll
if (IGNORED_REQUEST_PATHS.includes(url.pathname)) {
return true;
}
} catch (/** @type {any} */ cause) {
// If URL parsing errors then we log it and move on.
// We don't ignore URLs that result in an error because
// we're interested in the traces from bad requests.
logRecoverableError({
error: new UserInputError({
message: 'Failed to parse the request URL for filtering',
code: 'OTEL_REQUEST_FILTER_FAILURE',
cause
}),
includeHeaders: ['host'],
request
});
}
}
return false;
}
25 changes: 25 additions & 0 deletions packages/opentelemetry/lib/config/resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const appInfo = require('@dotcom-reliability-kit/app-info');
const { Resource } = require('@opentelemetry/resources');
const {
SEMRESATTRS_CLOUD_PROVIDER,
SEMRESATTRS_CLOUD_REGION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION
} = require('@opentelemetry/semantic-conventions');

/**
* Create a Resource object using gathered app info.
*
* @returns {import('@opentelemetry/sdk-node').NodeSDKConfiguration['resource']}
*/
exports.createResourceConfig = function createResourceConfig() {
// We set OpenTelemetry resource attributes based on app data
return new Resource({
[SEMRESATTRS_SERVICE_NAME]: appInfo.systemCode || undefined,
[SEMRESATTRS_SERVICE_VERSION]: appInfo.releaseVersion || undefined,
[SEMRESATTRS_CLOUD_PROVIDER]: appInfo.cloudProvider || undefined,
[SEMRESATTRS_CLOUD_REGION]: appInfo.region || undefined,
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: appInfo.environment || undefined
});
};
74 changes: 74 additions & 0 deletions packages/opentelemetry/lib/config/tracing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const {
OTLPTraceExporter
} = require('@opentelemetry/exporter-trace-otlp-proto');
const {
NoopSpanProcessor,
TraceIdRatioBasedSampler
} = require('@opentelemetry/sdk-trace-base');
const logger = require('@dotcom-reliability-kit/logger');
const { TRACING_USER_AGENT } = require('./user-agents');

const DEFAULT_SAMPLE_PERCENTAGE = 5;

/**
* @typedef {object} TracingOptions
* @property {string} [authorizationHeader]
* The HTTP `Authorization` header to send with OpenTelemetry tracing requests.
* @property {string} [endpoint]
* The URL to send OpenTelemetry trace segments to, for example http://localhost:4318/v1/traces.
* @property {number} [samplePercentage]
* What percentage of traces should be sent onto the collector.
*/

/**
* Create an OpenTelemetry tracing configuration.
*
* @param {TracingOptions} options
* @returns {Partial<import('@opentelemetry/sdk-node').NodeSDKConfiguration>}
*/
exports.createTracingConfig = function createTracingConfig(options) {
/** @type {Partial<import('@opentelemetry/sdk-node').NodeSDKConfiguration>} */
const config = {};

// If we have an OpenTelemetry tracing endpoint then set it up,
// otherwise we pass a noop span processor so that nothing is exported
if (options?.endpoint) {
const headers = {
'user-agent': TRACING_USER_AGENT
};
if (options.authorizationHeader) {
headers.authorization = options.authorizationHeader;
}
config.traceExporter = new OTLPTraceExporter({
url: options.endpoint,
headers
});

// Sample traces
let samplePercentage = DEFAULT_SAMPLE_PERCENTAGE;
if (options.samplePercentage && !Number.isNaN(options.samplePercentage)) {
samplePercentage = options.samplePercentage;
}
const sampleRatio = samplePercentage / 100;
config.sampler = new TraceIdRatioBasedSampler(sampleRatio);

logger.info({
event: 'OTEL_TRACE_STATUS',
message: `OpenTelemetry tracing is enabled and exporting to endpoint ${options.endpoint}`,
enabled: true,
endpoint: options.endpoint,
samplePercentage
});
} else {
logger.warn({
event: 'OTEL_TRACE_STATUS',
message:
'OpenTelemetry tracing is disabled because no tracing endpoint was set',
enabled: false,
endpoint: null
});
config.spanProcessor = new NoopSpanProcessor();
}

return config;
};
7 changes: 7 additions & 0 deletions packages/opentelemetry/lib/config/user-agents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const appInfo = require('@dotcom-reliability-kit/app-info');
const packageJson = require('../../package.json');
const traceExporterPackageJson = require('@opentelemetry/exporter-trace-otlp-proto/package.json');

const BASE_USER_AGENT = `FTSystem/${appInfo.systemCode} (${packageJson.name}/${packageJson.version})`;

exports.TRACING_USER_AGENT = `${BASE_USER_AGENT} (${traceExporterPackageJson.name}/${traceExporterPackageJson.version})`;
158 changes: 22 additions & 136 deletions packages/opentelemetry/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,32 @@
const packageJson = require('../package.json');
const { createInstrumentationConfig } = require('./config/instrumentations');
const { createResourceConfig } = require('./config/resource');
const { createTracingConfig } = require('./config/tracing');
const { diag, DiagLogLevel } = require('@opentelemetry/api');
const {
getNodeAutoInstrumentations
} = require('@opentelemetry/auto-instrumentations-node');
const { Resource } = require('@opentelemetry/resources');
const opentelemetrySDK = require('@opentelemetry/sdk-node');
const {
SEMRESATTRS_CLOUD_PROVIDER,
SEMRESATTRS_CLOUD_REGION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION
} = require('@opentelemetry/semantic-conventions');
const appInfo = require('@dotcom-reliability-kit/app-info');
const {
OTLPTraceExporter
} = require('@opentelemetry/exporter-trace-otlp-proto');
const traceExporterPackageJson = require('@opentelemetry/exporter-trace-otlp-proto/package.json');
const {
NoopSpanProcessor,
TraceIdRatioBasedSampler
} = require('@opentelemetry/sdk-trace-base');
const logger = require('@dotcom-reliability-kit/logger');
const { logRecoverableError } = require('@dotcom-reliability-kit/log-error');
const { UserInputError } = require('@dotcom-reliability-kit/errors');

const USER_AGENT = `FTSystem/${appInfo.systemCode} (${packageJson.name}/${packageJson.version})`;
const TRACING_USER_AGENT = `${USER_AGENT} (${traceExporterPackageJson.name}/${traceExporterPackageJson.version})`;

const DEFAULT_SAMPLE_PERCENTAGE = 5;

const IGNORED_REQUEST_PATHS = ['/__gtg', '/__health', '/favicon.ico'];
/**
* @typedef {import('./config/tracing').TracingOptions} TracingOptions
*/

/**
* @typedef {object} Options
* @property {string} [authorizationHeader]
* The HTTP `Authorization` header to send with OpenTelemetry requests.
* [DEPRECATED] The HTTP `Authorization` header to send with OpenTelemetry requests. Use `tracing.authorizationHeader` instead.
* @property {TracingOptions} [tracing]
* Configuration options for OpenTelemetry tracing.
*/

/**
* @typedef {object} TracingOptions
* @property {string} endpoint
* The URL to send OpenTelemetry trace segments to, for example http://localhost:4318/v1/traces.
* @property {number} [samplePercentage]
* What percentage of traces should be sent onto the collector.
*/

/**
* Set up OpenTelemetry tracing.
*
* @param {Options} [options]
* OpenTelemetry configuration options.
*/
function setupOpenTelemetry({ authorizationHeader, tracing } = {}) {
function setupOpenTelemetry({
authorizationHeader,
tracing: tracingOptions
} = {}) {
// We don't support using the built-in `OTEL_`-prefixed environment variables. We
// do want to know when these are used, though, so that we can easily spot when
// an app's use of these environment variables might be interfering.
Expand All @@ -77,104 +49,18 @@ function setupOpenTelemetry({ authorizationHeader, tracing } = {}) {
DiagLogLevel.INFO
);

// Construct the OpenTelemetry SDK configuration
/** @type {opentelemetrySDK.NodeSDKConfiguration} */
const openTelemetryConfig = {};

// Set OpenTelemetry resource attributes based on app data
openTelemetryConfig.resource = new Resource({
[SEMRESATTRS_SERVICE_NAME]: appInfo.systemCode || undefined,
[SEMRESATTRS_SERVICE_VERSION]: appInfo.releaseVersion || undefined,
[SEMRESATTRS_CLOUD_PROVIDER]: appInfo.cloudProvider || undefined,
[SEMRESATTRS_CLOUD_REGION]: appInfo.region || undefined,
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: appInfo.environment || undefined
});

// Auto-instrument common and built-in Node.js modules
openTelemetryConfig.instrumentations = [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
// NOTE: this is not a filter like you know it. The name
// gives us a clue: if the hook returns `true` then the
// request WILL be ignored.
ignoreIncomingRequestHook: (request) => {
if (request.url) {
try {
const url = new URL(
request.url,
`http://${request.headers.host}`
);

// Don't send traces for paths that we frequently poll
if (IGNORED_REQUEST_PATHS.includes(url.pathname)) {
return true;
}
} catch (/** @type {any} */ cause) {
// If URL parsing errors then we log it and move on.
// We don't ignore URLs that result in an error because
// we're interested in the traces from bad requests.
logRecoverableError({
error: new UserInputError({
message: 'Failed to parse the request URL for filtering',
code: 'OTEL_REQUEST_FILTER_FAILURE',
cause
}),
includeHeaders: ['host'],
request
});
}
}
return false;
}
},
'@opentelemetry/instrumentation-fs': {
enabled: false
}
})
];

// If we have an OpenTelemetry tracing endpoint then set it up,
// otherwise we pass a noop span processor so that nothing is exported
if (tracing?.endpoint) {
const headers = {
'user-agent': TRACING_USER_AGENT
};
if (authorizationHeader) {
headers.authorization = authorizationHeader;
}
openTelemetryConfig.traceExporter = new OTLPTraceExporter({
url: tracing.endpoint,
headers
});

// Sample traces
let samplePercentage = DEFAULT_SAMPLE_PERCENTAGE;
if (tracing.samplePercentage && !Number.isNaN(tracing.samplePercentage)) {
samplePercentage = tracing.samplePercentage;
}
const sampleRatio = samplePercentage / 100;
openTelemetryConfig.sampler = new TraceIdRatioBasedSampler(sampleRatio);

logger.info({
event: 'OTEL_TRACE_STATUS',
message: `OpenTelemetry tracing is enabled and exporting to endpoint ${tracing.endpoint}`,
enabled: true,
endpoint: tracing.endpoint,
samplePercentage
});
} else {
logger.warn({
event: 'OTEL_TRACE_STATUS',
message:
'OpenTelemetry tracing is disabled because no tracing endpoint was set',
enabled: false,
endpoint: null
});
openTelemetryConfig.spanProcessor = new NoopSpanProcessor();
}

// Set up and start OpenTelemetry
const sdk = new opentelemetrySDK.NodeSDK(openTelemetryConfig);
const sdk = new opentelemetrySDK.NodeSDK({
// Configurations we set regardless of whether we're using tracing
instrumentations: createInstrumentationConfig(),
resource: createResourceConfig(),

// Add tracing-specific configurations
...createTracingConfig({
authorizationHeader,
...tracingOptions
})
});
sdk.start();
}

Expand Down
Loading
Loading