1pub 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
36pub 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#[derive(Clone, Debug)]
90pub struct ExecutionTelemetry {
91 tracer: Arc<BoxedTracer>,
92 enabled: bool,
93 baggage_allowlist: Vec<String>,
94}
95
96impl ExecutionTelemetry {
97 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 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 pub fn with_enabled(mut self, enabled: bool) -> Self {
122 self.enabled = enabled;
123 self
124 }
125
126 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 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 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#[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#[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#[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}