Skip to content

Commit 1c0ce9c

Browse files
authored
Use opentelemetry semantic conventions (#34)
Migrates span field names to use [OpenTelemetry Semantic Convention](https://opentelemetry.io/docs/specs/semconv/) names. This ensures maximum consumer discoverability.
1 parent 28cafc4 commit 1c0ce9c

2 files changed

Lines changed: 132 additions & 59 deletions

File tree

src/graphql.rs

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use async_graphql::{
66
parser::types::{ExecutableDocument, OperationType, Selection},
77
Response, ServerError, ServerResult, ValidationResult, Value, Variables,
88
};
9-
use std::{sync::Arc, time::Instant};
10-
use tracing::{debug, info, span, warn, Instrument, Level};
9+
use std::sync::Arc;
10+
use tracing::{span, warn, Instrument, Level, Span};
1111

1212
/// Adds tracing to the GraphQL flow
1313
///
@@ -35,31 +35,24 @@ impl Extension for LoggingExtension {
3535
next: NextParseQuery<'_>,
3636
) -> ServerResult<ExecutableDocument> {
3737
let span = span!(Level::INFO, "parse");
38-
let now = Instant::now();
3938

4039
async move {
41-
debug!("started parsing request");
42-
let result = next.run(ctx, query, variables).await;
43-
debug!(
44-
latency = format_args!("{} ms", now.elapsed().as_millis()),
45-
"finished parsing request",
46-
);
47-
48-
let document = result?;
40+
let document = next.run(ctx, query, variables).await?;
4941

5042
let is_schema = document
5143
.operations
5244
.iter()
5345
.filter(|(_, operation)| operation.node.ty == OperationType::Query)
5446
.any(|(_, operation)| operation.node.selection_set.node.items.iter().any(|selection| matches!(&selection.node, Selection::Field(field) if field.node.name.node == "__schema")));
5547
if !is_schema {
56-
info!(document = ctx.stringify_execute_doc(&document, variables));
48+
let span = Span::current();
49+
span.record("graphql.document", ctx.stringify_execute_doc(&document, variables));
5750
}
5851

5952
Ok(document)
6053
}
61-
.instrument(span)
62-
.await
54+
.instrument(span)
55+
.await
6356
}
6457

6558
async fn validation(
@@ -68,7 +61,19 @@ impl Extension for LoggingExtension {
6861
next: NextValidation<'_>,
6962
) -> async_graphql::Result<ValidationResult, Vec<ServerError>> {
7063
let span = span!(Level::INFO, "validation");
71-
next.run(ctx).instrument(span).await
64+
async move {
65+
let result = next.run(ctx).await;
66+
67+
if let Ok(result) = result {
68+
let span = Span::current();
69+
span.record("graphql.complexity", result.complexity);
70+
span.record("graphql.depth", result.depth);
71+
}
72+
73+
result
74+
}
75+
.instrument(span)
76+
.await
7277
}
7378

7479
async fn execute(
@@ -77,21 +82,9 @@ impl Extension for LoggingExtension {
7782
operation_name: Option<&str>,
7883
next: NextExecute<'_>,
7984
) -> Response {
80-
let span = span!(Level::INFO, "execute", operation = %operation_name.unwrap_or_default());
81-
let now = Instant::now();
85+
let span = span!(Level::INFO, "execute", graphql.operation.name = %operation_name.unwrap_or_default());
8286

83-
async move {
84-
debug!("started execution");
85-
let response = next.run(ctx, operation_name).await;
86-
debug!(
87-
latency = format_args!("{} ms", now.elapsed().as_millis()),
88-
"finished execution",
89-
);
90-
91-
response
92-
}
93-
.instrument(span)
94-
.await
87+
next.run(ctx, operation_name).instrument(span).await
9588
}
9689

9790
async fn resolve(
@@ -107,23 +100,20 @@ impl Extension for LoggingExtension {
107100

108101
let span = span!(
109102
Level::INFO, "field",
110-
path = %path_node,
111-
parent_type = %info.parent_type,
112-
return_type = %info.return_type,
103+
graphql.node.path = %path_node,
104+
graphql.node.parent_type = %info.parent_type,
105+
graphql.node.return_type = %info.return_type,
113106
);
114-
let now = Instant::now();
115107

116108
async move {
117-
debug!("started resolution");
118109
let result = next.run(ctx, info).await;
119-
debug!(
120-
latency = format_args!("{} ms", now.elapsed().as_millis()),
121-
"finished resolution",
122-
);
123110

124111
// we do the actual error handling elsewhere, these errors are typically caused by
125112
// user error.
126113
if let Err(ref err) = result {
114+
let span = Span::current();
115+
span.record("error", true);
116+
span.record("error.type", "graphql");
127117
warn!(error = %err.message, locations = ?err.locations, path = ?err.path);
128118
}
129119

src/http.rs

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,132 @@
1-
use http::Request;
2-
#[cfg(feature = "opentelemetry")]
3-
use opentelemetry_sdk::propagation::TraceContextPropagator;
1+
use http::{header, HeaderMap, Request, Response};
2+
use std::time::Duration;
43
use tower_http::{
5-
classify::{ServerErrorsAsFailures, SharedClassifier},
6-
trace::{DefaultOnRequest, DefaultOnResponse, MakeSpan, TraceLayer},
4+
classify::{SharedClassifier, StatusInRangeAsFailures, StatusInRangeFailureClass},
5+
trace::{self, TraceLayer},
76
};
8-
use tracing::{span, Level, Span};
7+
use tracing::{info, span, Level, Span};
98
use uuid::Uuid;
109

1110
/// Creates a custom tracing span
12-
#[derive(Clone, Debug, Default)]
13-
pub struct MakeSpanWithId {
14-
#[cfg(feature = "opentelemetry")]
15-
propagator: TraceContextPropagator,
16-
}
11+
#[derive(Clone, Copy, Debug)]
12+
pub struct MakeSpan;
1713

18-
impl<B> MakeSpan<B> for MakeSpanWithId {
14+
impl<B> trace::MakeSpan<B> for MakeSpan {
1915
fn make_span(&mut self, request: &Request<B>) -> Span {
16+
let uri = request.uri();
2017
let span = span!(
2118
Level::INFO,
2219
"request",
23-
method = %request.method(),
24-
uri = %request.uri(),
25-
version = ?request.version(),
20+
span.kind = "server",
21+
http.request.method = %request.method(),
22+
network.protocol.version = ?request.version(),
23+
uri.path = uri.path(),
24+
uri.scheme = uri.scheme_str().unwrap_or_default(),
25+
uri.query = uri.query().unwrap_or_default(),
2626
id = %Uuid::new_v4(),
2727
);
2828

29+
let headers = request.headers();
30+
if let Some(user_agent) = headers
31+
.get(header::USER_AGENT)
32+
.and_then(|h| h.to_str().ok())
33+
{
34+
span.record("user_agent.original", user_agent);
35+
}
36+
37+
record_headers("request", headers, &span);
38+
2939
#[cfg(feature = "opentelemetry")]
3040
{
3141
use opentelemetry::propagation::TextMapPropagator;
3242
use opentelemetry_http::HeaderExtractor;
43+
use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};
3344
use tracing_opentelemetry::OpenTelemetrySpanExt;
3445

35-
let context = self.propagator.extract(&HeaderExtractor(request.headers()));
46+
let extractor = HeaderExtractor(headers);
47+
let context = TraceContextPropagator::new().extract(&extractor);
48+
let context = BaggagePropagator::new().extract_with_context(&context, &extractor);
3649
span.set_parent(context);
3750
}
3851

3952
span
4053
}
4154
}
4255

56+
/// Adds custom trace metadata from the request
57+
#[derive(Clone, Copy, Debug)]
58+
pub struct OnRequest;
59+
60+
impl<B> trace::OnRequest<B> for OnRequest {
61+
fn on_request(&mut self, _request: &Request<B>, _span: &Span) {
62+
info!("started processing request");
63+
}
64+
}
65+
66+
/// Adds custom trace metadata from the request
67+
#[derive(Clone, Copy, Debug)]
68+
pub struct OnResponse;
69+
70+
impl<B> trace::OnResponse<B> for OnResponse {
71+
fn on_response(self, response: &Response<B>, _latency: Duration, span: &Span) {
72+
span.record("http.response.status", response.status().as_u16());
73+
record_headers("response", response.headers(), span);
74+
info!("finished processing request");
75+
}
76+
}
77+
78+
/// Adds custom trace metadata when a failure occurs
79+
#[derive(Clone, Copy, Debug)]
80+
pub struct OnFailure;
81+
82+
impl trace::OnFailure<StatusInRangeFailureClass> for OnFailure {
83+
fn on_failure(&mut self, class: StatusInRangeFailureClass, _latency: Duration, span: &Span) {
84+
if span.has_field("error") || span.has_field("error.type") {
85+
return;
86+
}
87+
88+
span.record("error", true);
89+
90+
let kind = match class {
91+
StatusInRangeFailureClass::StatusCode(status) => {
92+
if status.is_client_error() {
93+
"client_error"
94+
} else {
95+
"server_error"
96+
}
97+
}
98+
StatusInRangeFailureClass::Error(_) => "internal_error",
99+
};
100+
span.record("error.type", kind);
101+
}
102+
}
103+
43104
/// Create a logging middleware layer
44-
pub fn layer() -> TraceLayer<SharedClassifier<ServerErrorsAsFailures>, MakeSpanWithId> {
45-
TraceLayer::new_for_http()
46-
.make_span_with(MakeSpanWithId::default())
47-
.on_request(DefaultOnRequest::new().level(Level::INFO))
48-
.on_response(DefaultOnResponse::new().level(Level::INFO))
105+
pub fn layer() -> TraceLayer<
106+
SharedClassifier<StatusInRangeAsFailures>,
107+
MakeSpan,
108+
OnRequest,
109+
OnResponse,
110+
(),
111+
(),
112+
OnFailure,
113+
> {
114+
let classifier = StatusInRangeAsFailures::new_for_client_and_server_errors();
115+
TraceLayer::new(classifier.into_make_classifier())
116+
.make_span_with(MakeSpan)
117+
.on_request(OnRequest)
118+
.on_response(OnResponse)
119+
.on_failure(OnFailure)
120+
.on_eos(())
121+
.on_body_chunk(())
122+
}
123+
124+
fn record_headers(direction: &'static str, headers: &HeaderMap, span: &Span) {
125+
for (header, value) in headers
126+
.iter()
127+
.filter_map(|(header, value)| value.to_str().ok().map(|value| (header, value)))
128+
{
129+
let header = header.as_str().to_lowercase();
130+
span.record(format!("http.{direction}.header.{header}").as_str(), value);
131+
}
49132
}

0 commit comments

Comments
 (0)