packages/fxa-shared/tracing/node-tracing.ts (153 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import api from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import { ILogger } from '../log'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { BatchSpanProcessor, NodeTracerProvider, ParentBasedSampler, SimpleSpanProcessor, SpanExporter, SpanProcessor, TraceIdRatioBasedSampler, } from '@opentelemetry/sdk-trace-node'; import { checkSampleRate, checkServiceName, TracingOpts } from './config'; import { getConsoleTraceExporter } from './exporters/fxa-console'; import { getGcpTraceExporter } from './exporters/fxa-gcp'; import { getOtlpTraceExporter } from './exporters/fxa-otlp'; import { createPiiFilter } from './pii-filters'; import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; import { resourceFromAttributes } from '@opentelemetry/resources'; const log_type = 'node-tracing'; export const TRACER_NAME = 'fxa'; /** * Responsible for initializing node tracing from a config object. This uses the auto instrumentation feature * which tries to add as much instrumentation as possible. See the 'supported instrumentations section at * https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node for more info. */ export class NodeTracingInitializer { protected provider: NodeTracerProvider; constructor( protected readonly opts: TracingOpts, protected readonly logger?: ILogger ) { // Error out if certain options are invalid checkServiceName(this.opts); checkSampleRate(this.opts); const filter = createPiiFilter(!!this.opts?.filterPii, this.logger); const spanProcessors = [ this.makeSpanProcessor( getOtlpTraceExporter(this.opts, undefined, filter) ), this.makeSpanProcessor(getGcpTraceExporter(this.opts, filter)), this.makeSpanProcessor(getConsoleTraceExporter(this.opts, filter)), // add more exporters here ].filter((x) => x !== undefined); this.provider = new NodeTracerProvider({ sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(this.opts.sampleRate), }), resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: this.opts.serviceName, }), spanProcessors, }); this.register(); } /** * Creates a new span processor for the exporter. * @param exporter * @returns */ private makeSpanProcessor = ( exporter: SpanExporter | undefined ): SpanProcessor | undefined => { if (!exporter) { return undefined; } return this.opts.batchProcessor ? new BatchSpanProcessor(exporter) : new SimpleSpanProcessor(exporter); }; protected register() { registerInstrumentations({ instrumentations: [ // ...extraInstrumentations, getNodeAutoInstrumentations({ // These instrumentations added a lot of unnecessary noise '@opentelemetry/instrumentation-dns': { enabled: false, }, '@opentelemetry/instrumentation-net': { enabled: false, }, '@opentelemetry/instrumentation-fs': { enabled: false, }, }), ], }); this.provider.register(); } public startSpan(name: string, action: () => void) { return this.provider.getTracer(TRACER_NAME).startActiveSpan(name, action); } /** Gets current traceId */ public getTraceId() { const currentSpan = api.trace.getSpan(api.context.active()); if (currentSpan) { return currentSpan.spanContext().traceId; } return null; } public getTraceParentId() { const tracer = this.provider.getTracer('fxa'); const span = tracer.startSpan('client-inject'); const version = '00'; const spanContext = span.spanContext(); let sampleDecision = '00'; if (Math.random() <= this.opts.sampleRate) { sampleDecision = '01'; } const parentId = `${version}-${spanContext.traceId}-${spanContext.spanId}-${sampleDecision}`; span.end(); return parentId; } public getProvider() { return this.provider; } } /** Singleton */ let nodeTracing: NodeTracingInitializer | undefined; /** Gets active trace parent id */ export function getTraceParentId() { if (nodeTracing == null) { return '00-0-0-00'; } return nodeTracing.getTraceParentId(); } /** Initializes tracing in node context */ export function initTracing(opts: TracingOpts, logger: ILogger) { if (nodeTracing != null) { logger?.debug(log_type, { msg: 'Trace initialization skipped. Tracing already initialized, ignoring new opts.', }); return nodeTracing; } if ( !opts.otel?.enabled && !opts.gcp?.enabled && !opts.console?.enabled && !opts.sentry?.enabled ) { logger.debug(log_type, { msg: 'Trace initialization skipped. No exporters configured. Enable gcp, otel or console to activate tracing.', }); return; } try { nodeTracing = new NodeTracingInitializer(opts, logger); logger.info(log_type, { msg: 'Trace initialized succeeded!' }); } catch (err) { logger.error(log_type, { msg: `Trace initialization failed: ${err.message}`, }); } return nodeTracing; } /** Get the current instance of the node tracing provider. */ export function getCurrent() { return nodeTracing; } /** Indicates that tracing has been initialized. */ export function isInitialized() { return !!nodeTracing; } /** Suppresses trace capture on the current context */ export function suppressTrace(action: () => any) { const currentCtx = api.context.active(); return api.context.with(suppressTracing(currentCtx), action); } /** Resets the current tracing instance. Use only for testing purposes. */ export function reset() { nodeTracing = undefined; }