Skip to content

Commit 4ed4583

Browse files
authored
Merge branch 'main' into chore/bump-python-1.31.0
2 parents 407ae43 + 06815d5 commit 4ed4583

21 files changed

Lines changed: 1534 additions & 2 deletions

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
* @temporalio/sdk
2-
/langgraph_plugin/ @temporalio/sdk @temporalio/ai-sdk
32

43
# SDK & Nexus own the README, pyproject.toml, and uv.lock
54
/README.md @temporalio/sdk @temporalio/nexus
@@ -16,11 +15,13 @@
1615
# The AI SDK team owns the AI integration samples and their tests. We add
1716
# @temporalio/sdk too, so the SDK team can continue to manage repo-wide concerns.
1817
/google_adk_agents/ @temporalio/sdk @temporalio/ai-sdk
18+
/langfuse_tracing/ @temporalio/sdk @temporalio/ai-sdk
1919
/langgraph_plugin/ @temporalio/sdk @temporalio/ai-sdk
2020
/langsmith_tracing/ @temporalio/sdk @temporalio/ai-sdk
2121
/openai_agents/ @temporalio/sdk @temporalio/ai-sdk
2222
/strands_plugin/ @temporalio/sdk @temporalio/ai-sdk
2323
/tests/google_adk_agents/ @temporalio/sdk @temporalio/ai-sdk
24+
/tests/langfuse_tracing/ @temporalio/sdk @temporalio/ai-sdk
2425
/tests/langgraph_plugin/ @temporalio/sdk @temporalio/ai-sdk
2526
/tests/langsmith_tracing/ @temporalio/sdk @temporalio/ai-sdk
2627
/tests/strands_plugin/ @temporalio/sdk @temporalio/ai-sdk

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ __pycache__
77
.mypy_cache/
88
**/client.key
99
**/client.pem
10+
.env

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Some examples require extra dependencies. See each sample's directory for specif
7777
* [hello_standalone_nexus](hello_standalone_nexus) - Use Nexus Operations without using a workflow.
7878
* [hello_standalone_activity](hello_standalone_activity) - Use activities without using a workflow.
7979
* [lambda_worker](lambda_worker) - Run a Temporal Worker inside an AWS Lambda function.
80+
* [langfuse_tracing](langfuse_tracing) - Trace Temporal workflows in Langfuse with the OpenTelemetry plugin and OTLP export.
8081
* [langgraph_plugin](langgraph_plugin) - Run LangGraph workflows as durable Temporal workflows (Graph API and Functional API).
8182
* [langsmith_tracing](langsmith_tracing) - Trace Temporal workflows with LangSmith via the LangSmith plugin.
8283
* [message_passing/introduction](message_passing/introduction/) - Introduction to queries, signals, and updates.

langfuse_tracing/.env.example

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copy to .env and adjust. Load with: set -a; source langfuse_tracing/.env; set +a
2+
3+
# Langfuse — these defaults match the headless-init values baked into
4+
# langfuse_tracing/langfuse/docker-compose.yml (local demo stack).
5+
LANGFUSE_HOST=http://localhost:3000
6+
LANGFUSE_PUBLIC_KEY=pk-lf-temporal-demo-0000
7+
LANGFUSE_SECRET_KEY=sk-lf-temporal-demo-0000
8+
# Used only for the trace link the starter prints; set to your project ID
9+
# when pointing at your own Langfuse instance or Langfuse Cloud.
10+
LANGFUSE_PROJECT_ID=langfuse-tracing-demo
11+
# Reported as the Langfuse user ID on each trace by the starter.
12+
LANGFUSE_DEMO_USER=demo-user
13+
14+
# LLM — any OpenAI-compatible endpoint works.
15+
OPENAI_API_KEY=sk-...
16+
MODEL_CLASSIFY=gpt-4o-mini
17+
MODEL_DRAFT=gpt-4o-mini
18+
# To use a local OpenAI-compatible gateway (e.g. a LiteLLM proxy) instead:
19+
# OPENAI_BASE_URL=http://localhost:4000/v1
20+
# OPENAI_API_KEY=<gateway key>
21+
# MODEL_CLASSIFY=<gateway model alias>
22+
# MODEL_DRAFT=<gateway model alias>
23+
24+
# LLM span instrumentation flavor: openinference (default) or openai-v2.
25+
# See README for the trade-offs.
26+
LLM_INSTRUMENTATION=openinference
27+
# Required only for LLM_INSTRUMENTATION=openai-v2 content capture:
28+
# OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
29+
# OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only

