Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ opentelemetry_sdk = {version = "0.28", features = [
"experimental_logs_batch_log_processor_with_async_runtime",
"experimental_async_runtime"
]}
opentelemetry-semantic-conventions = "0.28"
path-absolutize = "3"
pin-project-lite = "0.2.16"
quote = "1"
Expand Down Expand Up @@ -190,7 +191,7 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
toml = "0.8"
toml_edit = "0.22"
tower-service = "0.3.3"
tracing = { version = "0.1.41", features = ["log"] }
tracing = { version = "0.1.44", features = ["log"] }
url = "2.5.7"
tracing-opentelemetry = { version = "0.29", default-features = false, features = ["metrics"] }
walkdir = "2"
Expand Down
1 change: 1 addition & 0 deletions crates/factor-outbound-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ tokio-rustls = { workspace = true }
tower-service = { workspace = true }
tracing = { workspace = true }
tracing-opentelemetry = { workspace = true }
opentelemetry-semantic-conventions = { workspace = true }
wasmtime = { workspace = true }
wasmtime-wasi = { workspace = true }
wasmtime-wasi-http = { workspace = true }
Expand Down
18 changes: 11 additions & 7 deletions crates/factor-outbound-http/src/spin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::sync::Arc;

use futures::stream::TryStreamExt as _;
use http_body_util::BodyExt;
use opentelemetry_semantic_conventions::attribute as otel_attribute;
use spin_factor_outbound_networking::config::blocked_networks::BlockedNetworks;
use spin_world::MAX_HOST_BUFFERED_BYTES;
use spin_world::v1::{
Expand All @@ -14,8 +15,8 @@ use crate::intercept::InterceptOutcome;

impl spin_http::Host for crate::InstanceState {
#[instrument(name = "spin_outbound_http.send_request", skip_all,
fields(otel.kind = "client", url.full = Empty, http.request.method = Empty,
http.response.status_code = Empty, otel.name = Empty, server.address = Empty, server.port = Empty))]
fields(otel.kind = "client", {otel_attribute::URL_FULL} = Empty, {otel_attribute::HTTP_REQUEST_METHOD} = Empty,
{otel_attribute::HTTP_RESPONSE_STATUS_CODE} = Empty, otel.name = Empty, {otel_attribute::SERVER_ADDRESS} = Empty, {otel_attribute::SERVER_PORT} = Empty))]
async fn send_request(&mut self, req: Request) -> Result<Response, HttpError> {
self.hooks.otel.reparent_tracing_span();

Expand Down Expand Up @@ -120,7 +121,10 @@ impl spin_http::Host for crate::InstanceState {
drop(permit);

tracing::trace!("Returning response from outbound request to {req_url}");
span.record("http.response.status_code", resp.status().as_u16());
span.record(
otel_attribute::HTTP_RESPONSE_STATUS_CODE,
resp.status().as_u16(),
);
response_from_reqwest(resp).await
}
}
Expand Down Expand Up @@ -162,14 +166,14 @@ fn record_request_fields(span: &Span, req: &Request) {
// Set otel.name to just the method name to fit with OpenTelemetry conventions
// <https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name>
span.record("otel.name", method)
.record("http.request.method", method)
.record("url.full", req.uri.clone());
.record(otel_attribute::HTTP_REQUEST_METHOD, method)
.record(otel_attribute::URL_FULL, req.uri.clone());
if let Ok(uri) = req.uri.parse::<http::Uri>()
&& let Some(authority) = uri.authority()
{
span.record("server.address", authority.host());
span.record(otel_attribute::SERVER_ADDRESS, authority.host());
if let Some(port) = authority.port() {
span.record("server.port", port.as_u16());
span.record(otel_attribute::SERVER_PORT, port.as_u16());
}
}
}
Expand Down
40 changes: 24 additions & 16 deletions crates/factor-outbound-http/src/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use hyper_util::{
},
rt::{TokioExecutor, TokioIo},
};
use opentelemetry_semantic_conventions::attribute as otel_attribute;
use spin_factor_outbound_networking::{
ComponentTlsClientConfigs, TlsClientConfig,
config::{allowed_hosts::OutboundAllowedHosts, blocked_networks::BlockedNetworks},
Expand Down Expand Up @@ -135,13 +136,14 @@ impl p3::WasiHttpHooks for InstanceHttpHooks {
skip_all,
fields(
otel.kind = "client",
url.full = Empty,
http.request.method = %request.method(),
{otel_attribute::URL_FULL} = Empty,
{otel_attribute::HTTP_REQUEST_METHOD} = %request.method(),
otel.name = %request.method(),
// Incubating convention; not yet a stable `opentelemetry_semantic_conventions` constant.
http.response.body.size = Empty,
http.response.status_code = Empty,
server.address = Empty,
server.port = Empty,
{otel_attribute::HTTP_RESPONSE_STATUS_CODE} = Empty,
{otel_attribute::SERVER_ADDRESS} = Empty,
{otel_attribute::SERVER_PORT} = Empty,
)
)]
#[allow(clippy::type_complexity)]
Expand Down Expand Up @@ -269,6 +271,9 @@ impl<B: Body<Error = p2_types::ErrorCode>> Body for BetweenBytesTimeoutBody<B> {

let mut record_body_size_once = |body_size: u64| {
if let Some(span) = me.span.take() {
// `http.response.body.size` is incubating (behind semconv_experimental)
// in opentelemetry-semantic-conventions 0.28. Leave as literal to avoid
// enabling the experimental feature.
span.record("http.response.body.size", body_size);
}
};
Expand Down Expand Up @@ -397,12 +402,12 @@ impl p2::WasiHttpHooks for InstanceHttpHooks {
skip_all,
fields(
otel.kind = "client",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this one not a convention?

Copy link
Copy Markdown
Contributor Author

@ChihweiLHBird ChihweiLHBird May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, otel.kind is not an official convention and is a spin specific thing...
You can see all otel.* keys in the conventions on this page and it doesn't contain otel.kind.

Copy link
Copy Markdown
Contributor Author

@ChihweiLHBird ChihweiLHBird May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say another benefit of using the official convention crate is we can realize things like this earlier

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lann Thanks! Nice to know it

Copy link
Copy Markdown
Collaborator

@lann lann May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're switching to constants for standard keys it would probably be wise for us to create constants for these too, e.g. OTEL_KIND, OTEL_NAME. (tracing-opentelemetry doesn't seem to define them)

Scope creepily: I wouldn't be sad to have macros for these. We use otel.name and otel.kind quite a bit so e.g.:

#[instrument(
  otel_kind!("client"),
  // Lots of this pattern in crates/factor-outbound-redis/src/host.rs
  otel_name!("GET {key}"),
)]

