Skip to content

Commit f0977e5

Browse files
authored
Update axum, tracing integrations and tests for groups and named payloads (#62)
Integration layer and test updates: - Axum HTTP observer uses groups for request/response hierarchy - Tracing layer uses groups for span-based observation grouping - All tests updated for multi-payload and group APIs
1 parent f4bdd9f commit f0977e5

File tree

11 files changed

+349
-204
lines changed

11 files changed

+349
-204
lines changed

crates/observation-tools-client/src/axum/request_observer.rs

Lines changed: 45 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::context;
2-
use crate::execution::ExecutionHandle;
2+
use crate::group::GroupBuilder;
33
use crate::observation::ObservationBuilder;
4+
use crate::observation_handle::ObservationPayloadHandle;
45
use axum::body::Body;
56
use axum::extract::Request;
67
use axum::response::Response;
@@ -28,30 +29,21 @@ use tower::Layer;
2829
use tower::Service;
2930

3031
/// State shared between the streaming body and the observation emitter.
31-
/// This is used to collect data as it streams and emit the observation when
32-
/// complete. The observation is emitted in the Drop implementation when the
33-
/// body is finished.
32+
/// This is used to collect data as it streams and add the body payload when
33+
/// complete. The body payload is added in the Drop implementation via the
34+
/// ObservationPayloadHandle.
3435
struct StreamingObserverState {
3536
buffer: BytesMut,
3637
content_type: String,
37-
log_level: LogLevel,
38-
status_code: u16,
39-
execution: ExecutionHandle,
38+
payload_handle: ObservationPayloadHandle,
4039
}
4140

4241
impl StreamingObserverState {
43-
fn new(
44-
content_type: String,
45-
log_level: LogLevel,
46-
status_code: u16,
47-
execution: ExecutionHandle,
48-
) -> Self {
42+
fn new(content_type: String, payload_handle: ObservationPayloadHandle) -> Self {
4943
Self {
5044
buffer: BytesMut::new(),
5145
content_type,
52-
log_level,
53-
status_code,
54-
execution,
46+
payload_handle,
5547
}
5648
}
5749

@@ -64,7 +56,7 @@ impl Drop for StreamingObserverState {
6456
fn drop(&mut self) {
6557
let bytes = self.buffer.clone().freeze();
6658
tracing::debug!(
67-
"StreamingObserverBody: emitting observation with {} bytes on drop",
59+
"StreamingObserverBody: adding body payload with {} bytes on drop",
6860
bytes.len()
6961
);
7062
let payload = Payload {
@@ -73,19 +65,14 @@ impl Drop for StreamingObserverState {
7365
size: bytes.len(),
7466
};
7567

76-
ObservationBuilder::new("http/response/body")
77-
.label("http/response")
78-
.label("http/response/body")
79-
.metadata("status", &self.status_code.to_string())
80-
.log_level(self.log_level)
81-
.execution(&self.execution)
82-
.payload(payload);
68+
self.payload_handle.raw_payload("body", payload);
8369
}
8470
}
8571

8672
pin_project! {
8773
/// A body wrapper that streams data through while collecting it for observation.
88-
/// When the stream completes, it emits the observation with the collected body.
74+
/// When the stream completes, it adds the body as a named payload to the
75+
/// existing response observation.
8976
pub struct StreamingObserverBody {
9077
#[pin]
9178
inner: Body,
@@ -94,20 +81,12 @@ pin_project! {
9481
}
9582

9683
impl StreamingObserverBody {
97-
fn new(
98-
inner: Body,
99-
content_type: String,
100-
log_level: LogLevel,
101-
status_code: u16,
102-
execution: ExecutionHandle,
103-
) -> Self {
84+
fn new(inner: Body, content_type: String, payload_handle: ObservationPayloadHandle) -> Self {
10485
Self {
10586
inner,
10687
state: Arc::new(Mutex::new(StreamingObserverState::new(
10788
content_type,
108-
log_level,
109-
status_code,
110-
execution,
89+
payload_handle,
11190
))),
11291
}
11392
}
@@ -247,27 +226,33 @@ where
247226
};
248227

249228
let (parts, body) = req.into_parts();
250-
ObservationBuilder::new("http/request/headers")
251-
.label("http/request")
252-
.label("http/request/headers")
229+
230+
// Create a single group for the HTTP exchange
231+
let http_group = GroupBuilder::new("http_request")
253232
.metadata("method", parts.method.to_string())
254233
.metadata("uri", parts.uri.to_string())
255-
.serde(&json!(filter_headers(
256-
&parts.headers,
257-
&config.excluded_headers
258-
)));
234+
.build_with_execution(&execution)
235+
.into_handle();
259236

237+
// Collect request body
260238
let request_body_bytes = body
261239
.collect()
262240
.await
263241
.map(|collected| collected.to_bytes())
264242
.unwrap_or_else(|_| Bytes::new());
265-
ObservationBuilder::new("http/request/body")
266-
.label("http/request")
267-
.label("http/request/body")
243+
244+
// Single request observation with named payloads: "headers" + "body"
245+
let headers_json = json!(filter_headers(&parts.headers, &config.excluded_headers));
246+
let headers_payload = Payload::json(
247+
serde_json::to_string(&headers_json).unwrap_or_default(),
248+
);
249+
ObservationBuilder::new("http/request")
250+
.group(&http_group)
268251
.metadata("method", parts.method.to_string())
269252
.metadata("uri", parts.uri.to_string())
270-
.payload(bytes_to_payload(&request_body_bytes, &parts.headers));
253+
.execution(&execution)
254+
.named_raw_payload("headers", headers_payload)
255+
.raw_payload("body", bytes_to_payload(&request_body_bytes, &parts.headers));
271256

272257
let response = inner
273258
.call(Request::from_parts(parts, Body::from(request_body_bytes)))
@@ -280,31 +265,30 @@ where
280265
500..=599 => LogLevel::Error,
281266
_ => LogLevel::Info,
282267
};
283-
ObservationBuilder::new("http/response/headers")
284-
.label("http/response")
285-
.label("http/response/headers")
268+
269+
// Single response observation with named payload "headers" sent immediately,
270+
// "body" added later when streaming completes via the payload handle
271+
let resp_headers_json = json!(filter_headers(&parts.headers, &config.excluded_headers));
272+
let resp_headers_payload = Payload::json(
273+
serde_json::to_string(&resp_headers_json).unwrap_or_default(),
274+
);
275+
let payload_handle = ObservationBuilder::new("http/response")
276+
.group(&http_group)
286277
.metadata("status", &parts.status.as_u16().to_string())
287278
.log_level(log_level)
288-
.serde(&json!(filter_headers(
289-
&parts.headers,
290-
&config.excluded_headers
291-
)));
279+
.execution(&execution)
280+
.named_raw_payload("headers", resp_headers_payload);
292281

293282
// Wrap the response body in a streaming observer that captures data as it flows
294-
// through and emits the observation when the stream completes
283+
// through and adds the body payload when the stream completes
295284
let content_type = parts
296285
.headers
297286
.get(CONTENT_TYPE)
298287
.and_then(|v| v.to_str().ok())
299288
.unwrap_or("application/octet-stream")
300289
.to_string();
301-
let streaming_body = StreamingObserverBody::new(
302-
body,
303-
content_type,
304-
log_level,
305-
parts.status.as_u16(),
306-
execution,
307-
);
290+
let streaming_body =
291+
StreamingObserverBody::new(body, content_type, payload_handle);
308292

309293
Ok(Response::from_parts(parts, Body::new(streaming_body)))
310294
})

crates/observation-tools-client/src/tracing/layer.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use super::span_data::SpanData;
22
use crate::context;
3+
use crate::group::GroupHandle;
34
use crate::observation::ObservationBuilder;
5+
use observation_tools_shared::GroupId;
46
use observation_tools_shared::LogLevel;
57
use observation_tools_shared::ObservationType;
68
use observation_tools_shared::Payload;
@@ -70,9 +72,9 @@ where
7072

7173
fn on_close(&self, id: Id, ctx: Context<'_, S>) {
7274
// Skip if no execution context
73-
if context::get_current_execution().is_none() {
75+
let Some(execution) = context::get_current_execution() else {
7476
return;
75-
}
77+
};
7678

7779
let Some(span) = ctx.span(&id) else {
7880
return;
@@ -89,17 +91,20 @@ where
8991
let duration_ms = duration.as_secs_f64() * 1000.0;
9092

9193
// Get the span's own ID and parent span ID
92-
let span_id = id.into_u64().to_string();
94+
let span_id = id.into_u64();
9395
let parent_span_id = span
9496
.parent()
9597
.map(|parent| parent.id().into_u64().to_string());
9698

99+
// Create a group handle from the span ID
100+
let group_handle = GroupHandle::from_id(GroupId::from(span_id.to_string()), &execution);
101+
97102
// Build and send observation
98103
let builder = ObservationBuilder::new(&data.name)
99104
.observation_type(ObservationType::Span)
100105
.log_level(tracing_level_to_log_level(data.level))
101-
.label(format!("tracing/spans/{}", data.target))
102-
.metadata("span_id", &span_id)
106+
.group(&group_handle)
107+
.metadata("span_id", &span_id.to_string())
103108
.metadata("duration_ms", format!("{:.3}", duration_ms))
104109
.metadata("target", &data.target);
105110

@@ -127,9 +132,9 @@ where
127132
}
128133
}
129134

130-
if context::get_current_execution().is_none() {
135+
let Some(execution) = context::get_current_execution() else {
131136
return;
132-
}
137+
};
133138

134139
let mut visitor = FieldVisitor::new();
135140
event.record(&mut visitor);
@@ -153,13 +158,19 @@ where
153158
.unwrap_or_default();
154159

155160
// Get current span from tracing's span stack for parent attribution
156-
let parent_span_id = ctx.current_span().id().map(|id| id.into_u64().to_string());
161+
let current_span_id = ctx.current_span().id().map(|id| id.into_u64());
162+
let parent_span_id = current_span_id.map(|id| id.to_string());
157163

158164
// Build and send observation
159-
let builder = ObservationBuilder::new(metadata.name())
165+
let mut builder = ObservationBuilder::new(metadata.name())
160166
.observation_type(ObservationType::LogEntry)
161-
.log_level(tracing_level_to_log_level(*metadata.level()))
162-
.label(format!("tracing/events/{}", metadata.target()));
167+
.log_level(tracing_level_to_log_level(*metadata.level()));
168+
169+
// Add group reference from current span
170+
if let Some(span_id) = current_span_id {
171+
let group_handle = GroupHandle::from_id(GroupId::from(span_id.to_string()), &execution);
172+
builder = builder.group(&group_handle);
173+
}
163174

164175
let builder = if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) {
165176
builder.source(file, line)

0 commit comments

Comments
 (0)