Skip to main content

tork_core/logging/
span.rs

1//! Scoped spans built from a [`Logger`](crate::Logger).
2//!
3//! A span groups the logs of an operation and carries its fields for trace
4//! exporters. Enter one for a synchronous scope, or wrap a future so the span
5//! stays active across its `await` points.
6
7use std::future::Future;
8use std::sync::Arc;
9
10use serde::Serialize;
11use serde_json::{Map, Value};
12use tracing::span::EnteredSpan;
13use tracing::Instrument;
14
15/// A span being built. Add fields, then [`enter`](LogSpan::enter) a scope or
16/// [`run`](LogSpan::run) a future inside it.
17#[must_use = "a LogSpan does nothing until enter() or run() is called"]
18pub struct LogSpan {
19    context: Arc<str>,
20    name: String,
21    fields: Map<String, Value>,
22}
23
24impl LogSpan {
25    /// Builds a span for an operation named `name`, seeded with `base` fields.
26    pub(crate) fn new(
27        context: Arc<str>,
28        name: impl Into<String>,
29        fields: Map<String, Value>,
30    ) -> Self {
31        Self {
32            context,
33            name: name.into(),
34            fields,
35        }
36    }
37
38    /// Attaches a field to the span. A non-serializable value is skipped.
39    pub fn field<T: Serialize>(mut self, key: &'static str, value: T) -> Self {
40        if let Ok(value) = serde_json::to_value(value) {
41            self.fields.insert(key.to_owned(), value);
42        }
43        self
44    }
45
46    /// Builds the underlying `tracing` span.
47    fn build(&self) -> tracing::Span {
48        let fields = serde_json::to_string(&Value::Object(self.fields.clone()))
49            .unwrap_or_else(|_| "{}".to_owned());
50        let context = self.context.as_ref();
51        let name = self.name.as_str();
52        tracing::info_span!(
53            "op",
54            tork.context = %context,
55            tork.op = %name,
56            tork.fields = %fields
57        )
58    }
59
60    /// Enters the span, returning a guard that exits it when dropped.
61    pub fn enter(self) -> EnteredSpan {
62        self.build().entered()
63    }
64
65    /// Runs `future` inside the span, so all of its logs are grouped under it.
66    pub async fn run<F: Future>(self, future: F) -> F::Output {
67        future.instrument(self.build()).await
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use crate::logging::Logger;
74
75    #[tokio::test]
76    async fn instrument_runs_the_future_and_returns_its_value() {
77        let logger = Logger::new("Worker");
78        let output = logger
79            .instrument("job")
80            .field("attempt", 1)
81            .run(async { 21 * 2 })
82            .await;
83        assert_eq!(output, 42);
84    }
85
86    #[test]
87    fn span_enters_a_scope() {
88        let logger = Logger::new("Worker");
89        let guard = logger.span("scope").field("key", "value").enter();
90        drop(guard);
91    }
92}