google_cloud_lro/internal/
tracing.rs1use crate::{Poller, PollingResult, Result, sealed};
16use gaxi::observability::errors::error_type;
17use google_cloud_gax::polling_state::PollingState;
18use tracing::{Instrument, Span, info_span};
19
20tokio::task_local! {
21 static LRO_RECORDER: LroRecorder;
22}
23
24#[derive(Clone, Debug)]
35#[non_exhaustive]
36pub struct LroRecorder {
37 span: Span,
38 attempt_count: Option<u32>,
39 destination_id: std::sync::Arc<std::sync::OnceLock<String>>,
40}
41
42impl LroRecorder {
43 pub fn new(span: Span) -> Self {
45 Self {
46 span,
47 attempt_count: None,
48 destination_id: std::sync::Arc::new(std::sync::OnceLock::new()),
49 }
50 }
51
52 pub fn current() -> Option<Self> {
54 LRO_RECORDER.try_get().ok()
55 }
56
57 pub async fn scope<F, T>(&self, future: F) -> T
59 where
60 F: std::future::Future<Output = T>,
61 {
62 LRO_RECORDER.scope(self.clone(), future).await
63 }
64
65 pub fn span(&self) -> &Span {
67 &self.span
68 }
69
70 pub fn attempt_count(&self) -> Option<u32> {
75 self.attempt_count
76 }
77}
78
79#[macro_export]
81#[doc(hidden)]
82macro_rules! record_discovery_polling_result {
83 ($span:expr, $op:expr) => {
84 let span = &$span;
85 let op = &$op;
86 let done = $crate::internal::DiscoveryOperation::done(op);
87 span.record("gcp.longrunning.done", done);
88 if done {
89 let error = $crate::internal::DiscoveryOperation::error(op);
90 let code = error.as_ref().map(|e| e.code as i32).unwrap_or(0);
91 span.record("gcp.longrunning.status_code", code);
92 if let Some(status) = error {
93 span.record("otel.status_code", "ERROR");
94 span.record("otel.status_description", &status.message);
95 span.record("rpc.response.status_code", status.code as i32);
96 span.record("error.type", status.code.to_string());
97 }
98 }
99 };
100}
101
102impl LroRecorder {
103 pub fn with_attempt_count(&self, count: u32) -> Self {
108 Self {
109 span: self.span.clone(),
110 attempt_count: Some(count),
111 destination_id: self.destination_id.clone(),
112 }
113 }
114
115 pub fn record_destination_id(&self, name: &str) {
116 self.span.record("gcp.resource.destination.id", name);
117 let _ = self.destination_id.set(name.to_string());
118 }
119
120 pub fn destination_id(&self) -> Option<String> {
121 self.destination_id.get().cloned()
122 }
123
124 pub fn record_error(&self, err: &crate::Error) {
125 self.span.record("otel.status_code", "ERROR");
126 self.span.record("otel.status_description", err.to_string());
127 self.span.record("error.type", error_type(err));
128 }
129
130 pub async fn record_action<F, Fut, T>(&self, f: F) -> T
131 where
132 F: FnOnce(Span) -> Fut,
133 Fut: std::future::Future<Output = T>,
134 {
135 let span = self.span.clone();
136 self.scope(async move { f(span).await }).await
137 }
138}
139
140#[macro_export]
142#[doc(hidden)]
143macro_rules! record_polling_attributes {
144 ($span:expr) => {
145 if let Some(recorder) = $crate::LroRecorder::current() {
146 if let Some(attempt) = recorder.attempt_count() {
147 let span = &$span;
148 span.record("gcp.longrunning.poll_attempt_count", attempt);
149 }
150 if let Some(dest_id) = recorder.destination_id() {
151 let span = &$span;
152 span.record("gcp.resource.destination.id", dest_id);
153 }
154 }
155 };
156}
157
158#[derive(Clone, Debug)]
160pub struct Tracing<P> {
161 inner: P,
162 recorder: LroRecorder,
163 poll_attempt_count: u32,
165 started: bool,
166}
167
168impl<P> Tracing<P> {
169 pub(crate) fn new(inner: P, span: Span) -> Self {
170 Self {
171 inner,
172 recorder: LroRecorder::new(span),
173 poll_attempt_count: 0,
174 started: false,
175 }
176 }
177}
178
179impl<P> sealed::Poller for Tracing<P>
180where
181 P: sealed::Poller + Send,
182{
183 async fn backoff(&mut self, state: &PollingState) {
184 let span = info_span!("LRO Sleep");
185 let inner = &mut self.inner;
186 self.recorder
187 .record_action(|_| async move { inner.backoff(state).instrument(span).await })
188 .await
189 }
190}
191
192impl<P, ResponseType, MetadataType> Poller<ResponseType, MetadataType> for Tracing<P>
193where
194 P: Poller<ResponseType, MetadataType>,
195 ResponseType: Send,
196 MetadataType: Send,
197{
198 async fn poll(&mut self) -> Option<PollingResult<ResponseType, MetadataType>> {
199 let attempt = if self.started {
202 self.poll_attempt_count += 1;
203 self.poll_attempt_count
204 } else {
205 self.started = true;
206 0 };
208
209 let inner = &mut self.inner;
210 let span = self.recorder.span().clone();
211
212 let recorder = self.recorder.with_attempt_count(attempt);
215 recorder
216 .scope(async move { inner.poll().instrument(span).await })
217 .await
218 }
219
220 async fn until_done(self) -> Result<ResponseType> {
221 let this = self;
222 let recorder = this.recorder.clone();
223 let result = recorder
224 .record_action(|wait_span| async move {
225 crate::until_done(this).instrument(wait_span).await
226 })
227 .await;
228 if let Err(ref e) = result {
229 recorder.record_error(e);
230 }
231 result
232 }
233 #[cfg(feature = "unstable-stream")]
234 fn into_stream(
235 self,
236 ) -> impl futures::Stream<Item = PollingResult<ResponseType, MetadataType>> + Unpin {
237 crate::into_stream(self)
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::Error;
245 use gaxi::client_request_signals;
246 use gaxi::options::InstrumentationClientInfo;
247 use google_cloud_test_utils::test_layer::TestLayer;
248 use google_cloud_wkt::{Duration, Timestamp};
249
250 struct FailingPoller;
251 impl sealed::Poller for FailingPoller {
252 async fn backoff(&mut self, _state: &PollingState) {}
253 }
254 impl Poller<Duration, Timestamp> for FailingPoller {
255 async fn poll(&mut self) -> Option<PollingResult<Duration, Timestamp>> {
256 Some(PollingResult::Completed(Err(Error::io(
257 "logical-test-failure",
258 ))))
259 }
260 async fn until_done(self) -> Result<Duration> {
261 Err(Error::io("logical-test-failure"))
262 }
263 #[cfg(feature = "unstable-stream")]
264 fn into_stream(
265 self,
266 ) -> impl futures::Stream<Item = PollingResult<Duration, Timestamp>> + Unpin {
267 crate::into_stream(self)
268 }
269 }
270
271 #[tokio::test]
272 async fn test_tracing_decorator_error_reporting() {
273 let guard = TestLayer::initialize();
274
275 let span = tracing::info_span!(
276 "test_span",
277 "otel.status_code" = tracing::field::Empty,
278 "otel.status_description" = tracing::field::Empty,
279 );
280
281 let poller = Tracing::new(FailingPoller, span);
282
283 let got = poller.until_done().await;
284 assert!(got.is_err());
285
286 {
287 let captured = TestLayer::capture(&guard);
288 let got = captured
289 .iter()
290 .find(|s| s.name == "test_span")
291 .unwrap_or_else(|| panic!("missing `test_span` in captured spans: {captured:?}"));
292 assert_eq!(
293 got.attributes
294 .get("otel.status_code")
295 .and_then(|v| v.as_string()),
296 Some("ERROR".to_string())
297 );
298 assert!(
299 got.attributes
300 .get("otel.status_description")
301 .and_then(|v| v.as_string())
302 .unwrap()
303 .contains("logical-test-failure")
304 );
305 }
306 }
307
308 struct CountingPoller {
309 attempts: Vec<u32>,
310 }
311 impl sealed::Poller for CountingPoller {
312 async fn backoff(&mut self, _state: &PollingState) {}
313 }
314 impl Poller<Duration, Timestamp> for CountingPoller {
315 async fn poll(&mut self) -> Option<PollingResult<Duration, Timestamp>> {
316 let attempt = LroRecorder::current()
320 .and_then(|r| r.attempt_count())
321 .unwrap();
322 self.attempts.push(attempt);
323 Some(PollingResult::InProgress(None))
324 }
325 async fn until_done(self) -> Result<Duration> {
326 Ok(Duration::clamp(0, 0))
327 }
328 #[cfg(feature = "unstable-stream")]
329 fn into_stream(
330 self,
331 ) -> impl futures::Stream<Item = PollingResult<Duration, Timestamp>> + Unpin {
332 crate::into_stream(self)
333 }
334 }
335
336 #[tokio::test]
337 async fn test_tracing_decorator_attempt_counting() {
338 let span = tracing::info_span!("test_lro_span");
339 let poller = CountingPoller { attempts: vec![] };
340 let mut traced = Tracing::new(poller, span);
341
342 let _ = traced.poll().await;
344
345 let _ = traced.poll().await;
347
348 let _ = traced.poll().await;
350
351 assert_eq!(traced.inner.attempts, vec![0, 1, 2]);
352 }
353
354 #[tokio::test]
355 async fn test_lro_recorder_span_nesting() {
356 let _guard = TestLayer::initialize();
357 let span = tracing::info_span!("test_lro_span");
358 let recorder = LroRecorder::new(span.clone());
359
360 let span_clone = span.clone();
362 recorder
363 .record_action(|_| async move {
364 let active_recorder = LroRecorder::current().unwrap();
365 assert_eq!(
366 active_recorder.span.metadata().unwrap().name(),
367 "test_lro_span"
368 );
369 assert_eq!(active_recorder.span, span_clone);
370 })
371 .await;
372 }
373
374 #[tokio::test]
375 async fn record_polling_attributes_macro() {
376 let guard = TestLayer::initialize();
377
378 let span =
379 client_request_signals!(info: &InstrumentationClientInfo::default(), method: "test");
380
381 let recorder = LroRecorder::new(span.clone()).with_attempt_count(42);
382 recorder.record_destination_id("my-test-lro-id");
383
384 recorder
385 .scope(async move {
386 crate::record_polling_attributes!(&span);
387 })
388 .await;
389
390 drop(recorder);
391
392 let captured = TestLayer::capture(&guard);
393 let got = captured
394 .iter()
395 .find(|s| s.name == "client_request")
396 .unwrap();
397
398 assert_eq!(
399 got.attributes.get("gcp.longrunning.poll_attempt_count"),
400 Some(&google_cloud_test_utils::test_layer::AttributeValue::UInt64(42))
401 );
402 assert_eq!(
403 got.attributes.get("gcp.resource.destination.id"),
404 Some(
405 &google_cloud_test_utils::test_layer::AttributeValue::String(
406 std::borrow::Cow::Borrowed("my-test-lro-id")
407 )
408 )
409 );
410 }
411
412 #[tokio::test]
413 async fn record_polling_attributes_macro_no_recorder() {
414 let guard = TestLayer::initialize();
415
416 let span =
417 client_request_signals!(info: &InstrumentationClientInfo::default(), method: "test");
418
419 crate::record_polling_attributes!(&span);
420
421 drop(span); let captured = TestLayer::capture(&guard);
424 let got = captured
425 .iter()
426 .find(|s| s.name == "client_request")
427 .unwrap();
428
429 assert!(
430 !got.attributes
431 .contains_key("gcp.longrunning.poll_attempt_count"),
432 "found poll_attempt_count in attributes: {:#?}",
433 got.attributes
434 );
435 }
436
437 #[tokio::test]
438 async fn record_polling_attributes_macro_no_attempt_count() {
439 let guard = TestLayer::initialize();
440
441 let span =
442 client_request_signals!(info: &InstrumentationClientInfo::default(), method: "test");
443
444 let recorder = LroRecorder::new(span.clone());
445
446 recorder
447 .scope(async move {
448 crate::record_polling_attributes!(&span);
449 })
450 .await;
451
452 drop(recorder);
453
454 let captured = TestLayer::capture(&guard);
455 let got = captured
456 .iter()
457 .find(|s| s.name == "client_request")
458 .unwrap();
459
460 assert!(
461 !got.attributes
462 .contains_key("gcp.longrunning.poll_attempt_count"),
463 "found poll_attempt_count in attributes: {:#?}",
464 got.attributes
465 );
466 }
467
468 #[derive(Default)]
469 struct MockDiscoveryOperation {
470 done: bool,
471 error: Option<google_cloud_gax::error::rpc::Status>,
472 }
473
474 impl crate::internal::DiscoveryOperation for MockDiscoveryOperation {
475 fn done(&self) -> bool {
476 self.done
477 }
478
479 fn name(&self) -> Option<&String> {
480 None
481 }
482
483 fn error(&self) -> Option<google_cloud_gax::error::rpc::Status> {
484 self.error.clone()
485 }
486 }
487
488 #[tokio::test]
489 async fn record_discovery_polling_result_success() {
490 let guard = TestLayer::initialize();
491 let span =
492 client_request_signals!(info: &InstrumentationClientInfo::default(), method: "test");
493 let op = MockDiscoveryOperation {
494 done: true,
495 error: None,
496 };
497
498 record_discovery_polling_result!(span, op);
499
500 {
501 let captured = TestLayer::capture(&guard);
502 let got = captured
503 .iter()
504 .find(|s| s.name == "client_request")
505 .unwrap();
506
507 assert_eq!(
508 got.attributes
509 .get("gcp.longrunning.done")
510 .and_then(|v| v.as_bool()),
511 Some(true)
512 );
513 assert_eq!(
514 got.attributes
515 .get("gcp.longrunning.status_code")
516 .and_then(|v| v.as_i64()),
517 Some(0)
518 );
519 assert_eq!(
520 got.attributes
521 .get("otel.status_code")
522 .and_then(|v| v.as_string()),
523 Some("UNSET".to_string())
524 );
525 }
526 }
527
528 #[tokio::test]
529 async fn record_discovery_polling_result_error() {
530 let guard = TestLayer::initialize();
531 let span =
532 client_request_signals!(info: &InstrumentationClientInfo::default(), method: "test");
533 let status = google_cloud_gax::error::rpc::Status::default()
534 .set_code(google_cloud_gax::error::rpc::Code::NotFound)
535 .set_message("not found");
536 let op = MockDiscoveryOperation {
537 done: true,
538 error: Some(status),
539 };
540
541 record_discovery_polling_result!(span, op);
542
543 {
544 let captured = TestLayer::capture(&guard);
545 let got = captured
546 .iter()
547 .find(|s| s.name == "client_request")
548 .unwrap();
549
550 assert_eq!(
551 got.attributes
552 .get("gcp.longrunning.done")
553 .and_then(|v| v.as_bool()),
554 Some(true)
555 );
556 assert_eq!(
557 got.attributes
558 .get("gcp.longrunning.status_code")
559 .and_then(|v| v.as_i64()),
560 Some(google_cloud_gax::error::rpc::Code::NotFound as i64)
561 );
562 assert_eq!(
563 got.attributes
564 .get("otel.status_code")
565 .and_then(|v| v.as_string()),
566 Some("ERROR".to_string())
567 );
568 assert_eq!(
569 got.attributes
570 .get("otel.status_description")
571 .and_then(|v| v.as_string()),
572 Some("not found".to_string())
573 );
574 assert_eq!(
575 got.attributes.get("error.type").and_then(|v| v.as_string()),
576 Some("NOT_FOUND".to_string())
577 );
578 }
579 }
580
581 #[tokio::test]
582 async fn record_discovery_polling_result_in_progress() {
583 let guard = TestLayer::initialize();
584 let span =
585 client_request_signals!(info: &InstrumentationClientInfo::default(), method: "test");
586 let op = MockDiscoveryOperation {
587 done: false,
588 error: None,
589 };
590
591 record_discovery_polling_result!(span, op);
592
593 {
594 let captured = TestLayer::capture(&guard);
595 let got = captured
596 .iter()
597 .find(|s| s.name == "client_request")
598 .unwrap();
599
600 assert_eq!(
601 got.attributes
602 .get("gcp.longrunning.done")
603 .and_then(|v| v.as_bool()),
604 Some(false)
605 );
606 assert!(
607 !got.attributes.contains_key("gcp.longrunning.status_code"),
608 "found status_code in attributes: {:#?}",
609 got.attributes
610 );
611 }
612 }
613}