Telemetry
Explanation
Telemetry and distributed tracing is a important part of complete application monitoring.
Tramvai provides a deep integration with OpenTelemetry Node.js SDK, with custom automatic instrumentation for internal Node.js modules and core Tramvai mechanisms.
Browser OpenTelemetry SDK is not supported yet, because of it limitations:
- experimental status
- requires
zone.jsdependency - minimum 30+ kb gzip to bundle size and some performance overhead
However, module provides a lightweight BrowserTracer that generates traceId/spanId identifiers and automatically adds traceparent header to HTTP client requests. Span export and context propagation are not supported on the client.
Usage
Installation
You need to install @tramvai/module-opentelemetry:
- npm
- Yarn
npm i --save @tramvai/module-module-opentelemetry
yarn i --save @tramvai/module-module-opentelemetry
# couldn't auto-convert command
Then connect to the project:
import { createApp } from '@tramvai/core';
import { OpenTelemetryModule } from '@tramvai/module-opentelemetry';
createApp({
name: 'tincoin',
modules: [OpenTelemetryModule],
});
This will enable OpenTelemetry with automatic instrumentation, and provide Tracer into the application.
Create spans
Simplest way to wrap operation in span is to use Tracer.trace method, e.g.:
import { OPENTELEMETRY_TRACER_TOKEN } from '@tramvai/module-opentelemetry';
const provider = {
provide: commandLineListTokens.resolvePageDeps,
useFactory: ({ tracer, apiService }) => {
return async function getSmth() {
tracer.trace('get-smth', async (span) => {
// set attribute to the span
span.setAttribute('key', 'value');
// span will be ended automatically after `apiService.get` method resolves or rejects
return apiService.get('/smth');
});
};
},
deps: {
tracer: OPENTELEMETRY_TRACER_TOKEN,
apiService: API_SERVICE_TOKEN,
},
};
For more flexibility methods ProxyTracer.startActiveSpan and ProxyTracer.startSpan is available.
Tracer.trace and other span creation methods calls can be nested, with automatic context propagation.
Export traces
For export traces to OpenTelemetry collector, you need to provide custom span processor and exporter with OPENTELEMETRY_PROVIDER_SPAN_PROCESSOR_TOKEN token.
If you have a gRPC collector (e.g. Jaeger), library @opentelemetry/exporter-trace-otlp-grpc can be used:
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { OPENTELEMETRY_PROVIDER_SPAN_PROCESSOR_TOKEN } from '@tramvai/module-opentelemetry';
const provider = {
provide: OPENTELEMETRY_PROVIDER_SPAN_PROCESSOR_TOKEN,
useFactory: ({ envManager }) => {
const url = envManager.get('OTEL_GRPC_COLLECTOR_ENDPOINT');
const exporter = new OTLPTraceExporter({ url });
return new BatchSpanProcessor(exporter);
},
deps: {
envManager: ENV_MANAGER_TOKEN,
},
};
Resource attributes
If you need to extend resource attributes, use OPENTELEMETRY_PROVIDER_RESOURCE_ATTRIBUTES_TOKEN token. Also, @opentelemetry/semantic-conventions library contains some of attribute names.
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { OPENTELEMETRY_PROVIDER_RESOURCE_ATTRIBUTES_TOKEN } from '@tramvai/module-opentelemetry';
const provider = {
provide: OPENTELEMETRY_PROVIDER_RESOURCE_ATTRIBUTES_TOKEN,
useFactory: ({ appInfo }) => {
return {
[ATTR_SERVICE_NAME]: appInfo.appName,
};
},
deps: {
appInfo: APP_INFO_TOKEN,
},
};
Active span
If you need to set attributes to the active span, use Tracer.getActiveSpan method:
// tracer from OPENTELEMETRY_TRACER_TOKEN
function doSmt({ tracer }) {
const span = tracer.getActiveSpan();
// span can be absent, for example in `init` or `listen` command line stages,
// and always `undefined` on the client (no context propagation)
span?.setAttribute('key', 'value');
}
Automatic instrumentation
OpenTelemetry instrumentation libraries is not supported, because of it limitations:
- instrumentations are registered before the module to instrument is require'ed
- modules are not included in a bundle
Instead, module provides custom instrumentation for all significant cases:
Server module
All incoming requests are automatically wrapped in root span, where current request and response parameters will be set (path, method, status code, error).
Naming and attributes follow semantic conventions.
Context propagation
OpenTelemetryModule provides context propagation for incoming and outgoing requests.
Pass traceparent context
Server-side
OpenTelemetryModule inject traceparent header to all external API requests via Tramvai HTTP Clients.
On the server-side module includes traceparent meta-tag in resulting html according to W3C format. Also, it bypasses traceparent header to external APIs.
Client-side
On the client-side, if traceparent meta-tag exists need to be used, you can extract in at follows:
import { extractTraceparentHeader } from '@tramvai/module-opentelemetry';
const extractTraceparent = (): string | undefined => {
const traceparent = extractTraceparentHeader();
const [version, traceId, spanId, sampled] = tags[0].content.split('-');
return traceId;
};
OpenTelemetryModule inject traceparent header to all external API requests via Tramvai HTTP Clients in a browser context too. Each request gets a unique traceparent with fresh traceId and spanId. If consumer already provided traceparent header, it will not be overwritten.
Filter out traceparent header
In a browser context, some cross-origin requests can be blocked by CORS policy (or preflight requests can be not supported) if traceparent header is added.
So, you can filter out traceparent header for some requests with OPENTELEMETRY_HTTP_CLIENT_BROWSER_HEADERS_INCLUDE_TOKEN token, e.g. as a blacklist:
const provider = provide({
provide: OPENTELEMETRY_HTTP_CLIENT_BROWSER_HEADERS_INCLUDE_TOKEN,
useFactory:
({ envManager }) =>
(url: string) => {
const blacklist = ['/excluded-api-path', envManager.get('CDN_WITHOUT_ALLOWED_HEADERS_URL')];
return !blacklist.some((blockerUrl) => url.includes(blockerUrl));
},
deps: {
envManager: ENV_MANAGER_TOKEN,
},
});
Logs correlation
Server-side
OpenTelemetryModule inject context for logs correlation.
All application logs will be extended with current span and trace ids in spanId and traceId properties.
Client-side
In a browser context, spanId and traceId properties are not added to all client logs automatically, because it would connect current SSR logs and all corresponding backend's logs for the current browser session, which can lead to a huge amount of logs and inefficient debugging.
However, HTTP client logs are enriched with traceId and spanId on the client side. OpenTelemetryModule registers an HTTP_CLIENT_LOGGER_EXTENSION that extracts the traceparent header from outgoing HTTP requests and adds parsed traceId and spanId to the corresponding log entries. This allows correlating specific HTTP client requests with their server-side traces.
Debug and testing
In development mode you can enable ConsoleSpanExporter, which prints all spans to the console, with env variable TRAMVAI_OPENTELEMETRY_DEBUG=true.
For testing purposes, you can use InMemorySpanExporter.