inngest
Version:
Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.
309 lines (307 loc) • 12.8 kB
JavaScript
const require_rolldown_runtime = require('../../../_virtual/rolldown_runtime.cjs');
const require_als = require('../als.cjs');
const require_access = require('./access.cjs');
const require_consts = require('./consts.cjs');
const require_deterministicId = require('../../../helpers/deterministicId.cjs');
let debug = require("debug");
debug = require_rolldown_runtime.__toESM(debug);
let __opentelemetry_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
let __opentelemetry_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
let __opentelemetry_resources = require("@opentelemetry/resources");
//#region src/components/execution/otel/processor.ts
const processorDevDebug = (0, debug.default)(`${require_consts.debugPrefix}:InngestSpanProcessor`);
/**
* A set of resource attributes that are used to identify the Inngest app and
* the function that is being executed. This is used to store the resource
* attributes for the spans that are exported to the Inngest endpoint, and cache
* them for later use.
*/
let _resourceAttributes;
/**
* An OTel span processor that is used to export spans to the Inngest endpoint.
* This is used to track spans that are created within an Inngest run and export
* them to the Inngest endpoint for tracing.
*
* It's careful to only pick relevant spans to export and will not send any
* irrelevant spans to the Inngest endpoint.
*
* THIS IS THE INTERNAL IMPLEMENTATION OF THE SPAN PROCESSOR AND SHOULD NOT BE
* USED BY USERS DIRECTLY. USE THE {@link PublicInngestSpanProcessor} CLASS
* INSTEAD.
*/
var InngestSpanProcessor = class InngestSpanProcessor {
/**
* An OTel span processor that is used to export spans to the Inngest endpoint.
* This is used to track spans that are created within an Inngest run and export
* them to the Inngest endpoint for tracing.
*
* It's careful to only pick relevant spans to export and will not send any
* irrelevant spans to the Inngest endpoint.
*/
constructor(app) {
if (app) require_access.clientProcessorMap.set(app, this);
}
/**
* A `BatchSpanProcessor` that is used to export spans to the Inngest
* endpoint. This is created lazily to avoid creating it until the Inngest App
* has been initialized and has had a chance to receive environment variables,
* which may be from an incoming request.
*/
#batcher;
/**
* A set of spans used to track spans that we care about, so that we can
* export them to the OTel endpoint.
*
* If a span falls out of reference, it will be removed from this set as we'll
* never get a chance to export it or remove it anyway.
*/
#spansToExport = /* @__PURE__ */ new WeakSet();
/**
* A map of span IDs to their parent state, which includes a block of
* information that can be used and pushed back to the Inngest endpoint to
* ingest spans.
*/
#traceParents = /* @__PURE__ */ new Map();
/**
* A registry used to clean up items from the `traceParents` map when spans
* fall out of reference. This is used to avoid memory leaks in the case where
* a span is not exported, remains unended, and is left in memory before being
* GC'd.
*/
#spanCleanup = new FinalizationRegistry((spanId) => {
if (spanId) this.#traceParents.delete(spanId);
});
/**
* Tracks the currently-executing step for each execution, keyed by root span
* ID. Used to compute deterministic parent span IDs for userland spans when
* checkpointing is enabled and multiple steps run in a single invocation.
*/
#activeStepContext = /* @__PURE__ */ new Map();
/**
* Root span IDs that have had at least one step execution declared, meaning
* they are checkpointing runs. Used to filter out infrastructure spans
* (checkpoint POSTs, dev server polls) that fire between steps.
*/
#checkpointingRoots = /* @__PURE__ */ new Set();
/**
* In order to only capture a subset of spans, we need to declare the initial
* span that we care about and then export its children.
*
* Call this method (ideally just before execution starts) with that initial
* span to trigger capturing all following children as well as initialize the
* batcher.
*/
declareStartingSpan({ span, runId, traceparent, tracestate }) {
this.ensureBatcherInitialized();
if (!traceparent) return processorDevDebug("no traceparent found for span", span.spanContext().spanId, "so skipping it");
let appId;
let functionId;
let traceRef;
if (tracestate) try {
const entries = Object.fromEntries(tracestate.split(",").map((kv) => kv.split("=")));
appId = entries[require_consts.TraceStateKey.AppId];
functionId = entries[require_consts.TraceStateKey.FunctionId];
traceRef = entries[require_consts.TraceStateKey.TraceRef];
} catch (err) {
processorDevDebug("failed to parse tracestate", tracestate, "so skipping it;", err);
}
processorDevDebug.extend("declareStartingSpan")("declaring:", span.spanContext().spanId, "for traceparent", traceparent);
span.setAttributes(InngestSpanProcessor.resourceAttributes.attributes);
this.trackSpan({
appId,
functionId,
runId,
traceparent,
traceRef,
rootSpanId: span.spanContext().spanId
}, span, true);
}
/**
* Declare that a step is currently executing. Userland spans created while
* a step context is active will have their `inngest.traceparent` rewritten
* to reference a deterministic span ID derived from the step, matching the
* span the Go executor will create via checkpoint.
*/
declareStepExecution(rootSpanId, hashedStepId, attempt) {
processorDevDebug("declareStepExecution: rootSpanId=%s hashedStepId=%s attempt=%d", rootSpanId, hashedStepId, attempt);
this.#checkpointingRoots.add(rootSpanId);
this.#activeStepContext.set(rootSpanId, {
hashedStepId,
attempt
});
}
/**
* Clear the active step context after a step finishes executing.
*/
clearStepExecution(rootSpanId) {
processorDevDebug("clearStepExecution: rootSpanId=%s", rootSpanId);
this.#activeStepContext.delete(rootSpanId);
}
/**
* A getter for retrieving resource attributes for the current process. This
* is used to set the resource attributes for the spans that are exported to
* the Inngest endpoint, and cache them for later use.
*/
static get resourceAttributes() {
if (!_resourceAttributes) _resourceAttributes = (0, __opentelemetry_resources.detectResources)({ detectors: [
__opentelemetry_resources.osDetector,
__opentelemetry_resources.envDetector,
__opentelemetry_resources.hostDetector,
__opentelemetry_resources.processDetector,
__opentelemetry_resources.serviceInstanceIdDetector
] });
return _resourceAttributes;
}
/**
* The batcher is a singleton that is used to export spans to the OTel
* endpoint. It is created lazily to avoid creating it until the Inngest App
* has been initialized and has had a chance to receive environment variables,
* which may be from an incoming request.
*
* The batcher is only referenced once we've found a span we're interested in,
* so this should always have everything it needs on the app by then.
*/
ensureBatcherInitialized() {
if (!this.#batcher) this.#batcher = new Promise(async (resolve, reject) => {
try {
const store = await require_als.getAsyncCtx();
if (!store) throw new Error("No async context found; cannot create batcher to export traces");
const app = store.app;
const url = new URL("/v1/traces/userland", app.apiBaseUrl);
processorDevDebug("batcher lazily accessed; creating new batcher with URL", url);
resolve(new __opentelemetry_sdk_trace_base.BatchSpanProcessor(new __opentelemetry_exporter_trace_otlp_http.OTLPTraceExporter({
url: url.href,
headers: {
...app.headers,
Authorization: `Bearer ${app.signingKey}`
}
})));
} catch (err) {
reject(err);
}
});
return this.#batcher;
}
/**
* Mark a span as being tracked by this processor, meaning it will be exported
* to the Inggest endpoint when it ends.
*/
trackSpan(parentState, span, isRoot = false) {
const trackDebug = processorDevDebug.extend("trackSpan");
const spanId = span.spanContext().spanId;
this.#spanCleanup.register(span, spanId, span);
this.#spansToExport.add(span);
this.#traceParents.set(spanId, parentState);
if (!isRoot) {
if ((span.parentSpanContext?.spanId ?? span.parentSpanId) === parentState.rootSpanId) {
const stepCtx = this.#activeStepContext.get(parentState.rootSpanId);
if (stepCtx) {
const seed = stepCtx.hashedStepId + ":" + String(stepCtx.attempt);
const newSpanId = require_deterministicId.deterministicSpanID(seed);
trackDebug("setting inngest.step.parentSpanId=%s (seed=%s) on span %s", newSpanId, seed, spanId);
span.setAttribute(require_consts.Attribute.InngestStepParentSpanId, newSpanId);
}
}
}
span.setAttribute(require_consts.Attribute.InngestTraceparent, parentState.traceparent);
span.setAttribute(require_consts.Attribute.InngestRunId, parentState.runId);
if (parentState.appId) {
span.setAttribute(require_consts.Attribute.InngestAppId1, parentState.appId);
span.setAttribute(require_consts.Attribute.InngestAppId2, parentState.appId);
}
if (parentState.functionId) span.setAttribute(require_consts.Attribute.InngestFunctionId, parentState.functionId);
if (parentState.traceRef) span.setAttribute(require_consts.Attribute.InngestTraceRef, parentState.traceRef);
}
/**
* Clean up any references to a span that has ended. This is used to avoid
* memory leaks in the case where a span is not exported, remains unended, and
* is left in memory before being GC'd.
*/
cleanupSpan(span) {
const spanId = span.spanContext().spanId;
this.#spanCleanup.unregister(span);
this.#spansToExport.delete(span);
this.#traceParents.delete(spanId);
}
/**
* An implementation of the `onStart` method from the `SpanProcessor`
* interface. This is called when a span is started, and is used to track
* spans that are children of spans we care about.
*/
onStart(span) {
const devDebug = processorDevDebug.extend("onStart");
const spanId = span.spanContext().spanId;
const parentSpanId = span.parentSpanContext?.spanId ?? span.parentSpanId;
if (!parentSpanId) {
devDebug("no parent span ID for", spanId, "so skipping it");
return;
}
const parentState = this.#traceParents.get(parentSpanId);
if (parentState) {
if (this.#checkpointingRoots.has(parentState.rootSpanId) && !this.#activeStepContext.has(parentState.rootSpanId)) {
processorDevDebug("skipping span", spanId, "- checkpointing between steps");
return;
}
devDebug("found traceparent", parentState, "in span ID", parentSpanId, "so adding", spanId);
this.trackSpan(parentState, span);
}
}
/**
* An implementation of the `onEnd` method from the `SpanProcessor` interface.
* This is called when a span ends, and is used to export spans to the Inngest
* endpoint.
*/
onEnd(span) {
const devDebug = processorDevDebug.extend("onEnd");
const spanId = span.spanContext().spanId;
try {
if (this.#spansToExport.has(span)) {
if (!this.#batcher) return devDebug("batcher not initialized, so failed exporting span", spanId);
devDebug("exporting span", spanId);
this.#batcher.then((batcher) => batcher.onEnd(span));
return;
}
devDebug("not exporting span", spanId, "as we don't care about it");
} finally {
this.cleanupSpan(span);
}
}
/**
* An implementation of the `forceFlush` method from the `SpanProcessor`
* interface. This is called to force the processor to flush any spans that
* are currently in the batcher. This is used to ensure that spans are
* exported to the Inngest endpoint before the process exits.
*
* Notably, we call this in the `wrapRequest` middleware hook to ensure
* that spans for a run as exported as soon as possible and before the
* serverless process is killed.
*/
async forceFlush() {
const flushDebug = processorDevDebug.extend("forceFlush");
flushDebug("force flushing batcher");
return this.#batcher?.then((batcher) => batcher.forceFlush()).catch((err) => {
flushDebug("error flushing batcher", err, "ignoring");
});
}
async shutdown() {
processorDevDebug.extend("shutdown")("shutting down batcher");
return this.#batcher?.then((batcher) => batcher.shutdown());
}
};
/**
* An OTel span processor that is used to export spans to the Inngest endpoint.
* This is used to track spans that are created within an Inngest run and export
* them to the Inngest endpoint for tracing.
*
* It's careful to only pick relevant spans to export and will not send any
* irrelevant spans to the Inngest endpoint.
*/
var PublicInngestSpanProcessor = class extends InngestSpanProcessor {
constructor(app) {
super(app);
}
};
//#endregion
exports.InngestSpanProcessor = InngestSpanProcessor;
exports.PublicInngestSpanProcessor = PublicInngestSpanProcessor;
//# sourceMappingURL=processor.cjs.map