Skip to content

Commit 2efad6a

Browse files
authored
Merge pull request #2441 from calebschoepp/trigger-tracing
Trigger tracing
2 parents 3cb1869 + 6eb783d commit 2efad6a

File tree

8 files changed

+173
-51
lines changed

8 files changed

+173
-51
lines changed

crates/trigger-http/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ harness = false
5454
llm = ["spin-trigger/llm"]
5555
llm-metal = ["llm", "spin-trigger/llm-metal"]
5656
llm-cublas = ["llm", "spin-trigger/llm-cublas"]
57+
58+
[lints]
59+
workspace = true

crates/trigger-http/src/handler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ use spin_trigger::TriggerAppEngine;
1717
use spin_world::v1::http_types;
1818
use std::sync::Arc;
1919
use tokio::{sync::oneshot, task};
20-
use tracing::{instrument, Instrument};
20+
use tracing::{instrument, Instrument, Level};
2121
use wasmtime_wasi_http::{proxy::Proxy, WasiHttpView};
2222

2323
#[derive(Clone)]
2424
pub struct HttpHandlerExecutor;
2525

2626
#[async_trait]
2727
impl HttpExecutor for HttpHandlerExecutor {
28-
#[instrument(name = "execute_wasm", skip_all, fields(otel.kind = "server"))]
28+
#[instrument(name = "spin_trigger_http.execute_wasm", skip_all, err(level = Level::INFO), fields(otel.name = format!("execute_wasm_component {}", component_id)))]
2929
async fn execute(
3030
&self,
3131
engine: Arc<TriggerAppEngine<HttpTrigger>>,

crates/trigger-http/src/instrument.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use anyhow::Result;
2+
use http::Response;
3+
use tracing::Level;
4+
use wasmtime_wasi_http::body::HyperIncomingBody;
5+
6+
/// Create a span for an HTTP request.
7+
macro_rules! http_span {
8+
($request:tt, $addr:tt) => {
9+
tracing::info_span!(
10+
"spin_trigger_http.handle_http_request",
11+
"otel.kind" = "server",
12+
"http.request.method" = %$request.method(),
13+
"network.peer.address" = %$addr.ip(),
14+
"network.peer.port" = %$addr.port(),
15+
"network.protocol.name" = "http",
16+
"url.path" = $request.uri().path(),
17+
"url.query" = $request.uri().query().unwrap_or(""),
18+
"url.scheme" = $request.uri().scheme_str().unwrap_or(""),
19+
"client.address" = $request.headers().get("x-forwarded-for").and_then(|val| val.to_str().ok()),
20+
// Recorded later
21+
"error.type" = Empty,
22+
"http.response.status_code" = Empty,
23+
"http.route" = Empty,
24+
"otel.name" = Empty,
25+
)
26+
};
27+
}
28+
29+
pub(crate) use http_span;
30+
31+
/// Finish setting attributes on the HTTP span.
32+
pub(crate) fn finalize_http_span(
33+
response: Result<Response<HyperIncomingBody>>,
34+
method: String,
35+
) -> Result<Response<HyperIncomingBody>> {
36+
let span = tracing::Span::current();
37+
match response {
38+
Ok(response) => {
39+
let matched_route = response.extensions().get::<MatchedRoute>();
40+
// Set otel.name and http.route
41+
if let Some(MatchedRoute { route }) = matched_route {
42+
span.record("http.route", route);
43+
span.record("otel.name", format!("{method} {route}"));
44+
} else {
45+
span.record("otel.name", method);
46+
}
47+
48+
// Set status code
49+
span.record("http.response.status_code", response.status().as_u16());
50+
51+
Ok(response)
52+
}
53+
Err(err) => {
54+
instrument_error(&err);
55+
span.record("http.response.status_code", 500);
56+
span.record("otel.name", method);
57+
Err(err)
58+
}
59+
}
60+
}
61+
62+
/// Marks the current span as errored.
63+
pub(crate) fn instrument_error(err: &anyhow::Error) {
64+
let span = tracing::Span::current();
65+
tracing::event!(target:module_path!(), Level::INFO, error = %err);
66+
span.record("error.type", format!("{:?}", err));
67+
}
68+
69+
/// MatchedRoute is used as a response extension to track the route that was matched for OTel
70+
/// tracing purposes.
71+
#[derive(Clone)]
72+
pub struct MatchedRoute {
73+
pub route: String,
74+
}
75+
76+
impl MatchedRoute {
77+
pub fn set_response_extension(
78+
resp: &mut Response<HyperIncomingBody>,
79+
route: impl Into<String>,
80+
) {
81+
resp.extensions_mut().insert(MatchedRoute {
82+
route: route.into(),
83+
});
84+
}
85+
86+
pub fn with_response_extension(
87+
mut resp: Response<HyperIncomingBody>,
88+
route: impl Into<String>,
89+
) -> Response<HyperIncomingBody> {
90+
Self::set_response_extension(&mut resp, route);
91+
resp
92+
}
93+
}

crates/trigger-http/src/lib.rs

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Implementation for the Spin HTTP engine.
22
33
mod handler;
4+
mod instrument;
45
mod tls;
56
mod wagi;
67

@@ -25,6 +26,7 @@ use hyper::{
2526
Request, Response,
2627
};
2728
use hyper_util::rt::tokio::TokioIo;
29+
use instrument::{finalize_http_span, http_span};
2830
use spin_app::{AppComponent, APP_DESCRIPTION_KEY};
2931
use spin_core::{Engine, OutboundWasiHttpHandler};
3032
use spin_http::{
@@ -42,10 +44,17 @@ use tokio::{
4244
net::TcpListener,
4345
task,
4446
};
45-
use tracing::{log, Instrument};
46-
use wasmtime_wasi_http::{body::HyperIncomingBody as Body, WasiHttpView};
47+
use tracing::{field::Empty, log, Instrument};
48+
use wasmtime_wasi_http::{
49+
body::{HyperIncomingBody as Body, HyperOutgoingBody},
50+
WasiHttpView,
51+
};
4752

48-
use crate::{handler::HttpHandlerExecutor, wagi::WagiHttpExecutor};
53+
use crate::{
54+
handler::HttpHandlerExecutor,
55+
instrument::{instrument_error, MatchedRoute},
56+
wagi::WagiHttpExecutor,
57+
};
4958

5059
pub use tls::TlsConfig;
5160

@@ -240,19 +249,22 @@ impl HttpTrigger {
240249
req.uri()
241250
);
242251

243-
let path = req.uri().path();
252+
let path = req.uri().path().to_string();
244253

245254
// Handle well-known spin paths
246255
if let Some(well_known) = path.strip_prefix(spin_http::WELL_KNOWN_PREFIX) {
247256
return match well_known {
248-
"health" => Ok(Response::new(body::full(Bytes::from_static(b"OK")))),
249-
"info" => self.app_info(),
257+
"health" => Ok(MatchedRoute::with_response_extension(
258+
Response::new(body::full(Bytes::from_static(b"OK"))),
259+
path,
260+
)),
261+
"info" => self.app_info(path),
250262
_ => Self::not_found(NotFoundRouteKind::WellKnown),
251263
};
252264
}
253265

254266
// Route to app component
255-
match self.router.route(path) {
267+
match self.router.route(&path) {
256268
Ok(component_id) => {
257269
let trigger = self.component_trigger_configs.get(component_id).unwrap();
258270

@@ -293,10 +305,14 @@ impl HttpTrigger {
293305
}
294306
};
295307
match res {
296-
Ok(res) => Ok(res),
308+
Ok(res) => Ok(MatchedRoute::with_response_extension(
309+
res,
310+
raw_route.to_string(),
311+
)),
297312
Err(e) => {
298313
log::error!("Error processing request: {:?}", e);
299-
Self::internal_error(None)
314+
instrument_error(&e);
315+
Self::internal_error(None, raw_route.to_string())
300316
}
301317
}
302318
}
@@ -305,24 +321,30 @@ impl HttpTrigger {
305321
}
306322

307323
/// Returns spin status information.
308-
fn app_info(&self) -> Result<Response<Body>> {
324+
fn app_info(&self, route: String) -> Result<Response<Body>> {
309325
let info = AppInfo::new(self.engine.app());
310326
let body = serde_json::to_vec_pretty(&info)?;
311-
Ok(Response::builder()
312-
.header("content-type", "application/json")
313-
.body(body::full(body.into()))?)
327+
Ok(MatchedRoute::with_response_extension(
328+
Response::builder()
329+
.header("content-type", "application/json")
330+
.body(body::full(body.into()))?,
331+
route,
332+
))
314333
}
315334

316335
/// Creates an HTTP 500 response.
317-
fn internal_error(body: Option<&str>) -> Result<Response<Body>> {
336+
fn internal_error(body: Option<&str>, route: String) -> Result<Response<Body>> {
318337
let body = match body {
319338
Some(body) => body::full(Bytes::copy_from_slice(body.as_bytes())),
320339
None => body::empty(),
321340
};
322341

323-
Ok(Response::builder()
324-
.status(StatusCode::INTERNAL_SERVER_ERROR)
325-
.body(body)?)
342+
Ok(MatchedRoute::with_response_extension(
343+
Response::builder()
344+
.status(StatusCode::INTERNAL_SERVER_ERROR)
345+
.body(body)?,
346+
route,
347+
))
326348
}
327349

328350
/// Creates an HTTP 404 response.
@@ -351,38 +373,7 @@ impl HttpTrigger {
351373
.keep_alive(true)
352374
.serve_connection(
353375
TokioIo::new(stream),
354-
service_fn(move |request| {
355-
let self_ = self_.clone();
356-
let span = tracing::info_span!(
357-
"handle_http_request",
358-
"otel.kind" = "server",
359-
"http.request.method" = %request.method(),
360-
"network.peer.address" = %addr.ip(),
361-
"network.peer.port" = %addr.port(),
362-
"network.protocol.name" = "http",
363-
"url.path" = request.uri().path(),
364-
"url.query" = request.uri().query().unwrap_or(""),
365-
"url.scheme" = request.uri().scheme_str().unwrap_or(""),
366-
"client.address" = request.headers().get("x-forwarded-for").and_then(|val| val.to_str().ok()),
367-
// TODO(Caleb): Recorded later
368-
// "error.type" = Empty,
369-
// "http.response.status_code" = Empty,
370-
// "http.route" = Empty,
371-
);
372-
async move {
373-
self_
374-
.handle(
375-
request.map(|body: Incoming| {
376-
body.map_err(wasmtime_wasi_http::hyper_response_error)
377-
.boxed()
378-
}),
379-
Scheme::HTTP,
380-
addr,
381-
)
382-
.instrument(span)
383-
.await
384-
}
385-
}),
376+
service_fn(move |request| self_.clone().instrumented_service_fn(addr, request)),
386377
)
387378
.await
388379
{
@@ -391,6 +382,30 @@ impl HttpTrigger {
391382
});
392383
}
393384

385+
async fn instrumented_service_fn(
386+
self: Arc<Self>,
387+
addr: SocketAddr,
388+
request: Request<Incoming>,
389+
) -> Result<Response<HyperOutgoingBody>> {
390+
let span = http_span!(request, addr);
391+
let method = request.method().to_string();
392+
async {
393+
let result = self
394+
.handle(
395+
request.map(|body: Incoming| {
396+
body.map_err(wasmtime_wasi_http::hyper_response_error)
397+
.boxed()
398+
}),
399+
Scheme::HTTP,
400+
addr,
401+
)
402+
.await;
403+
finalize_http_span(result, method)
404+
}
405+
.instrument(span)
406+
.await
407+
}
408+
394409
async fn serve(self, listen_addr: SocketAddr) -> Result<()> {
395410
let self_ = Arc::new(self);
396411

crates/trigger-http/src/wagi.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use hyper::{Request, Response};
88
use spin_core::WasiVersion;
99
use spin_http::{config::WagiTriggerConfig, routes::RoutePattern, wagi};
1010
use spin_trigger::TriggerAppEngine;
11+
use tracing::{instrument, Level};
1112
use wasi_common_preview1::{pipe::WritePipe, I32Exit};
1213

1314
use crate::{Body, HttpExecutor, HttpTrigger};
@@ -19,6 +20,7 @@ pub struct WagiHttpExecutor {
1920

2021
#[async_trait]
2122
impl HttpExecutor for WagiHttpExecutor {
23+
#[instrument(name = "spin_trigger_http.execute_wagi", skip_all, err(level = Level::INFO), fields(otel.name = format!("execute_wagi_component {}", component)))]
2224
async fn execute(
2325
&self,
2426
engine: Arc<TriggerAppEngine<HttpTrigger>>,

crates/trigger-redis/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ tokio = { version = "1.23", features = ["full"] }
2424

2525
[dev-dependencies]
2626
spin-testing = { path = "../testing" }
27+
28+
[lints]
29+
workspace = true

crates/trigger-redis/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use spin_core::{async_trait, InstancePre};
1111
use spin_trigger::{cli::NoArgs, TriggerAppEngine, TriggerExecutor};
1212
use std::collections::HashMap;
1313
use std::sync::Arc;
14+
use tracing::{instrument, Level};
1415

1516
use crate::spin::SpinRedisExecutor;
1617

@@ -110,6 +111,9 @@ impl TriggerExecutor for RedisTrigger {
110111

111112
impl RedisTrigger {
112113
// Handle the message.
114+
#[instrument(name = "spin_trigger_redis.handle_message", skip(self, channel_components, msg),
115+
err(level = Level::INFO), fields(otel.name = format!("{} receive", msg.get_channel_name()),
116+
otel.kind = "consumer", messaging.operation = "receive", messaging.system = "redis"))]
113117
async fn handle(
114118
&self,
115119
address: &str,

crates/trigger-redis/src/spin.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use async_trait::async_trait;
33
use spin_core::Instance;
44
use spin_trigger::TriggerAppEngine;
55
use spin_world::v1::redis_types::{Error, Payload};
6+
use tracing::{instrument, Level};
67

78
use crate::{RedisExecutor, RedisTrigger, Store};
89

@@ -11,6 +12,7 @@ pub struct SpinRedisExecutor;
1112

1213
#[async_trait]
1314
impl RedisExecutor for SpinRedisExecutor {
15+
#[instrument(name = "spin_trigger_redis.execute_wasm", skip(self, engine, payload), err(level = Level::INFO), fields(otel.name = format!("execute_wasm_component {}", component_id)))]
1416
async fn execute(
1517
&self,
1618
engine: &TriggerAppEngine<RedisTrigger>,

0 commit comments

Comments
 (0)