1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! Seeing what the engine is doing: spans, their attributes, and how to
//! export them.
//!
//! durare instruments itself with the [`tracing`] facade — the engine emits
//! structured **spans** around every workflow and step execution, plus the
//! log events it always emitted. With no subscriber installed all of it
//! compiles to near-nothing; install one and you get a live execution tree:
//!
//! ```text
//! workflow{otel.name=checkout dbos.operation.workflow_id=order-1001 …}
//! ├── step{otel.name=reserve_inventory dbos.step.id=0 …}
//! ├── step{otel.name=charge_card dbos.step.id=1 …}
//! └── workflow{otel.name=dispatch_order …} // a child workflow
//! ```
//!
//! # The spans
//!
//! **Workflow span** — one per workflow execution, covering the body *and*
//! the terminal status write. Every path that runs a workflow gets one:
//! direct starts, queued runs claimed by a dispatcher, scheduled ticks,
//! child workflows, and recovery re-runs.
//!
//! | Field | Value |
//! |---|---|
//! | `otel.name` | the workflow's registered name |
//! | `dbos.operation.type` | `workflow` |
//! | `dbos.operation.workflow_id` | the workflow id |
//! | `dbos.application.version` | the engine's application version |
//! | `dbos.executor.id` | the executor running it |
//! | `dbos.queue.name` | the queue it was claimed from (queued runs only) |
//! | `dbos.user.name`, `dbos.user.roles`, `dbos.user.assumed_role` | the [`AuthContext`], when one is attached |
//! | `dbos.workflow.status` | recorded at completion: `SUCCESS`, `ERROR`, or `CANCELLED` |
//! | `otel.status_code` | recorded at completion: `OK` on success, `ERROR` otherwise |
//!
//! A body panic records `otel.status_code = ERROR` but **no**
//! `dbos.workflow.status`: the row keeps its non-terminal state so recovery
//! can re-run it (see [`durability`](crate::durability)).
//!
//! **Step span** — one per durable operation inside a workflow: a
//! [`step`](crate::DurableContext::step) /
//! [`step_with`](crate::DurableContext::step_with) call (all retry attempts
//! included) or a [`transaction`](crate::DurableContext::transaction).
//!
//! | Field | Value |
//! |---|---|
//! | `otel.name` | the step name |
//! | `dbos.operation.type` | `step` or `transaction` |
//! | `dbos.operation.workflow_id` | the owning workflow id |
//! | `dbos.step.id` | the checkpoint sequence number (matches [`get_workflow_steps`](crate::DurableEngine::get_workflow_steps)) |
//! | `dbos.step.replayed` | `true` when the recorded outcome was served and the body did not run |
//! | `otel.status_code` | `OK` / `ERROR` by the step's final outcome |
//!
//! `dbos.step.replayed` is the recovery story made visible: after a crash,
//! the re-run's trace shows every already-checkpointed step as a short
//! replayed span and only the frontier step doing real work.
//!
//! # Parenting
//!
//! Step spans nest under their workflow span, and a child workflow's span
//! nests under the parent workflow's — the tree above. A workflow started
//! from an already-instrumented context (say, an HTTP handler span) parents
//! under it. Queued, scheduled, and recovered runs have no live parent and
//! form roots.
//!
//! The attribute names are the `dbos.*` ones the other DBOS SDKs emit in
//! their OpenTelemetry semantic-convention mode, so dashboards keyed on
//! them work across languages.
//!
//! # Subscribing
//!
//! Any [`tracing`] subscriber works. The spans are `INFO`-level under the
//! `durare` crate target:
//!
//! ```
//! # use durare::{DurableEngine, InMemoryProvider, Result, WorkflowOptions};
//! # use std::sync::Arc;
//! # #[durare::workflow]
//! # async fn hello(ctx: durare::DurableContext, name: String) -> Result<String> {
//! # ctx.step("greet", || async { Ok(format!("hello, {name}")) }).await
//! # }
//! # #[tokio::main(flavor = "current_thread")]
//! # async fn main() -> Result<()> {
//! tracing_subscriber::fmt()
//! .with_max_level(tracing::Level::INFO)
//! .init();
//!
//! let engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
//! let handle = engine
//! .start_with(Hello, "world".into(), WorkflowOptions::default())
//! .await?;
//! let out: String = handle.await?; // spans print to stderr as they close
//! # assert_eq!(out, "hello, world");
//! # Ok(())
//! # }
//! ```
//!
//! For OpenTelemetry export, bridge the same spans with
//! [`tracing-opentelemetry`](https://docs.rs/tracing-opentelemetry) — no
//! engine configuration involved; the layer picks the spans up from the
//! subscriber like any other:
//!
//! ```ignore
//! use tracing_subscriber::layer::SubscriberExt;
//!
//! let tracer = /* an opentelemetry SDK tracer wired to your OTLP endpoint */;
//! tracing_subscriber::registry()
//! .with(tracing_opentelemetry::layer().with_tracer(tracer))
//! .init();
//! ```
//!
//! The bridge understands the `otel.name` and `otel.status_code` fields:
//! exported spans are named after the workflow or step (not the literal
//! `workflow` / `step` span names) and carry OTel status. Under a plain
//! [`fmt`](https://docs.rs/tracing-subscriber) subscriber the two appear as
//! ordinary fields.
//!
//! [`tracing`]: https://docs.rs/tracing
//! [`AuthContext`]: crate::AuthContext
// This module is documentation only; the instrumentation lives in the engine
// and context implementations.