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.
This repo contains both the plugin source and a runnable demo so the integration can be exercised end-to-end. When the plugin is published to npm, src/plugin/** will be extracted to a standalone package.
src/
├── plugin/ # the integration — this is what becomes @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
│ ├── exporters.ts # OTLP HTTP exporters (logs + traces) + SanitizingSpanExporter
│ ├── version.ts # PLUGIN_VERSION constant
│ └── types.ts # ParseableEventRecord schema
│
├── activities.ts # demo: greet (success), chargeCard (always fails)
├── workflows.ts # demo: example, failingExample, userEventExample, parentExample
├── worker.ts # demo worker — wires up ParseablePlugin
├── client.ts # demo: triggers happy-path workflow
├── fail-client.ts # demo: triggers failing workflow
├── event-client.ts # demo: triggers user-event workflow
├── parent-client.ts # demo: triggers parent → child workflow
└── mocha/
└── replay-safety.test.ts # asserts workflow sink fires zero times during history replay
┌───────────────────┐
│ 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
npm run start.watchWorker connects to Temporal and starts polling the hello-world task queue. Auto-restarts on src/ changes via nodemon.
By default the worker ships logs and traces to http://anton:8010 with credentials admin:admin. Override via env:
PARSEABLE_URL=https://your-parseable-host \
PARSEABLE_USERNAME=youruser \
PARSEABLE_PASSWORD=yourpass \
npm run start.watchTerminal 3 — Client (run on demand):
npm run workflow # success path: greet activity
npm run workflow:fail # failure path: chargeCard with 3-retry policy
npm run workflow:event # user-event path: workflow emits custom domain events via workflowEvent()
npm run 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 src/mocha/replay-safety.test.ts # run only replay-safetyThe replay-safety test:
- Runs a real workflow with the plugin, captures emitted records.
- Fetches the workflow's history from the Temporal server.
- Replays the history via
Worker.runReplayHistory()with a fresh plugin instance. - Asserts the replay emits zero workflow records (sink correctly skipped) and zero activity records (activities don't re-execute on replay).
This requires a running Temporal dev server (temporal server start-dev) — the test 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.