Skip to content

Instantly share code, notes, and snippets.

@codinronan
Created January 23, 2025 21:38
Show Gist options
  • Save codinronan/8dde92116aadb2c7d7e7831a835d6d70 to your computer and use it in GitHub Desktop.
Save codinronan/8dde92116aadb2c7d7e7831a835d6d70 to your computer and use it in GitHub Desktop.
Shows how to set up OpenTelemetry for Node using their NodeSDK customized with exporters, loggers, and processors.
import * as otelApi from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { logs, Logger } from '@opentelemetry/api-logs';
import {
BatchLogRecordProcessor,
BufferConfig,
ConsoleLogRecordExporter,
LogRecordProcessor,
} from '@opentelemetry/sdk-logs';
type TelemetryCarrier = {
traceparent: string;
tracestate: string;
};
const telemetryBatchConfig: BufferConfig = {
scheduledDelayMillis: 250,
maxExportBatchSize: 25,
};
let otelConfig: {
sdk: NodeSDK;
/**
* The current tracing context, which can be used for trace propagation.
* See https://opentelemetry.io/docs/languages/js/propagation/#generic-example
*/
context: AsyncLocalStorageContextManager;
/**
* The global trace currently active in opentelemetry.
* You can use this to get spans, get the active span to add events,
* and get a named tracer instance via getTracer, which is useful for
* trace propagation.
* This should mostly not be necessary since we use the OTEL lambda layers,
* and the supabase client is automatically provided the context; but if you
* need to use fetch or `ky` directly, this can be useful for that purpose.
*/
trace: otelApi.TraceAPI;
/**
* The current service's tracer instance. Use this to create new spans and active spans.
*/
currentTracer: otelApi.Tracer;
logger: Logger;
} | undefined;
// External utility for code that is not inside the middleware context
// By the way, this only works because AsyncLocalStorage is a global singleton;
// otherwise, accessing it outside the middleware would still result in
// getting an undefined object.
export const getTelemetry = () => otelConfig;
/**
* Gets the current trace state. Note that if the request middleware has not been initialized
* this will fail and throw an error, even though OpenTelemetry has a default context that
* could otherwise be used. This is to ensure the active request context is used, which may
* have been set by the middleware by consuming an external trace context.
* @returns The current trace context, which can be used to propagate the active trace
* to systems that are not directly managed by the OpenTelemetry instrumentation.
*/
export const getTraceContext = () => {
if (!otelConfig) {
throw new Error('Cannot generate trace propagation context: OpenTelemetry instrumentation is not initialized');
}
const output = {} as TelemetryCarrier;
// We could technically use otelApi.context here but I prefer to use the
// one we know we've initialized.
otelApi.propagation.inject(otelConfig.context.active(), output);
return output;
};
/**
* Restores a trace context from a carrier object and returns the restored context. If you provide a context
* it will inherit the trace state. If you don't provide a context, the default context will be used.
* If manually restoring a trace context you should subsequently create a new Span whose context will
* be set to the restored context, e.g.
* ```
* const span = tracer.startSpan('my-operation', undefined, restoredContext);
* otelConfig.trace.setSpan(activeContext, span);
* ```
*
* @param traceRestore Contains the traceparent and tracestate headers to restore an active trace context
* @param withContext The context that will inherit the restored trace context. If not provided, the default
* context will be used.
* @returns The activated context. This can be used to set the active context for the current request.
*/
export const restoreTraceContext = (traceRestore: TelemetryCarrier, withContext?: otelApi.ContextAPI) => {
// See https://opentelemetry.io/docs/languages/js/propagation/#custom-protocol-example
// For now we won't create a new context, we'll expect whatever calls this to create it e.g.
// const withContext = new AsyncLocalStorageContextManager().enable();
// Note that as of last year this process had a bug, but there's a piece of reference
// code in the issue that may be useful:
// https://github.com/open-telemetry/opentelemetry-js/issues/4851
const context = withContext ?? otelConfig?.context ?? otelApi.context;
const activeContext: otelApi.Context = otelApi.propagation.extract(context.active(), traceRestore);
return activeContext;
};
export const initInstrumentation = (options: {
serviceName: string;
functionName: string;
}) => {
if (otelConfig) {
return otelConfig;
}
const otelServiceName = `${options.serviceName}/${options.functionName}`;
// Note that if we are only adding serviceName here, we don't need to do this, the NodeSDK does it
// automatically. If we add instance ID and version, we do have to do them here and the NodeSDK will
// merge them into the data.
const resource = Resource.default().merge(
new Resource({
[ATTR_SERVICE_NAME]: otelServiceName,
// [ATTR_SERVICE_INSTANCE_ID]: 'todo', // get this from the aws event request context
// [ATTR_SERVICE_VERSION]: 'todo', // e.g. '1.0.0', get this from the injected package version.
})
);
const logRecordProcessors: LogRecordProcessor[] = [
new BatchLogRecordProcessor(new ConsoleLogRecordExporter(), telemetryBatchConfig),
// todo: only add this one if environment is not-local
new BatchLogRecordProcessor(new OTLPLogExporter(), telemetryBatchConfig)
];
const spanProcessors = [
new BatchSpanProcessor(new ConsoleSpanExporter(), telemetryBatchConfig),
// todo: only add this one if environment is not-local
new BatchSpanProcessor(new OTLPTraceExporter(), telemetryBatchConfig),
];
const contextManager = new AsyncLocalStorageContextManager().enable();
// For more info on the constructor parameters, see:
// https://opentelemetry.io/docs/languages/js/libraries/
// See the following to potentially replace this instrumentation
// and use auto instrumentation instead:
// https://opentelemetry.io/docs/faas/lambda-auto-instrument/
// https://opentelemetry.io/docs/languages/js/getting-started/nodejs/
// Also, part of the collector config is set up via the collector.yaml file also
// in this project. See https://www.hyperdx.io/docs/install/aws-lambda
// for what it does.
const sdk = new NodeSDK({
autoDetectResources: true,
logRecordProcessors,
contextManager,
serviceName: otelServiceName,
resource,
spanProcessors,
// This function accepts a config object to turn instrumentations on and off, if needed.
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
// k we need to wrap this in an object to provide meaningful log methods and levels.
const logger = logs.getLogger(otelServiceName);
otelConfig = {
sdk,
context: contextManager,
trace: otelApi.trace,
currentTracer: otelApi.trace.getTracer(otelServiceName),
logger,
};
return otelConfig;
};
export const shutdownInstrumentation = async () => {
if (otelConfig?.sdk) {
await otelConfig.sdk.shutdown();
otelConfig = undefined;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment