-
Notifications
You must be signed in to change notification settings - Fork 67
fix(stargate-k8s-router): add OpenTelemetry tracing instrumentation #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,14 +35,22 @@ use stargate_runtime::{ | |
| }; | ||
| use tokio::net::TcpListener; | ||
| use tokio::sync::watch; | ||
| use tracing::{debug, error, info}; | ||
| use tracing_subscriber::EnvFilter; | ||
| use tracing::{debug, error, info, warn}; | ||
|
|
||
| const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 5_000; | ||
| const DEFAULT_WATCH_HEARTBEAT_MS: u64 = 5_000; | ||
| const DEFAULT_RELAY_MAX_IDLE_TIMEOUT_MS: u64 = 300_000; | ||
| const DEFAULT_RELAY_KEEP_ALIVE_MS: u64 = 10_000; | ||
| const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS: u64 = 30_000; | ||
| /// OpenTelemetry `service.name` resource and tracer name default. | ||
| const DEFAULT_SERVICE_NAME: &str = "stargate-k8s-router"; | ||
| /// Root span name used to gate OTLP export; see `stargate_telemetry::init_telemetry`. | ||
| /// | ||
| /// NOTE: neither the endpoint-watch loop (`watcher::run_endpoint_slice_watcher`) nor | ||
| /// the relay paths (`grpc`, `quic`, `webtransport`) currently open a span with this | ||
| /// name -- this wiring alone establishes the exporter but will not emit spans until | ||
| /// that instrumentation is added. | ||
| const TRACED_ROOT_SPAN: &str = "relay_request"; | ||
|
|
||
| #[derive(Clone, Debug, PartialEq, ValueEnum)] | ||
| enum RouterTunnelProtocol { | ||
|
|
@@ -125,6 +133,15 @@ struct Args { | |
| /// CA bundle used to verify the selected upstream Stargate pod. | ||
| #[arg(long, env = "STARGATE_UPSTREAM_TLS_CERT_PATH", value_name = "PATH")] | ||
| upstream_tls_cert_path: Option<String>, | ||
| /// OTLP/gRPC trace export endpoint. Tracing export is disabled if omitted. | ||
| #[arg(long, env = "OTEL_EXPORTER_OTLP_ENDPOINT", value_name = "ENDPOINT")] | ||
| otel_endpoint: Option<String>, | ||
| /// OpenTelemetry service.name resource and tracer name. | ||
| #[arg(long, default_value = DEFAULT_SERVICE_NAME, value_name = "NAME")] | ||
| otel_service_name: String, | ||
| /// JSON secrets file path containing the OTLP `tracingAccessToken`. | ||
| #[arg(long, env = "SECRETS_PATH", value_name = "PATH")] | ||
| secrets_path: Option<String>, | ||
| } | ||
|
|
||
| struct RouterStartupConfig { | ||
|
|
@@ -382,11 +399,58 @@ impl RouterRuntime { | |
| } | ||
| } | ||
|
|
||
| /// Resolves the OTLP tracing access token, read only when tracing is enabled. | ||
| /// Missing/empty key yields `None` (caller warns after the subscriber exists); | ||
| /// an unreadable or malformed secrets file is a hard error. | ||
| async fn resolve_otel_access_token( | ||
| tracing_enabled: bool, | ||
| secrets_path: Option<&str>, | ||
| ) -> Result<Option<String>> { | ||
| if !tracing_enabled { | ||
| return Ok(None); | ||
| } | ||
| let Some(path) = secrets_path else { | ||
| return Ok(None); | ||
| }; | ||
| let bytes = tokio::fs::read(path) | ||
| .await | ||
| .with_context(|| format!("failed to read secrets file '{path}' for tracingAccessToken"))?; | ||
| let secrets: serde_json::Value = serde_json::from_slice(&bytes) | ||
| .with_context(|| format!("secrets file '{path}' is not valid JSON"))?; | ||
| match secrets.get("tracingAccessToken") { | ||
| None => Ok(None), | ||
| Some(value) => { | ||
| let token = value | ||
| .as_str() | ||
| .context("tracingAccessToken in secrets file is not a string")? | ||
| .trim(); | ||
| if token.is_empty() { | ||
| Ok(None) | ||
| } else { | ||
| Ok(Some(token.to_owned())) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> Result<()> { | ||
| init_logging(); | ||
| let args = Args::parse(); | ||
| let tracing_enabled = args.otel_endpoint.is_some(); | ||
| let otel_access_token = | ||
| resolve_otel_access_token(tracing_enabled, args.secrets_path.as_deref()).await?; | ||
| let _telemetry_guard = stargate_telemetry::init_telemetry( | ||
| args.otel_endpoint.as_deref(), | ||
| &args.otel_service_name, | ||
| TRACED_ROOT_SPAN, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Add the relay and endpoint-watch root spans before enabling this filter.
Instrument each relay entry point and the endpoint-watch loop with exported root spans. Extend the shared filter if the watcher requires a distinct root span. As per path instructions, check “tracing spans on cross-service calls.” 🤖 Prompt for AI AgentsSource: Path instructions
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. confirmed and already called out in the PR description this PR wires up the |
||
| otel_access_token.as_deref(), | ||
| )?; | ||
| // Warn after init_telemetry so the subscriber captures it. | ||
| if tracing_enabled && otel_access_token.is_none() { | ||
| warn!("no tracingAccessToken; OTLP trace export is unauthenticated"); | ||
| } | ||
| install_default_crypto_provider(); | ||
| let config = RouterStartupConfig::from_args(Args::parse())?; | ||
| let config = RouterStartupConfig::from_args(args)?; | ||
| run_router(config).await | ||
| } | ||
|
|
||
|
|
@@ -506,16 +570,6 @@ fn relay_endpoint_config_from_args(args: &Args) -> Result<RelayEndpointConfig> { | |
| }) | ||
| } | ||
|
|
||
| fn init_logging() { | ||
| tracing_subscriber::fmt() | ||
| .with_env_filter( | ||
| EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), | ||
| ) | ||
| .with_target(false) | ||
| .compact() | ||
| .init(); | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
@@ -1025,6 +1079,87 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn otel_service_name_defaults_and_can_be_overridden() { | ||
| let defaults = router_args(&[]); | ||
| assert_eq!(defaults.otel_service_name, DEFAULT_SERVICE_NAME); | ||
| assert_eq!(defaults.otel_endpoint, None); | ||
| assert_eq!(defaults.secrets_path, None); | ||
|
|
||
| let overridden = router_args(&["--otel-service-name", "stargate-k8s-router-canary"]); | ||
| assert_eq!(overridden.otel_service_name, "stargate-k8s-router-canary"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_none_when_tracing_disabled() { | ||
| let file = test_file(br#"{"tracingAccessToken":"tok"}"#); | ||
| let token = resolve_otel_access_token(false, Some(test_file_path(&file))) | ||
| .await | ||
| .expect("resolve should succeed"); | ||
| assert_eq!(token, None); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_none_when_no_secrets_path() { | ||
| let token = resolve_otel_access_token(true, None) | ||
| .await | ||
| .expect("resolve should succeed"); | ||
| assert_eq!(token, None); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_reads_and_trims_value() { | ||
| let file = test_file(br#"{"nvcfApiToken":"x","tracingAccessToken":" tok-123 "}"#); | ||
| let token = resolve_otel_access_token(true, Some(test_file_path(&file))) | ||
| .await | ||
| .expect("resolve should succeed"); | ||
| assert_eq!(token.as_deref(), Some("tok-123")); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_absent_key_is_allowed() { | ||
| let file = test_file(br#"{"nvcfApiToken":"x"}"#); | ||
| let token = resolve_otel_access_token(true, Some(test_file_path(&file))) | ||
| .await | ||
| .expect("missing tracingAccessToken must not error"); | ||
| assert_eq!(token, None); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_empty_value_is_allowed() { | ||
| let file = test_file(br#"{"tracingAccessToken":" "}"#); | ||
| let token = resolve_otel_access_token(true, Some(test_file_path(&file))) | ||
| .await | ||
| .expect("empty tracingAccessToken must not error"); | ||
| assert_eq!(token, None); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_non_string_value_fails() { | ||
| let file = test_file(br#"{"tracingAccessToken":42}"#); | ||
| let error = resolve_otel_access_token(true, Some(test_file_path(&file))) | ||
| .await | ||
| .expect_err("non-string tracingAccessToken must fail"); | ||
| assert!(error.to_string().contains("not a string"), "{error:#}"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_invalid_json_fails() { | ||
| let file = test_file(b"not json"); | ||
| let error = resolve_otel_access_token(true, Some(test_file_path(&file))) | ||
| .await | ||
| .expect_err("invalid JSON secrets file must fail"); | ||
| assert!(error.to_string().contains("not valid JSON"), "{error:#}"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn otel_access_token_unreadable_file_fails() { | ||
| let error = resolve_otel_access_token(true, Some("/nonexistent/secrets.json")) | ||
| .await | ||
| .expect_err("unreadable secrets file must fail"); | ||
| assert!(error.to_string().contains("failed to read"), "{error:#}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn relay_endpoint_config_uses_long_idle_defaults() { | ||
| let config = relay_config(&[]).expect("default relay endpoint config should be valid"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA/nvcf
Length of output: 21605
🏁 Script executed:
Repository: NVIDIA/nvcf
Length of output: 21480
Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information
Reachability: Internal · Exploitability: Difficult
Require TLS before sending tracing access tokens.
When
otel_access_tokenis present, reject non-https://endpoints or omit the token.init_telemetryattaches the token to any endpoint, but enables TLS only forhttps://.🤖 Prompt for AI Agents
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
confirmed this is real. in stargate-telemetry/src/lib.rs, init_telemetry() only
configures TLS when the endpoint starts with https://, but the access_token
metadata attachment right below it has no matching scheme check so a plaintext
http:// endpoint with a token set will send that token unencrypted.
This is pre-existing behavior in the shared crate, not something this PR
introduces: stargate's telemetry::init_telemetry wrapper calls the same
underlying function the same way, so stargate is exposed to this today too.
Since the real fix belongs in stargate-telemetry rather than in
stargate-k8s-router's main.rs, I'd rather not patch around it locally here.
@jjayaraman-1 do want me to fix this in the shared crate as part of this PR,
split it into its own issue, or is it already tracked?