mockforge_graphql/
graphql_tracing.rs

1//! Distributed tracing for GraphQL queries and mutations
2
3use mockforge_tracing::{create_request_span, record_error, record_success, Protocol};
4use opentelemetry::{global::BoxedSpan, trace::Span, KeyValue};
5use std::collections::HashMap;
6use tracing::debug;
7
8/// Create a span for a GraphQL query/mutation
9pub fn create_graphql_span(
10    operation_type: &str,
11    operation_name: Option<&str>,
12    query: &str,
13) -> BoxedSpan {
14    let span_name = if let Some(name) = operation_name {
15        format!("GraphQL {} {}", operation_type, name)
16    } else {
17        format!("GraphQL {}", operation_type)
18    };
19
20    let mut attributes = vec![
21        KeyValue::new("graphql.operation.type", operation_type.to_string()),
22        KeyValue::new("graphql.document", query.to_string()),
23    ];
24
25    if let Some(name) = operation_name {
26        attributes.push(KeyValue::new("graphql.operation.name", name.to_string()));
27    }
28
29    create_request_span(Protocol::GraphQL, &span_name, attributes)
30}
31
32/// Record successful GraphQL query execution
33pub fn record_graphql_success(
34    span: &mut BoxedSpan,
35    duration_ms: u64,
36    field_count: usize,
37    resolver_calls: usize,
38) {
39    let attributes = vec![
40        KeyValue::new("graphql.duration_ms", duration_ms as i64),
41        KeyValue::new("graphql.fields_resolved", field_count as i64),
42        KeyValue::new("graphql.resolver_calls", resolver_calls as i64),
43    ];
44
45    record_success(span, attributes);
46
47    debug!(
48        duration_ms = duration_ms,
49        field_count = field_count,
50        resolver_calls = resolver_calls,
51        "GraphQL query completed successfully"
52    );
53}
54
55/// Record GraphQL query error
56pub fn record_graphql_error(
57    span: &mut BoxedSpan,
58    error_message: &str,
59    error_path: Option<Vec<String>>,
60    duration_ms: u64,
61) {
62    span.set_attribute(KeyValue::new("graphql.duration_ms", duration_ms as i64));
63    span.set_attribute(KeyValue::new("graphql.error.message", error_message.to_string()));
64
65    if let Some(path) = error_path {
66        span.set_attribute(KeyValue::new("graphql.error.path", format!("{:?}", path)));
67    }
68
69    record_error(span, error_message);
70
71    debug!(error_message = error_message, duration_ms = duration_ms, "GraphQL query failed");
72}
73
74/// Create a span for a specific field resolver
75pub fn create_resolver_span(parent_type: &str, field_name: &str) -> BoxedSpan {
76    create_request_span(
77        Protocol::GraphQL,
78        &format!("Resolve {}.{}", parent_type, field_name),
79        vec![
80            KeyValue::new("graphql.resolver.parent_type", parent_type.to_string()),
81            KeyValue::new("graphql.resolver.field_name", field_name.to_string()),
82        ],
83    )
84}
85
86/// Record resolver success
87pub fn record_resolver_success(span: &mut BoxedSpan, duration_us: u64) {
88    let attributes = vec![KeyValue::new(
89        "graphql.resolver.duration_us",
90        duration_us as i64,
91    )];
92
93    record_success(span, attributes);
94}
95
96/// Record resolver error
97pub fn record_resolver_error(span: &mut BoxedSpan, error_message: &str) {
98    record_error(span, error_message);
99}
100
101/// Extract trace context from GraphQL request headers
102pub fn extract_graphql_trace_context(headers: &HashMap<String, String>) -> opentelemetry::Context {
103    mockforge_tracing::extract_trace_context(headers)
104}
105
106/// Inject trace context into GraphQL response headers
107pub fn inject_graphql_trace_context(
108    ctx: &opentelemetry::Context,
109    headers: &mut HashMap<String, String>,
110) {
111    mockforge_tracing::inject_trace_context(ctx, headers);
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use opentelemetry::global;
118    use opentelemetry_sdk::propagation::TraceContextPropagator;
119
120    #[test]
121    fn test_create_graphql_span() {
122        global::set_text_map_propagator(TraceContextPropagator::new());
123
124        let query = "query GetUser($id: ID!) { user(id: $id) { name } }";
125        let mut span = create_graphql_span("query", Some("GetUser"), query);
126        record_graphql_success(&mut span, 150, 3, 5);
127    }
128
129    #[test]
130    fn test_create_resolver_span() {
131        global::set_text_map_propagator(TraceContextPropagator::new());
132
133        let mut span = create_resolver_span("User", "name");
134        record_resolver_success(&mut span, 250);
135    }
136
137    #[test]
138    fn test_graphql_trace_context_propagation() {
139        global::set_text_map_propagator(TraceContextPropagator::new());
140
141        let mut headers = HashMap::new();
142        headers.insert(
143            "traceparent".to_string(),
144            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
145        );
146
147        let ctx = extract_graphql_trace_context(&headers);
148
149        let mut output_headers = HashMap::new();
150        inject_graphql_trace_context(&ctx, &mut output_headers);
151
152        assert!(output_headers.contains_key("traceparent"));
153    }
154}