nestjs-otel
Version:
NestJS OpenTelemetry Library
150 lines (149 loc) • 6.57 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WideEventInterceptor = void 0;
const common_1 = require("@nestjs/common");
const api_1 = require("@opentelemetry/api");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const opentelemetry_constants_1 = require("../opentelemetry.constants");
const wide_event_context_1 = require("./wide-event.context");
const wide_event_span_processor_1 = require("./wide-event.span-processor");
/**
* Opens a wide event for each request and flushes the accumulated
* attributes onto the span that was active when the request entered the
* interceptor (usually the root HTTP span created by instrumentation).
*
* Register globally with APP_INTERCEPTOR or per controller with
* UseInterceptors.
*
* @publicApi
*/
let WideEventInterceptor = class WideEventInterceptor {
options;
constructor(options) {
this.options = options;
}
intercept(executionContext, next) {
const bag = new Map();
bag.set("code.function.name", `${executionContext.getClass().name}.${executionContext.getHandler().name}`);
this.seed(bag, executionContext);
const activeContext = api_1.context.active();
// The middleware-captured span is the local root on Express / plain
// Fastify (alive until the response ends). On stacks that wrap each
// request phase in its own span (e.g. @fastify/otel) it can be an
// ephemeral span that has already ended by flush time, so we keep the
// interceptor-time active span as a live fallback and pick whichever is
// still recording when we flush.
const rootSpan = this.rootSpanFromRequest(executionContext);
const activeSpan = api_1.trace.getSpan(activeContext);
const contextWithBag = activeContext.setValue(wide_event_context_1.WIDE_EVENT_CONTEXT_KEY, bag);
return new rxjs_1.Observable((subscriber) => {
let terminated = false;
const subscription = api_1.context.with(contextWithBag, () => next
.handle()
.pipe((0, operators_1.tap)({
error: (error) => {
terminated = true;
bag.set("error.type", error instanceof Error
? (error.constructor?.name ?? "Error")
: "unknown");
if (error instanceof Error) {
bag.set("error.message", error.message);
if (error.stack) {
bag.set("error.stack", error.stack);
}
}
this.targetSpan(rootSpan, activeSpan)?.setStatus({
code: api_1.SpanStatusCode.ERROR,
});
},
complete: () => {
terminated = true;
},
}), (0, operators_1.finalize)(() => {
if (terminated) {
this.flush(bag, rootSpan, activeSpan);
}
}))
.subscribe(subscriber));
return () => subscription.unsubscribe();
});
}
rootSpanFromRequest(executionContext) {
if (typeof executionContext.getType !== "function" ||
executionContext.getType() !== "http") {
return;
}
const request = executionContext
.switchToHttp()
.getRequest();
return (request?.[wide_event_context_1.WIDE_EVENT_ROOT_SPAN] ??
request?.raw?.[wide_event_context_1.WIDE_EVENT_ROOT_SPAN]);
}
/**
* Picks the span the wide event should be written to, preferring the
* local-root span (when the WideEventSpanProcessor is registered), then the
* middleware-captured root span, then the interceptor-time active span —
* skipping any that have already ended.
*/
targetSpan(rootSpan, activeSpan) {
const traceId = (activeSpan ?? rootSpan)?.spanContext().traceId;
const localRoot = traceId ? (0, wide_event_span_processor_1.getLocalRootSpan)(traceId) : undefined;
return this.pickSpan(localRoot, rootSpan, activeSpan);
}
pickSpan(...candidates) {
for (const candidate of candidates) {
if (candidate?.isRecording()) {
return candidate;
}
}
return;
}
seed(bag, executionContext) {
const seed = this.options?.wideEvents?.seed;
if (!seed) {
return;
}
try {
const result = seed(executionContext);
if (result == null) {
return;
}
for (const [key, value] of Object.entries(result)) {
if (value !== undefined) {
bag.set(key, value);
}
}
}
catch (error) {
bag.set("wide_event.seed.error", error instanceof Error ? error.message : String(error));
}
}
flush(bag, rootSpan, activeSpan) {
const span = this.targetSpan(rootSpan, activeSpan);
if (!span) {
return;
}
span.setAttributes(Object.fromEntries(bag));
span.setAttribute("nestjs_otel.wide_event", true);
}
};
exports.WideEventInterceptor = WideEventInterceptor;
exports.WideEventInterceptor = WideEventInterceptor = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Optional)()),
__param(0, (0, common_1.Inject)(opentelemetry_constants_1.OPENTELEMETRY_MODULE_OPTIONS)),
__metadata("design:paramtypes", [Object])
], WideEventInterceptor);