diff --git a/Cargo.lock b/Cargo.lock index 11d66990aa..4516c95b23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6134,6 +6134,12 @@ dependencies = [ "tonic", ] +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb3a2f78c2d55362cd6c313b8abedfbc0142ab3c2676822068fd2ab7d51f9b7" + [[package]] name = "opentelemetry_sdk" version = "0.28.0" @@ -8807,6 +8813,7 @@ dependencies = [ "http-body-util", "hyper 1.8.1", "hyper-util", + "opentelemetry-semantic-conventions", "pin-project-lite", "reqwest 0.12.9", "rustls 0.23.37", @@ -8877,6 +8884,7 @@ dependencies = [ "futures-util", "http 1.3.1", "ip_network", + "opentelemetry-semantic-conventions", "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", @@ -9525,6 +9533,7 @@ dependencies = [ "http-body-util", "hyper 1.8.1", "hyper-util", + "opentelemetry-semantic-conventions", "pin-project-lite", "rand 0.9.1", "rustls 0.23.37", @@ -10503,9 +10512,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -10515,9 +10524,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -10526,9 +10535,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", diff --git a/Cargo.toml b/Cargo.toml index 41f3807b93..c5cf89a120 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" diff --git a/crates/factor-outbound-http/Cargo.toml b/crates/factor-outbound-http/Cargo.toml index 51046dc150..7ccfd3f63b 100644 --- a/crates/factor-outbound-http/Cargo.toml +++ b/crates/factor-outbound-http/Cargo.toml @@ -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 } diff --git a/crates/factor-outbound-http/src/spin.rs b/crates/factor-outbound-http/src/spin.rs index 02ff1411d1..5c47204bfb 100644 --- a/crates/factor-outbound-http/src/spin.rs +++ b/crates/factor-outbound-http/src/spin.rs @@ -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::{ @@ -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 { self.hooks.otel.reparent_tracing_span(); @@ -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 } } @@ -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 // 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::() && 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()); } } } diff --git a/crates/factor-outbound-http/src/wasi.rs b/crates/factor-outbound-http/src/wasi.rs index 9b80e1df20..ed151f5647 100644 --- a/crates/factor-outbound-http/src/wasi.rs +++ b/crates/factor-outbound-http/src/wasi.rs @@ -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}, @@ -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)] @@ -269,6 +271,9 @@ impl> Body for BetweenBytesTimeoutBody { 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); } }; @@ -397,12 +402,12 @@ impl p2::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(), - 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( @@ -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); } } @@ -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 @@ -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"); diff --git a/crates/factor-outbound-networking/Cargo.toml b/crates/factor-outbound-networking/Cargo.toml index 3bdd471954..f340c27093 100644 --- a/crates/factor-outbound-networking/Cargo.toml +++ b/crates/factor-outbound-networking/Cargo.toml @@ -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" diff --git a/crates/factor-outbound-networking/src/lib.rs b/crates/factor-outbound-networking/src/lib.rs index 57b22041ff..5b20c46be3 100644 --- a/crates/factor-outbound-networking/src/lib.rs +++ b/crates/factor-outbound-networking/src/lib.rs @@ -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::{ @@ -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('/')); } } diff --git a/crates/trigger-http/Cargo.toml b/crates/trigger-http/Cargo.toml index ee595bd55b..811e69067c 100644 --- a/crates/trigger-http/Cargo.toml +++ b/crates/trigger-http/Cargo.toml @@ -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 } diff --git a/crates/trigger-http/src/instrument.rs b/crates/trigger-http/src/instrument.rs index 81974a0096..e214835cdc 100644 --- a/crates/trigger-http/src/instrument.rs +++ b/crates/trigger-http/src/instrument.rs @@ -1,5 +1,6 @@ use anyhow::Result; use http::Response; +use opentelemetry_semantic_conventions::attribute as otel_attribute; use tracing::Level; use crate::Body; @@ -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, ) }; @@ -45,20 +46,23 @@ pub(crate) fn finalize_http_span( let matched_route = response.extensions().get::(); // 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) } @@ -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