Temporal middleware plugin that ships workflow and activity execution events to Parseable as OpenTelemetry logs and traces.
End-user integration guide: INTEGRATION.md - install, configure, schema reference, query examples.
Submission status: STATUS.md - what's done and what's pending for the Temporal AI Partner Program submission.
The plugin emits structured logs (workflow/activity start, complete, fail, retry, duration) into a Parseable log stream, alongside OpenTelemetry traces (RunWorkflow:*, StartActivity:*, RunActivity:*) into a Parseable trace stream. Users get a flat queryable schema for analytics plus a waterfall view of workflow execution.
This README is the developer-facing landing page for the repo (architecture, repo layout, how to run the demo, caveats). For end-user consumption see INTEGRATION.md.
src/ is the publishable plugin package (@parseable/temporal). examples/ contains a runnable demo worker and clients. test/ contains mocha tests against the plugin using the demo workflows as fixtures.
src/ # the integration - published as @parseable/temporal
├── index.ts # ParseablePlugin class (extends SimplePlugin)
├── activity-interceptor.ts # ActivityInbound interceptor (worker process)
├── workflow-interceptor.ts # WorkflowInbound + Outbound interceptors (workflow isolate, replay-safe via sinks)
├── workflow.ts # public workflowEvent() helper (consumer imports from @parseable/temporal/workflow)
├── exporters.ts # OTLP HTTP exporters (logs + traces) + SanitizingSpanExporter
├── version.ts # PLUGIN_VERSION constant
└── types.ts # ParseableEventRecord schema
examples/ # runnable demo - not published
├── activities.ts # greet (success), chargeCard (always fails)
├── workflows.ts # example, failingExample, userEventExample, parentExample, signalEventExample, queryUpdateExample, childSignalParent, continueAsNewExample, updateFailureExample, parentFailingExample
├── worker.ts # demo worker wired up with ParseablePlugin
├── client.ts # triggers happy-path workflow
├── fail-client.ts # triggers failing workflow
├── event-client.ts # triggers user-event workflow
└── parent-client.ts # triggers parent → child workflow
test/ # mocha tests (use examples/* as fixtures)
├── replay-safety.test.ts # interceptor coverage: workflow/activity/signal/query/update/child_workflow/continue_as_new + failure paths, asserts zero emissions during history replay
├── activities.test.ts # unit test for greet activity
├── workflows.test.ts # workflow integration test (requires TestWorkflowEnvironment)
└── workflows-mocks.test.ts # workflow integration test with mocked activities
┌───────────────────┐
│ Temporal Server │
│ (localhost:7233) │
└─────────┬─────────┘
│ gRPC
┌───────────────┴───────────────┐
│ Worker │
│ │
│ ┌─────────────────────────┐ │
│ │ Workflow V8 isolate │ │ ← replay-safe; cannot do I/O
│ │ │ │
│ │ WorkflowInbound + │ │
│ │ WorkflowOutbound │ │
│ │ interceptors │ │
│ │ │ │
│ │ proxySinks ──────┐ │ │
│ └───────────────────┼─────┘ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Sink consumer (worker proc)│
│ │ enriches with service_name│
│ └──────────────┬───────────┘ │
│ │ │
│ ┌──────────────▼───────────┐ │
│ │ ActivityInbound │ │
│ │ interceptor │ │
│ └──────────────┬───────────┘ │
│ │ │
│ ┌──────────────▼───────────┐ │
│ │ emit(record) │ │
│ │ → OTel Logger │ │
│ │ → BatchLogRecordProc │ │
│ │ → OTLPLogExporter │ │
│ └──────────────┬───────────┘ │
│ │ │
│ ┌──────────────┴────────────┐│
│ │ Temporal OpenTelemetryPlug││
│ │ → BatchSpanProcessor ││
│ │ → SanitizingSpanExporter ││
│ │ → OTLPTraceExporter ││
│ └──────────────┬────────────┘│
└─────────────────┼─────────────┘
│ HTTPS
┌─────────▼──────────┐
│ Parseable │
│ /v1/logs (logs) │
│ /v1/traces (spans)│
└────────────────────┘
- Replay safety. Workflow events are emitted via
proxySinkswithcallDuringReplay: false. When Temporal replays a workflow's history (after a worker crash, cache eviction, or manual replay), the sink is skipped - no duplicate logs or spans. Verified bysrc/mocha/replay-safety.test.ts. - Two layers, one plugin.
ParseablePluginextends@temporalio/plugin'sSimplePluginand internally composes Temporal's officialOpenTelemetryPluginfor trace emission. Logs are emitted from our own interceptors directly to OTel's log API. Both flow into Parseable through OTLP/HTTP. SanitizingSpanExporter. Temporal's OTel plugin emits spans with nested objects,Dateinstances, andundefinedfields as attributes. OTLP attribute values are restricted to primitives or arrays of primitives, so Parseable's strict OTLP parser rejects the raw payload with400 Invalid data for Value. The sanitizer wraps the trace exporter and flattens nested objects to JSON strings,Dateto ISO, and dropsundefineds before serialization.- OTel pinned to 1.x. Temporal's
OpenTelemetryPluginpins@opentelemetry/sdk-trace-base@^1.25.1. The OTel ecosystem has split between 1.x (mature) and 2.x (newer). We ride the 1.x line -sdk-trace-base@1.30.x,resources@1.30.x,exporter-{logs,trace}-otlp-http@0.57.x,sdk-logs@0.57.x- until Temporal moves.
- Node.js 20+
- Temporal CLI (
brew install temporalon macOS) - A Parseable instance reachable on the network. For dev: a local instance with default credentials.
Terminal 1 - Temporal dev server:
temporal server start-devRuns on localhost:7233 (gRPC) and http://localhost:8233 (UI).
Terminal 2 - Worker:
npm install
PARSEABLE_URL=https://your-parseable-host \
PARSEABLE_USERNAME=youruser \
PARSEABLE_PASSWORD=yourpass \
npm run examples:worker.watchPARSEABLE_URL is required - the worker refuses to start without it. Username/password default to admin/admin if unset (matching a default Parseable dev install). The worker connects to Temporal at localhost:7233, polls the hello-world task queue, and auto-restarts on src/ or examples/ changes via nodemon.
Terminal 3 - Client (run on demand):
npm run examples:workflow # success path: greet activity
npm run examples:workflow:fail # failure path: chargeCard with 3-retry policy
npm run examples:workflow:event # user-event path: workflow emits custom domain events via workflowEvent()
npm run examples:workflow:parent # parent → child workflow path: exercises the outbound interceptorAfter running, check Parseable at ${PARSEABLE_URL}:
- Stream
temporal-logs- workflow/activity records with attributesworkflow_id,activity_name,attempt,status,duration_ms,service_name, etc. - Stream
temporal-traces- spansRunWorkflow:example,StartActivity:greet,RunActivity:greet.
npm test # runs all mocha tests
npx mocha --require ts-node/register test/replay-safety.test.ts # run only replay-safetyThe replay-safety suite exercises every interceptor path and asserts that replay re-emits zero records (sinks correctly skipped via callDuringReplay: false, activities and queries don't re-execute on replay):
| Test | Effects covered | Live invariants asserted |
|---|---|---|
example + greet |
workflow inbound, activity | 2 workflow records (started+completed), 2 activity records |
signalEventExample |
handleSignal, workflowEvent |
2 signal records, 2 user_event records |
queryUpdateExample |
handleQuery, handleUpdate |
2 query records, 2 update records |
childSignalParent |
startChildWorkflowExecution, signalWorkflow outbound |
child_workflow start+complete (after child finishes, not at start RPC), 2 outbound signal records |
continueAsNewExample |
continueAsNew outbound |
single record with status: 'started' only |
failingExample |
retries + workflow failure | 3 activity started + 3 activity failed (with attempt 1/2/3 and duration_ms/error), 1 failed workflow record |
updateFailureExample |
update handler ApplicationFailure |
1 update started + 1 update failed (no completed), error propagated |
parentFailingExample |
child-workflow run-time failure | outbound child_workflow started + failed, parent workflow failed |
Each test fetches the workflow history, replays it via Worker.runReplayHistory() with a fresh plugin, and re-asserts that the record stream is empty for the effect under test.
This requires a running Temporal dev server (temporal server start-dev) - the suite connects to localhost:7233 rather than spinning up an in-process test server.
- OTel ecosystem version split. We pin to OTel 1.x because Temporal's plugin does. When Temporal moves to 2.x, we follow.
- Empty-body warning on OTLP success. Parseable returns HTTP 200 with an empty body for accepted OTLP payloads. OTel's deserializer logs
Export succeeded but could not deserialize response - is the response specification compliant?- this is benign and only visible atDiagLogLevel.DEBUGor above. - Span attribute sanitization. The
SanitizingSpanExporteris a workaround for an interop gap between Temporal's OTel plugin (emits non-primitive span attributes) and strict OTLP parsers (require primitive attribute values). Without it, Parseable returns400 Invalid data for Value. - Throw
ApplicationFailurefor graceful handler failures. Signal/update handlers that throw a plainErrorare treated by Temporal as a workflow-task failure: the task is retried until it succeeds, and the plugin will emit onestarted+failedrecord pair per retry. To fail an update (or any handler) cleanly without retry storms, throwApplicationFailure.create({ message, nonRetryable: true })from@temporalio/workflow. The interceptor then records exactly onefailedevent and the error propagates to the client as an update failure rather than a task failure. child_workflowcompletion is tracked from the child, not the start RPC. The outbound interceptor wraps the result promise returned bynext(input)sostatus: 'completed'(orfailed) fires when the child actually finishes - not when the start call returns. Start-time RPC errors and run-time child failures are reported with distinctfailedrecords.