uhg_custom_appollo_roouter/plugins/telemetry/
span_factory.rs

1use schemars::JsonSchema;
2use serde::Deserialize;
3use tracing::error_span;
4use tracing::info_span;
5
6use crate::context::OPERATION_NAME;
7use crate::plugins::telemetry::Telemetry;
8use crate::plugins::telemetry::consts::REQUEST_SPAN_NAME;
9use crate::plugins::telemetry::consts::ROUTER_SPAN_NAME;
10use crate::plugins::telemetry::consts::SUBGRAPH_SPAN_NAME;
11use crate::plugins::telemetry::consts::SUPERGRAPH_SPAN_NAME;
12use crate::services::SubgraphRequest;
13use crate::services::SupergraphRequest;
14use crate::tracer::TraceId;
15use crate::uplink::license_enforcement::LICENSE_EXPIRED_SHORT_MESSAGE;
16use crate::uplink::license_enforcement::LicenseState;
17
18#[derive(Debug, Copy, Clone, Deserialize, JsonSchema, Default, Eq, PartialEq)]
19/// Span mode to create new or deprecated spans
20#[serde(rename_all = "snake_case")]
21pub(crate) enum SpanMode {
22    /// Keep the request span as root span and deprecated attributes. This option will eventually removed.
23    #[default]
24    Deprecated,
25    /// Use new OpenTelemetry spec compliant span attributes or preserve existing. This will be the default in future.
26    SpecCompliant,
27}
28
29impl SpanMode {
30    pub(crate) fn create_request<B>(
31        &self,
32        request: &http::Request<B>,
33        license_state: LicenseState,
34    ) -> ::tracing::span::Span {
35        // HCP customization: add lables
36        let (azure_region, consumer_name, role_id, correlation_id, cid) = crate::uhg_custom::get_uhg_labels(Some(request.headers()), None);
37
38        match self {
39            SpanMode::Deprecated => {
40                if matches!(
41                    license_state,
42                    LicenseState::LicensedWarn | LicenseState::LicensedHalt
43                ) {
44                    error_span!(
45                        REQUEST_SPAN_NAME,
46                        // HCP customization - begin!
47                        "consumerName" = consumer_name,
48                        "roles" = role_id,
49                        "correlationId" = correlation_id,
50                        "cid" = cid,
51                        "azure_region" = azure_region,
52                        // HCP customization - end!
53                        "http.method" = %request.method(),
54                        "http.request.method" = %request.method(),
55                        "http.route" = %request.uri().path(),
56                        "http.flavor" = ?request.version(),
57                        "http.status" = 500, // This prevents setting later
58                        "otel.name" = ::tracing::field::Empty,
59                        "otel.kind" = "SERVER",
60                        "graphql.operation.name" = ::tracing::field::Empty,
61                        "graphql.operation.type" = ::tracing::field::Empty,
62                        "apollo_router.license" = LICENSE_EXPIRED_SHORT_MESSAGE,
63                        "apollo_private.request" = true,
64                    )
65                } else {
66                    info_span!(
67                        REQUEST_SPAN_NAME,
68                        // HCP customization - begin!
69                        "consumerName" = consumer_name,
70                        "roles" = role_id,
71                        "correlationId" = correlation_id,
72                        "cid" = cid,
73                        "azure_region" = azure_region,
74                        // HCP customization - end!
75                        "http.method" = %request.method(),
76                        "http.request.method" = %request.method(),
77                        "http.route" = %request.uri().path(),
78                        "http.flavor" = ?request.version(),
79                        "otel.name" = ::tracing::field::Empty,
80                        "otel.kind" = "SERVER",
81                        "graphql.operation.name" = ::tracing::field::Empty,
82                        "graphql.operation.type" = ::tracing::field::Empty,
83                        "apollo_private.request" = true,
84                    )
85                }
86            }
87            SpanMode::SpecCompliant => {
88                unreachable!("this code path should not be reachable, this is a bug!")
89            }
90        }
91    }
92
93    pub(crate) fn create_router<B>(&self, request: &http::Request<B>) -> ::tracing::span::Span {
94        match self {
95            SpanMode::Deprecated => {
96                let trace_id = TraceId::maybe_new()
97                    .map(|t| t.to_string())
98                    .unwrap_or_default();
99                let span = info_span!(ROUTER_SPAN_NAME,
100                    "http.method" = %request.method(),
101                    "http.request.method" = %request.method(),
102                    "http.route" = %request.uri().path(),
103                    "http.flavor" = ?request.version(),
104                    "trace_id" = %trace_id,
105                    "client.name" = ::tracing::field::Empty,
106                    "client.version" = ::tracing::field::Empty,
107                    "otel.kind" = "INTERNAL",
108                    "otel.status_code" = ::tracing::field::Empty,
109                    "apollo_private.duration_ns" = ::tracing::field::Empty,
110                    "apollo_private.http.request_headers" = ::tracing::field::Empty,
111                    "apollo_private.http.response_headers" = ::tracing::field::Empty
112                );
113                span
114            }
115            SpanMode::SpecCompliant => {
116                info_span!(ROUTER_SPAN_NAME,
117                    // Needed for apollo_telemetry and datadog span mapping
118                    "http.route" = %request.uri().path(),
119                    "http.request.method" = %request.method(),
120                    "otel.name" = ::tracing::field::Empty,
121                    "otel.kind" = "SERVER",
122                    "otel.status_code" = ::tracing::field::Empty,
123                    "apollo_router.license" = ::tracing::field::Empty,
124                    "apollo_private.duration_ns" = ::tracing::field::Empty,
125                    "apollo_private.http.request_headers" = ::tracing::field::Empty,
126                    "apollo_private.http.response_headers" = ::tracing::field::Empty,
127                    "apollo_private.request" = true,
128                )
129            }
130        }
131    }
132
133    pub(crate) fn create_supergraph(
134        &self,
135        config: &crate::plugins::telemetry::apollo::Config,
136        request: &SupergraphRequest,
137        field_level_instrumentation_ratio: f64,
138    ) -> ::tracing::span::Span {
139        match self {
140            SpanMode::Deprecated => {
141                let send_variable_values = config.send_variable_values.clone();
142                let span = info_span!(
143                    SUPERGRAPH_SPAN_NAME,
144                    otel.kind = "INTERNAL",
145                    graphql.operation.name = ::tracing::field::Empty,
146                    graphql.document = request
147                        .supergraph_request
148                        .body()
149                        .query
150                        .as_deref()
151                        .unwrap_or_default(),
152                    apollo_private.field_level_instrumentation_ratio =
153                        field_level_instrumentation_ratio,
154                    apollo_private.operation_signature = ::tracing::field::Empty,
155                    apollo_private.graphql.variables = Telemetry::filter_variables_values(
156                        &request.supergraph_request.body().variables,
157                        &send_variable_values,
158                    ),
159                );
160
161                if let Some(operation_name) = request
162                    .context
163                    .get::<_, String>(OPERATION_NAME)
164                    .unwrap_or_default()
165                {
166                    span.record("graphql.operation.name", operation_name);
167                }
168                span
169            }
170            SpanMode::SpecCompliant => {
171                let send_variable_values = config.send_variable_values.clone();
172                info_span!(
173                    SUPERGRAPH_SPAN_NAME,
174                    "otel.kind" = "INTERNAL",
175                    apollo_private.field_level_instrumentation_ratio =
176                        field_level_instrumentation_ratio,
177                    apollo_private.operation_signature = ::tracing::field::Empty,
178                    apollo_private.graphql.variables = Telemetry::filter_variables_values(
179                        &request.supergraph_request.body().variables,
180                        &send_variable_values,
181                    )
182                )
183            }
184        }
185    }
186
187    pub(crate) fn create_subgraph(
188        &self,
189        subgraph_name: &str,
190        req: &SubgraphRequest,
191    ) -> ::tracing::span::Span {
192        // HCP customization: Add label azure_region and consumer_name to subgraph span
193        let (azure_region, consumer_name, _, _, _) = crate::uhg_custom::get_uhg_labels(None, Some(&req.context));
194
195        match self {
196            SpanMode::Deprecated => {
197                let query = req
198                    .subgraph_request
199                    .body()
200                    .query
201                    .as_deref()
202                    .unwrap_or_default();
203                let operation_name = req
204                    .subgraph_request
205                    .body()
206                    .operation_name
207                    .as_deref()
208                    .unwrap_or_default();
209
210                info_span!(
211                    SUBGRAPH_SPAN_NAME,
212                    "apollo.subgraph.name" = subgraph_name,
213                    graphql.document = query,
214                    graphql.operation.name = operation_name,
215                    "otel.kind" = "INTERNAL",
216                    "apollo_private.ftv1" = ::tracing::field::Empty,
217                    "otel.status_code" = ::tracing::field::Empty,
218                    // HCP customization - begin!
219                    azure_region = %azure_region,
220                    consumer_name = %consumer_name,
221                    // HCP customization - end!
222                )
223            }
224            SpanMode::SpecCompliant => {
225                info_span!(
226                    SUBGRAPH_SPAN_NAME,
227                    "otel.kind" = "INTERNAL",
228                    "apollo_private.ftv1" = ::tracing::field::Empty,
229                    "otel.status_code" = ::tracing::field::Empty,
230                    // HCP customization - begin!
231                    azure_region = %azure_region,
232                    consumer_name = %consumer_name,
233                    // HCP customization - end!
234                )
235            }
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use opentelemetry_api::Key;
243    use tracing_subscriber::layer::SubscriberExt;
244
245    use crate::plugins::telemetry::SpanMode;
246    use crate::plugins::telemetry::consts::REQUEST_SPAN_NAME;
247    use crate::plugins::telemetry::consts::ROUTER_SPAN_NAME;
248    use crate::plugins::telemetry::otel::layer;
249    use crate::plugins::telemetry::otel::layer::tests::TestTracer;
250    use crate::uplink::license_enforcement::LicenseState;
251
252    #[test]
253    fn test_specific_span() {
254        // NB: this test checks the attributes of a specific span. In 2.x this uses
255        // `tracing_mock`.
256        let tracer = TestTracer::default();
257        let subscriber = tracing_subscriber::registry()
258            .with(layer().force_sampling().with_tracer(tracer.clone()));
259
260        let request = http::Request::builder()
261            .method("GET")
262            .uri("http://example.com/path/to/location?with=query&another=UN1QU3_query")
263            .header("apollographql-client-name", "client")
264            .body("useful info")
265            .unwrap();
266
267        tracing::subscriber::with_default(subscriber, || {
268            let span = SpanMode::SpecCompliant.create_router(&request);
269            let _guard = span.enter();
270            tracing::info!("event");
271        });
272
273        let span = tracer.with_data(|data| data.builder.clone());
274        let span_attributes = span.attributes.unwrap();
275        let span_events = span.events.unwrap();
276        assert_eq!(span.name, "router");
277        assert_eq!(span_events[0].name, "event");
278
279        let get_attribute = |key| {
280            span_attributes
281                .get(&Key::from_static_str(key))
282                .unwrap()
283                .as_str()
284        };
285        assert_eq!(get_attribute("http.route"), "/path/to/location");
286        assert_eq!(get_attribute("http.request.method"), "GET");
287        assert_eq!(get_attribute("apollo_private.request"), "true");
288    }
289
290    #[test]
291    fn test_http_route_on_array_of_router_spans() {
292        let expected_routes = [
293            ("https://www.example.com/", "/"),
294            ("https://www.example.com/path", "/path"),
295            ("http://example.com/path/to/location", "/path/to/location"),
296            ("http://www.example.com/path?with=query", "/path"),
297            ("/foo/bar?baz", "/foo/bar"),
298        ];
299
300        let span_modes = [SpanMode::SpecCompliant, SpanMode::Deprecated];
301        let license_states = [LicenseState::LicensedHalt, LicenseState::Unlicensed];
302        let http_route_key = Key::from_static_str("http.route");
303
304        for (uri, expected_route) in expected_routes {
305            let request = http::Request::builder().uri(uri).body("").unwrap();
306
307            // test `request` spans
308            for license_state in license_states {
309                let tracer = TestTracer::default();
310                let subscriber = tracing_subscriber::registry()
311                    .with(layer().force_sampling().with_tracer(tracer.clone()));
312                tracing::subscriber::with_default(subscriber, || {
313                    let span = SpanMode::Deprecated.create_request(&request, license_state);
314                    let _guard = span.enter();
315                });
316
317                let span = tracer.with_data(|data| data.builder.clone());
318                let span_attributes = span.attributes.unwrap();
319                let span_route = span_attributes.get(&http_route_key).unwrap();
320                assert_eq!(span_route.as_str(), expected_route);
321                assert_eq!(span.name, REQUEST_SPAN_NAME);
322            }
323
324            // test `router` spans
325            for span_mode in span_modes {
326                let tracer = TestTracer::default();
327                let subscriber = tracing_subscriber::registry()
328                    .with(layer().force_sampling().with_tracer(tracer.clone()));
329                tracing::subscriber::with_default(subscriber, || {
330                    let span = span_mode.create_router(&request);
331                    let _guard = span.enter();
332                });
333
334                let span = tracer.with_data(|data| data.builder.clone());
335                let span_attributes = span.attributes.unwrap();
336                let span_route = span_attributes.get(&http_route_key).unwrap();
337                assert_eq!(span_route.as_str(), expected_route);
338                assert_eq!(span.name, ROUTER_SPAN_NAME);
339            }
340        }
341    }
342}