Skip to content

Commit ad3b139

Browse files
alangenfeldclaude
andcommitted
feat(core): emit faas.instance span attribute for compute instance identity
Synthesize a per-warm-instance id (cinst_<ulid>) once at module load and emit it as the OTEL faas.instance attribute on the flow and step route spans. Vercel exposes no native per-instance id under Fluid compute, so this lets traces distinguish which compute instance handled each request. Purely additive telemetry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Alex Langenfeld <alex.langenfeld@vercel.com>
1 parent bd5fc50 commit ad3b139

5 files changed

Lines changed: 53 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@workflow/core': minor
3+
---
4+
5+
Emit a `faas.instance` OTEL span attribute (a synthesized per-warm-instance id) on the flow and step route spans, so traces can distinguish which compute instance handled each request.

packages/core/src/runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
handleReplayBudgetExhausted,
5353
ReplayBudget,
5454
} from './runtime/replay-budget.js';
55+
import { COMPUTE_INSTANCE_ID } from './runtime/compute-instance.js';
5556
import { runIdCreatedAt } from './runtime/run-id-time.js';
5657
import { executeStep } from './runtime/step-executor.js';
5758
import { computeStepLatencyTracking } from './runtime/step-latency.js';
@@ -2442,6 +2443,7 @@ export function workflowEntrypoint(
24422443
kind: spanKind,
24432444
attributes: {
24442445
...Attribute.WorkflowRouteType('flow'),
2446+
...Attribute.FaasInstance(COMPUTE_INSTANCE_ID),
24452447
...Attribute.WorkflowRouteHandlerCached(handlerCached),
24462448
...Attribute.WorkflowRouteInvocationCount(invocationCount),
24472449
...Attribute.WorkflowRouteEntrypointAgeMs(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ulid } from 'ulid';
2+
3+
/**
4+
* Identifier for the compute instance (the warm serverless instance /
5+
* microVM) that this module was loaded into.
6+
*
7+
* Vercel exposes no native per-instance id under Fluid compute — all
8+
* `AWS_LAMBDA_*` env vars (including `AWS_LAMBDA_LOG_STREAM_NAME`) are
9+
* blocked — so we synthesize our own once at module load. The value is
10+
* therefore:
11+
*
12+
* - **stable for the life of the warm instance**, and
13+
* - **shared across every invocation that instance handles**, including the
14+
* concurrent invocations Fluid compute packs onto a single instance.
15+
*
16+
* Distinct warm instances (and every cold start) get distinct ids. Both the
17+
* flow and step route entrypoints import this module, so any bundles
18+
* co-resident on one instance report the same id.
19+
*
20+
* The value is a prefixed ULID (`cinst_<ulid>`), matching the id convention
21+
* used elsewhere (`wrun_`, `step_`, `hook_`, …). Using a ULID means the
22+
* timestamp component encodes the instance's birth time, so ids sort
23+
* lexicographically by creation — useful for ordering/aging instances and
24+
* spotting cold starts in observability tooling.
25+
*
26+
* Surfaced two ways:
27+
* - as a span attribute, and
28+
* - persisted on `step_started` events as `computeInstanceId`, so tooling can
29+
* tell which step attempts ran on the same instance vs different instances,
30+
* and within an instance which ran in the same invocation vs different ones.
31+
*/
32+
export const COMPUTE_INSTANCE_ID = `cinst_${ulid()}`;

packages/core/src/runtime/step-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
queueMessage,
6363
withHealthCheck,
6464
} from './helpers.js';
65+
import { COMPUTE_INSTANCE_ID } from './compute-instance.js';
6566
import { safeWaitUntil } from './wait-until.js';
6667
import { getWorld, getWorldHandlers, type WorldHandlers } from './world.js';
6768

@@ -243,6 +244,7 @@ function createStepHandler(namespace?: string) {
243244
span?.setAttributes({
244245
...Attribute.StepName(stepName),
245246
...Attribute.StepAttempt(metadata.attempt),
247+
...Attribute.FaasInstance(COMPUTE_INSTANCE_ID),
246248
// Standard OTEL messaging conventions
247249
...Attribute.MessagingSystem('vercel-queue'),
248250
...Attribute.MessagingDestinationName(metadata.queueName),

packages/core/src/telemetry/semantic-conventions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,18 @@ export const WorkflowRouteModuleBodyInitMs = SemanticConvention<number>(
175175
'workflow.route.module_body_init_ms'
176176
);
177177

178+
/**
179+
* Identifier for the compute instance (warm serverless instance / microVM)
180+
* handling this route — the synthesized `COMPUTE_INSTANCE_ID`, stable for the
181+
* instance's lifetime and shared across every invocation it serves.
182+
*
183+
* Uses the OpenTelemetry `faas.instance` convention: "the execution
184+
* environment ID as a string, potentially reused for other invocations to the
185+
* same function/version." See
186+
* https://opentelemetry.io/docs/specs/semconv/attributes-registry/faas/
187+
*/
188+
export const FaasInstance = SemanticConvention<string>('faas.instance');
189+
178190
// Step attributes
179191

180192
/** Name of the step function being executed */

0 commit comments

Comments
 (0)