async_graphql/extensions/
tracing.rs

1use std::sync::Arc;
2
3use futures_util::{TryFutureExt, stream::BoxStream};
4use tracing_futures::Instrument;
5use tracinglib::{Level, span};
6
7use crate::{
8    Response, ServerError, ServerResult, ValidationResult, Value, Variables,
9    extensions::{
10        Extension, ExtensionContext, ExtensionFactory, NextExecute, NextParseQuery, NextRequest,
11        NextResolve, NextSubscribe, NextValidation, ResolveInfo,
12    },
13    parser::types::ExecutableDocument,
14    registry::MetaTypeName,
15};
16
17/// Tracing extension
18///
19/// # References
20///
21/// <https://crates.io/crates/tracing>
22///
23/// # Examples
24///
25/// ```no_run
26/// use async_graphql::{extensions::Tracing, *};
27///
28/// #[derive(SimpleObject)]
29/// struct Query {
30///     value: i32,
31/// }
32///
33/// let schema = Schema::build(Query { value: 100 }, EmptyMutation, EmptySubscription)
34///     .extension(Tracing)
35///     .finish();
36///
37/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
38/// schema.execute(Request::new("{ value }")).await;
39/// # });
40/// ```
41#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
42pub struct Tracing;
43
44impl Tracing {
45    /// Create a configurable tracing extension.
46    ///
47    /// # Example
48    ///
49    /// ```no_run
50    /// use async_graphql::{extensions::Tracing, *};
51    ///
52    /// #[derive(SimpleObject)]
53    /// struct Query {
54    ///     value: i32,
55    /// }
56    ///
57    /// // Trace all fields including scalars
58    /// let schema = Schema::build(Query { value: 100 }, EmptyMutation, EmptySubscription)
59    ///     .extension(Tracing::config().with_trace_scalars(true))
60    ///     .finish();
61    /// ```
62    pub fn config() -> TracingConfig {
63        TracingConfig::default()
64    }
65}
66
67impl ExtensionFactory for Tracing {
68    fn create(&self) -> Arc<dyn Extension> {
69        Arc::new(TracingExtension {
70            trace_scalars: false,
71        })
72    }
73}
74
75/// Configuration for the [`Tracing`] extension.
76#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
77#[derive(Clone, Copy, Debug, Default)]
78pub struct TracingConfig {
79    trace_scalars: bool,
80}
81
82impl TracingConfig {
83    /// Enable or disable tracing for scalar and enum field resolutions.
84    ///
85    /// When `false` (the default), spans are not created for fields that return
86    /// scalar or enum types. This significantly reduces the number of spans
87    /// generated, preventing span explosion in queries with many scalar fields.
88    ///
89    /// When `true`, spans are created for all field resolutions, including
90    /// scalars and enums.
91    pub fn with_trace_scalars(mut self, trace_scalars: bool) -> Self {
92        self.trace_scalars = trace_scalars;
93        self
94    }
95}
96
97impl ExtensionFactory for TracingConfig {
98    fn create(&self) -> Arc<dyn Extension> {
99        Arc::new(TracingExtension {
100            trace_scalars: self.trace_scalars,
101        })
102    }
103}
104
105struct TracingExtension {
106    trace_scalars: bool,
107}
108
109#[async_trait::async_trait]
110impl Extension for TracingExtension {
111    async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
112        next.run(ctx)
113            .instrument(span!(
114                target: "async_graphql::graphql",
115                Level::INFO,
116                "request",
117            ))
118            .await
119    }
120
121    fn subscribe<'s>(
122        &self,
123        ctx: &ExtensionContext<'_>,
124        stream: BoxStream<'s, Response>,
125        next: NextSubscribe<'_>,
126    ) -> BoxStream<'s, Response> {
127        Box::pin(next.run(ctx, stream).instrument(span!(
128            target: "async_graphql::graphql",
129            Level::INFO,
130            "subscribe",
131        )))
132    }
133
134    async fn parse_query(
135        &self,
136        ctx: &ExtensionContext<'_>,
137        query: &str,
138        variables: &Variables,
139        next: NextParseQuery<'_>,
140    ) -> ServerResult<ExecutableDocument> {
141        let span = span!(
142            target: "async_graphql::graphql",
143            Level::INFO,
144            "parse",
145            source = tracinglib::field::Empty
146        );
147        async move {
148            let res = next.run(ctx, query, variables).await;
149            if let Ok(doc) = &res {
150                tracinglib::Span::current()
151                    .record("source", ctx.stringify_execute_doc(doc, variables).as_str());
152            }
153            res
154        }
155        .instrument(span)
156        .await
157    }
158
159    async fn validation(
160        &self,
161        ctx: &ExtensionContext<'_>,
162        next: NextValidation<'_>,
163    ) -> Result<ValidationResult, Vec<ServerError>> {
164        let span = span!(
165            target: "async_graphql::graphql",
166            Level::INFO,
167            "validation"
168        );
169        next.run(ctx).instrument(span).await
170    }
171
172    async fn execute(
173        &self,
174        ctx: &ExtensionContext<'_>,
175        operation_name: Option<&str>,
176        next: NextExecute<'_>,
177    ) -> Response {
178        let span = span!(
179            target: "async_graphql::graphql",
180            Level::INFO,
181            "execute"
182        );
183        next.run(ctx, operation_name).instrument(span).await
184    }
185
186    async fn resolve(
187        &self,
188        ctx: &ExtensionContext<'_>,
189        info: ResolveInfo<'_>,
190        next: NextResolve<'_>,
191    ) -> ServerResult<Option<Value>> {
192        // Check if we should skip tracing for this field
193        let should_trace = if info.is_for_introspection {
194            false
195        } else if !self.trace_scalars {
196            // Check if the return type is a scalar or enum (leaf type)
197            let concrete_type = MetaTypeName::concrete_typename(info.return_type);
198            !ctx.schema_env
199                .registry
200                .types
201                .get(concrete_type)
202                .map(|ty| ty.is_leaf())
203                .unwrap_or(false)
204        } else {
205            true
206        };
207
208        let span = if should_trace {
209            Some(span!(
210                target: "async_graphql::graphql",
211                Level::INFO,
212                "field",
213                path = %info.path_node,
214                parent_type = %info.parent_type,
215                return_type = %info.return_type,
216            ))
217        } else {
218            None
219        };
220
221        let fut = next.run(ctx, info).inspect_err(|err| {
222            tracinglib::info!(
223                target: "async_graphql::graphql",
224                error = %err.message,
225                "error",
226            );
227        });
228        match span {
229            Some(span) => fut.instrument(span).await,
230            None => fut.await,
231        }
232    }
233}