expanding to

#[instrument(
  {$crate::something_goes_here::OTEL_KIND} = "client",
  // Lots of this pattern in crates/factor-outbound-redis/src/host.rs
  {$crate::something_goes_here::OTEL_NAME} = format!("GET {path}"),
)]

Note: if you haven't seen it, $crate is a magic macro variable: https://doc.rust-lang.org/reference/macros-by-example.html#r-macro.decl.meta.dollar-crate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope creepily: I wouldn't be sad to have macros for the otel.* fields. We use otel.name and otel.kind quite a bit

This looks like something for tracing-opentelemetry to add.

url.full = Empty,
http.request.method = %request.method(),
{otel_attribute::URL_FULL} = Empty,
{otel_attribute::HTTP_REQUEST_METHOD} = %request.method(),
otel.name = %request.method(),
http.response.status_code = Empty,
server.address = Empty,
server.port = Empty,
{otel_attribute::HTTP_RESPONSE_STATUS_CODE} = Empty,
{otel_attribute::SERVER_ADDRESS} = Empty,
{otel_attribute::SERVER_PORT} = Empty,
)
)]
fn send_request(
Expand Down Expand Up @@ -484,12 +489,12 @@ impl RequestSender {
// Backfill span fields after potentially updating the URL in the interceptor
let span = tracing::Span::current();
if let Some(addr) = override_connect_addr {
span.record("server.address", addr.ip().to_string());
span.record("server.port", addr.port());
span.record(otel_attribute::SERVER_ADDRESS, addr.ip().to_string());
span.record(otel_attribute::SERVER_PORT, addr.port());
} else if let Some(authority) = request.uri().authority() {
span.record("server.address", authority.host());
span.record(otel_attribute::SERVER_ADDRESS, authority.host());
if let Some(port) = authority.port_u16() {
span.record("server.port", port);
span.record(otel_attribute::SERVER_PORT, port);
}
}

Expand Down Expand Up @@ -523,7 +528,7 @@ impl RequestSender {
}
*uri = builder.build().unwrap();
}
tracing::Span::current().record("url.full", uri.to_string());
tracing::Span::current().record(otel_attribute::URL_FULL, uri.to_string());

let is_self_request = match request.uri().authority() {
// Some SDKs require an authority, so we support e.g. http://self.alt/self-request
Expand Down Expand Up @@ -630,7 +635,10 @@ impl RequestSender {
.map(|body| body.map_err(hyper_request_error).boxed_unsync());

let span = tracing::Span::current();
span.record("http.response.status_code", resp.status().as_u16());
span.record(
otel_attribute::HTTP_RESPONSE_STATUS_CODE,
resp.status().as_u16(),
);

record_content_length_header(&span, resp.headers(), "http.response.header.content-length");

Expand Down
1 change: 1 addition & 0 deletions crates/factor-outbound-networking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ spin-manifest = { path = "../manifest" }
spin-outbound-networking-config = { path = "../outbound-networking-config" }
spin-serde = { path = "../serde" }
tracing = { workspace = true }
opentelemetry-semantic-conventions = { workspace = true }
url = { workspace = true }
webpki-root-certs = "1.0.7"

Expand Down
5 changes: 4 additions & 1 deletion crates/factor-outbound-networking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod tls;
use std::{collections::HashMap, sync::Arc};

use futures_util::FutureExt as _;
use opentelemetry_semantic_conventions::attribute::SERVER_PORT;
use spin_factor_variables::VariablesFactor;
use spin_factor_wasi::{SocketAddrUse, WasiFactor};
use spin_factors::{
Expand Down Expand Up @@ -227,8 +228,10 @@ impl FactorInstanceBuilder for InstanceBuilder {
pub fn record_address_fields(address: &str) {
if let Ok(url) = Url::parse(address) {
let span = tracing::Span::current();
// `db.address` and `db.namespace` are incubating in opentelemetry-semantic-conventions 0.28.
// Leaving as string literals to avoid enabling the semconv_experimental feature.
span.record("db.address", url.host_str().unwrap_or_default());
span.record("server.port", url.port().unwrap_or_default());
span.record(SERVER_PORT, url.port().unwrap_or_default());
span.record("db.namespace", url.path().trim_start_matches('/'));
}
}
1 change: 1 addition & 0 deletions crates/trigger-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ terminal = { path = "../terminal" }
tokio = { workspace = true, features = ["full"] }
tokio-rustls = { workspace = true }
tracing = { workspace = true }
opentelemetry-semantic-conventions = { workspace = true }
wasmtime = { workspace = true }
wasmtime-wasi = { workspace = true }
wasmtime-wasi-http = { workspace = true }
Expand Down
34 changes: 19 additions & 15 deletions crates/trigger-http/src/instrument.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Result;
use http::Response;
use opentelemetry_semantic_conventions::attribute as otel_attribute;
use tracing::Level;

use crate::Body;
Expand All @@ -10,18 +11,18 @@ macro_rules! http_span {
tracing::info_span!(
"spin_trigger_http.handle_http_request",
"otel.kind" = "server",
"http.request.method" = %$request.method(),
"network.peer.address" = %$addr.ip(),
"network.peer.port" = %$addr.port(),
"network.protocol.name" = "http",
"url.path" = $request.uri().path(),
"url.query" = $request.uri().query().unwrap_or(""),
"url.scheme" = $request.uri().scheme_str().unwrap_or(""),
"client.address" = $request.headers().get("x-forwarded-for").and_then(|val| val.to_str().ok()),
{opentelemetry_semantic_conventions::attribute::HTTP_REQUEST_METHOD} = %$request.method(),
{opentelemetry_semantic_conventions::attribute::NETWORK_PEER_ADDRESS} = %$addr.ip(),
{opentelemetry_semantic_conventions::attribute::NETWORK_PEER_PORT} = %$addr.port(),
{opentelemetry_semantic_conventions::attribute::NETWORK_PROTOCOL_NAME} = "http",
{opentelemetry_semantic_conventions::attribute::URL_PATH} = $request.uri().path(),
{opentelemetry_semantic_conventions::attribute::URL_QUERY} = $request.uri().query().unwrap_or(""),
{opentelemetry_semantic_conventions::attribute::URL_SCHEME} = $request.uri().scheme_str().unwrap_or(""),
{opentelemetry_semantic_conventions::attribute::CLIENT_ADDRESS} = $request.headers().get("x-forwarded-for").and_then(|val| val.to_str().ok()),
// Recorded later
"error.type" = ::tracing::field::Empty,
"http.response.status_code" = ::tracing::field::Empty,
"http.route" = ::tracing::field::Empty,
{opentelemetry_semantic_conventions::attribute::ERROR_TYPE} = ::tracing::field::Empty,
{opentelemetry_semantic_conventions::attribute::HTTP_RESPONSE_STATUS_CODE} = ::tracing::field::Empty,
{opentelemetry_semantic_conventions::attribute::HTTP_ROUTE} = ::tracing::field::Empty,
"otel.name" = ::tracing::field::Empty,
)
};
Expand All @@ -45,20 +46,23 @@ pub(crate) fn finalize_http_span(
let matched_route = response.extensions().get::<MatchedRoute>();
// Set otel.name and http.route
if let Some(MatchedRoute { route }) = matched_route {
span.record("http.route", route);
span.record(otel_attribute::HTTP_ROUTE, route);
span.record("otel.name", format!("{method} {route}"));
} else {
span.record("otel.name", method);
}

// Set status code
span.record("http.response.status_code", response.status().as_u16());
span.record(
otel_attribute::HTTP_RESPONSE_STATUS_CODE,
response.status().as_u16(),
);

Ok(response)
}
Err(err) => {
instrument_error(&err);
span.record("http.response.status_code", 500);
span.record(otel_attribute::HTTP_RESPONSE_STATUS_CODE, 500);
span.record("otel.name", method);
Err(err)
}
Expand All @@ -69,7 +73,7 @@ pub(crate) fn finalize_http_span(
pub(crate) fn instrument_error(err: &anyhow::Error) {
let span = tracing::Span::current();
tracing::event!(target:module_path!(), Level::INFO, error = %err);
span.record("error.type", format!("{err:?}"));
span.record(otel_attribute::ERROR_TYPE, format!("{err:?}"));
}

/// MatchedRoute is used as a response extension to track the route that was matched for OTel
Expand Down
Loading