@netlify/opentelemetry-sdk-setup 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2020 Netlify <team@netlify.com>
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Opentelemetry SDK Setup
2
+
3
+ This package extracts the logic necessary to initialise the Opentelemetry JS SDK using our tracing exporter. This not
4
+ only allows us to reuse the initialisation logic across different node process executions but also means **our modules
5
+ don't need to depend on any @opentelemetry module other than the @opentelemetry/api**
6
+
7
+ ## How to use it?
8
+
9
+ This module is designed to be preloaded via [--import](https://nodejs.org/docs/latest-v18.x/api/cli.html#--importmodule)
10
+ on any node execution. For example:
11
+
12
+ ```
13
+ $> node --import=./lib/bin.js ../build/lib/core/bin.js --debug --tracing.enabled=false --tracing.httpProtocol=https --tracing.host=api.honeycomb.io --tracing.port=443 --tracing.debug=true --tracing.preloadingEnabled=true .
14
+ ```
15
+
16
+ On the script we're instrumenting we can just rely on `@opentelemetry/api` to create spans and interact with the SDK:
17
+
18
+ ```ts
19
+ import { trace } from '@opentelemetry/api'
20
+ const tracer = trace.getTracer('secrets-scanning')
21
+
22
+ const myInstrumentedFunction = async function() {
23
+ await tracer.startActiveSpan(
24
+ 'scanning-files',
25
+ { attributes: { myAttribute: 'foobar' } },
26
+ async (span) => {
27
+ doSomeWork()
28
+ span.end()
29
+ }
30
+ }
31
+
32
+ ```
33
+
34
+ ## Sharing and receiving context from outside of the process
35
+
36
+ Our SDK initialisation is prepared to receive [trace](https://opentelemetry.io/docs/concepts/signals/traces/) and
37
+ [baggage](https://opentelemetry.io/docs/concepts/signals/baggage/) context from outside of the process. This allow us
38
+ to, for example, hook this node process execution to an ongoing trace which is already taking place or share the baggage
39
+ attributes for that execution with the spans created in this process. The list of tracing options show the options
40
+ available to the executable and what they mean.
41
+
42
+ Unfortunately, to our knowledge, the current `@opentelemetry` setup does not allow us to define an initial global
43
+ context that the root span can inherit from. As a consequence we had to get creative in order to pass the ingested
44
+ attributes to our main script execution, so that the root span can get the newly ingested attributes. We're relying on a
45
+ global property which can be accessed via `@netlify/opentelemetry-utils`. If your process receives any outside
46
+ attributes you can do the following:
47
+
48
+ ```
49
+ $> node --import=./lib/bin.js my-instrumented-script --tracing.httpProtocol=https --tracing.host=api.honeycomb.io --tracing.port=443 --tracing.debug=true --tracing.preloadingEnabled=true --tracing.baggageFilePath='./my-baggage-filepath' --tracing.traceId=<my-trace-id> --tracing.parentSpanId=<the-span-id-of-the-parent>
50
+ ```
51
+
52
+ And on the instrumented script:
53
+
54
+ ```ts
55
+ import { trace } from '@opentelemetry/api'
56
+ import { getGlobalContext } from '@netlify/opentelemetry-utils'
57
+ const tracer = trace.getTracer('secrets-scanning')
58
+
59
+ const myInstrumentedFunction = async function() {
60
+ await tracer.startActiveSpan(
61
+ 'scanning-files',
62
+ { attributes: { myAttribute: 'foobar' } },
63
+ getGlobalContext(),
64
+ async (span) => {
65
+ doSomeWork()
66
+ span.end()
67
+ }
68
+ }
69
+
70
+ ```
package/bin.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This is a workaround for npm issue: https://github.com/npm/cli/issues/2632
4
+
5
+ import './lib/bin.js'
package/lib/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/lib/bin.js ADDED
@@ -0,0 +1,47 @@
1
+ import process from 'node:process';
2
+ import { diag } from '@opentelemetry/api';
3
+ import argsParser from 'yargs-parser';
4
+ import { startTracing, stopTracing } from './sdk-setup.js';
5
+ import { findExecutablePackageJSON, setGlobalContext } from './util.js';
6
+ const DEFAULT_OTEL_TRACING_PORT = 4317;
7
+ const DEFAULT_OTEL_ENDPOINT_PROTOCOL = 'http';
8
+ const defaultOptions = {
9
+ preloadingEnabled: false,
10
+ httpProtocol: DEFAULT_OTEL_ENDPOINT_PROTOCOL,
11
+ host: 'locahost',
12
+ port: DEFAULT_OTEL_TRACING_PORT,
13
+ sampleRate: 1,
14
+ baggageFilePath: '',
15
+ apiKey: '-',
16
+ parentSpanId: '',
17
+ traceId: '',
18
+ debug: false,
19
+ };
20
+ const args = argsParser(process.argv);
21
+ // Apply the defaults making sure we're not tripped by falsy values
22
+ const options = Object.entries(defaultOptions)
23
+ .map(([key, defaultValue]) => {
24
+ if (args.tracing !== undefined && args.tracing[key] !== undefined) {
25
+ return { [key]: args.tracing[key] };
26
+ }
27
+ return { [key]: defaultValue };
28
+ })
29
+ .reduce((acc, prop) => ({ ...acc, ...prop }), {});
30
+ const executablePath = args._[1];
31
+ try {
32
+ const pkg = await findExecutablePackageJSON(executablePath);
33
+ const rootCtx = await startTracing(options, pkg);
34
+ if (rootCtx !== undefined) {
35
+ diag.debug('Setting global root context imported from bagage file');
36
+ setGlobalContext(rootCtx);
37
+ }
38
+ else {
39
+ diag.debug('Root context undefined, skip setting global root context');
40
+ }
41
+ }
42
+ catch {
43
+ // don't blow up the execution in case something fails
44
+ }
45
+ //TODO handle `stopTracing` via `process` event emitter for all the other cases such as
46
+ //SIGINT and SIGTERM signals and potential uncaught exceptions
47
+ process.on('beforeExit', async () => await stopTracing());
@@ -0,0 +1,32 @@
1
+ import type { PackageJson } from 'read-pkg-up';
2
+ export type TracingOptions = {
3
+ /** This is a temporary property to signal preloading is enabled, can be replaced with `enabled` once we retire build's internal sdk setup */
4
+ preloadingEnabled: boolean;
5
+ httpProtocol: string;
6
+ host: string;
7
+ port: number;
8
+ /** API Key used for a dedicated trace provider */
9
+ apiKey: string;
10
+ /** Sample rate being used for this trace, this allows for consistent probability sampling */
11
+ sampleRate: number;
12
+ /** Properties of the root span and trace id used to stitch context */
13
+ traceId?: string;
14
+ traceFlags?: number;
15
+ parentSpanId?: string;
16
+ baggageFilePath?: string;
17
+ /** Debug mode enabled - logs to stdout */
18
+ debug: boolean;
19
+ /** System log file descriptor */
20
+ systemLogFile?: number;
21
+ };
22
+ /** Starts the tracing SDK, if there's already a tracing service this will be a no-op */
23
+ export declare const startTracing: (options: TracingOptions, packageJson: PackageJson) => Promise<import("@opentelemetry/api").Context | undefined>;
24
+ /** Stops the tracing service if there's one running. This will flush any ongoing events */
25
+ export declare const stopTracing: () => Promise<void>;
26
+ /** Sets attributes to be propagated across child spans under the current active context
27
+ * TODO this method will be removed from this package once we move it to a dedicated one to be shared between build,
28
+ * this setup and any other node module which might use our open telemetry setup
29
+ * */
30
+ export declare const setMultiSpanAttributes: (attributes: {
31
+ [key: string]: string;
32
+ }) => import("@opentelemetry/api").Context;
@@ -0,0 +1,74 @@
1
+ import { HoneycombSDK } from '@honeycombio/opentelemetry-node';
2
+ import { trace, diag, context, propagation, DiagLogLevel, TraceFlags } from '@opentelemetry/api';
3
+ import { Resource } from '@opentelemetry/resources';
4
+ import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
5
+ import { getDiagLogger, loadBaggageFromFile } from './util.js';
6
+ let sdk;
7
+ /** Starts the tracing SDK, if there's already a tracing service this will be a no-op */
8
+ export const startTracing = async function (options, packageJson) {
9
+ if (!options.preloadingEnabled)
10
+ return;
11
+ if (sdk)
12
+ return;
13
+ sdk = new HoneycombSDK({
14
+ resource: new Resource({
15
+ [SemanticResourceAttributes.SERVICE_VERSION]: packageJson.version,
16
+ }),
17
+ serviceName: packageJson.name,
18
+ protocol: 'grpc',
19
+ apiKey: options.apiKey,
20
+ endpoint: `${options.httpProtocol}://${options.host}:${options.port}`,
21
+ sampleRate: options.sampleRate,
22
+ // Turn off auto resource detection so that we fully control the attributes we export
23
+ autoDetectResources: false,
24
+ });
25
+ // Set the diagnostics logger to our system logger. We also need to suppress the override msg
26
+ // in case there's a default console logger already registered (it would log a msg to it)
27
+ diag.setLogger(getDiagLogger(options.debug, options.systemLogFile), {
28
+ logLevel: options.debug ? DiagLogLevel.DEBUG : DiagLogLevel.INFO,
29
+ suppressOverrideMessage: true,
30
+ });
31
+ sdk.start();
32
+ // Loads the contents of the passed baggageFilePath into the baggage
33
+ const baggageAttributes = await loadBaggageFromFile(options.baggageFilePath);
34
+ const baggageCtx = setMultiSpanAttributes(baggageAttributes);
35
+ const traceFlags = options.traceFlags !== undefined ? options.traceFlags : TraceFlags.NONE;
36
+ // Sets the current trace ID and span ID based on the options received
37
+ // this is used as a way to propagate trace context from other processes such as Buildbot
38
+ if (options.traceId !== undefined && options.parentSpanId !== undefined) {
39
+ return trace.setSpanContext(baggageCtx, {
40
+ traceId: options.traceId,
41
+ spanId: options.parentSpanId,
42
+ traceFlags: traceFlags,
43
+ isRemote: true,
44
+ });
45
+ }
46
+ return context.active();
47
+ };
48
+ /** Stops the tracing service if there's one running. This will flush any ongoing events */
49
+ export const stopTracing = async function () {
50
+ if (!sdk)
51
+ return;
52
+ try {
53
+ // The shutdown method might return an error if we fail to flush the traces
54
+ // We handle it and use our diagnostics logger
55
+ await sdk.shutdown();
56
+ sdk = undefined;
57
+ }
58
+ catch (e) {
59
+ diag.error(e);
60
+ }
61
+ };
62
+ /** Sets attributes to be propagated across child spans under the current active context
63
+ * TODO this method will be removed from this package once we move it to a dedicated one to be shared between build,
64
+ * this setup and any other node module which might use our open telemetry setup
65
+ * */
66
+ export const setMultiSpanAttributes = function (attributes) {
67
+ const currentBaggage = propagation.getBaggage(context.active());
68
+ // Create a baggage if there's none
69
+ let baggage = currentBaggage === undefined ? propagation.createBaggage() : currentBaggage;
70
+ Object.entries(attributes).forEach(([key, value]) => {
71
+ baggage = baggage.setEntry(key, { value });
72
+ });
73
+ return propagation.setBaggage(context.active(), baggage);
74
+ };
package/lib/util.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { DiagLogger, Context } from '@opentelemetry/api';
2
+ import { PackageJson } from 'read-pkg-up';
3
+ /** Given a simple logging function return a `DiagLogger`. Used to setup our system logger as the diag logger.*/
4
+ export declare const getDiagLogger: (debug: boolean, systemLogFile?: number) => DiagLogger;
5
+ export declare const loadBaggageFromFile: (baggageFilePath?: string) => Promise<Record<string, string>>;
6
+ /**
7
+ * Given a path to a node executable (potentially a symlink) get the module packageJson
8
+ */
9
+ export declare const findExecutablePackageJSON: (path: string) => Promise<PackageJson>;
10
+ /**
11
+ * Sets global context to be used when initialising our root span
12
+ * TODO this will move to a shared package (opentelemetry-utils) to scope the usage of this global property there
13
+ */
14
+ export declare const setGlobalContext: (ctx: Context) => void;
15
+ /**
16
+ * Gets the global context to be used when initialising our root span
17
+ * TODO this will move to a shared package (opentelemetry-utils) to scope the usage of this global property there
18
+ */
19
+ export declare const getGlobalContext: () => Context;
package/lib/util.js ADDED
@@ -0,0 +1,110 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { realpath, readFile } from 'node:fs/promises';
3
+ import { diag, context } from '@opentelemetry/api';
4
+ import { parseKeyPairsIntoRecord } from '@opentelemetry/core/build/src/baggage/utils.js';
5
+ import { readPackageUp } from 'read-pkg-up';
6
+ /**
7
+ * Builds a function for logging data to a provided fileDescriptor (i.e. hidden from
8
+ * the user-facing build logs)
9
+ * This has been pulled from @netlify/build as a quick way to hook into the system logger
10
+ */
11
+ const getSystemLogger = function (debug,
12
+ /** A system log file descriptor, if non is provided it will be a noop logger */
13
+ systemLogFile) {
14
+ // If the `debug` flag is used, we return a function that pipes system logs
15
+ // to the regular logger, as the intention is for them to end up in stdout.
16
+ // For now we just use plain `console.log`, later on we can revise it
17
+ if (debug) {
18
+ return (...args) => console.log(...args);
19
+ }
20
+ // If there's not a file descriptor configured for system logs and `debug`
21
+ // is not set, we return a no-op function that will swallow the errors.
22
+ if (!systemLogFile) {
23
+ return () => {
24
+ // no-op
25
+ };
26
+ }
27
+ // Return a function that writes to the file descriptor configured for system
28
+ // logs.
29
+ const fileDescriptor = createWriteStream('', { fd: systemLogFile });
30
+ fileDescriptor.on('error', () => {
31
+ console.error('Could not write to system log file');
32
+ });
33
+ return (...args) => fileDescriptor.write(`${console.log(...args)}\n`);
34
+ };
35
+ /** Given a simple logging function return a `DiagLogger`. Used to setup our system logger as the diag logger.*/
36
+ export const getDiagLogger = function (debug,
37
+ /** A system log file descriptor, if non is provided it will be a noop logger */
38
+ systemLogFile) {
39
+ const logger = getSystemLogger(debug, systemLogFile);
40
+ const otelLogger = (...args) => {
41
+ // Debug log msgs can be an array of 1 or 2 elements with the second element being an array fo multiple elements
42
+ const msgs = args.flat(1);
43
+ logger('[otel-traces]', ...msgs);
44
+ };
45
+ return {
46
+ debug: otelLogger,
47
+ info: otelLogger,
48
+ error: otelLogger,
49
+ verbose: otelLogger,
50
+ warn: otelLogger,
51
+ };
52
+ };
53
+ //** Loads the baggage attributes from a baggage file which follows W3C Baggage specification */
54
+ export const loadBaggageFromFile = async function (baggageFilePath) {
55
+ if (baggageFilePath === undefined || baggageFilePath.length === 0) {
56
+ diag.warn('No baggage file path provided, no context loaded');
57
+ return {};
58
+ }
59
+ let baggageString;
60
+ try {
61
+ baggageString = await readFile(baggageFilePath, 'utf-8');
62
+ }
63
+ catch (error) {
64
+ diag.error(error);
65
+ return {};
66
+ }
67
+ return parseKeyPairsIntoRecord(baggageString);
68
+ };
69
+ /**
70
+ * Given a path to a node executable (potentially a symlink) get the module packageJson
71
+ */
72
+ export const findExecutablePackageJSON = async function (path) {
73
+ let pathToSearch;
74
+ try {
75
+ // resolve symlinks
76
+ pathToSearch = await realpath(path);
77
+ }
78
+ catch {
79
+ // bail early if we can't resolve the path
80
+ return {};
81
+ }
82
+ try {
83
+ const result = await readPackageUp({ cwd: pathToSearch, normalize: false });
84
+ if (result === undefined)
85
+ return {};
86
+ const { packageJson } = result;
87
+ return packageJson;
88
+ }
89
+ catch {
90
+ // packageJson read failed, we ignore the error and return an empty obj
91
+ return {};
92
+ }
93
+ };
94
+ /**
95
+ * Sets global context to be used when initialising our root span
96
+ * TODO this will move to a shared package (opentelemetry-utils) to scope the usage of this global property there
97
+ */
98
+ export const setGlobalContext = function (ctx) {
99
+ global['NETLIFY_GLOBAL_CONTEXT'] = ctx;
100
+ };
101
+ /**
102
+ * Gets the global context to be used when initialising our root span
103
+ * TODO this will move to a shared package (opentelemetry-utils) to scope the usage of this global property there
104
+ */
105
+ export const getGlobalContext = function () {
106
+ if (global['NETLIFY_GLOBAL_CONTEXT'] === undefined) {
107
+ return context.active();
108
+ }
109
+ return global['NETLIFY_GLOBAL_CONTEXT'];
110
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@netlify/opentelemetry-sdk-setup",
3
+ "version": "1.0.0",
4
+ "description": "Opentelemetry SDK setup script",
5
+ "type": "module",
6
+ "bin": {
7
+ "otel-sdk-setup": "./bin.js"
8
+ },
9
+ "files": [
10
+ "bin.js",
11
+ "lib/**/*"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "build:logos": "vite build",
16
+ "test": "vitest run",
17
+ "test:dev": "vitest --ui",
18
+ "test:ci": "vitest run --reporter=default"
19
+ },
20
+ "keywords": [],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/netlify/build.git",
25
+ "directory": "packages/opentelemetry-sdk-setup"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/netlify/build/issues"
29
+ },
30
+ "author": "Netlify Inc.",
31
+ "dependencies": {
32
+ "@honeycombio/opentelemetry-node": "^0.6.0",
33
+ "@opentelemetry/api": "~1.6.0",
34
+ "@opentelemetry/core": "^1.17.1",
35
+ "@opentelemetry/resources": "^1.18.1",
36
+ "@opentelemetry/semantic-conventions": "^1.18.1",
37
+ "yargs-parser": "^21.1.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^14.18.53",
41
+ "@vitest/coverage-c8": "^0.30.1",
42
+ "@vitest/ui": "^0.30.1",
43
+ "typescript": "^5.0.0",
44
+ "vite": "^4.0.4",
45
+ "vitest": "^0.30.1"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "gitHead": "9c8c452edb6d062ac7f03eaa64e9a23e0791ad7c"
51
+ }