durable_workflow/
telemetry.rs

1//! Optional execution spans and application log correlation.
2//!
3//! Applications own providers, exporters, sampling and shutdown. Enabling this
4//! feature installs none of those components. Propagation requires the exact
5//! advertised capability and existing original claim gates. A configured
6//! [PHP Server](https://github.com/durable-workflow/server/blob/2.10.0/docs/observability-context.md)
7//! can advertise support. Missing, malformed or unknown capabilities use
8//! independent task traces. A Server version alone does not enable propagation.
9//! Attempt spans are children of their task.
10//!
11//! See the [setup example](https://github.com/durable-workflow/sdk-rust/blob/4.1.0/README.md#optional-execution-telemetry)
12//! for the optional Cargo feature, application-owned provider/exporter and W3C
13//! propagator configuration. Native Server and Cloud adoption require separate
14//! qualification, as described in the
15//! [runtime support matrix](https://durable-workflow.com/docs/monitoring/#service-mode-tracing).
16
17pub mod carrier;
18
19use std::{
20    borrow::Cow, collections::BTreeMap, future::Future, panic::AssertUnwindSafe, sync::Arc,
21    time::SystemTime,
22};
23
24use opentelemetry::{
25    global::BoxedTracer,
26    logs::{AnyValue, LogRecord, Logger, LoggerProvider, Severity},
27    propagation::TextMapPropagator,
28    trace::{FutureExt, Link, Status, TraceContextExt, Tracer},
29    Context, InstrumentationScope, Key, KeyValue, SpanId, TraceFlags, TraceId,
30};
31use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};
32use serde_json::Value;
33
34use crate::{ActivityContext, ActivityTask, QueryTask, Worker, WorkflowTask};
35
36/// Bounded execution identifiers. Values contain no arguments, results, headers,
37/// exception messages or baggage. Trace IDs reflect the current application
38/// span, including an application-created HTTP/database child span.
39pub fn current_log_context() -> BTreeMap<String, String> {
40    let context = Context::current();
41    let Some(fields) = context.get::<ExecutionFields>() else {
42        return BTreeMap::new();
43    };
44    let mut values = fields.0.clone();
45    let span = context.span();
46    let identity = span.span_context();
47    if identity.is_valid() {
48        values.insert("trace_id".into(), identity.trace_id().to_string());
49        values.insert("span_id".into(), identity.span_id().to_string());
50    }
51    values
52}
53
54#[derive(Clone, Debug, Default)]
55pub(crate) struct ExecutionFields(BTreeMap<String, String>);
56
57pub(crate) struct ObservationContext {
58    parent: Context,
59    links: Vec<Link>,
60    diagnostics: Vec<KeyValue>,
61}
62
63impl ExecutionFields {
64    fn insert(mut self, name: &'static str, value: Option<&str>) -> Self {
65        if let Some(value) = value.filter(|value| {
66            !value.is_empty() && value.len() <= 256 && !value.chars().any(char::is_control)
67        }) {
68            self.0.insert(name.into(), value.into());
69        }
70        self
71    }
72
73    fn attempt(self, name: &'static str, value: u64) -> Self {
74        self.insert(name, Some(&value.to_string()))
75    }
76
77    fn attributes(&self) -> Vec<KeyValue> {
78        self.0
79            .iter()
80            .map(|(key, value)| KeyValue::new(key.clone(), value.clone()))
81            .collect()
82    }
83}
84
85/// Opt-in worker telemetry using an application-owned tracer.
86///
87/// This value is cloneable; it never shuts down or replaces a global provider.
88/// No exporter is required; official W3C validation uses the SDK propagators.
89#[derive(Clone, Debug)]
90pub struct ExecutionTelemetry {
91    tracer: Arc<BoxedTracer>,
92    enabled: bool,
93    baggage_allowlist: Vec<String>,
94}
95
96impl ExecutionTelemetry {
97    /// Snapshot the application's current global tracer provider. Configure it
98    /// before constructing this value. An unconfigured provider emits no spans.
99    pub fn new() -> Self {
100        let scope = InstrumentationScope::builder("durable-workflow/sdk-rust")
101            .with_version(env!("CARGO_PKG_VERSION"))
102            .build();
103        use opentelemetry::trace::TracerProvider;
104        Self::from_tracer(opentelemetry::global::tracer_provider().tracer_with_scope(scope))
105    }
106
107    /// Use a tracer without changing any application globals.
108    pub fn from_tracer<T>(tracer: T) -> Self
109    where
110        T: Tracer + Send + Sync + 'static,
111        T::Span: Send + Sync + 'static,
112    {
113        Self {
114            tracer: Arc::new(BoxedTracer::new(Box::new(tracer))),
115            enabled: true,
116            baggage_allowlist: Vec::new(),
117        }
118    }
119
120    /// Disable spans and propagation without changing business execution.
121    pub fn with_enabled(mut self, enabled: bool) -> Self {
122        self.enabled = enabled;
123        self
124    }
125
126    /// Retain only these baggage keys on admission/run parents. Default empty.
127    pub fn with_baggage_allowlist(mut self, keys: Vec<String>) -> Self {
128        self.baggage_allowlist = keys;
129        self
130    }
131
132    pub(crate) fn capture(&self) -> Option<serde_json::Value> {
133        self.enabled
134            .then(|| carrier::capture(&self.baggage_allowlist).0)
135            .flatten()
136    }
137
138    pub(crate) fn is_enabled(&self) -> bool {
139        self.enabled
140    }
141
142    fn scope(
143        &self,
144        name: &'static str,
145        fields: ExecutionFields,
146        nested: bool,
147        observations: Option<ObservationContext>,
148    ) -> ExecutionScope {
149        // Only an existing SDK execution may parent another SDK scope. Poller
150        // ambient spans and baggage must not leak into unrelated durable work.
151        let current = Context::current();
152        let mut parent = if nested && current.get::<ExecutionFields>().is_some() {
153            current
154        } else {
155            Context::new()
156        };
157        let context = std::panic::catch_unwind(AssertUnwindSafe(|| {
158            let mut attributes = fields.attributes();
159            let mut builder = self.tracer.span_builder(name);
160            if let Some(observations) = observations {
161                parent = observations.parent;
162                attributes.extend(observations.diagnostics);
163                builder = builder.with_links(observations.links);
164            }
165            let span = self
166                .tracer
167                .build_with_context(builder.with_attributes(attributes), &parent);
168            parent.with_span(span)
169        }))
170        .unwrap_or_else(|_| Context::new())
171        .with_value(fields);
172        ExecutionScope {
173            context,
174            finished: false,
175        }
176    }
177
178    pub(crate) async fn run<T, E, F>(
179        &self,
180        name: &'static str,
181        fields: ExecutionFields,
182        nested: bool,
183        future: F,
184    ) -> Result<T, E>
185    where
186        F: Future<Output = Result<T, E>>,
187    {
188        self.run_observed(name, fields, nested, None, future).await
189    }
190
191    pub(crate) async fn run_observed<T, E, F>(
192        &self,
193        name: &'static str,
194        fields: ExecutionFields,
195        nested: bool,
196        observations: Option<ObservationContext>,
197        future: F,
198    ) -> Result<T, E>
199    where
200        F: Future<Output = Result<T, E>>,
201    {
202        if !self.enabled {
203            return future.await;
204        }
205        let mut scope = self.scope(name, fields, nested, observations);
206        // Attaches only during each poll, never across an await on a Tokio thread.
207        let result = future.with_context(scope.context.clone()).await;
208        scope.finish(result.is_err());
209        result
210    }
211
212    #[cfg(test)]
213    pub(crate) fn run_sync<T, E>(
214        &self,
215        name: &'static str,
216        fields: ExecutionFields,
217        callback: impl FnOnce() -> Result<T, E>,
218    ) -> Result<T, E> {
219        self.run_sync_observed(name, fields, None, callback)
220    }
221
222    pub(crate) fn run_sync_observed<T, E>(
223        &self,
224        name: &'static str,
225        fields: ExecutionFields,
226        observations: Option<ObservationContext>,
227        callback: impl FnOnce() -> Result<T, E>,
228    ) -> Result<T, E> {
229        if !self.enabled {
230            return callback();
231        }
232        let mut scope = self.scope(name, fields, false, observations);
233        let result = {
234            let _guard = scope.context.clone().attach();
235            callback()
236        };
237        scope.finish(result.is_err());
238        result
239    }
240
241    fn observations(
242        &self,
243        task_id: &str,
244        run_id: &str,
245        namespace: &str,
246        origin: Option<&Value>,
247        dispatch: Option<&Value>,
248        secondary: Option<&Value>,
249    ) -> ObservationContext {
250        std::panic::catch_unwind(AssertUnwindSafe(|| {
251            self.observations_inner(task_id, run_id, namespace, origin, dispatch, secondary)
252        }))
253        .unwrap_or_else(|_| ObservationContext {
254            parent: Context::new(),
255            links: Vec::new(),
256            diagnostics: vec![KeyValue::new("durable.observability_context", "discarded")],
257        })
258    }
259
260    fn observations_inner(
261        &self,
262        task_id: &str,
263        run_id: &str,
264        namespace: &str,
265        origin: Option<&Value>,
266        dispatch: Option<&Value>,
267        secondary: Option<&Value>,
268    ) -> ObservationContext {
269        let mut result = ObservationContext {
270            parent: Context::new(),
271            links: Vec::new(),
272            diagnostics: Vec::new(),
273        };
274        let propagator = TraceContextPropagator::new();
275        let extract = |value: &Value| {
276            let headers: std::collections::HashMap<_, _> = value["carrier"]
277                .as_object()
278                .expect("normalized carrier")
279                .iter()
280                .map(|(key, value)| {
281                    (
282                        key.clone(),
283                        value.as_str().expect("normalized header").to_string(),
284                    )
285                })
286                .collect();
287            let parent = propagator.extract_with_context(&Context::new(), &headers);
288            BaggagePropagator::new().extract_with_context(&parent, &headers)
289        };
290        let diagnostic = |name: &'static str, value: Option<carrier::Diagnostic>| {
291            value.map(|value| {
292                KeyValue::new(
293                    name,
294                    match value {
295                        carrier::Diagnostic::Discarded => "discarded",
296                        carrier::Diagnostic::Filtered => "filtered",
297                    },
298                )
299            })
300        };
301        let (origin, origin_diagnostic) =
302            carrier::normalize(origin.unwrap_or(&Value::Null), &self.baggage_allowlist);
303        if let Some(origin) = origin {
304            result.parent = extract(&origin);
305        }
306        result.diagnostics.extend(diagnostic(
307            "durable.observability_context",
308            origin_diagnostic,
309        ));
310        let (dispatch, dispatch_diagnostic) =
311            carrier::dispatch(dispatch.unwrap_or(&Value::Null), task_id);
312        if let Some(dispatch) = dispatch {
313            let linked = extract(&dispatch);
314            result.links.push(Link::new(
315                linked.span().span_context().clone(),
316                vec![
317                    KeyValue::new("durable.link.type", "dispatch"),
318                    KeyValue::new("task_id", task_id.to_string()),
319                ],
320                0,
321            ));
322        }
323        result
324            .diagnostics
325            .extend(diagnostic("durable.dispatch_context", dispatch_diagnostic));
326        let (entries, links_diagnostic) = carrier::links(
327            secondary.unwrap_or(&Value::Null),
328            task_id,
329            run_id,
330            namespace,
331        );
332        for entry in entries {
333            let linked = extract(&entry["context"]);
334            let mut attributes = vec![KeyValue::new(
335                "durable.link.type",
336                entry["kind"].as_str().unwrap().to_string(),
337            )];
338            for (key, value) in entry.as_object().expect("normalized link") {
339                if matches!(key.as_str(), "kind" | "context") || value.is_null() {
340                    continue;
341                }
342                if let Some(value) = value.as_str() {
343                    attributes.push(KeyValue::new(key.clone(), value.to_string()));
344                } else if let Some(value) = value.as_i64() {
345                    attributes.push(KeyValue::new(key.clone(), value));
346                }
347            }
348            result.links.push(Link::new(
349                linked.span().span_context().clone(),
350                attributes,
351                0,
352            ));
353        }
354        result
355            .diagnostics
356            .extend(diagnostic("durable.observability_links", links_diagnostic));
357        result
358    }
359}
360
361impl Default for ExecutionTelemetry {
362    fn default() -> Self {
363        Self::new()
364    }
365}
366
367struct ExecutionScope {
368    context: Context,
369    finished: bool,
370}
371
372impl ExecutionScope {
373    fn finish(&mut self, failed: bool) {
374        self.finished = true;
375        let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
376            if failed {
377                self.context
378                    .span()
379                    .set_attribute(KeyValue::new("error.type", "durable.execution.failed"));
380                self.context
381                    .span()
382                    .set_status(Status::error("durable execution failed"));
383            }
384            self.context.span().end();
385        }));
386    }
387}
388
389impl Drop for ExecutionScope {
390    fn drop(&mut self) {
391        if !self.finished {
392            self.finish(true);
393        }
394    }
395}
396
397/// Decorate the application's OpenTelemetry logger provider with execution IDs.
398/// Use this provider in an OpenTelemetry `log` or `tracing` bridge. Its lifecycle
399/// remains owned by the application; ordinary log bodies and request IDs remain
400/// application-owned. No logging bridge is installed automatically.
401#[derive(Clone, Debug)]
402pub struct ExecutionLoggerProvider<P>(pub P);
403
404impl<P: LoggerProvider> LoggerProvider for ExecutionLoggerProvider<P> {
405    type Logger = ExecutionLogger<P::Logger>;
406
407    fn logger_with_scope(&self, scope: InstrumentationScope) -> Self::Logger {
408        ExecutionLogger(self.0.logger_with_scope(scope))
409    }
410}
411
412/// Logger returned by [`ExecutionLoggerProvider`].
413#[derive(Debug)]
414pub struct ExecutionLogger<L>(L);
415
416impl<L: Logger> Logger for ExecutionLogger<L> {
417    type LogRecord = ExecutionLogRecord<L::LogRecord>;
418
419    fn create_log_record(&self) -> Self::LogRecord {
420        ExecutionLogRecord {
421            inner: self.0.create_log_record(),
422            attributes: Vec::new(),
423        }
424    }
425
426    fn emit(&self, mut record: Self::LogRecord) {
427        let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
428            let fields = current_log_context();
429            for (key, value) in record.attributes {
430                if !fields.contains_key(key.as_str()) {
431                    record.inner.add_attribute(key, value);
432                }
433            }
434            record.inner.add_attributes(fields);
435            self.0.emit(record.inner);
436        }));
437    }
438
439    fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool {
440        std::panic::catch_unwind(AssertUnwindSafe(|| {
441            self.0.event_enabled(level, target, name)
442        }))
443        .unwrap_or(false)
444    }
445}
446
447/// Application log record whose execution IDs are resolved when it is emitted.
448/// Buffering attributes permits replacing stale durable IDs without duplicate
449/// keys; application attributes such as `request_id` keep their original value.
450#[derive(Clone, Debug)]
451pub struct ExecutionLogRecord<R> {
452    inner: R,
453    attributes: Vec<(Key, AnyValue)>,
454}
455
456impl<R: LogRecord> LogRecord for ExecutionLogRecord<R> {
457    fn set_event_name(&mut self, name: &'static str) {
458        self.inner.set_event_name(name);
459    }
460    fn set_target<T: Into<Cow<'static, str>>>(&mut self, target: T) {
461        self.inner.set_target(target);
462    }
463    fn set_timestamp(&mut self, timestamp: SystemTime) {
464        self.inner.set_timestamp(timestamp);
465    }
466    fn set_observed_timestamp(&mut self, timestamp: SystemTime) {
467        self.inner.set_observed_timestamp(timestamp);
468    }
469    fn set_severity_text(&mut self, text: &'static str) {
470        self.inner.set_severity_text(text);
471    }
472    fn set_severity_number(&mut self, number: Severity) {
473        self.inner.set_severity_number(number);
474    }
475    fn set_body(&mut self, body: AnyValue) {
476        self.inner.set_body(body);
477    }
478    fn add_attributes<I, K, V>(&mut self, attributes: I)
479    where
480        I: IntoIterator<Item = (K, V)>,
481        K: Into<Key>,
482        V: Into<AnyValue>,
483    {
484        self.attributes.extend(
485            attributes
486                .into_iter()
487                .map(|(key, value)| (key.into(), value.into())),
488        );
489    }
490    fn add_attribute<K: Into<Key>, V: Into<AnyValue>>(&mut self, key: K, value: V) {
491        self.attributes.push((key.into(), value.into()));
492    }
493    fn set_trace_context(&mut self, trace_id: TraceId, span_id: SpanId, flags: Option<TraceFlags>) {
494        self.inner.set_trace_context(trace_id, span_id, flags);
495    }
496}
497
498impl Worker {
499    fn allows_observations(
500        &self,
501        task_id: &str,
502        run_id: Option<&str>,
503        owner: Option<&str>,
504        attempt: u64,
505    ) -> bool {
506        self.client
507            .observability_context_supported
508            .load(std::sync::atomic::Ordering::SeqCst)
509            && self
510                .telemetry
511                .as_ref()
512                .is_some_and(ExecutionTelemetry::is_enabled)
513            && carrier::identity(task_id)
514            && run_id.is_some_and(carrier::identity)
515            && carrier::identity(&self.client.namespace)
516            && owner == Some(self.worker_id.as_str())
517            && attempt > 0
518            && attempt <= i64::MAX as u64
519    }
520
521    pub(crate) fn workflow_observations(&self, task: &WorkflowTask) -> Option<ObservationContext> {
522        if !self.allows_observations(
523            &task.task_id,
524            task.run_id.as_deref(),
525            task.lease_owner.as_deref(),
526            task.workflow_task_attempt,
527        ) || !self.workflows.contains_key(&task.workflow_type)
528        {
529            return None;
530        }
531        Some(self.telemetry.as_ref()?.observations(
532            &task.task_id,
533            task.run_id.as_deref()?,
534            &self.client.namespace,
535            task.observability_context.as_ref(),
536            task.dispatch_observability_context.as_ref(),
537            task.observability_links.as_ref(),
538        ))
539    }
540
541    pub(crate) fn query_observations(&self, task: &QueryTask) -> Option<ObservationContext> {
542        if !self.allows_observations(
543            &task.query_task_id,
544            task.run_id.as_deref(),
545            task.lease_owner.as_deref(),
546            task.query_task_attempt,
547        ) || !self.workflows.contains_key(&task.workflow_type)
548            || !self
549                .queries
550                .get(&task.workflow_type)
551                .is_some_and(|queries| queries.contains_key(&task.query_name))
552        {
553            return None;
554        }
555        Some(self.telemetry.as_ref()?.observations(
556            &task.query_task_id,
557            task.run_id.as_deref()?,
558            &self.client.namespace,
559            task.observability_context.as_ref(),
560            task.dispatch_observability_context.as_ref(),
561            task.observability_links.as_ref(),
562        ))
563    }
564
565    pub(crate) fn activity_observations(&self, task: &ActivityTask) -> Option<ObservationContext> {
566        if !self.allows_observations(
567            &task.task_id,
568            task.run_id.as_deref(),
569            task.lease_owner.as_deref(),
570            task.attempt_number,
571        ) || !self.activities.contains_key(&task.activity_type)
572            || !task
573                .activity_attempt_id
574                .as_deref()
575                .or(task.attempt_id.as_deref())
576                .is_some_and(carrier::identity)
577        {
578            return None;
579        }
580        Some(self.telemetry.as_ref()?.observations(
581            &task.task_id,
582            task.run_id.as_deref()?,
583            &self.client.namespace,
584            task.observability_context.as_ref(),
585            task.dispatch_observability_context.as_ref(),
586            task.observability_links.as_ref(),
587        ))
588    }
589
590    pub(crate) fn stamp_observability_commands(&self, commands: &mut [Value]) {
591        if Context::current().get::<ExecutionFields>().is_none() {
592            return;
593        }
594        for command in commands {
595            if matches!(
596                command["type"].as_str(),
597                Some("start_child_workflow" | "continue_as_new")
598            ) {
599                *command = self.client.with_observability_context(command.take());
600            }
601        }
602    }
603
604    fn telemetry_fields(&self) -> ExecutionFields {
605        ExecutionFields::default()
606            .insert("namespace", Some(&self.client.namespace))
607            .insert("worker_id", Some(&self.worker_id))
608            .insert("task_queue", Some(&self.task_queue))
609    }
610
611    pub(crate) fn workflow_telemetry_fields(&self, task: &WorkflowTask) -> ExecutionFields {
612        self.telemetry_fields()
613            .insert("workflow_instance_id", task.workflow_id.as_deref())
614            .insert("workflow_run_id", task.run_id.as_deref())
615            .insert("task_id", Some(&task.task_id))
616            .insert("workflow_type", Some(&task.workflow_type))
617            .insert("command_id", task.workflow_command_id.as_deref())
618            .attempt("workflow_task_attempt", task.workflow_task_attempt)
619    }
620
621    pub(crate) fn query_telemetry_fields(&self, task: &QueryTask) -> ExecutionFields {
622        self.telemetry_fields()
623            .insert("workflow_instance_id", task.workflow_id.as_deref())
624            .insert("workflow_run_id", task.run_id.as_deref())
625            .insert("task_id", Some(&task.query_task_id))
626            .insert("workflow_type", Some(&task.workflow_type))
627            .attempt("query_task_attempt", task.query_task_attempt)
628    }
629
630    pub(crate) fn activity_telemetry_fields(&self, task: &ActivityTask) -> ExecutionFields {
631        self.telemetry_fields()
632            .insert("workflow_instance_id", task.workflow_id.as_deref())
633            .insert("workflow_run_id", task.run_id.as_deref())
634            .insert("activity_id", task.activity_execution_id.as_deref())
635            .insert("task_id", Some(&task.task_id))
636            .insert("activity_type", Some(&task.activity_type))
637            .insert(
638                "activity_attempt_id",
639                task.activity_attempt_id
640                    .as_deref()
641                    .or(task.attempt_id.as_deref()),
642            )
643            .attempt("activity_attempt_number", task.attempt_number)
644    }
645
646    pub(crate) fn callback_telemetry_fields(
647        &self,
648        context: &ActivityContext,
649        local: bool,
650    ) -> ExecutionFields {
651        let fields = Context::current()
652            .get::<ExecutionFields>()
653            .filter(|fields| local || fields.0.get("task_id") == Some(&context.task_id))
654            .cloned()
655            .unwrap_or_else(|| self.telemetry_fields());
656        fields
657            .insert("task_id", Some(&context.task_id))
658            .insert("activity_type", Some(&context.activity_type))
659            .insert("activity_attempt_id", Some(&context.activity_attempt_id))
660            .attempt("activity_attempt_number", context.attempt_number)
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use crate::{json, AvroValue, Client, Error, Value};
668    use opentelemetry::trace::{Span, SpanId, TracerProvider};
669    use opentelemetry_sdk::{
670        logs::{InMemoryLogExporter, SdkLoggerProvider},
671        trace::{InMemorySpanExporter, SdkTracerProvider, SpanData, SpanExporter},
672    };
673
674    fn telemetry() -> (ExecutionTelemetry, SdkTracerProvider, InMemorySpanExporter) {
675        let exporter = InMemorySpanExporter::default();
676        let provider = SdkTracerProvider::builder()
677            .with_simple_exporter(exporter.clone())
678            .build();
679        (
680            ExecutionTelemetry::from_tracer(provider.tracer("test")),
681            provider,
682            exporter,
683        )
684    }
685
686    fn worker() -> Worker {
687        Worker::new(
688            Client::builder("http://127.0.0.1:9")
689                .namespace("shipping")
690                .build()
691                .unwrap(),
692            "shipping",
693        )
694        .worker_id("test-worker")
695    }
696
697    fn activity_task(attempt: u64) -> ActivityTask {
698        serde_json::from_value(json!({
699            "task_id": "task-1", "activity_type": "ship", "activity_attempt_id": format!("attempt-{attempt}"),
700            "attempt_number": attempt, "payload_codec": "avro",
701            "arguments": crate::encode_value_envelope(&json!([]), "avro").unwrap(),
702        })).unwrap()
703    }
704
705    #[tokio::test]
706    async fn actual_activity_attempts_correlate_application_child_logs_and_preserve_result() {
707        let (telemetry, provider, exporter) = telemetry();
708        let log_exporter = InMemoryLogExporter::default();
709        let logs = SdkLoggerProvider::builder()
710            .with_simple_exporter(log_exporter.clone())
711            .build();
712        let logger = Arc::new(ExecutionLoggerProvider(logs.clone()).logger("application"));
713        let tracer = Arc::new(provider.tracer("http-client"));
714        let mut worker = worker().telemetry(telemetry);
715        worker.register_activity("ship", move |_, _| {
716            let logger = logger.clone();
717            let tracer = tracer.clone();
718            async move {
719                assert_eq!(current_log_context()["task_id"], "task-1");
720                assert_eq!(current_log_context()["namespace"], "shipping");
721                let child = tracer.start("HTTP POST shipping");
722                let child_identity = child.span_context().clone();
723                let child_context = Context::current_with_span(child);
724                async {
725                    tokio::task::yield_now().await;
726                    assert_eq!(
727                        current_log_context()["span_id"],
728                        child_identity.span_id().to_string()
729                    );
730                    let mut record = logger.create_log_record();
731                    record.set_body("application message".into());
732                    record.add_attribute("request_id", "application-request");
733                    record.add_attribute("activity_attempt_id", "stale-application-id");
734                    logger.emit(record);
735                }
736                .with_context(child_context.clone())
737                .await;
738                child_context.span().end();
739                Ok(json!("shipped"))
740            }
741        });
742        for attempt in 1..=2 {
743            assert_eq!(
744                worker
745                    .execute_activity_task(activity_task(attempt))
746                    .await
747                    .unwrap(),
748                AvroValue::String("shipped".into())
749            );
750            assert!(current_log_context().is_empty());
751        }
752        let spans = exporter.get_finished_spans().unwrap();
753        assert_eq!(spans.len(), 6);
754        let roots: Vec<_> = spans
755            .iter()
756            .filter(|span| span.name == "durable.activity.task")
757            .collect();
758        assert_eq!(roots.len(), 2);
759        assert!(roots
760            .iter()
761            .all(|span| span.parent_span_id == SpanId::INVALID));
762        assert_ne!(
763            roots[0].span_context.trace_id(),
764            roots[1].span_context.trace_id()
765        );
766        for attempt in spans
767            .iter()
768            .filter(|span| span.name == "durable.activity.attempt")
769        {
770            assert!(roots
771                .iter()
772                .any(|root| root.span_context.span_id() == attempt.parent_span_id));
773        }
774        let emitted = log_exporter.get_emitted_logs().unwrap();
775        assert_eq!(emitted.len(), 2);
776        for (index, log) in emitted.iter().enumerate() {
777            assert_eq!(
778                log.record
779                    .attributes_iter()
780                    .filter(|(key, _)| key.as_str() == "activity_attempt_id")
781                    .count(),
782                1
783            );
784            let attributes: BTreeMap<_, _> = log
785                .record
786                .attributes_iter()
787                .map(|(key, value)| (key.as_str(), value))
788                .collect();
789            assert_eq!(attributes["request_id"], &"application-request".into());
790            assert_eq!(
791                attributes["activity_attempt_id"],
792                &format!("attempt-{}", index + 1).into()
793            );
794            let child = spans
795                .iter()
796                .find(|span| {
797                    span.span_context.span_id() == log.record.trace_context().unwrap().span_id
798                })
799                .unwrap();
800            assert_eq!(child.name, "HTTP POST shipping");
801        }
802    }
803
804    #[tokio::test]
805    async fn concurrent_tokio_tasks_keep_identifiers_isolated() {
806        let (telemetry, _provider, exporter) = telemetry();
807        let barrier = Arc::new(tokio::sync::Barrier::new(2));
808        let mut worker = worker().telemetry(telemetry);
809        worker.register_activity("ship", move |context, _| {
810            let barrier = barrier.clone();
811            async move {
812                let before = current_log_context();
813                barrier.wait().await;
814                tokio::task::yield_now().await;
815                assert_eq!(current_log_context(), before);
816                assert_eq!(before["activity_attempt_id"], context.activity_attempt_id);
817                Ok(Value::Null)
818            }
819        });
820        let worker = Arc::new(worker);
821        let handles: Vec<_> = (1..=2)
822            .map(|attempt| {
823                let worker = worker.clone();
824                tokio::spawn(
825                    async move { worker.execute_activity_task(activity_task(attempt)).await },
826                )
827            })
828            .collect();
829        for handle in handles {
830            handle.await.unwrap().unwrap();
831        }
832        assert!(current_log_context().is_empty());
833        assert_eq!(exporter.get_finished_spans().unwrap().len(), 4);
834    }
835
836    #[tokio::test]
837    async fn workflow_yield_replay_and_disabled_controls_preserve_commands() {
838        let (telemetry, _provider, exporter) = telemetry();
839        let mut enabled = worker().telemetry(telemetry);
840        enabled.register_workflow("ship-order", |context, _| async move {
841            assert_eq!(current_log_context()["workflow_run_id"], "run-order");
842            context.activity("ship", json!([])).await
843        });
844        let mut disabled = worker();
845        disabled.register_workflow("ship-order", |context, _| async move {
846            assert!(current_log_context().is_empty());
847            context.activity("ship", json!([])).await
848        });
849        let task: WorkflowTask = serde_json::from_value(json!({
850            "task_id": "workflow-task-1", "workflow_id": "order", "run_id": "run-order",
851            "workflow_type": "ship-order", "payload_codec": "avro",
852            "arguments": crate::encode_value_envelope(&json!([]), "avro").unwrap(),
853        }))
854        .unwrap();
855        let expected = disabled.execute_workflow_task(task.clone()).unwrap();
856        assert_eq!(
857            enabled.execute_workflow_task(task.clone()).unwrap(),
858            expected
859        );
860        let decision = enabled
861            .execute_workflow_with_local_activities(task)
862            .await
863            .unwrap()
864            .unwrap();
865        assert_eq!(decision.commands, expected);
866        let spans = exporter.get_finished_spans().unwrap();
867        assert_eq!(spans.len(), 2);
868        assert!(spans
869            .iter()
870            .all(|span| span.name == "durable.workflow.task"));
871        assert!(current_log_context().is_empty());
872    }
873
874    #[tokio::test]
875    async fn genuine_failure_is_preserved_without_recording_payload_or_exception_text() {
876        let (telemetry, _provider, exporter) = telemetry();
877        let mut worker = worker().telemetry(telemetry);
878        worker.register_activity("ship", |_, _| async {
879            Err(Error::WorkerLoop("private-token-123".into()))
880        });
881        assert!(
882            matches!(worker.execute_activity_task(activity_task(1)).await, Err(Error::WorkerLoop(message)) if message == "private-token-123")
883        );
884        let spans = exporter.get_finished_spans().unwrap();
885        assert_eq!(spans.len(), 2);
886        for span in spans {
887            assert_eq!(span.status, Status::error("durable execution failed"));
888            assert!(span.events.events.is_empty());
889            assert!(!format!("{span:?}").contains("private-token-123"));
890        }
891        assert!(current_log_context().is_empty());
892    }
893
894    #[tokio::test]
895    async fn dropped_future_and_panicking_callback_end_scopes_and_restore_caller() {
896        let (telemetry, provider, exporter) = telemetry();
897        let caller = provider.tracer("application").start("caller");
898        let caller_id = caller.span_context().span_id();
899        let _caller = Context::current_with_span(caller).attach();
900        let mut pending = Box::pin(telemetry.run::<(), (), _>(
901            "pending",
902            ExecutionFields::default(),
903            false,
904            std::future::pending(),
905        ));
906        assert!(futures_util::poll!(pending.as_mut()).is_pending());
907        assert!(current_log_context().is_empty());
908        assert_eq!(
909            Context::current().span().span_context().span_id(),
910            caller_id
911        );
912        drop(pending);
913        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| {
914            telemetry.run_sync::<(), ()>("panicking", ExecutionFields::default(), || {
915                panic!("business panic")
916            })
917        }));
918        assert!(panic.is_err());
919        assert_eq!(
920            Context::current().span().span_context().span_id(),
921            caller_id
922        );
923        let spans = exporter.get_finished_spans().unwrap();
924        assert_eq!(spans.len(), 2);
925        assert!(spans
926            .iter()
927            .all(|span| span.parent_span_id == SpanId::INVALID));
928    }
929
930    #[derive(Debug)]
931    struct RejectingExporter;
932    impl SpanExporter for RejectingExporter {
933        async fn export(&self, _: Vec<SpanData>) -> opentelemetry_sdk::error::OTelSdkResult {
934            Err(opentelemetry_sdk::error::OTelSdkError::InternalFailure(
935                "export unavailable".into(),
936            ))
937        }
938    }
939
940    #[tokio::test]
941    async fn exporter_failure_and_invalid_identifiers_do_not_change_business_result() {
942        let provider = SdkTracerProvider::builder()
943            .with_simple_exporter(RejectingExporter)
944            .build();
945        let telemetry = ExecutionTelemetry::from_tracer(provider.tracer("test"));
946        let fields = ExecutionFields::default()
947            .insert("task_id", Some("invalid\nidentifier"))
948            .insert("worker_id", Some(&"x".repeat(257)))
949            .insert("workflow_run_id", Some("valid"));
950        let result = telemetry
951            .run("test", fields, false, async {
952                assert_eq!(
953                    current_log_context().get("workflow_run_id").unwrap(),
954                    "valid"
955                );
956                assert!(!current_log_context().contains_key("task_id"));
957                assert!(!current_log_context().contains_key("worker_id"));
958                Ok::<_, ()>(42)
959            })
960            .await;
961        assert_eq!(result, Ok(42));
962        assert!(current_log_context().is_empty());
963    }
964}