diff --git a/packages/interceptors-opentelemetry-v2/README.md b/packages/interceptors-opentelemetry-v2/README.md new file mode 100644 index 000000000..84f3bd69f --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/README.md @@ -0,0 +1,10 @@ +# `@temporalio/interceptors-opentelemetry-v2` + +[![NPM](https://img.shields.io/npm/v/@temporalio/interceptors-opentelemetry-v2?style=for-the-badge)](https://www.npmjs.com/package/@temporalio/interceptors-opentelemetry-v2) + +[Temporal](https://temporal.io)'s [TypeScript SDK](https://docs.temporal.io/typescript/introduction) interceptors for tracing Workflow and Activity executions with [OpenTelemetry](https://opentelemetry.io/) v2. + +This package targets OpenTelemetry JS SDK v2. For OpenTelemetry JS SDK v1, use [`@temporalio/interceptors-opentelemetry`](https://www.npmjs.com/package/@temporalio/interceptors-opentelemetry). + +- [Interceptors docs](https://docs.temporal.io/typescript/interceptors) +- [OpenTelemetry Interceptor example setup](https://github.com/temporalio/samples-typescript/tree/main/interceptors-opentelemetry) diff --git a/packages/interceptors-opentelemetry-v2/package.json b/packages/interceptors-opentelemetry-v2/package.json new file mode 100644 index 000000000..91a2c94d5 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/package.json @@ -0,0 +1,76 @@ +{ + "name": "@temporalio/interceptors-opentelemetry-v2", + "version": "1.15.0", + "description": "Temporal.io SDK interceptors bundle for tracing with opentelemetry v2", + "main": "lib/index.js", + "types": "./lib/index.d.ts", + "scripts": { + "build": "tsc --build", + "test": "ava ./lib/__tests__/test-*.js" + }, + "keywords": [ + "temporal", + "workflow", + "interceptors", + "opentelemetry" + ], + "author": "Temporal Technologies Inc. ", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", + "@temporalio/plugin": "workspace:*" + }, + "devDependencies": { + "@opentelemetry/exporter-trace-otlp-grpc": "^0.208.0", + "@opentelemetry/sdk-node": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.38.0", + "@temporalio/activity": "workspace:*", + "@temporalio/client": "workspace:*", + "@temporalio/common": "workspace:*", + "@temporalio/proto": "workspace:*", + "@temporalio/test-helpers": "workspace:*", + "@temporalio/testing": "workspace:*", + "@temporalio/worker": "workspace:*", + "@temporalio/workflow": "workspace:*", + "ava": "^5.3.1", + "uuid": "^11.1.0" + }, + "peerDependencies": { + "@temporalio/common": "workspace:*", + "@temporalio/workflow": "workspace:*" + }, + "peerDependenciesMeta": { + "@temporalio/workflow": { + "optional": true + } + }, + "bugs": { + "url": "https://github.com/temporalio/sdk-typescript/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/temporalio/sdk-typescript.git", + "directory": "packages/interceptors-opentelemetry-v2" + }, + "homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/interceptors-opentelemetry-v2", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">= 20.0.0" + }, + "files": [ + "src", + "lib", + "!src/__tests__", + "!lib/__tests__" + ], + "ava": { + "timeout": "120s", + "concurrency": 1, + "workerThreads": false + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/activities/helpers.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/activities/helpers.ts new file mode 100644 index 000000000..f571e57f4 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/activities/helpers.ts @@ -0,0 +1,19 @@ +import { WorkflowHandle } from '@temporalio/client'; +import { QueryDefinition } from '@temporalio/common'; +import { Context } from '@temporalio/activity'; + +function getSchedulingWorkflowHandle(): WorkflowHandle { + const { info, client } = Context.current(); + const { workflowExecution } = info; + return client.workflow.getHandle(workflowExecution.workflowId, workflowExecution.runId); +} + +export async function signalSchedulingWorkflow(signalName: string): Promise { + const handle = getSchedulingWorkflowHandle(); + await handle.signal(signalName); +} + +export async function queryOwnWf(queryDef: QueryDefinition, ...args: A): Promise { + const handle = getSchedulingWorkflowHandle(); + return await handle.query(queryDef, ...args); +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/activities/index.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/activities/index.ts new file mode 100644 index 000000000..7de3434d7 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/activities/index.ts @@ -0,0 +1,39 @@ +import { activityInfo, Context } from '@temporalio/activity'; +import { ApplicationFailure, ApplicationFailureCategory, CancelledFailure } from '@temporalio/common'; + +export { queryOwnWf, signalSchedulingWorkflow } from './helpers'; + +export async function echo(message?: string): Promise { + return message ?? 'echo'; +} + +export async function fakeProgress(sleepIntervalMs = 1000, numIters = 100): Promise { + await signalSchedulingWorkflow('activityStarted'); + try { + for (let progress = 1; progress <= numIters; ++progress) { + await Context.current().sleep(sleepIntervalMs); + Context.current().heartbeat(progress); + } + } catch (err) { + if (!(err instanceof CancelledFailure)) { + throw err; + } + throw err; + } +} + +async function signalSchedulingWorkflow(signalName: string): Promise { + const { info, client } = Context.current(); + const { workflowExecution } = info; + const handle = client.workflow.getHandle(workflowExecution.workflowId, workflowExecution.runId); + await handle.signal(signalName); +} + +export async function throwMaybeBenign(): Promise { + if (activityInfo().attempt === 1) { + throw ApplicationFailure.create({ message: 'not benign' }); + } + if (activityInfo().attempt === 2) { + throw ApplicationFailure.create({ message: 'benign', category: ApplicationFailureCategory.BENIGN }); + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/history_files/otel_smorgasbord_1_15_0.json b/packages/interceptors-opentelemetry-v2/src/__tests__/history_files/otel_smorgasbord_1_15_0.json new file mode 100644 index 000000000..43e46bcd4 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/history_files/otel_smorgasbord_1_15_0.json @@ -0,0 +1,786 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-02-25T14:57:14.909195Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1176734", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "smorgasbord" + }, + "taskQueue": { + "name": "test-otel-record-history", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mg==" + } + ] + }, + "workflowRunTimeout": "0s", + "workflowTaskTimeout": "10s", + "continuedExecutionRunId": "fd207c92-65bb-4acb-9917-71033c267734", + "initiator": "CONTINUE_AS_NEW_INITIATOR_WORKFLOW", + "originalExecutionRunId": "68d80caf-aef3-46e3-98d2-e28dbca9d759", + "firstExecutionRunId": "019c954d-d895-7e57-af1e-8a96bd82816d", + "attempt": 1, + "prevAutoResetPoints": { + "points": [ + { + "runId": "019c954d-d895-7e57-af1e-8a96bd82816d", + "firstWorkflowTaskCompletedId": "4", + "createTime": "2026-02-25T14:57:12.900229Z", + "expireTime": "2026-02-26T14:57:12.900432Z", + "resettable": true, + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + ] + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLWRiOTdiMjIwMmUyYWFkN2RlNDA5YzM2ZGRkMTc3MWNmLTM5NWViOTY0MTQ4YTVlYTEtMDEifQ==" + } + } + }, + "workflowId": "cdddebed-19ae-49ce-80ff-c848b66545d6" + } + }, + { + "eventId": "2", + "eventTime": "2026-02-25T14:57:14.909253Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176735", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "test-otel-record-history", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-02-25T14:57:14.911852Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176742", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "21239@mac.lan", + "requestId": "9dbf5dc1-9554-430f-8825-e463d6093ac1", + "historySizeBytes": "620", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "4", + "eventTime": "2026-02-25T14:57:14.923803Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176746", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": { + "coreUsedFlags": [1, 2, 3], + "langUsedFlags": [5, 4], + "sdkName": "temporal-typescript", + "sdkVersion": "1.15.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2026-02-25T14:57:14.923830Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1176747", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "fakeProgress" + }, + "taskQueue": { + "name": "test-otel-record-history", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLWRiOTdiMjIwMmUyYWFkN2RlNDA5YzM2ZGRkMTc3MWNmLTJiN2EwNGFiMzNmYTIzNjctMDEifQ==" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MTAw" + }, + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MTA=" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "60s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "6", + "eventTime": "2026-02-25T14:57:14.923844Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1176748", + "activityTaskScheduledEventAttributes": { + "activityId": "2", + "activityType": { + "name": "queryOwnWf" + }, + "taskQueue": { + "name": "test-otel-record-history", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLWRiOTdiMjIwMmUyYWFkN2RlNDA5YzM2ZGRkMTc3MWNmLTVmZjZiODZlN2U2OWY4MWUtMDEifQ==" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0eXBlIjoicXVlcnkiLCJuYW1lIjoic3RlcCJ9" + } + ] + }, + "scheduleToCloseTimeout": "0s", + "scheduleToStartTimeout": "0s", + "startToCloseTimeout": "60s", + "heartbeatTimeout": "0s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true + } + }, + { + "eventId": "7", + "eventTime": "2026-02-25T14:57:14.923850Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1176749", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "1s", + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "8", + "eventTime": "2026-02-25T14:57:14.924005Z", + "eventType": "EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1176750", + "startChildWorkflowExecutionInitiatedEventAttributes": { + "namespace": "default", + "workflowId": "09f59c5f-5485-4d79-b0db-553311ba45af", + "workflowType": { + "name": "signalTarget" + }, + "taskQueue": { + "name": "test-otel-record-history", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "workflowRunTimeout": "0s", + "workflowTaskTimeout": "10s", + "parentClosePolicy": "PARENT_CLOSE_POLICY_TERMINATE", + "workflowTaskCompletedEventId": "4", + "workflowIdReusePolicy": "WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE", + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLWRiOTdiMjIwMmUyYWFkN2RlNDA5YzM2ZGRkMTc3MWNmLTVmMzY1NTJjZjM1ZGE1YTktMDEifQ==" + } + } + }, + "memo": { + "fields": {} + }, + "searchAttributes": { + "indexedFields": {} + }, + "namespaceId": "019c1ffb-866d-7923-92d6-91b5cd447e46", + "inheritBuildId": true + } + }, + { + "eventId": "9", + "eventTime": "2026-02-25T14:57:14.924017Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1176751", + "markerRecordedEventAttributes": { + "details": { + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzZXEiOjMsImF0dGVtcHQiOjEsImFjdGl2aXR5X2lkIjoiMyIsImFjdGl2aXR5X3R5cGUiOiJlY2hvIiwiY29tcGxldGVfdGltZSI6eyJzZWNvbmRzIjoxNzcyMDMxNDM0LCJuYW5vcyI6OTEyNDg2NTgzfSwiYmFja29mZiI6bnVsbCwib3JpZ2luYWxfc2NoZWR1bGVfdGltZSI6eyJzZWNvbmRzIjoxNzcyMDMxNDM0LCJuYW5vcyI6OTIxMjgyMDAwfX0=" + } + ] + }, + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImxvY2FsLWFjdGl2aXR5Ig==" + } + ] + } + }, + "markerName": "core_local_activity", + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "10", + "eventTime": "2026-02-25T14:57:14.925178Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED", + "taskId": "1176762", + "childWorkflowExecutionStartedEventAttributes": { + "namespace": "default", + "initiatedEventId": "8", + "workflowExecution": { + "workflowId": "09f59c5f-5485-4d79-b0db-553311ba45af", + "runId": "019c954d-e0ac-7c50-8742-b7f8e872e889" + }, + "workflowType": { + "name": "signalTarget" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLWRiOTdiMjIwMmUyYWFkN2RlNDA5YzM2ZGRkMTc3MWNmLTVmMzY1NTJjZjM1ZGE1YTktMDEifQ==" + } + } + }, + "namespaceId": "019c1ffb-866d-7923-92d6-91b5cd447e46" + } + }, + { + "eventId": "11", + "eventTime": "2026-02-25T14:57:14.925182Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176763", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "21239@mac.lan-b092b43e50124306ad901ff99b92f80a", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-record-history" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "12", + "eventTime": "2026-02-25T14:57:14.927434Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176774", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "11", + "identity": "21239@mac.lan", + "requestId": "6da477dc-868a-4d63-bd2f-e63083caa2fd", + "historySizeBytes": "2650", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "13", + "eventTime": "2026-02-25T14:57:14.934184Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176785", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "11", + "startedEventId": "12", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "14", + "eventTime": "2026-02-25T14:57:14.934201Z", + "eventType": "EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1176786", + "signalExternalWorkflowExecutionInitiatedEventAttributes": { + "workflowTaskCompletedEventId": "13", + "namespace": "default", + "workflowExecution": { + "workflowId": "09f59c5f-5485-4d79-b0db-553311ba45af" + }, + "signalName": "unblock", + "childWorkflowOnly": true, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLWRiOTdiMjIwMmUyYWFkN2RlNDA5YzM2ZGRkMTc3MWNmLWIyNTkwNzg0YWVkMTMwN2EtMDEifQ==" + } + } + }, + "namespaceId": "019c1ffb-866d-7923-92d6-91b5cd447e46" + } + }, + { + "eventId": "15", + "eventTime": "2026-02-25T14:57:14.929624Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1176787", + "workflowExecutionSignaledEventAttributes": { + "signalName": "activityStarted", + "input": {}, + "identity": "21239@mac.lan", + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "e30=" + } + } + } + } + }, + { + "eventId": "16", + "eventTime": "2026-02-25T14:57:14.934211Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176788", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "21239@mac.lan-b092b43e50124306ad901ff99b92f80a", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-record-history" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "17", + "eventTime": "2026-02-25T14:57:14.934212Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176789", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "16", + "identity": "21239@mac.lan", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "2830", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "18", + "eventTime": "2026-02-25T14:57:14.941611Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176808", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "16", + "startedEventId": "17", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": { + "langUsedFlags": [2, 3] + }, + "meteringMetadata": {} + } + }, + { + "eventId": "19", + "eventTime": "2026-02-25T14:57:14.935951Z", + "eventType": "EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1176809", + "externalWorkflowExecutionSignaledEventAttributes": { + "initiatedEventId": "14", + "namespace": "default", + "workflowExecution": { + "workflowId": "09f59c5f-5485-4d79-b0db-553311ba45af" + }, + "namespaceId": "019c1ffb-866d-7923-92d6-91b5cd447e46" + } + }, + { + "eventId": "20", + "eventTime": "2026-02-25T14:57:14.940949Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1176810", + "childWorkflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + }, + "namespace": "default", + "workflowExecution": { + "workflowId": "09f59c5f-5485-4d79-b0db-553311ba45af", + "runId": "019c954d-e0ac-7c50-8742-b7f8e872e889" + }, + "workflowType": { + "name": "signalTarget" + }, + "initiatedEventId": "8", + "startedEventId": "10", + "namespaceId": "019c1ffb-866d-7923-92d6-91b5cd447e46" + } + }, + { + "eventId": "21", + "eventTime": "2026-02-25T14:57:14.941622Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176811", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "21239@mac.lan-b092b43e50124306ad901ff99b92f80a", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-record-history" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "22", + "eventTime": "2026-02-25T14:57:14.941624Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176812", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "21", + "identity": "21239@mac.lan", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "3626", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "23", + "eventTime": "2026-02-25T14:57:14.943920Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176816", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "21", + "startedEventId": "22", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "24", + "eventTime": "2026-02-25T14:57:14.925799Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1176817", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "6", + "identity": "21239@mac.lan", + "requestId": "001dd426-ff9e-4edb-9857-af4fb603df8c", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "25", + "eventTime": "2026-02-25T14:57:14.942693Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1176818", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "Mg==" + } + ] + }, + "scheduledEventId": "6", + "startedEventId": "24", + "identity": "21239@mac.lan" + } + }, + { + "eventId": "26", + "eventTime": "2026-02-25T14:57:14.943934Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176819", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "21239@mac.lan-b092b43e50124306ad901ff99b92f80a", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-record-history" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "27", + "eventTime": "2026-02-25T14:57:14.943935Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176820", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "26", + "identity": "21239@mac.lan", + "requestId": "request-from-RespondWorkflowTaskCompleted", + "historySizeBytes": "4388", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "28", + "eventTime": "2026-02-25T14:57:14.945884Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176823", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "26", + "startedEventId": "27", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "29", + "eventTime": "2026-02-25T14:57:15.925643Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1176825", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "7" + } + }, + { + "eventId": "30", + "eventTime": "2026-02-25T14:57:15.925650Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176826", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "21239@mac.lan-b092b43e50124306ad901ff99b92f80a", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-record-history" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "31", + "eventTime": "2026-02-25T14:57:15.927302Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176830", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "30", + "identity": "21239@mac.lan", + "requestId": "b6a218a1-404d-42c9-968a-06d077f37573", + "historySizeBytes": "5370", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "32", + "eventTime": "2026-02-25T14:57:15.931793Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176834", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "30", + "startedEventId": "31", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "33", + "eventTime": "2026-02-25T14:57:14.927158Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1176836", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "5", + "identity": "21239@mac.lan", + "requestId": "317baba1-6a78-4ff1-8771-d9b1a293ae96", + "attempt": 1, + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "34", + "eventTime": "2026-02-25T14:57:15.943085Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1176837", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + }, + "scheduledEventId": "5", + "startedEventId": "33", + "identity": "21239@mac.lan" + } + }, + { + "eventId": "35", + "eventTime": "2026-02-25T14:57:15.943089Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1176838", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "21239@mac.lan-b092b43e50124306ad901ff99b92f80a", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-record-history" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "36", + "eventTime": "2026-02-25T14:57:15.944032Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1176842", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "35", + "identity": "21239@mac.lan", + "requestId": "9afc6de1-32e7-4848-ace6-a1c4de0ce4ba", + "historySizeBytes": "6059", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + } + } + }, + { + "eventId": "37", + "eventTime": "2026-02-25T14:57:15.949011Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1176846", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "35", + "startedEventId": "36", + "identity": "21239@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.15.0+023a93a5578a3d7389ffdf5fc19b3fb9e4b80776ff22d7863fe910a5cf421e7e" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "38", + "eventTime": "2026-02-25T14:57:15.949031Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1176847", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "YmluYXJ5L251bGw=" + } + } + ] + }, + "workflowTaskCompletedEventId": "37" + } + } + ] +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/history_files/signal_workflow_1_13_1.json b/packages/interceptors-opentelemetry-v2/src/__tests__/history_files/signal_workflow_1_13_1.json new file mode 100644 index 000000000..5cb466d4e --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/history_files/signal_workflow_1_13_1.json @@ -0,0 +1,459 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-10-22T21:01:27.376670Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048587", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "topSecretGreeting" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IlRlbXBvcmFsIg==" + } + ] + }, + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "019a0dba-09d0-7a35-ba8f-b2843b67584b", + "identity": "69998@mac.lan", + "firstExecutionRunId": "019a0dba-09d0-7a35-ba8f-b2843b67584b", + "attempt": 1, + "firstWorkflowTaskBackoff": "0s", + "header": { + "fields": {} + }, + "workflowId": "bfe00f6e-5861-4b54-b4da-21433123ba75" + } + }, + { + "eventId": "2", + "eventTime": "2025-10-22T21:01:27.376737Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048588", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2025-10-22T21:01:27.378833Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048593", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "69998@mac.lan", + "requestId": "c234f432-3d91-4391-a647-d2cb1313ebab", + "historySizeBytes": "311", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "4", + "eventTime": "2025-10-22T21:01:27.407936Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048597", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": { + "coreUsedFlags": [1, 2, 3], + "sdkName": "temporal-typescript", + "sdkVersion": "1.13.1" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2025-10-22T21:01:27.408157Z", + "eventType": "EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1048598", + "startChildWorkflowExecutionInitiatedEventAttributes": { + "namespace": "default", + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd", + "workflowType": { + "name": "topSecretGreetingChild" + }, + "taskQueue": { + "name": "test-otel-inbound", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IlRlbXBvcmFsIg==" + } + ] + }, + "workflowRunTimeout": "0s", + "workflowTaskTimeout": "10s", + "parentClosePolicy": "PARENT_CLOSE_POLICY_TERMINATE", + "workflowTaskCompletedEventId": "4", + "workflowIdReusePolicy": "WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE", + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTA4NjhlNzEyM2YxODZhOGM1MjVhNDFiYzNjZTg5YmI2LTJmODU3NjczZjA3YWFkMjEtMDEifQ==" + } + } + }, + "memo": { + "fields": {} + }, + "searchAttributes": { + "indexedFields": {} + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5", + "inheritBuildId": true + } + }, + { + "eventId": "6", + "eventTime": "2025-10-22T21:01:27.409970Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048606", + "childWorkflowExecutionStartedEventAttributes": { + "namespace": "default", + "initiatedEventId": "5", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd", + "runId": "019a0dba-09f1-7290-b42c-21b73590c34c" + }, + "workflowType": { + "name": "topSecretGreetingChild" + }, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTA4NjhlNzEyM2YxODZhOGM1MjVhNDFiYzNjZTg5YmI2LTJmODU3NjczZjA3YWFkMjEtMDEifQ==" + } + } + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "7", + "eventTime": "2025-10-22T21:01:27.409973Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048607", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "8", + "eventTime": "2025-10-22T21:01:27.410484Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048615", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "7", + "identity": "69998@mac.lan", + "requestId": "5b04c436-286f-4e6a-9f4e-0fb06a7c939c", + "historySizeBytes": "1418", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "9", + "eventTime": "2025-10-22T21:01:27.418593Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048626", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "7", + "startedEventId": "8", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "10", + "eventTime": "2025-10-22T21:01:27.418615Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1048627", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "0.001s", + "workflowTaskCompletedEventId": "9" + } + }, + { + "eventId": "11", + "eventTime": "2025-10-22T21:01:27.418643Z", + "eventType": "EVENT_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_INITIATED", + "taskId": "1048628", + "signalExternalWorkflowExecutionInitiatedEventAttributes": { + "workflowTaskCompletedEventId": "9", + "namespace": "default", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd" + }, + "signalName": "approve", + "childWorkflowOnly": true, + "header": { + "fields": { + "_tracer-data": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0cmFjZXBhcmVudCI6IjAwLTA4NjhlNzEyM2YxODZhOGM1MjVhNDFiYzNjZTg5YmI2LTZjMDAyMDQ2YTRiMDZjNzktMDEifQ==" + } + } + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "12", + "eventTime": "2025-10-22T21:01:27.420266Z", + "eventType": "EVENT_TYPE_EXTERNAL_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048637", + "externalWorkflowExecutionSignaledEventAttributes": { + "initiatedEventId": "11", + "namespace": "default", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd" + }, + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "13", + "eventTime": "2025-10-22T21:01:27.420268Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048638", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "14", + "eventTime": "2025-10-22T21:01:27.420682Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048645", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "13", + "identity": "69998@mac.lan", + "requestId": "0c1f58c3-ef05-4166-9d81-61b92a0d29c9", + "historySizeBytes": "2247", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "15", + "eventTime": "2025-10-22T21:01:27.425241Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048650", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "13", + "startedEventId": "14", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "16", + "eventTime": "2025-10-22T21:01:27.425828Z", + "eventType": "EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048658", + "childWorkflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIFRlbXBvcmFsIg==" + } + ] + }, + "namespace": "default", + "workflowExecution": { + "workflowId": "1663347e-aa08-4ee5-b426-c23f71678ddd", + "runId": "019a0dba-09f1-7290-b42c-21b73590c34c" + }, + "workflowType": { + "name": "topSecretGreetingChild" + }, + "initiatedEventId": "5", + "startedEventId": "6", + "namespaceId": "019a0db9-e40b-714c-a297-7071f9a97da5" + } + }, + { + "eventId": "17", + "eventTime": "2025-10-22T21:01:27.425831Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048659", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "18", + "eventTime": "2025-10-22T21:01:27.426657Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048663", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "17", + "identity": "69998@mac.lan", + "requestId": "3c12f20e-7a18-4970-ac5f-fe5df677b9cb", + "historySizeBytes": "2904", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "19", + "eventTime": "2025-10-22T21:01:27.429513Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048667", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "17", + "startedEventId": "18", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "20", + "eventTime": "2025-10-22T21:01:28.413510Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1048669", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "10" + } + }, + { + "eventId": "21", + "eventTime": "2025-10-22T21:01:28.413530Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048670", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "69998@mac.lan-2ddaef81986a47848e1757b47ae2ca8d", + "kind": "TASK_QUEUE_KIND_STICKY", + "normalName": "test-otel-inbound" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "22", + "eventTime": "2025-10-22T21:01:28.417428Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048674", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "21", + "identity": "69998@mac.lan", + "requestId": "9d9b15b7-ad92-4436-a25e-a30e4fa68682", + "historySizeBytes": "3363", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + } + } + }, + { + "eventId": "23", + "eventTime": "2025-10-22T21:01:28.427848Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048678", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "21", + "startedEventId": "22", + "identity": "69998@mac.lan", + "workerVersion": { + "buildId": "@temporalio/worker@1.13.1+1cad42a4a1f0261c43927dc90a2d7e8ee078ad455d5a2876a91cffc71a6c1aa5" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "24", + "eventTime": "2025-10-22T21:01:28.427896Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048679", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIFRlbXBvcmFsIg==" + } + ] + }, + "workflowTaskCompletedEventId": "23" + } + } + ] +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/test-otel.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/test-otel.ts new file mode 100644 index 000000000..c32f5cde0 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/test-otel.ts @@ -0,0 +1,810 @@ +/** + * Manual tests to inspect tracing output + */ +import * as http2 from 'http2'; +import * as path from 'path'; +import * as otelApi from '@opentelemetry/api'; +import { SpanStatusCode, createTraceState } from '@opentelemetry/api'; +import { ExportResultCode } from '@opentelemetry/core'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; +import * as opentelemetry from '@opentelemetry/sdk-node'; +import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; +import test from 'ava'; +import { v4 as uuid4 } from 'uuid'; +import { WorkflowClient, WithStartWorkflowOperation, WorkflowClientInterceptor, Client } from '@temporalio/client'; +import { + ActivityInboundCallsInterceptor, + ActivityOutboundCallsInterceptor, + BundlerPlugin, + bundleWorkflowCode, + DefaultLogger, + InjectedSinks, + Runtime, + Worker, +} from '@temporalio/worker'; +import { WorkflowInboundCallsInterceptor, WorkflowOutboundCallsInterceptor } from '@temporalio/workflow'; + +import { + RUN_INTEGRATION_TESTS, + loadHistory as loadHistoryBase, + createBaseBundlerOptions, + createTestWorkflowBundle, +} from '@temporalio/test-helpers'; + +import type * as workflowImportStub from '../workflow/workflow-imports'; +import type * as workflowImportImpl from '../workflow/workflow-imports-impl'; +import { OpenTelemetryWorkflowClientInterceptor } from '../client'; +import { OpenTelemetryPlugin } from '..'; +import { instrument } from '../instrumentation'; +import { + makeWorkflowExporter, + OpenTelemetryActivityInboundInterceptor, + OpenTelemetryActivityOutboundInterceptor, +} from '../worker'; +import { + OpenTelemetrySinks, + SpanName, + SPAN_DELIMITER, + OpenTelemetryOutboundInterceptor, + OpenTelemetryInboundInterceptor, +} from '../workflow'; +import * as activities from './activities'; +import * as workflows from './workflows'; + +async function loadHistory(fname: string) { + const fpath = path.resolve(__dirname, `../../src/__tests__/history_files/${fname}`); + return loadHistoryBase(fpath); +} + +async function createOtelTestWorkflowBundle(opts: { + workflowsPath: string; + workflowInterceptorModules?: string[]; + plugins?: BundlerPlugin[]; +}) { + return createTestWorkflowBundle({ + ...opts, + additionalIgnoreModules: [require.resolve('./activities')], + }); +} + +async function withFakeGrpcServer( + fn: (port: number) => Promise, + requestListener?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void +): Promise { + return new Promise((resolve, reject) => { + const srv = http2.createServer(); + srv.listen({ port: 0, host: '127.0.0.1' }, () => { + const addr = srv.address(); + if (typeof addr === 'string' || addr === null) { + throw new Error('Unexpected server address type'); + } + srv.on('request', async (req, res) => { + if (requestListener) await requestListener(req, res); + res.statusCode = 200; + res.addTrailers({ + 'grpc-status': '0', + 'grpc-message': 'OK', + }); + res.write( + // This is a raw gRPC response, of length 0 + Buffer.from([ + // Frame Type: Data; Not Compressed + 0, + // Message Length: 0 + 0, 0, 0, 0, + ]) + ); + res.end(); + }); + fn(addr.port) + .catch((e) => reject(e)) + .finally(() => { + resolve(); + + // The OTel exporter will try to flush metrics on drop, which may result in tons of ERROR + // messages on the console if the server has had time to complete shutdown before then. + // Delaying closing the server by 1 second is enough to avoid that situation, and doesn't + // need to be awaited, so that doesn't slow down tests. + setTimeout(() => { + srv.close(); + }, 1000).unref(); + }); + }); + }); +} + +if (RUN_INTEGRATION_TESTS) { + test.serial('Otel interceptor spans are connected and complete', async (t) => { + Runtime.install({}); + try { + const spans = Array(); + + const staticResource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'ts-test-otel-worker', + }); + const traceExporter: opentelemetry.tracing.SpanExporter = { + export(spans_, resultCallback) { + spans.push(...spans_); + resultCallback({ code: ExportResultCode.SUCCESS }); + }, + async shutdown() { + // Nothing to shutdown + }, + }; + const otel = new opentelemetry.NodeSDK({ + resource: staticResource, + traceExporter, + }); + otel.start(); + + const plugin = new OpenTelemetryPlugin({ + resource: staticResource, + spanProcessor: new SimpleSpanProcessor(traceExporter), + }); + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + taskQueue: 'test-otel-v2', + plugins: [plugin], + }); + + const client = new Client({ + plugins: [plugin], + }); + await worker.runUntil( + client.workflow.execute(workflows.smorgasbord, { taskQueue: 'test-otel-v2', workflowId: uuid4() }) + ); + await otel.shutdown(); + const originalSpan = spans.find(({ name }) => name === `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}smorgasbord`); + t.true(originalSpan !== undefined); + t.log( + spans.map((span) => ({ + name: span.name, + parentSpanId: span.parentSpanContext?.spanId, + spanId: span.spanContext().spanId, + })) + ); + + const firstExecuteSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}smorgasbord` && + parentSpanContext?.spanId === originalSpan?.spanContext().spanId + ); + t.true(firstExecuteSpan !== undefined); + t.true(firstExecuteSpan!.status.code === SpanStatusCode.OK); + + const continueAsNewSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}smorgasbord` && + parentSpanContext?.spanId === firstExecuteSpan?.spanContext().spanId + ); + t.true(continueAsNewSpan !== undefined); + t.true(continueAsNewSpan!.status.code === SpanStatusCode.OK); + + const parentExecuteSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}smorgasbord` && + parentSpanContext?.spanId === continueAsNewSpan?.spanContext().spanId + ); + t.true(parentExecuteSpan !== undefined); + const firstActivityStartSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}fakeProgress` && + parentSpanContext?.spanId === parentExecuteSpan?.spanContext().spanId + ); + t.true(firstActivityStartSpan !== undefined); + + const firstActivityExecuteSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}fakeProgress` && + parentSpanContext?.spanId === firstActivityStartSpan?.spanContext().spanId + ); + t.true(firstActivityExecuteSpan !== undefined); + + const secondActivityStartSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}queryOwnWf` && + parentSpanContext?.spanId === parentExecuteSpan?.spanContext().spanId + ); + t.true(secondActivityStartSpan !== undefined); + + const secondActivityExecuteSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}queryOwnWf` && + parentSpanContext?.spanId === secondActivityStartSpan?.spanContext().spanId + ); + t.true(secondActivityExecuteSpan !== undefined); + + const childWorkflowStartSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}signalTarget` && + parentSpanContext?.spanId === parentExecuteSpan?.spanContext().spanId + ); + t.true(childWorkflowStartSpan !== undefined); + + const childWorkflowExecuteSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}signalTarget` && + parentSpanContext?.spanId === childWorkflowStartSpan?.spanContext().spanId + ); + t.true(childWorkflowExecuteSpan !== undefined); + + const signalChildWithUnblockSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}unblock` && + parentSpanContext?.spanId === parentExecuteSpan?.spanContext().spanId + ); + t.true(signalChildWithUnblockSpan !== undefined); + + const localActivityStartSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}echo` && + parentSpanContext?.spanId === parentExecuteSpan?.spanContext().spanId + ); + t.true(localActivityStartSpan !== undefined); + + const localActivityExecuteSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}echo` && + parentSpanContext?.spanId === localActivityStartSpan?.spanContext().spanId + ); + t.true(localActivityExecuteSpan !== undefined); + + const activityStartedSignalSpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}activityStarted` && + parentSpanContext?.spanId === firstActivityExecuteSpan?.spanContext().spanId + ); + t.true(activityStartedSignalSpan !== undefined); + + const querySpan = spans.find( + ({ name, parentSpanContext }) => + name === `${SpanName.WORKFLOW_QUERY}${SPAN_DELIMITER}step` && + parentSpanContext?.spanId === secondActivityExecuteSpan?.spanContext().spanId + ); + t.true(querySpan !== undefined); + + t.deepEqual(new Set(spans.map((span) => span.spanContext().traceId)).size, 1); + } finally { + // Cleanup the runtime so that it doesn't interfere with other tests + await Runtime._instance?.shutdown(); + } + }); + + // FIXME: This tests take ~9 seconds to complete on my local machine, even + // more in CI, and yet, it doesn't really do any assertion by itself. + // To be revisited at a later time. + test.skip('Otel spans connected', async (t) => { + const logger = new DefaultLogger('DEBUG'); + Runtime.install({ + logger, + }); + try { + const oTelUrl = 'http://127.0.0.1:4317'; + const exporter = new OTLPTraceExporter({ url: oTelUrl }); + const staticResource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'ts-test-otel-worker', + }); + const otel = new opentelemetry.NodeSDK({ + resource: staticResource, + traceExporter: exporter, + }); + await otel.start(); + + const sinks: InjectedSinks = { + exporter: makeWorkflowExporter(new SimpleSpanProcessor(exporter), staticResource), + }; + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + enableSDKTracing: true, + taskQueue: 'test-otel-v2', + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [(ctx) => ({ inbound: new OpenTelemetryActivityInboundInterceptor(ctx) })], + }, + sinks, + }); + + const client = new WorkflowClient({ + interceptors: [new OpenTelemetryWorkflowClientInterceptor()], + }); + await worker.runUntil(client.execute(workflows.smorgasbord, { taskQueue: 'test-otel-v2', workflowId: uuid4() })); + // Allow some time to ensure spans are flushed out to collector + await new Promise((resolve) => setTimeout(resolve, 5000)); + t.pass(); + } finally { + // Cleanup the runtime so that it doesn't interfere with other tests + await Runtime._instance?.shutdown(); + } + }); + + test('Otel workflow module does not patch node window object', (t) => { + // Importing the otel workflow modules above should patch globalThis + t.falsy((globalThis as any).window); + }); + + test.serial('instrumentation: Error status includes message and records exception', async (t) => { + const memoryExporter = new InMemorySpanExporter(); + const provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(memoryExporter)], + }); + const tracer = provider.getTracer('test-error-tracer'); + + const errorMessage = 'Test error message'; + + await t.throwsAsync( + instrument({ + tracer, + spanName: 'test-error-span', + fn: async () => { + throw new Error(errorMessage); + }, + }), + { message: errorMessage } + ); + + const spans = memoryExporter.getFinishedSpans(); + t.is(spans.length, 1); + + const span = spans[0]!; + + t.is(span.status.code, SpanStatusCode.ERROR); + + t.is(span.status.message, errorMessage); + + const exceptionEvents = span.events.filter((event) => event.name === 'exception'); + t.is(exceptionEvents.length, 1); + }); + + test.serial('Otel workflow omits ApplicationError with BENIGN category', async (t) => { + const memoryExporter = new InMemorySpanExporter(); + const provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(memoryExporter)], + }); + const tracer = provider.getTracer('test-error-tracer'); + + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + taskQueue: 'test-otel-v2-benign-err', + interceptors: { + activity: [ + (ctx) => { + return { inbound: new OpenTelemetryActivityInboundInterceptor(ctx, { tracer }) }; + }, + ], + }, + }); + + const client = new WorkflowClient(); + + await worker.runUntil( + client.execute(workflows.throwMaybeBenignErr, { + taskQueue: 'test-otel-v2-benign-err', + workflowId: uuid4(), + retry: { maximumAttempts: 3 }, + }) + ); + + const spans = memoryExporter.getFinishedSpans(); + t.is(spans.length, 3); + t.is(spans[0]!.status.code, SpanStatusCode.ERROR); + t.is(spans[0]!.status.message, 'not benign'); + t.is(spans[1]!.status.code, SpanStatusCode.UNSET); + t.is(spans[1]!.status.message, 'benign'); + t.is(spans[2]!.status.code, SpanStatusCode.OK); + }); + + test.serial('executeUpdateWithStart works correctly with OTEL interceptors', async (t) => { + const staticResource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'ts-test-otel-worker', + }); + const traceExporter: opentelemetry.tracing.SpanExporter = { + export(_spans, resultCallback) { + resultCallback({ code: ExportResultCode.SUCCESS }); + }, + async shutdown() {}, + }; + + const plugin = new OpenTelemetryPlugin({ + resource: staticResource, + spanProcessor: new SimpleSpanProcessor(traceExporter), + }); + const worker = await Worker.create({ + workflowBundle: await createOtelTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows'), + plugins: [plugin], + }), + activities, + taskQueue: 'test-otel-v2-update-start', + plugins: [plugin], + }); + + const client = new WorkflowClient(); + + const startWorkflowOperation = new WithStartWorkflowOperation(workflows.updateStartOtel, { + workflowId: uuid4(), + taskQueue: 'test-otel-v2-update-start', + workflowIdConflictPolicy: 'FAIL', + }); + + const { updateResult, workflowResult } = await worker.runUntil(async () => { + const updateResult = await client.executeUpdateWithStart(workflows.otelUpdate, { + args: [true], + startWorkflowOperation, + }); + + const handle = await startWorkflowOperation.workflowHandle(); + const workflowResult = await handle.result(); + + return { updateResult, workflowResult }; + }); + + t.is(updateResult, true); + t.is(workflowResult, true); + }); + + // These tests verify makeWorkflowExporter's handling of async resource attributes: + // https://github.com/temporalio/sdk-typescript/issues/1779 + test.serial(`makeWorkflowExporter with SpanProcessor does await async resource attributes`, async (t) => { + const taskQueue = `test-otel-v2-async-processor`; + const serviceName = `ts-test-otel-async-attributes`; + + // In OTel v2, async resource attributes are created by passing Promise + // values as attributes. resourceFromAttributes detects promises via + // isPromiseLike and resolves them when waitForAsyncAttributes() is called. + let resolveAsyncAttr: (value: string) => void; + const asyncAttrPromise = new Promise((resolve) => { + resolveAsyncAttr = resolve; + }); + + // Cast the promise to bypass type restrictions - the runtime supports it + const resource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: serviceName, + 'async.attr': asyncAttrPromise as unknown as string, + }); + + const spans: opentelemetry.tracing.ReadableSpan[] = []; + const traceExporter: opentelemetry.tracing.SpanExporter = { + export(spans_, resultCallback) { + spans.push(...spans_); + resultCallback({ code: ExportResultCode.SUCCESS }); + }, + async shutdown() {}, + }; + + // Custom SpanProcessor that resolves async resource attributes after the first onEnd is called. + // SpanProcessors are expected to wait on async resource attributes to settle before exporting the span. + class TestSpanProcessor extends SimpleSpanProcessor { + override onEnd(span: opentelemetry.tracing.ReadableSpan): void { + super.onEnd(span); + // Resolve async attribute so waitForAsyncAttributes can complete + resolveAsyncAttr('resolved'); + } + } + + const sinks: InjectedSinks = { + exporter: makeWorkflowExporter(new TestSpanProcessor(traceExporter), resource), + }; + + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + taskQueue, + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + outbound: new OpenTelemetryActivityOutboundInterceptor(ctx), + }), + ], + }, + sinks, + }); + + const client = new WorkflowClient(); + await worker.runUntil(client.execute(workflows.successString, { taskQueue, workflowId: uuid4() })); + + t.deepEqual(spans[0]!.resource.attributes, { + [ATTR_SERVICE_NAME]: serviceName, + // If not using a span processor, then we do not expect the async attr to be present + 'async.attr': 'resolved', + }); + }); + + test.serial('OpenTelemetryPlugin works with prebundled workflow code', async (t) => { + Runtime.install({}); + try { + const spans = Array(); + + const staticResource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'ts-test-otel-v2-prebundled-worker', + }); + const traceExporter: opentelemetry.tracing.SpanExporter = { + export(spans_, resultCallback) { + spans.push(...spans_); + resultCallback({ code: ExportResultCode.SUCCESS }); + }, + async shutdown() { + // Nothing to shutdown + }, + }; + + // Use BasicTracerProvider and get a tracer directly from it. + // We pass this tracer explicitly to the client interceptor to avoid relying on + // the global tracer provider, which may have been polluted by previous tests. + const provider = new BasicTracerProvider({ + resource: staticResource, + spanProcessors: [new SimpleSpanProcessor(traceExporter)], + }); + const tracer = provider.getTracer('@temporalio/interceptor-client'); + + const plugin = new OpenTelemetryPlugin({ + resource: staticResource, + spanProcessor: new SimpleSpanProcessor(traceExporter), + tracer, + }); + + // Bundle workflow code with the plugin - this tests that configureBundler passes workflowInterceptorModules + const workflowBundle = await bundleWorkflowCode({ + ...createBaseBundlerOptions([require.resolve('./activities')]), + workflowsPath: require.resolve('./workflows'), + plugins: [plugin], + logger: new DefaultLogger('WARN'), + }); + + const worker = await Worker.create({ + workflowBundle, + activities, + taskQueue: 'test-otel-v2-prebundled', + plugins: [plugin], + }); + + // Create client with explicit tracer to bypass global tracer provider pollution from other tests + const client = new Client({ + plugins: [plugin], + }); + await worker.runUntil( + client.workflow.execute(workflows.smorgasbord, { taskQueue: 'test-otel-v2-prebundled', workflowId: uuid4() }) + ); + await provider.shutdown(); + + // Verify that workflow spans were created + const workflowStartSpan = spans.find( + ({ name }) => name === `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}smorgasbord` + ); + t.true(workflowStartSpan !== undefined, 'WORKFLOW_START span should exist'); + + const workflowExecuteSpan = spans.find( + ({ name }) => name === `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}smorgasbord` + ); + t.true(workflowExecuteSpan !== undefined, 'WORKFLOW_EXECUTE span should exist'); + + const activityStartSpan = spans.find( + ({ name }) => name === `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}fakeProgress` + ); + t.true(activityStartSpan !== undefined, 'ACTIVITY_START span should exist'); + } finally { + // Cleanup the runtime so that it doesn't interfere with other tests + await Runtime._instance?.shutdown(); + } + }); + + // Regression test for https://github.com/temporalio/sdk-typescript/issues/1738 + test.serial('traceState properly crosses V8 isolate boundary', async (t) => { + const exportErrors: Error[] = []; + // Collect spans exported from workflow (those that crossed the isolate boundary) + const workflowExportedSpans: opentelemetry.tracing.ReadableSpan[] = []; + + await withFakeGrpcServer(async (port) => { + const staticResource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'test-tracestate-issue-1738', + }); + + const traceExporter = new OTLPTraceExporter({ url: `http://127.0.0.1:${port}` }); + + // Wrap the exporter to catch errors + const wrappedExporter: opentelemetry.tracing.SpanExporter = { + export(spans, resultCallback) { + traceExporter.export(spans, (result) => { + if (result.code === ExportResultCode.FAILED && result.error) { + exportErrors.push(result.error); + } + resultCallback(result); + }); + }, + async shutdown() { + await traceExporter.shutdown(); + }, + }; + + // Create a separate exporter for workflow spans that captures them for inspection + const workflowSpanExporter: opentelemetry.tracing.SpanExporter = { + export(spans, resultCallback) { + // Capture spans for later inspection + workflowExportedSpans.push(...spans); + // Also send to the real exporter to test serialization + wrappedExporter.export(spans, resultCallback); + }, + async shutdown() { + await wrappedExporter.shutdown(); + }, + }; + + const otel = new opentelemetry.NodeSDK({ + resource: staticResource, + traceExporter: wrappedExporter, + }); + otel.start(); + + const sinks: InjectedSinks = { + exporter: makeWorkflowExporter(new SimpleSpanProcessor(workflowSpanExporter), staticResource), + }; + + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + taskQueue: 'test-otel-v2-tracestate', + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + }, + sinks, + }); + + const client = new WorkflowClient({ + interceptors: [new OpenTelemetryWorkflowClientInterceptor()], + }); + + // Create a parent span with traceState and run the workflow within that context + const tracer = otelApi.trace.getTracer('test-tracestate'); + + await worker.runUntil(async () => { + // Create a span with traceState by starting a span and then creating a new context with traceState + const parentSpan = tracer.startSpan('parent-with-tracestate'); + const parentContext = otelApi.trace.setSpan(otelApi.context.active(), parentSpan); + + // Get the span context and create a new one with traceState + const originalSpanContext = parentSpan.spanContext(); + const traceState = createTraceState('vendor1=value1,vendor2=value2'); + const spanContextWithTraceState = { + ...originalSpanContext, + traceState, + }; + + // Create a new context with the modified span context + const contextWithTraceState = otelApi.trace.setSpanContext(parentContext, spanContextWithTraceState); + + // Execute the workflow within this context so the traceState is propagated + await otelApi.context.with(contextWithTraceState, async () => { + await client.execute(workflows.successString, { + taskQueue: 'test-otel-v2-tracestate', + workflowId: uuid4(), + }); + }); + + parentSpan.end(); + }); + + await otel.shutdown(); + }); + + t.deepEqual(exportErrors, [], 'should have no errors exporting spans'); + + const traceStates = workflowExportedSpans.map((span) => span.spanContext().traceState).filter(Boolean); + t.assert(traceStates.length > 0, 'Should have spans with traceState'); + + // Verify the traceState was properly reconstructed with working methods + for (const traceState of traceStates) { + // Verify serialize() method works and returns expected value + const serialized = traceState!.serialize(); + t.is(serialized, 'vendor1=value1,vendor2=value2'); + t.is(traceState!.get('vendor1'), 'value1'); + t.is(traceState!.get('vendor2'), 'value2'); + } + }); + + test.serial('DeterministicIdGenerator isolates PRNG and replays correctly', async (t) => { + const spans = Array(); + const staticResource = opentelemetry.resources.resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'ts-test-otel-v2-idgen', + }); + const traceExporter: opentelemetry.tracing.SpanExporter = { + export(spans_, resultCallback) { + spans.push(...spans_); + resultCallback({ code: ExportResultCode.SUCCESS }); + }, + async shutdown() {}, + }; + + const plugin = new OpenTelemetryPlugin({ + resource: staticResource, + spanProcessor: new SimpleSpanProcessor(traceExporter), + }); + + const taskQueue = 'test-otel-v2-idgen'; + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + taskQueue, + plugins: [plugin], + }); + + const client = new Client({ plugins: [plugin] }); + const workflowId = uuid4(); + + await worker.runUntil(client.workflow.execute(workflows.prngIsolation, { taskQueue, workflowId })); + + // Verify span IDs from DeterministicIdGenerator are valid hex + t.is(spans.length, 3); + for (const span of spans) { + const { traceId, spanId } = span.spanContext(); + t.regex(traceId, /^[0-9a-f]{32}$/); + t.regex(spanId, /^[0-9a-f]{16}$/); + t.not(traceId, '00000000000000000000000000000000'); + t.not(spanId, '0000000000000000'); + } + + // Replay the history to verify child ids remain the same + const history = await client.workflow.getHandle(workflowId).fetchHistory(); + await t.notThrowsAsync( + Worker.runReplayHistory( + { + workflowBundle: await createOtelTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows'), + plugins: [plugin], + }), + }, + history + ) + ); + }); +} + +test('Can replay smorgasbord history from 1.15.0', async (t) => { + const hist = await loadHistory('otel_smorgasbord_1_15_0.json'); + await t.notThrowsAsync(async () => { + await Worker.runReplayHistory( + { + workflowBundle: await createOtelTestWorkflowBundle({ + workflowsPath: require.resolve('./workflows'), + workflowInterceptorModules: [require.resolve('./workflows/otel-interceptors')], + }), + interceptors: { + workflowModules: [require.resolve('./workflows/otel-interceptors')], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx), + outbound: new OpenTelemetryActivityOutboundInterceptor(ctx), + }), + ], + }, + }, + hist + ); + }); +}); + +// Skipped as we only care that it compiles +test.skip('otel interceptors are complete', async (t) => { + // We only use this to verify that we trace all spans via typechecking + // Doing this instead of directly changing the `implements` to avoid leaking this in the docs + const _wfl_inbound = {} as OpenTelemetryInboundInterceptor satisfies Required; + const _wfl_outbound = {} as OpenTelemetryOutboundInterceptor satisfies Required< + Omit + >; + const _act_inbound = + {} as OpenTelemetryActivityInboundInterceptor satisfies Required; + const _act_outbound = + {} as OpenTelemetryActivityOutboundInterceptor satisfies Required; + const _client = {} as OpenTelemetryWorkflowClientInterceptor satisfies Required; + t.pass(); +}); + +test.skip('workflow-imports stub and impl have same type', async (t) => { + const _implSatisfiesStub = {} as typeof workflowImportImpl satisfies typeof workflowImportStub; + const _stubSatisfiesImpl = {} as typeof workflowImportStub satisfies typeof workflowImportImpl; + t.pass(); +}); diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/definitions.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/definitions.ts new file mode 100644 index 000000000..eb9d53d1d --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/definitions.ts @@ -0,0 +1,8 @@ +import { defineQuery, defineSignal } from '@temporalio/workflow'; + +export const activityStartedSignal = defineSignal('activityStarted'); +export const failSignal = defineSignal('fail'); +export const failWithMessageSignal = defineSignal<[string]>('fail'); +export const argsTestSignal = defineSignal<[number, string]>('argsTest'); +export const unblockSignal = defineSignal('unblock'); +export const versionQuery = defineQuery('version'); diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/index.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/index.ts new file mode 100644 index 000000000..27be4e75d --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/index.ts @@ -0,0 +1,7 @@ +export * from './definitions'; +export * from './signal-target'; +export * from './smorgasbord'; +export * from './success-string'; +export * from './throw-maybe-benign'; +export * from './update-start-otel'; +export * from './prng-isolation'; diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/otel-interceptors.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/otel-interceptors.ts new file mode 100644 index 000000000..b0dae0c90 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/otel-interceptors.ts @@ -0,0 +1,14 @@ +/** Not a workflow, just interceptors */ + +import { WorkflowInterceptors } from '@temporalio/workflow'; +import { + OpenTelemetryInboundInterceptor, + OpenTelemetryOutboundInterceptor, + OpenTelemetryInternalsInterceptor, +} from '../../workflow'; + +export const interceptors = (): WorkflowInterceptors => ({ + inbound: [new OpenTelemetryInboundInterceptor()], + outbound: [new OpenTelemetryOutboundInterceptor()], + internals: [new OpenTelemetryInternalsInterceptor()], +}); diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/prng-isolation.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/prng-isolation.ts new file mode 100644 index 000000000..c0d632c5d --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/prng-isolation.ts @@ -0,0 +1,12 @@ +/** + * Workflow that starts a child workflow with an auto-generated ID. + * The child workflow ID comes from uuid4() which uses Math.random(). + * If OTel span creation consumes Math.random(), the generated child + * workflow ID will differ on replay, causing a nondeterminism error. + */ +import { executeChild } from '@temporalio/workflow'; +import { successString } from './success-string'; + +export async function prngIsolation(): Promise { + return executeChild(successString, {}); +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-start-otel.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-start-otel.ts new file mode 100644 index 000000000..a33c4ff6a --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-start-otel.ts @@ -0,0 +1,33 @@ +import * as workflow from '@temporalio/workflow'; +import { + OpenTelemetryInboundInterceptor, + OpenTelemetryOutboundInterceptor, + OpenTelemetryInternalsInterceptor, +} from '../../workflow'; + +export const startSignal = workflow.defineSignal('startSignal'); + +const { a, b, c } = workflow.proxyLocalActivities<{ + a: () => Promise; + b: () => Promise; + c: () => Promise; +}>({ + scheduleToCloseTimeout: '1m', +}); + +export async function signalStartOtel(): Promise { + const order: string[] = []; + order.push(await a()); + workflow.setHandler(startSignal, async () => { + order.push(await b()); + }); + order.push(await c()); + + return order.join(''); +} + +export const interceptors = (): workflow.WorkflowInterceptors => ({ + inbound: [new OpenTelemetryInboundInterceptor()], + outbound: [new OpenTelemetryOutboundInterceptor()], + internals: [new OpenTelemetryInternalsInterceptor()], +}); diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-target.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-target.ts new file mode 100644 index 000000000..80ef8543f --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-target.ts @@ -0,0 +1,25 @@ +/** + * Workflow that can be failed and unblocked using signals. + * Useful for testing Workflow interactions. + * + * @module + */ +import { condition, setHandler } from '@temporalio/workflow'; +import { argsTestSignal, failWithMessageSignal, unblockSignal } from './definitions'; + +export async function signalTarget(): Promise { + let unblocked = false; + + // Verify arguments are sent correctly + setHandler(argsTestSignal, (num, str) => { + if (!(num === 123 && str === 'kid')) { + throw new Error('Invalid arguments'); + } + }); + setHandler(failWithMessageSignal, (message) => { + throw new Error(message); + }); + setHandler(unblockSignal, () => void (unblocked = true)); + + await condition(() => unblocked); +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-workflow.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-workflow.ts new file mode 100644 index 000000000..702fa8c56 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/signal-workflow.ts @@ -0,0 +1,21 @@ +import { defineSignal, startChild, setHandler, sleep, condition } from '@temporalio/workflow'; + +const approveTopSecret = defineSignal('approve'); + +// A workflow that simply calls an activity +export async function topSecretGreeting(name: string): Promise { + const handle = await startChild(topSecretGreetingChild, { + args: [name], + }); + await Promise.all([handle.signal(approveTopSecret), sleep('1ms')]); + return await handle.result(); +} + +export async function topSecretGreetingChild(name: string): Promise { + let approved = false; + setHandler(approveTopSecret, () => { + approved = true; + }); + await condition(() => approved); + return `Hello ${name}`; +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/smorgasbord.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/smorgasbord.ts new file mode 100644 index 000000000..b71c714dc --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/smorgasbord.ts @@ -0,0 +1,74 @@ +/** + * This workflow does a little bit of everything + */ +import { + sleep, + startChild, + proxyActivities, + ActivityCancellationType, + CancellationScope, + isCancellation, + defineQuery, + setHandler, + condition, + continueAsNew, + proxyLocalActivities, +} from '@temporalio/workflow'; +import * as activities from '../activities'; +import { signalTarget } from './signal-target'; +import { activityStartedSignal, unblockSignal } from './definitions'; + +const { fakeProgress, queryOwnWf } = proxyActivities({ + startToCloseTimeout: '1m', + cancellationType: ActivityCancellationType.WAIT_CANCELLATION_COMPLETED, +}); + +const { echo } = proxyLocalActivities({ + startToCloseTimeout: '1m', + cancellationType: ActivityCancellationType.WAIT_CANCELLATION_COMPLETED, +}); + +export const stepQuery = defineQuery('step'); + +export async function smorgasbord(iteration = 0): Promise { + let unblocked = false; + + setHandler(stepQuery, () => iteration); + setHandler(activityStartedSignal, () => void (unblocked = true)); + + try { + await CancellationScope.cancellable(async () => { + const activityPromise = fakeProgress(100, 10); + const queryActPromise = queryOwnWf(stepQuery); + const timerPromise = sleep(1000); + + const childWfPromise = (async () => { + const childWf = await startChild(signalTarget, {}); + await childWf.signal(unblockSignal); + await childWf.result(); + })(); + + const localActivityPromise = echo('local-activity'); + + if (iteration === 0) { + CancellationScope.current().cancel(); + } + + await Promise.all([ + activityPromise, + queryActPromise, + timerPromise, + childWfPromise, + localActivityPromise, + condition(() => unblocked), + ]); + }); + } catch (e) { + if (iteration !== 0 || !isCancellation(e)) { + throw e; + } + } + if (iteration < 2) { + await continueAsNew(iteration + 1); + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/success-string.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/success-string.ts new file mode 100644 index 000000000..7e872c165 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/success-string.ts @@ -0,0 +1,3 @@ +export async function successString(): Promise { + return 'success'; +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/throw-maybe-benign.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/throw-maybe-benign.ts new file mode 100644 index 000000000..86d94e12c --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/throw-maybe-benign.ts @@ -0,0 +1,11 @@ +import * as workflow from '@temporalio/workflow'; +import * as activities from '../activities'; + +const { throwMaybeBenign } = workflow.proxyActivities({ + startToCloseTimeout: '5s', + retry: { maximumAttempts: 3, backoffCoefficient: 1, initialInterval: 500 }, +}); + +export async function throwMaybeBenignErr(): Promise { + await throwMaybeBenign(); +} diff --git a/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/update-start-otel.ts b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/update-start-otel.ts new file mode 100644 index 000000000..db50f438f --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/__tests__/workflows/update-start-otel.ts @@ -0,0 +1,12 @@ +import * as workflow from '@temporalio/workflow'; + +export const otelUpdate = workflow.defineUpdate('otelUpdate'); + +export async function updateStartOtel(): Promise { + let updateResult = false; + workflow.setHandler(otelUpdate, (value: boolean): boolean => { + updateResult = value; + return true; + }); + return updateResult; +} diff --git a/packages/interceptors-opentelemetry-v2/src/client/index.ts b/packages/interceptors-opentelemetry-v2/src/client/index.ts new file mode 100644 index 000000000..3e78488d2 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/client/index.ts @@ -0,0 +1,218 @@ +import * as otel from '@opentelemetry/api'; +import type { + Next, + WorkflowSignalInput, + WorkflowSignalWithStartInput, + WorkflowStartInput, + WorkflowStartOutput, + WorkflowStartUpdateInput, + WorkflowStartUpdateOutput, + WorkflowStartUpdateWithStartInput, + WorkflowStartUpdateWithStartOutput, + WorkflowQueryInput, + WorkflowTerminateInput, + WorkflowCancelInput, + WorkflowDescribeInput, + WorkflowClientInterceptor, + TerminateWorkflowExecutionResponse, + RequestCancelWorkflowExecutionResponse, + DescribeWorkflowExecutionResponse, +} from '@temporalio/client'; +import { + instrument, + headersWithContext, + RUN_ID_ATTR_KEY, + WORKFLOW_ID_ATTR_KEY, + UPDATE_ID_ATTR_KEY, + TERMINATE_REASON_ATTR_KEY, +} from '../instrumentation'; +import { SpanName, SPAN_DELIMITER } from '../workflow/definitions'; + +export interface InterceptorOptions { + readonly tracer?: otel.Tracer; +} + +/** + * Intercepts calls to start a Workflow. + * + * Wraps the operation in an opentelemetry Span and passes it to the Workflow via headers. + */ +export class OpenTelemetryWorkflowClientInterceptor implements WorkflowClientInterceptor { + protected readonly tracer: otel.Tracer; + + constructor(options?: InterceptorOptions) { + this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-client'); + } + + async start(input: WorkflowStartInput, next: Next): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, + fn: async (span) => { + const headers = headersWithContext(input.headers); + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.options.workflowId); + const runId = await next({ ...input, headers }); + span.setAttribute(RUN_ID_ATTR_KEY, runId); + return runId; + }, + }); + } + + async signal(input: WorkflowSignalInput, next: Next): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId); + const headers = headersWithContext(input.headers); + await next({ ...input, headers }); + }, + }); + } + + async startWithDetails( + input: WorkflowStartInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, + fn: async (span) => { + const headers = headersWithContext(input.headers); + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.options.workflowId); + const output = await next({ ...input, headers }); + span.setAttribute(RUN_ID_ATTR_KEY, output.runId); + return output; + }, + }); + } + + async startUpdate( + input: WorkflowStartUpdateInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_START_UPDATE}${SPAN_DELIMITER}${input.updateName}`, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId); + if (input.options.updateId) { + span.setAttribute(UPDATE_ID_ATTR_KEY, input.options.updateId); + } + const headers = headersWithContext(input.headers); + const output = await next({ ...input, headers }); + span.setAttribute(RUN_ID_ATTR_KEY, output.workflowRunId); + return output; + }, + }); + } + + async startUpdateWithStart( + input: WorkflowStartUpdateWithStartInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_UPDATE_WITH_START}${SPAN_DELIMITER}${input.updateName}`, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowStartOptions.workflowId); + if (input.updateOptions.updateId) { + span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateOptions.updateId); + } + const workflowStartHeaders = headersWithContext(input.workflowStartHeaders); + const updateHeaders = headersWithContext(input.updateHeaders); + const output = await next({ ...input, workflowStartHeaders, updateHeaders }); + if (output.workflowExecution.runId) { + span.setAttribute(RUN_ID_ATTR_KEY, output.workflowExecution.runId); + } + return output; + }, + }); + } + + async signalWithStart( + input: WorkflowSignalWithStartInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_SIGNAL_WITH_START}${SPAN_DELIMITER}${input.workflowType}`, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.options.workflowId); + const headers = headersWithContext(input.headers); + const runId = await next({ ...input, headers }); + span.setAttribute(RUN_ID_ATTR_KEY, runId); + return runId; + }, + }); + } + + async query(input: WorkflowQueryInput, next: Next): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_QUERY}${SPAN_DELIMITER}${input.queryType}`, + fn: async (span) => { + const headers = headersWithContext(input.headers); + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId); + if (input.workflowExecution.runId) { + span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId); + } + return await next({ ...input, headers }); + }, + }); + } + + async terminate( + input: WorkflowTerminateInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: SpanName.WORKFLOW_TERMINATE, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId); + if (input.workflowExecution.runId) { + span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId); + } + if (input.reason) { + span.setAttribute(TERMINATE_REASON_ATTR_KEY, input.reason); + } + return await next(input); + }, + }); + } + + async cancel( + input: WorkflowCancelInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: SpanName.WORKFLOW_CANCEL, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId); + if (input.workflowExecution.runId) { + span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId); + } + return await next(input); + }, + }); + } + + async describe( + input: WorkflowDescribeInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: SpanName.WORKFLOW_DESCRIBE, + fn: async (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId); + if (input.workflowExecution.runId) { + span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId); + } + return await next(input); + }, + }); + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/index.ts b/packages/interceptors-opentelemetry-v2/src/index.ts new file mode 100644 index 000000000..eeccd2372 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/index.ts @@ -0,0 +1,18 @@ +/** + * `npm i @temporalio/interceptors-opentelemetry-v2` + * + * Interceptors that add OpenTelemetry tracing. + * + * [Documentation](https://docs.temporal.io/typescript/logging#opentelemetry-tracing) + * + * @module + */ + +export * from './plugin'; +export * from './workflow'; +export * from './worker'; +export { + OpenTelemetryWorkflowClientInterceptor, + /** deprecated: Use OpenTelemetryWorkflowClientInterceptor instead */ + OpenTelemetryWorkflowClientInterceptor as OpenTelemetryWorkflowClientCallsInterceptor, +} from './client'; diff --git a/packages/interceptors-opentelemetry-v2/src/instrumentation.ts b/packages/interceptors-opentelemetry-v2/src/instrumentation.ts new file mode 100644 index 000000000..f8d89a73f --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/instrumentation.ts @@ -0,0 +1,135 @@ +/** + * opentelemetry instrumentation helper functions + * @module + */ +import * as otel from '@opentelemetry/api'; +import { + type Headers, + ApplicationFailure, + ApplicationFailureCategory, + defaultPayloadConverter, +} from '@temporalio/common'; + +/** Default trace header for opentelemetry interceptors */ +export const TRACE_HEADER = '_tracer-data'; +/** As in workflow run id */ +export const RUN_ID_ATTR_KEY = 'run_id'; +/** As in workflow id */ +export const WORKFLOW_ID_ATTR_KEY = 'temporalWorkflowId'; +/** As in activity id */ +export const ACTIVITY_ID_ATTR_KEY = 'temporalActivityId'; +/** As in update id */ +export const UPDATE_ID_ATTR_KEY = 'temporalUpdateId'; +/** As in termination reason */ +export const TERMINATE_REASON_ATTR_KEY = 'temporalTerminateReason'; +/** As in Nexus service */ +export const NEXUS_SERVICE_ATTR_KEY = 'temporalNexusService'; +/** As in Nexus operation */ +export const NEXUS_OPERATION_ATTR_KEY = 'temporalNexusOperation'; +/** As in Nexus endpoint */ +export const NEXUS_ENDPOINT_ATTR_KEY = 'temporalNexusEndpoint'; + +const payloadConverter = defaultPayloadConverter; + +/** + * If found, return an otel Context deserialized from the provided headers + */ +export function extractContextFromHeaders(headers: Headers): otel.Context | undefined { + const encodedSpanContext = headers[TRACE_HEADER]; + if (encodedSpanContext === undefined) { + return undefined; + } + const textMap: Record = payloadConverter.fromPayload(encodedSpanContext); + return otel.propagation.extract(otel.context.active(), textMap, otel.defaultTextMapGetter); +} + +/** + * Given headers, return new headers with the current otel context inserted + */ +export function headersWithContext(headers: Headers): Headers { + const carrier = {}; + otel.propagation.inject(otel.context.active(), carrier, otel.defaultTextMapSetter); + return { ...headers, [TRACE_HEADER]: payloadConverter.toPayload(carrier) }; +} + +async function wrapWithSpan( + span: otel.Span, + fn: (span: otel.Span) => Promise, + acceptableErrors?: (err: unknown) => boolean +): Promise { + try { + const ret = await fn(span); + span.setStatus({ code: otel.SpanStatusCode.OK }); + return ret; + } catch (err: any) { + maybeAddErrorToSpan(err, span, acceptableErrors); + throw err; + } finally { + span.end(); + } +} + +function wrapWithSpanSync( + span: otel.Span, + fn: (span: otel.Span) => T, + acceptableErrors?: (err: unknown) => boolean +): T { + try { + const ret = fn(span); + span.setStatus({ code: otel.SpanStatusCode.OK }); + return ret; + } catch (err: any) { + maybeAddErrorToSpan(err, span, acceptableErrors); + throw err; + } finally { + span.end(); + } +} + +function maybeAddErrorToSpan(err: any, span: otel.Span, acceptableErrors?: (err: unknown) => boolean): void { + const isBenignErr = err instanceof ApplicationFailure && err.category === ApplicationFailureCategory.BENIGN; + if (acceptableErrors === undefined || !acceptableErrors(err)) { + const statusCode = isBenignErr ? otel.SpanStatusCode.UNSET : otel.SpanStatusCode.ERROR; + span.setStatus({ code: statusCode, message: (err as Error).message ?? String(err) }); + span.recordException(err); + } else { + span.setStatus({ code: otel.SpanStatusCode.OK }); + } +} + +export interface InstrumentOptions { + tracer: otel.Tracer; + spanName: string; + fn: (span: otel.Span) => Promise; + context?: otel.Context; + acceptableErrors?: (err: unknown) => boolean; +} + +export type InstrumentOptionsSync = Omit, 'fn'> & { fn: (span: otel.Span) => T }; + +/** + * Wraps `fn` in a span which ends when function returns or throws + */ +export async function instrument({ + tracer, + spanName, + fn, + context, + acceptableErrors, +}: InstrumentOptions): Promise { + if (context) { + return await otel.context.with(context, async () => { + return await tracer.startActiveSpan(spanName, async (span) => await wrapWithSpan(span, fn, acceptableErrors)); + }); + } + return await tracer.startActiveSpan(spanName, async (span) => await wrapWithSpan(span, fn, acceptableErrors)); +} + +export function instrumentSync({ tracer, spanName, fn, context, acceptableErrors }: InstrumentOptionsSync): T { + if (context) { + return otel.context.with(context, () => { + return tracer.startActiveSpan(spanName, (span) => wrapWithSpanSync(span, fn, acceptableErrors)); + }); + } + return tracer.startActiveSpan(spanName, (span) => wrapWithSpanSync(span, fn, acceptableErrors)); +} diff --git a/packages/interceptors-opentelemetry-v2/src/plugin.ts b/packages/interceptors-opentelemetry-v2/src/plugin.ts new file mode 100644 index 000000000..9dfef9559 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/plugin.ts @@ -0,0 +1,77 @@ +import { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { Resource } from '@opentelemetry/resources'; +import { SimplePlugin } from '@temporalio/plugin'; +import { InjectedSinks, ReplayWorkerOptions, WorkerOptions } from '@temporalio/worker'; +import { InterceptorOptions, OpenTelemetryWorkflowClientInterceptor } from './client'; +import { + makeWorkflowExporter, + OpenTelemetryActivityInboundInterceptor, + OpenTelemetryActivityOutboundInterceptor, +} from './worker'; +import { OpenTelemetrySinks } from './workflow'; + +/** + * Configuration options for {@link OpenTelemetryPlugin}. + * + * @experimental Plugins is an experimental feature; APIs may change without notice. + */ +export interface OpenTelemetryPluginOptions extends InterceptorOptions { + /** OpenTelemetry resource attributes to attach to exported spans */ + readonly resource: Resource; + /** Exporter used to send spans to a tracing backend */ + readonly spanProcessor: SpanProcessor; +} + +/** + * A plugin that adds OpenTelemetry tracing. + * + * Configures Client, Activity, and Workflow interceptors for trace propagation and injects + * a span exporter sink for Workflow spans. + * + * @experimental Plugins is an experimental feature; APIs may change without notice. + */ +export class OpenTelemetryPlugin extends SimplePlugin { + constructor(readonly otelOptions: OpenTelemetryPluginOptions) { + const workflowInterceptorsPath = require.resolve('./workflow-interceptors'); + const interceptorOptions = otelOptions.tracer ? { tracer: otelOptions.tracer } : {}; + super({ + name: 'OpenTelemetryPlugin', + clientInterceptors: { + workflow: [new OpenTelemetryWorkflowClientInterceptor(interceptorOptions)], + }, + workerInterceptors: { + client: { + workflow: [new OpenTelemetryWorkflowClientInterceptor(interceptorOptions)], + }, + workflowModules: [workflowInterceptorsPath], + activity: [ + (ctx) => ({ + inbound: new OpenTelemetryActivityInboundInterceptor(ctx, interceptorOptions), + outbound: new OpenTelemetryActivityOutboundInterceptor(ctx), + }), + ], + }, + }); + } + + configureWorker(options: WorkerOptions): WorkerOptions { + return super.configureWorker(this.injectSinks(options)); + } + + configureReplayWorker(options: ReplayWorkerOptions): ReplayWorkerOptions { + return super.configureReplayWorker(this.injectSinks(options)); + } + + private injectSinks }>(options: T): T { + const sinks: InjectedSinks = { + exporter: makeWorkflowExporter(this.otelOptions.spanProcessor, this.otelOptions.resource), + }; + return { + ...options, + sinks: { + ...options.sinks, + ...sinks, + }, + }; + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/worker/index.ts b/packages/interceptors-opentelemetry-v2/src/worker/index.ts new file mode 100644 index 000000000..918d9d56c --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/worker/index.ts @@ -0,0 +1,171 @@ +import * as otel from '@opentelemetry/api'; +import { createTraceState } from '@opentelemetry/api'; +import type { Resource } from '@opentelemetry/resources'; +import type { ReadableSpan, SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import type { Context as ActivityContext } from '@temporalio/activity'; +import type { + Next, + ActivityInboundCallsInterceptor, + ActivityOutboundCallsInterceptor, + InjectedSink, + GetLogAttributesInput, + GetMetricTagsInput, + ActivityExecuteInput, +} from '@temporalio/worker'; +import { + instrument, + extractContextFromHeaders, + WORKFLOW_ID_ATTR_KEY, + RUN_ID_ATTR_KEY, + ACTIVITY_ID_ATTR_KEY, +} from '../instrumentation'; +import { + type OpenTelemetryWorkflowExporter, + type SerializableSpan, + SpanName, + SPAN_DELIMITER, +} from '../workflow/definitions'; + +export interface InterceptorOptions { + readonly tracer?: otel.Tracer; +} + +/** + * Intercepts calls to start an Activity. + * + * Wraps the operation in an opentelemetry Span and links it to a parent Span context if one is + * provided in the Activity input headers. + */ +export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundCallsInterceptor { + protected readonly tracer: otel.Tracer; + + constructor( + protected readonly ctx: ActivityContext, + options?: InterceptorOptions + ) { + this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-activity'); + } + + async execute(input: ActivityExecuteInput, next: Next): Promise { + const context = extractContextFromHeaders(input.headers); + const spanName = `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}${this.ctx.info.activityType}`; + return await instrument({ + tracer: this.tracer, + spanName, + fn: (span) => { + span.setAttribute(WORKFLOW_ID_ATTR_KEY, this.ctx.info.workflowExecution.workflowId); + span.setAttribute(RUN_ID_ATTR_KEY, this.ctx.info.workflowExecution.runId); + span.setAttribute(ACTIVITY_ID_ATTR_KEY, this.ctx.info.activityId); + return next(input); + }, + context, + }); + } +} + +/** + * Intercepts calls to emit logs and metrics from an Activity. + * + * Attach OpenTelemetry context tracing attributes to emitted log messages and metrics, if appropriate. + */ +export class OpenTelemetryActivityOutboundInterceptor implements ActivityOutboundCallsInterceptor { + constructor(protected readonly ctx: ActivityContext) {} + + public getLogAttributes( + input: GetLogAttributesInput, + next: Next + ): Record { + const span = otel.trace.getSpan(otel.context.active()); + const spanContext = span?.spanContext(); + if (spanContext && otel.isSpanContextValid(spanContext)) { + return next({ + trace_id: spanContext.traceId, + span_id: spanContext.spanId, + trace_flags: `0${spanContext.traceFlags.toString(16)}`, + ...input, + }); + } else { + return next(input); + } + } + + public getMetricTags( + input: GetMetricTagsInput, + next: Next + ): GetMetricTagsInput { + const span = otel.trace.getSpan(otel.context.active()); + const spanContext = span?.spanContext(); + if (spanContext && otel.isSpanContextValid(spanContext)) { + return next({ + trace_id: spanContext.traceId, + span_id: spanContext.spanId, + trace_flags: `0${spanContext.traceFlags.toString(16)}`, + ...input, + }); + } else { + return next(input); + } + } +} + +/** + * Takes an opentelemetry SpanProcessor and turns it into an injected Workflow span exporter sink. + */ +export function makeWorkflowExporter( + spanProcessor: SpanProcessor, + resource: Resource +): InjectedSink; +export function makeWorkflowExporter( + processor: SpanProcessor, + resource: Resource +): InjectedSink { + return { + export: { + fn: (info, spanData) => { + const spans = spanData.map((serialized) => { + Object.assign(serialized.attributes, info); + // Spans are copied over from the isolate and are converted to ReadableSpan instances + return extractReadableSpan(serialized, resource); + }); + + spans.forEach((span) => processor.onEnd(span)); + }, + }, + }; +} + +/** + * Deserialize a serialized span created by the Workflow isolate + */ +function extractReadableSpan(serializable: SerializableSpan, resource: Resource): ReadableSpan { + const { + spanContext: { traceState, ...restSpanContext }, + parentSpanContext: serializedParentSpanContext, + instrumentationScope, + ...rest + } = serializable; + const spanContext: otel.SpanContext = { + // Reconstruct the TraceState from the serialized string. + traceState: traceState ? createTraceState(traceState) : undefined, + ...restSpanContext, + }; + + let parentSpanContext: otel.SpanContext | undefined; + if (serializedParentSpanContext) { + const { traceState: parentTraceState, ...restParentSpanContext } = serializedParentSpanContext; + parentSpanContext = { + traceState: parentTraceState ? createTraceState(parentTraceState) : undefined, + ...restParentSpanContext, + }; + } + + return { + spanContext() { + return spanContext; + }, + parentSpanContext, + instrumentationScope, + resource, + ...rest, + }; +} diff --git a/packages/interceptors-opentelemetry-v2/src/workflow-interceptors.ts b/packages/interceptors-opentelemetry-v2/src/workflow-interceptors.ts new file mode 100644 index 000000000..23de97f55 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow-interceptors.ts @@ -0,0 +1,14 @@ +/** Not a workflow, just interceptors */ + +import type { WorkflowInterceptors } from '@temporalio/workflow'; +import { + OpenTelemetryInboundInterceptor, + OpenTelemetryOutboundInterceptor, + OpenTelemetryInternalsInterceptor, +} from './workflow'; + +export const interceptors = (): WorkflowInterceptors => ({ + inbound: [new OpenTelemetryInboundInterceptor()], + outbound: [new OpenTelemetryOutboundInterceptor()], + internals: [new OpenTelemetryInternalsInterceptor()], +}); diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/context-manager.ts b/packages/interceptors-opentelemetry-v2/src/workflow/context-manager.ts new file mode 100644 index 000000000..eb9643c30 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/context-manager.ts @@ -0,0 +1,53 @@ +import { type AsyncLocalStorage } from 'async_hooks'; +import * as otel from '@opentelemetry/api'; + +export class ContextManager implements otel.ContextManager { + // The workflow sandbox provides AsyncLocalStorage through globalThis. + protected storage: AsyncLocalStorage = new (globalThis as any).AsyncLocalStorage(); + + active(): otel.Context { + return this.storage.getStore() || otel.ROOT_CONTEXT; + } + + bind(context: otel.Context, target: T): T { + if (typeof target !== 'function') { + throw new TypeError(`Only function binding is supported, got ${typeof target}`); + } + + // Stolen from https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts + const contextWrapper = (...args: unknown[]) => { + return this.with(context, () => target.apply(this, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + /** + * It isn't possible to tell Typescript that contextWrapper is the same as T + * so we forced to cast as any here. + */ + + return contextWrapper as any; + } + + enable(): this { + return this; + } + + disable(): this { + this.storage.disable(); + return this; + } + + with ReturnType>( + context: otel.Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const cb = thisArg == null ? fn : fn.bind(thisArg); + return this.storage.run(context, cb, ...args); + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/definitions.ts b/packages/interceptors-opentelemetry-v2/src/workflow/definitions.ts new file mode 100644 index 000000000..196d87caa --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/definitions.ts @@ -0,0 +1,137 @@ +import * as otel from '@opentelemetry/api'; +import * as tracing from '@opentelemetry/sdk-trace-base'; +import { InstrumentationScope } from '@opentelemetry/core'; +import type { Sink, Sinks } from '@temporalio/workflow'; + +/** + * Serializable version of SpanContext where traceState is converted to a string. + */ +export type SerializableSpanContext = Omit & { traceState?: string }; + +/** + * Serializable version of the opentelemetry Span for cross isolate copying + */ +export interface SerializableSpan { + readonly name: string; + readonly kind: otel.SpanKind; + readonly spanContext: SerializableSpanContext; + readonly parentSpanContext?: SerializableSpanContext; + readonly startTime: otel.HrTime; + readonly endTime: otel.HrTime; + readonly status: otel.SpanStatus; + readonly attributes: otel.Attributes; + readonly links: otel.Link[]; + readonly events: tracing.TimedEvent[]; + readonly duration: otel.HrTime; + readonly ended: boolean; + readonly droppedAttributesCount: number; + readonly droppedLinksCount: number; + readonly droppedEventsCount: number; + // readonly resource: Resource; + readonly instrumentationScope: InstrumentationScope; +} + +export interface OpenTelemetryWorkflowExporter extends Sink { + export(span: SerializableSpan[]): void; +} + +/** + * Required external dependencies for Workflow interceptor to export spans + */ +export interface OpenTelemetrySinks extends Sinks { + exporter: OpenTelemetryWorkflowExporter; +} + +export enum SpanName { + /** + * Workflow is scheduled by a client + */ + WORKFLOW_START = 'StartWorkflow', + + /** + * Workflow is signalled + */ + WORKFLOW_SIGNAL = 'SignalWorkflow', + + /** + * Workflow is client calls signalWithStart + */ + WORKFLOW_SIGNAL_WITH_START = 'SignalWithStartWorkflow', + + /** + * Workflow is queried + */ + WORKFLOW_QUERY = 'QueryWorkflow', + + /** + * Workflow update is started by client + */ + WORKFLOW_START_UPDATE = 'StartWorkflowUpdate', + + /** + * Workflow is started with an update + */ + WORKFLOW_UPDATE_WITH_START = 'UpdateWithStartWorkflow', + + /** + * Workflow handles an incoming signal + */ + WORKFLOW_HANDLE_SIGNAL = 'HandleSignal', + + /** + * Workflow handles an incoming query + */ + WORKFLOW_HANDLE_QUERY = 'HandleQuery', + + /** + * Workflow handles an incoming update + */ + WORKFLOW_HANDLE_UPDATE = 'HandleUpdate', + + /** + * Workflow validates an incoming update + */ + WORKFLOW_VALIDATE_UPDATE = 'ValidateUpdate', + + /** + * Workflow is terminated + */ + WORKFLOW_TERMINATE = 'TerminateWorkflow', + + /** + * Workflow is cancelled + */ + WORKFLOW_CANCEL = 'CancelWorkflow', + + /** + * Workflow is described + */ + WORKFLOW_DESCRIBE = 'DescribeWorkflow', + + /** + * Workflow run is executing + */ + WORKFLOW_EXECUTE = 'RunWorkflow', + /** + * Child Workflow is started (by parent Workflow) + */ + CHILD_WORKFLOW_START = 'StartChildWorkflow', + /** + * Activity is scheduled by a Workflow + */ + ACTIVITY_START = 'StartActivity', + /** + * Activity is executing + */ + ACTIVITY_EXECUTE = 'RunActivity', + /** + * Workflow is continuing as new + */ + CONTINUE_AS_NEW = 'ContinueAsNew', + /** + * Nexus operation is started + */ + NEXUS_OPERATION_START = 'StartNexusOperation', +} + +export const SPAN_DELIMITER = ':'; diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/id-generator.ts b/packages/interceptors-opentelemetry-v2/src/workflow/id-generator.ts new file mode 100644 index 000000000..94b263224 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/id-generator.ts @@ -0,0 +1,54 @@ +import type { IdGenerator } from '@opentelemetry/sdk-trace-base'; +import { alea, type RNG } from './workflow-imports'; + +// Seed the OTel ID generator's PRNG from the workflow's PRNG, then advance +// the workflow PRNG once so the two sequences diverge immediately. +let otelRandom: RNG | undefined; + +function getOtelRandom(): RNG { + if (otelRandom === undefined) { + // Use Math.random() (which is the workflow's deterministic PRNG) to + // produce a seed for a SEPARATE alea instance. After this, the OTel + // PRNG and the workflow PRNG are independent sequences. + const seed = [Math.random(), Math.random(), Math.random(), Math.random()]; + otelRandom = alea(seed.map((v) => (v * 0x100000000) >>> 0)); + } + return otelRandom; +} + +function randomHex(length: number): string { + const rng = getOtelRandom(); + const chars: string[] = []; + for (let i = 0; i < length; i++) { + const nibble = (rng() * 16) >>> 0; + chars.push(nibble.toString(16)); + } + return chars.join(''); +} + +/** + * Reset the OTel PRNG so the next workflow gets a freshly seeded generator. + * Must be called during workflow dispose since modules are shared across + * workflow instances in the reusable VM. + */ +export function resetIdGenerator(): void { + otelRandom = undefined; +} + +/** + * Generates span and trace IDs using a PRNG that is separate from the + * workflow's Math.random(), preventing OTel ID generation from affecting + * the deterministic sequence used by uuid4() for child workflow IDs. + */ +export class DeterministicIdGenerator implements IdGenerator { + generateTraceId(): string { + const id = randomHex(32); + // Ensure non-zero per W3C Trace Context spec + return id === '00000000000000000000000000000000' ? '00000000000000000000000000000001' : id; + } + + generateSpanId(): string { + const id = randomHex(16); + return id === '0000000000000000' ? '0000000000000001' : id; + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/index.ts b/packages/interceptors-opentelemetry-v2/src/workflow/index.ts new file mode 100644 index 000000000..60a23d914 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/index.ts @@ -0,0 +1,313 @@ +// eslint-disable-next-line import/no-unassigned-import +import './performance-polyfill'; // Zero-import polyfill; must run before OTel modules access `performance` +// eslint-disable-next-line import/no-unassigned-import +import './runtime'; // Patch the Workflow isolate runtime for opentelemetry +import * as otel from '@opentelemetry/api'; +import * as tracing from '@opentelemetry/sdk-trace-base'; +import { W3CTraceContextPropagator } from '@opentelemetry/core'; +import type { + ActivityInput, + ContinueAsNewInput, + DisposeInput, + GetLogAttributesInput, + GetMetricTagsInput, + LocalActivityInput, + Next, + QueryInput, + SignalInput, + SignalWorkflowInput, + StartChildWorkflowExecutionInput, + UpdateInput, + WorkflowExecuteInput, + WorkflowInboundCallsInterceptor, + WorkflowInternalsInterceptor, + WorkflowOutboundCallsInterceptor, + StartNexusOperationInput, + StartNexusOperationOutput, +} from '@temporalio/workflow'; +import { + instrument, + instrumentSync, + extractContextFromHeaders, + headersWithContext, + UPDATE_ID_ATTR_KEY, + NEXUS_SERVICE_ATTR_KEY, + NEXUS_OPERATION_ATTR_KEY, + NEXUS_ENDPOINT_ATTR_KEY, +} from '../instrumentation'; +import { ContextManager } from './context-manager'; +import { SpanName, SPAN_DELIMITER } from './definitions'; +import { SpanExporter } from './span-exporter'; +import { DeterministicIdGenerator, resetIdGenerator } from './id-generator'; +import { workflowInfo, ContinueAsNew } from './workflow-imports'; + +export * from './definitions'; + +let tracer: undefined | otel.Tracer = undefined; +let contextManager: undefined | ContextManager = undefined; + +function getTracer(): otel.Tracer { + if (contextManager === undefined) { + contextManager = new ContextManager(); + } + if (tracer === undefined) { + const provider = new tracing.BasicTracerProvider({ + idGenerator: new DeterministicIdGenerator(), + spanProcessors: [new tracing.SimpleSpanProcessor(new SpanExporter())], + }); + otel.propagation.setGlobalPropagator(new W3CTraceContextPropagator()); + otel.trace.setGlobalTracerProvider(provider); + otel.context.setGlobalContextManager(contextManager); + tracer = provider.getTracer('@temporalio/interceptor-workflow'); + } + return tracer; +} + +/** + * Intercepts calls to run a Workflow + * + * Wraps the operation in an opentelemetry Span and links it to a parent Span context if one is + * provided in the Workflow input headers. + * + * `@temporalio/workflow` must be provided by host package in order to function. + */ +export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInterceptor { + protected readonly tracer = getTracer(); + + public async execute( + input: WorkflowExecuteInput, + next: Next + ): Promise { + const context = extractContextFromHeaders(input.headers); + + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`, + fn: () => next(input), + context, + acceptableErrors: (err) => err instanceof ContinueAsNew, + }); + } + + public async handleSignal( + input: SignalInput, + next: Next + ): Promise { + const context = extractContextFromHeaders(input.headers); + + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_HANDLE_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, + fn: () => next(input), + context, + }); + } + + public async handleUpdate( + input: UpdateInput, + next: Next + ): Promise { + const context = extractContextFromHeaders(input.headers); + + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_HANDLE_UPDATE}${SPAN_DELIMITER}${input.name}`, + fn: (span) => { + span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateId); + return next(input); + }, + context, + }); + } + + public validateUpdate(input: UpdateInput, next: Next): void { + const context = extractContextFromHeaders(input.headers); + instrumentSync({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_VALIDATE_UPDATE}${SPAN_DELIMITER}${input.name}`, + fn: (span) => { + span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateId); + return next(input); + }, + context, + }); + } + + public async handleQuery( + input: QueryInput, + next: Next + ): Promise { + const context = extractContextFromHeaders(input.headers); + + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_HANDLE_QUERY}${SPAN_DELIMITER}${input.queryName}`, + fn: () => next(input), + context, + }); + } +} + +/** + * Intercepts outbound calls to schedule an Activity + * + * Wraps the operation in an opentelemetry Span and passes it to the Activity via headers. + * + * `@temporalio/workflow` must be provided by host package in order to function. + */ +export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsInterceptor { + protected readonly tracer = getTracer(); + + public async scheduleActivity( + input: ActivityInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, + fn: async () => { + const headers = headersWithContext(input.headers); + + return next({ + ...input, + headers, + }); + }, + }); + } + + public async scheduleLocalActivity( + input: LocalActivityInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`, + fn: async () => { + const headers = headersWithContext(input.headers); + + return next({ + ...input, + headers, + }); + }, + }); + } + + public async startNexusOperation( + input: StartNexusOperationInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.NEXUS_OPERATION_START}${SPAN_DELIMITER}${input.service}${SPAN_DELIMITER}${input.operation}`, + fn: async (span) => { + span.setAttribute(NEXUS_SERVICE_ATTR_KEY, input.service); + span.setAttribute(NEXUS_OPERATION_ATTR_KEY, input.operation); + span.setAttribute(NEXUS_ENDPOINT_ATTR_KEY, input.endpoint); + return await next(input); + }, + }); + } + + public async startChildWorkflowExecution( + input: StartChildWorkflowExecutionInput, + next: Next + ): Promise<[Promise, Promise]> { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`, + fn: async () => { + const headers = headersWithContext(input.headers); + + return next({ + ...input, + headers, + }); + }, + }); + } + + public async continueAsNew( + input: ContinueAsNewInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`, + fn: async () => { + const headers = headersWithContext(input.headers); + + return next({ + ...input, + headers, + }); + }, + acceptableErrors: (err) => err instanceof ContinueAsNew, + }); + } + + public async signalWorkflow( + input: SignalWorkflowInput, + next: Next + ): Promise { + return await instrument({ + tracer: this.tracer, + spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`, + fn: async () => { + const headers = headersWithContext(input.headers); + + return next({ + ...input, + headers, + }); + }, + }); + } + + public getLogAttributes( + input: GetLogAttributesInput, + next: Next + ): Record { + const span = otel.trace.getSpan(otel.context.active()); + const spanContext = span?.spanContext(); + if (spanContext && otel.isSpanContextValid(spanContext)) { + return next({ + trace_id: spanContext.traceId, + span_id: spanContext.spanId, + trace_flags: `0${spanContext.traceFlags.toString(16)}`, + ...input, + }); + } else { + return next(input); + } + } + + public getMetricTags( + input: GetMetricTagsInput, + next: Next + ): GetMetricTagsInput { + const span = otel.trace.getSpan(otel.context.active()); + const spanContext = span?.spanContext(); + if (spanContext && otel.isSpanContextValid(spanContext)) { + return next({ + trace_id: spanContext.traceId, + span_id: spanContext.spanId, + trace_flags: `0${spanContext.traceFlags.toString(16)}`, + ...input, + }); + } else { + return next(input); + } + } +} + +export class OpenTelemetryInternalsInterceptor implements WorkflowInternalsInterceptor { + async dispose(input: DisposeInput, next: Next): Promise { + if (contextManager !== undefined) { + contextManager.disable(); + } + resetIdGenerator(); + next(input); + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/performance-polyfill.ts b/packages/interceptors-opentelemetry-v2/src/workflow/performance-polyfill.ts new file mode 100644 index 000000000..d16ddce5d --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/performance-polyfill.ts @@ -0,0 +1,30 @@ +/** + * Polyfill performance for the workflow isolate. + * + * OTel v2's browser platform accesses `performance` at module scope. + * This file MUST have zero imports so webpack initializes it before any + * OTel module that references `performance`. + * + * The guard uses two checks: + * - `__webpack_module_cache__` on globalThis is a positive indicator of the + * workflow sandbox (set by the SDK's VM creators before the bundle evaluates). + * - `performance` being undefined confirms polyfilling is needed (Date.now() + * is deterministic inside the sandbox, so this polyfill is safe). + * + * @module + */ + +if ('__webpack_module_cache__' in globalThis && typeof performance === 'undefined') { + Object.assign(globalThis, { + performance: { + timeOrigin: Date.now(), + now() { + return Date.now() - this.timeOrigin; + }, + }, + }); +} + +// Empty export to mark this as a module for ESLint's import/unambiguous rule. +// This file intentionally has no imports to ensure it initializes before OTel. +export {}; diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/runtime.ts b/packages/interceptors-opentelemetry-v2/src/workflow/runtime.ts new file mode 100644 index 000000000..d72ab36ce --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/runtime.ts @@ -0,0 +1,17 @@ +/** + * Sets global variables required for importing opentelemetry in isolate. + * + * Note: `performance` is polyfilled separately in `performance-polyfill.ts` + * (a zero-import file) to ensure it's available before OTel v2 modules that + * eagerly access `performance` at module scope. + * + * @module + */ +import { inWorkflowContext } from './workflow-imports'; + +if (inWorkflowContext()) { + // OTel uses `window` to detect a browser environment + Object.assign(globalThis, { + window: globalThis, + }); +} diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/span-exporter.ts b/packages/interceptors-opentelemetry-v2/src/workflow/span-exporter.ts new file mode 100644 index 000000000..ddbd60e6d --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/span-exporter.ts @@ -0,0 +1,59 @@ +import * as tracing from '@opentelemetry/sdk-trace-base'; +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; +import { OpenTelemetrySinks, SerializableSpan, SerializableSpanContext } from './definitions'; +import { proxySinks } from './workflow-imports'; + +export class SpanExporter implements tracing.SpanExporter { + private exporter?: OpenTelemetrySinks['exporter']; + + public export(spans: tracing.ReadableSpan[], resultCallback: (result: ExportResult) => void): void { + if (!this.exporter) { + this.exporter = proxySinks().exporter; + } + this.exporter.export(spans.map((span) => this.makeSerializable(span))); + resultCallback({ code: ExportResultCode.SUCCESS }); + } + + public async shutdown(): Promise { + // Nothing to shut down + } + + public makeSerializable(span: tracing.ReadableSpan): SerializableSpan { + const { traceState, ...restSpanContext } = span.spanContext(); + // Serialize traceState to a string because TraceState objects lose their + // prototype methods when crossing the V8 isolate boundary. + // See: https://github.com/temporalio/sdk-typescript/issues/1738 + const serializableSpanContext: SerializableSpanContext = { + traceState: traceState?.serialize(), + ...restSpanContext, + }; + + let serializableParentSpanContext: SerializableSpanContext | undefined; + if (span.parentSpanContext) { + const { traceState: parentTraceState, ...restParentSpanContext } = span.parentSpanContext; + serializableParentSpanContext = { + traceState: parentTraceState?.serialize(), + ...restParentSpanContext, + }; + } + + return { + name: span.name, + kind: span.kind, + spanContext: serializableSpanContext, + parentSpanContext: serializableParentSpanContext, + startTime: span.startTime, + endTime: span.endTime, + status: span.status, + attributes: span.attributes, + links: span.links, + events: span.events, + duration: span.duration, + ended: span.ended, + droppedAttributesCount: span.droppedAttributesCount, + droppedEventsCount: span.droppedEventsCount, + droppedLinksCount: span.droppedLinksCount, + instrumentationScope: span.instrumentationScope, + }; + } +} diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/workflow-imports-impl.ts b/packages/interceptors-opentelemetry-v2/src/workflow/workflow-imports-impl.ts new file mode 100644 index 000000000..838dec46e --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/workflow-imports-impl.ts @@ -0,0 +1,8 @@ +/** + * Real workflow imports for otel interceptors. + * This replaces the stub via webpack alias when bundled. + * + * @module + */ +export { inWorkflowContext, proxySinks, workflowInfo, AsyncLocalStorage, ContinueAsNew } from '@temporalio/workflow'; +export { alea, type RNG } from '@temporalio/workflow/lib/alea'; diff --git a/packages/interceptors-opentelemetry-v2/src/workflow/workflow-imports.ts b/packages/interceptors-opentelemetry-v2/src/workflow/workflow-imports.ts new file mode 100644 index 000000000..b77d32e87 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/src/workflow/workflow-imports.ts @@ -0,0 +1,41 @@ +/** + * Workflow imports stub module. + * + * This module provides stubs for workflow functionality needed by interceptors. + * When bundled by the workflow bundler, this is replaced with the real + * implementation via NormalModuleReplacementPlugin. + * + * @module + */ +import type { + inWorkflowContext as inWorkflowContextT, + workflowInfo as workflowInfoT, + proxySinks as proxySinksT, + AsyncLocalStorage as AsyncLocalStorageT, + ContinueAsNew as ContinueAsNewT, +} from '@temporalio/workflow'; +import type { alea as aleaT, RNG } from '@temporalio/workflow/lib/alea'; + +import { IllegalStateError } from '@temporalio/common'; + +// always returns false since if using this implementation, we are outside of workflow context +export const inWorkflowContext: typeof inWorkflowContextT = () => false; + +// All of the following stubs will throw if used +export const workflowInfo: typeof workflowInfoT = () => { + throw new IllegalStateError('Workflow.workflowInfo(...) may only be used from a Workflow Execution.'); +}; + +export const ContinueAsNew = class ContinueAsNew {} as unknown as typeof ContinueAsNewT; + +export const AsyncLocalStorage = class AsyncLocalStorage {} as unknown as typeof AsyncLocalStorageT; + +export const proxySinks: typeof proxySinksT = () => { + throw new IllegalStateError('Proxied sinks functions may only be used from a Workflow Execution.'); +}; + +export type { RNG }; + +export const alea: typeof aleaT = () => { + throw new IllegalStateError('alea may only be used from a Workflow Execution.'); +}; diff --git a/packages/interceptors-opentelemetry-v2/tsconfig.json b/packages/interceptors-opentelemetry-v2/tsconfig.json new file mode 100644 index 000000000..61d3ef5c7 --- /dev/null +++ b/packages/interceptors-opentelemetry-v2/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src" + }, + "references": [ + { "path": "../activity" }, + { "path": "../client" }, + { "path": "../common" }, + { "path": "../plugin" }, + { "path": "../proto" }, + { "path": "../testing" }, + { "path": "../worker" }, + { "path": "../workflow" } + ], + "include": ["./src/**/*.ts"] +} diff --git a/packages/worker/src/workflow/bundler.ts b/packages/worker/src/workflow/bundler.ts index d869df381..bbb71296c 100644 --- a/packages/worker/src/workflow/bundler.ts +++ b/packages/worker/src/workflow/bundler.ts @@ -223,7 +223,7 @@ exports.importInterceptors = function importInterceptors() { // Outside of workflow context the module used only contains stubs that will error if they are used. // When creating the workflow bundle we replace the module containing the stubs with a module that reexports the actual implementations. new NormalModuleReplacementPlugin( - /[\\/](?:@temporalio|packages)[\\/]interceptors-opentelemetry[\\/](?:src|lib)[\\/]workflow[\\/]workflow-imports\.[jt]s$/, + /[\\/](?:@temporalio|packages)[\\/]interceptors-opentelemetry(?:-v2)?[\\/](?:src|lib)[\\/]workflow[\\/]workflow-imports\.[jt]s$/, './workflow-imports-impl.js' ), ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 397d7fa9d..5103c4744 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -465,6 +465,64 @@ importers: specifier: ^11.1.0 version: 11.1.0 + packages/interceptors-opentelemetry-v2: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/core': + specifier: ^2.2.0 + version: 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.2.0 + version: 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^2.2.0 + version: 2.5.1(@opentelemetry/api@1.9.0) + '@temporalio/plugin': + specifier: workspace:* + version: link:../plugin + devDependencies: + '@opentelemetry/exporter-trace-otlp-grpc': + specifier: ^0.208.0 + version: 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.208.0 + version: 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.38.0 + version: 1.39.0 + '@temporalio/activity': + specifier: workspace:* + version: link:../activity + '@temporalio/client': + specifier: workspace:* + version: link:../client + '@temporalio/common': + specifier: workspace:* + version: link:../common + '@temporalio/proto': + specifier: workspace:* + version: link:../proto + '@temporalio/test-helpers': + specifier: workspace:* + version: link:../test-helpers + '@temporalio/testing': + specifier: workspace:* + version: link:../testing + '@temporalio/worker': + specifier: workspace:* + version: link:../worker + '@temporalio/workflow': + specifier: workspace:* + version: link:../workflow + ava: + specifier: ^5.3.1 + version: 5.3.1 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + packages/meta: dependencies: '@temporalio/activity': @@ -1344,6 +1402,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -1358,24 +1420,102 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/context-async-hooks@2.2.0': + resolution: {integrity: sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@1.25.1': resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0': + resolution: {integrity: sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.208.0': + resolution: {integrity: sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0': + resolution: {integrity: sha512-YbEnk7jjYmvhIwp2xJGkEvdgnayrA2QSr28R1LR1klDPvCxsoQPxE6TokDbQpoCEhD3+KmJVEXfb4EeEQxjymg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.208.0': + resolution: {integrity: sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.208.0': + resolution: {integrity: sha512-CvvVD5kRDmRB/uSMalvEF6kiamY02pB46YAqclHtfjJccNZFxbkkXkMMmcJ7NgBFa5THmQBNVQ2AHyX29nRxOw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.208.0': + resolution: {integrity: sha512-Rgws8GfIfq2iNWCD3G1dTD9xwYsCof1+tc5S5X0Ahdb5CrAPE+k5P70XCWHqrFFurVCcKaHLJ/6DjIBHWVfLiw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.208.0': + resolution: {integrity: sha512-E/eNdcqVUTAT7BC+e8VOw/krqb+5rjzYkztMZ/o+eyJl+iEY6PfczPXpwWuICwvsm0SIhBoh9hmYED5Vh5RwIw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.52.1': resolution: {integrity: sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/exporter-trace-otlp-http@0.208.0': + resolution: {integrity: sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.52.1': resolution: {integrity: sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/exporter-trace-otlp-proto@0.208.0': + resolution: {integrity: sha512-q844Jc3ApkZVdWYd5OAl+an3n1XXf3RWHa3Zgmnhw3HpsM3VluEKHckUUEqHPzbwDUx2lhPRVkqK7LsJ/CbDzA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-proto@0.52.1': resolution: {integrity: sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==} engines: {node: '>=14'} @@ -1388,24 +1528,54 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/exporter-zipkin@2.2.0': + resolution: {integrity: sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation@0.208.0': + resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.52.1': resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.52.1': resolution: {integrity: sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/otlp-grpc-exporter-base@0.208.0': + resolution: {integrity: sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.52.1': resolution: {integrity: sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.52.1': resolution: {integrity: sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==} engines: {node: '>=14'} @@ -1418,18 +1588,48 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-b3@2.2.0': + resolution: {integrity: sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@1.25.1': resolution: {integrity: sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@2.2.0': + resolution: {integrity: sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@1.25.1': resolution: {integrity: sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.52.1': resolution: {integrity: sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==} engines: {node: '>=14'} @@ -1442,6 +1642,18 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.2.0': + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.208.0': + resolution: {integrity: sha512-pbAqpZ7zTMFuTf3YecYsecsto/mheuvnK2a/jgstsE5ynWotBjgF5bnz5500W9Xl2LeUfg04WMt63TWtAgzRMw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-node@0.52.1': resolution: {integrity: sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==} engines: {node: '>=14'} @@ -1454,16 +1666,38 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.5.1': + resolution: {integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-node@1.25.1': resolution: {integrity: sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace-node@2.2.0': + resolution: {integrity: sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.25.1': resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} + engines: {node: '>=14'} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2350,6 +2584,9 @@ packages: cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} engines: {node: '>=12'} @@ -3242,6 +3479,9 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -3812,6 +4052,9 @@ packages: module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -4223,6 +4466,10 @@ packages: resolution: {integrity: sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==} engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + requizzle@0.2.4: resolution: {integrity: sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==} @@ -5365,6 +5612,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.16.0 + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -5375,11 +5626,104 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.12.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.12.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.12.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.12.4 @@ -5390,6 +5734,15 @@ snapshots: '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5399,6 +5752,15 @@ snapshots: '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5416,6 +5778,23 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/exporter-zipkin@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5428,12 +5807,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.12.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.12.4 @@ -5442,6 +5835,17 @@ snapshots: '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.1 + '@opentelemetry/otlp-transformer@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5458,17 +5862,46 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5483,6 +5916,40 @@ snapshots: '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) lodash.merge: 4.6.2 + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + '@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5509,6 +5976,20 @@ snapshots: '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace-node@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -5519,8 +6000,17 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) semver: 7.7.3 + '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions@1.25.1': {} + '@opentelemetry/semantic-conventions@1.39.0': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -6597,6 +7087,8 @@ snapshots: cjs-module-lexer@1.2.3: {} + cjs-module-lexer@2.2.0: {} + clean-stack@4.2.0: dependencies: escape-string-regexp: 5.0.0 @@ -7668,6 +8160,13 @@ snapshots: cjs-module-lexer: 1.2.3 module-details-from-path: 1.0.3 + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} indent-string@5.0.0: {} @@ -8187,6 +8686,8 @@ snapshots: module-details-from-path@1.0.3: {} + module-details-from-path@1.0.4: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -8611,6 +9112,13 @@ snapshots: transitivePeerDependencies: - supports-color + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.3 + transitivePeerDependencies: + - supports-color + requizzle@0.2.4: dependencies: lodash: 4.17.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b8c5bbea9..222f39f9a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ packages: - packages/create-project - packages/envconfig - packages/interceptors-opentelemetry + - packages/interceptors-opentelemetry-v2 - packages/meta - packages/nexus - packages/nyc-test-coverage