langfuse_tracing/README.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Langfuse Tracing
2+
3+
This sample shows the recommended way to get Temporal workflow traces into
4+
[Langfuse](https://langfuse.com/): Temporal's
5+
[`OpenTelemetryPlugin`](https://python.temporal.io/temporalio.contrib.opentelemetry.html)
6+
plus a standard OTLP/HTTP exporter pointed at Langfuse's native OpenTelemetry
7+
endpoint. No Langfuse SDK or Langfuse-specific plugin is involved, workflow
8+
code stays deterministic and sandboxed, and traces are correctly nested,
9+
correctly typed, and duplicate-free across replay and worker restarts.
10+
11+
Contents:
12+
13+
- **[ticket_triage/](ticket_triage/)** — the recommended pattern: an LLM
14+
ticket-triage workflow (two LLM activities, one plain activity, one human
15+
approval delivered as a workflow update).
16+
- **[verify_trace.py](verify_trace.py)** — checks a trace via the Langfuse
17+
public API: whole-tree equality, observation types, token usage, and
18+
no-duplicates.
19+
- **[langfuse/docker-compose.yml](langfuse/docker-compose.yml)** — pinned
20+
self-hosted Langfuse with org/project/API keys provisioned headlessly.
21+
- **[telemetry.py](telemetry.py)** — the OpenTelemetry wiring (replay-safe
22+
tracer provider, OTLP exporter with Langfuse auth, LLM instrumentation).
23+
24+
## Prerequisites
25+
26+
- Docker (for Langfuse), a local Temporal server
27+
(`temporal server start-dev`), and `uv`.
28+
- An OpenAI-compatible LLM endpoint: either a real `OPENAI_API_KEY`, or any
29+
OpenAI-compatible gateway via `OPENAI_BASE_URL`.
30+
31+
## Run it
32+
33+
```bash
34+
# 1. Start Langfuse (first pull takes a few minutes)
35+
cd langfuse_tracing/langfuse
36+
docker compose up -d
37+
curl -sf http://localhost:3000/api/public/health # repeat until {"status":"OK",...}
38+
# UI: http://localhost:3000 — login demo@temporal.io / langfuse-demo-pw-1
39+
40+
# 2. Install dependencies and set environment (repo root)
41+
cd ../..
42+
uv sync --group langfuse-tracing
43+
cp langfuse_tracing/.env.example langfuse_tracing/.env # edit the LLM settings
44+
set -a; source langfuse_tracing/.env; set +a
45+
46+
# 3. Run the sample (two terminals, same environment)
47+
uv run python -m langfuse_tracing.ticket_triage.worker
48+
uv run python -m langfuse_tracing.ticket_triage.starter
49+
50+
# 4. Verify the trace through the Langfuse API (uses the printed trace ID)
51+
uv run python -m langfuse_tracing.verify_trace --trace-id <printed trace id>
52+
```
53+
54+
The starter prints a direct link to the trace in the Langfuse UI. You should
55+
see one trace shaped like this (types as Langfuse derives them):
56+
57+
```
58+
ticket-triage SPAN (root; session/user/tags)
59+
├─ StartWorkflow:TicketTriageWorkflow SPAN
60+
│ └─ RunWorkflow:TicketTriageWorkflow SPAN
61+
│ ├─ triage SPAN (custom span from workflow code)
62+
│ │ ├─ StartActivity:classify_ticket → RunActivity:classify_ticket
63+
│ │ │ └─ ChatCompletion GENERATION (model, tokens, cost)
64+
│ │ └─ StartActivity:lookup_account → RunActivity:lookup_account
65+
│ └─ StartActivity:draft_reply → RunActivity:draft_reply
66+
│ └─ ChatCompletion GENERATION
67+
└─ StartWorkflowUpdate:approve SPAN
68+
├─ ValidateUpdate:approve SPAN
69+
└─ HandleUpdate:approve SPAN
70+
```
71+
72+
## Prove the replay-safety claims
73+
74+
Durable execution means workflow code re-executes (replays) on worker
75+
restarts and cache evictions. These two experiments show tracing is
76+
unaffected — each run still verifies cleanly with the same tree shape and no
77+
duplicate observations:
78+
79+
```bash
80+
# Replay stress: disable the workflow cache so EVERY workflow task replays
81+
# the workflow from the start of history.
82+
uv run python -m langfuse_tracing.ticket_triage.worker --replay-stress
83+
uv run python -m langfuse_tracing.ticket_triage.starter
84+
uv run python -m langfuse_tracing.verify_trace --trace-id <printed trace id>
85+
86+
# Worker restart mid-workflow: the starter waits 20s before sending the
87+
# approval. Give the triage activities a few seconds to finish, then kill the
88+
# worker while the workflow durably awaits approval; start a new worker and
89+
# watch the workflow (and its trace) complete cleanly.
90+
uv run python -m langfuse_tracing.ticket_triage.starter --pause-before-approval 20
91+
# ... after ~5s, ctrl+c the worker, then start it again in another terminal
92+
uv run python -m langfuse_tracing.verify_trace --trace-id <printed trace id>
93+
```
94+
95+
## Where spans come from
96+
97+
| Span | Emitted by | Where it runs |
98+
|---|---|---|
99+
| `ticket-triage` (root) + `langfuse.*` trace attributes | starter code | starter |
100+
| `StartWorkflow:*`, `StartWorkflowUpdate:*` | `OpenTelemetryPlugin` | starter (client side) |
101+
| `RunWorkflow:*`, `StartActivity:*`, `ValidateUpdate:*`, `HandleUpdate:*` | `OpenTelemetryPlugin` | worker (workflow) |
102+
| `triage` | plain OpenTelemetry API in workflow code | worker (workflow) |
103+
| `RunActivity:*` | `OpenTelemetryPlugin` | worker (activity) |
104+
| `ChatCompletion` / `chat <model>` GENERATIONs | OpenAI auto-instrumentation | worker (activity) |
105+
106+
## Where tracing works
107+
108+
| Location | Works? | Notes |
109+
|---|---|---|
110+
| Activity bodies || Plain OpenTelemetry + any auto-instrumentation, no restrictions. This is where LLM calls (and their GENERATION spans) belong. |
111+
| Workflow bodies || Plain OpenTelemetry APIs are replay-safe under the plugin: deterministic span IDs, no re-export on replay. Spans export when they end; the `RunWorkflow` span exports when the run completes. |
112+
| Signal/query/update handlers || Handled by the plugin automatically (`HandleUpdate:*` etc.). |
113+
| Client / starter code || Standard OpenTelemetry; put Langfuse trace-level attributes on your root span. |
114+
115+
## LLM instrumentation flavors
116+
117+
`LLM_INSTRUMENTATION` selects how OpenAI calls are instrumented (both are
118+
verified against Langfuse by this sample):
119+
120+
| | `openinference` (default) | `openai-v2` |
121+
|---|---|---|
122+
| Package | `openinference-instrumentation-openai` | `opentelemetry-instrumentation-openai-v2` |
123+
| Semantic conventions | OpenInference | OpenTelemetry GenAI (`gen_ai.*`) |
124+
| GENERATION type, model, token usage, cost |||
125+
| Prompt/completion content | ✅ by default | Requires `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` and `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only` |
126+
| GENERATION span name | `ChatCompletion` | `chat <model>` |
127+
128+
## Operational notes
129+
130+
- Langfuse's OTLP endpoint is HTTP-only — this sample uses
131+
`opentelemetry-exporter-otlp-proto-http` (the gRPC exporter will not work).
132+
- Short-lived processes must flush: the starter and worker call
133+
`force_flush()` on exit (see `telemetry.py`).
134+
- Use a fresh workflow ID per run: the starter reports it as the Langfuse
135+
session ID, so each run groups cleanly in the Sessions view. (The trace
136+
itself is keyed by the starter's root span, which is new on every run.)
137+
- `OTEL_SDK_DISABLED=true` turns off export without code changes.
138+
- Ingestion is asynchronous; `verify_trace.py` polls until the trace is
139+
stable.
140+
141+
## Tests
142+
143+
`tests/langfuse_tracing/` runs without Langfuse, Docker, or an LLM: mocked
144+
activities, an in-memory span exporter, a worker with the workflow cache
145+
disabled, whole-tree span assertions, and a `Replayer` pass asserting that
146+
replaying the finished workflow's history emits zero new spans.
147+
148+
```bash
149+
uv run --group langfuse-tracing pytest tests/langfuse_tracing -v
150+
```
151+
152+
## Using this outside samples-python
153+
154+
The sample is self-contained: copy the `langfuse_tracing/` directory, change
155+
the absolute imports (`langfuse_tracing.ticket_triage.activities`
156+
`ticket_triage.activities` or similar), and install the dependencies listed
157+
under `langfuse-tracing` in this repo's `pyproject.toml`.

langfuse_tracing/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)