Skip to content

Commit f68867b

Browse files
authored
feat(gateway): identify gateways in exported traces (#2647)
* feat(gateway): add installation name configuration Add a first-class operator-assigned gateway name with TOML, CLI, environment, and Helm configuration surfaces. Local gateways default to openshell, while Helm defaults to the chart fullname; operators sharing a collector across namespaces or clusters can set a globally distinct name. Signed-off-by: Kris Hicks <khicks@nvidia.com> * feat(gateway): identify gateways in exported traces Attach the configured gateway installation name and compute driver to the gateway OpenTelemetry resource so operators can filter traces from multiple installations that share a collector. Forward the gateway name and OTLP endpoint to managed external drivers so their distinct service resources carry the same installation identity. Keep service.name stable per process type, omit blank resource values, and leave per-span operation names and request attributes unchanged. Refs #2507 Signed-off-by: Kris Hicks <khicks@nvidia.com> --------- Signed-off-by: Kris Hicks <khicks@nvidia.com>
1 parent 981606d commit f68867b

29 files changed

Lines changed: 425 additions & 109 deletions

File tree

architecture/gateway.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,15 @@ Driver implementation settings live in the TOML driver tables. See
673673
`docs/reference/gateway-config.mdx` for worked per-driver examples and RFC
674674
0003 for the full schema.
675675

676+
Each installation has an operator-assigned gateway name. Configure it with
677+
`[openshell.gateway].name`, `--name`, or `OPENSHELL_GATEWAY_NAME`.
678+
The built-in default is `openshell`; the Helm chart defaults it to the chart
679+
fullname so every replica in one installation reports the same identity.
680+
Operators must set a globally distinct name when one telemetry collector serves
681+
installations in multiple Kubernetes namespaces or clusters.
682+
The name identifies the gateway installation independently of client-side
683+
aliases, network names, and the sandbox JWT issuer.
684+
676685
`database_url` is env-only and rejected when present in the file
677686
(`OPENSHELL_DB_URL` / `--db-url`).
678687

@@ -721,12 +730,14 @@ between a trace and its log lines. Store and compute-driver spans become
721730
children of the request span. Reconciliation, provider refresh, and
722731
driver-watch loops create their own operation spans because they have no
723732
inbound request to provide a parent. gRPC status is recorded when response
724-
trailers arrive.
725-
726-
The gateway forwards OTLP configuration and W3C trace context to managed
727-
external drivers. Built-in drivers use dedicated in-process providers that
728-
preserve the same RPC trace boundary. Each driver exports to the configured
729-
collector under its own service name.
733+
trailers arrive. Gateway spans carry resource attributes for the gateway
734+
identity and configured compute driver.
735+
736+
The gateway forwards OTLP configuration, its configured gateway name, and W3C
737+
trace context to managed external drivers. Built-in drivers use dedicated
738+
in-process providers that preserve the same RPC trace boundary. Each driver
739+
exports to the configured collector under its own service name and carries the
740+
gateway name as a resource attribute.
730741

731742
Two invariants shape the failure behavior. Telemetry is diagnostic, so no OTLP
732743
failure stops the gateway from serving: a malformed endpoint is logged at

crates/openshell-core/src/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub const DEFAULT_SSH_PORT: u16 = 2222;
2929
/// Default gateway server port.
3030
pub const DEFAULT_SERVER_PORT: u16 = 17670;
3131

32+
/// Default operator-facing name for a gateway installation.
33+
pub const DEFAULT_GATEWAY_NAME: &str = "openshell";
34+
3235
/// Default container stop timeout in seconds (SIGTERM → SIGKILL).
3336
pub const DEFAULT_STOP_TIMEOUT_SECS: u32 = 10;
3437

@@ -761,6 +764,9 @@ fn docker_socket_responds(path: &Path) -> bool {
761764
/// `Deserialize` impls for that purpose).
762765
#[derive(Debug, Clone)]
763766
pub struct Config {
767+
/// Operator-assigned name for this gateway installation.
768+
pub name: String,
769+
764770
/// Address to bind the server to.
765771
pub bind_address: SocketAddr,
766772

@@ -1168,6 +1174,7 @@ impl Config {
11681174
/// Create a new config with optional TLS.
11691175
pub fn new(tls: Option<TlsConfig>) -> Self {
11701176
Self {
1177+
name: DEFAULT_GATEWAY_NAME.to_string(),
11711178
bind_address: default_bind_address(),
11721179
health_bind_address: None,
11731180
metrics_bind_address: None,
@@ -1195,6 +1202,13 @@ impl Config {
11951202
}
11961203
}
11971204

1205+
/// Create a new configuration with the gateway installation name.
1206+
#[must_use]
1207+
pub fn with_name(mut self, name: impl Into<String>) -> Self {
1208+
self.name = name.into();
1209+
self
1210+
}
1211+
11981212
/// Create a new configuration with the given bind address.
11991213
#[must_use]
12001214
pub const fn with_bind_address(mut self, addr: SocketAddr) -> Self {
@@ -1573,6 +1587,15 @@ mod tests {
15731587
assert_eq!(cfg.ttl_secs, 0);
15741588
}
15751589

1590+
#[test]
1591+
fn name_defaults_and_can_be_overridden() {
1592+
assert_eq!(Config::new(None).name, "openshell");
1593+
assert_eq!(
1594+
Config::new(None).with_name("production-us-west").name,
1595+
"production-us-west"
1596+
);
1597+
}
1598+
15761599
#[test]
15771600
fn gateway_interceptor_failure_policy_rejects_ignore() {
15781601
let err =

crates/openshell-driver-docker/src/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@ struct Args {
3838

3939
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
4040
otlp_endpoint: Option<String>,
41+
42+
#[arg(long, env = "OPENSHELL_GATEWAY_NAME")]
43+
gateway_name: Option<String>,
4144
}
4245

4346
#[tokio::main]
4447
async fn main() -> Result<()> {
4548
let args = Args::parse();
46-
let (tracer_provider, setup_error) =
47-
openshell_driver_docker::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
49+
let (tracer_provider, setup_error) = openshell_driver_docker::otel_tracing::provider_for(
50+
args.otlp_endpoint.as_deref(),
51+
args.gateway_name.as_deref(),
52+
);
4853
tracing_subscriber::registry()
4954
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
5055
.with(tracing_subscriber::fmt::layer())

crates/openshell-driver-docker/src/otel_tracing.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,28 @@ pub(crate) fn compute_driver_rpc_operation(path: &str) -> (&'static str, &'stati
9090
}
9191
}
9292

93+
/// Build a tracer provider for the configured OTLP/gRPC endpoint and gateway.
9394
#[must_use]
94-
pub fn provider_for(endpoint: Option<&str>) -> (Option<SdkTracerProvider>, Option<SetupError>) {
95-
openshell_otel::provider_for(endpoint.map(|endpoint| OtlpTraceConfig {
96-
endpoint,
97-
service_name: ServiceName::Fixed(SERVICE_NAME),
98-
service_version: Some(openshell_core::VERSION),
99-
resource_attributes: Vec::new(),
95+
pub fn provider_for(
96+
endpoint: Option<&str>,
97+
gateway_name: Option<&str>,
98+
) -> (Option<SdkTracerProvider>, Option<SetupError>) {
99+
openshell_otel::provider_for(endpoint.map(|endpoint| {
100+
OtlpTraceConfig {
101+
endpoint,
102+
service_name: ServiceName::Fixed(SERVICE_NAME),
103+
service_version: Some(openshell_core::VERSION),
104+
resource_attributes: gateway_name
105+
.map(str::trim)
106+
.filter(|name| !name.is_empty())
107+
.map(|name| {
108+
vec![opentelemetry::KeyValue::new(
109+
"openshell.gateway.name",
110+
name.to_string(),
111+
)]
112+
})
113+
.unwrap_or_default(),
114+
}
100115
}))
101116
}
102117

@@ -151,11 +166,11 @@ mod tests {
151166
}
152167

153168
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
154-
async fn tracing_docker_driver_spans_reach_otlp_collector_with_distinct_service_name() {
169+
async fn tracing_docker_driver_spans_reach_otlp_collector_with_resource_identity() {
155170
let _tracing_lock = super::test_lock().await;
156171
let collector = OtlpTestServer::start().await;
157172

158-
let (provider, error) = super::provider_for(Some(collector.endpoint()));
173+
let (provider, error) = super::provider_for(Some(collector.endpoint()), Some("docker-dev"));
159174
assert!(error.is_none());
160175
let provider = provider.expect("provider");
161176
let subscriber = tracing_subscriber::registry().with(super::layer(&provider));
@@ -175,6 +190,7 @@ mod tests {
175190
.iter()
176191
.any(|span| span.name == "docker.schedule_sandbox")
177192
);
193+
assert_eq!(received.gateway_names, ["docker-dev"]);
178194
assert!(
179195
received
180196
.service_names

crates/openshell-driver-kubernetes/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ openshell-core = { path = "../openshell-core", default-features = false }
1919
openshell-otel = { path = "../openshell-otel" }
2020
openshell-policy = { path = "../openshell-policy" }
2121

22+
opentelemetry = { workspace = true }
2223
tokio = { workspace = true }
2324
tonic = { workspace = true, features = ["transport"] }
2425
prost = { workspace = true }
@@ -39,7 +40,6 @@ notify = "8"
3940

4041
[dev-dependencies]
4142
openshell-otel-test-support = { path = "../openshell-otel-test-support" }
42-
opentelemetry = { workspace = true }
4343
opentelemetry_sdk = { workspace = true, features = ["testing"] }
4444
bytes = { workspace = true }
4545
http = { workspace = true }

crates/openshell-driver-kubernetes/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ When the gateway configures `[openshell.gateway.otlp]`, Kubernetes
2323
compute-driver spans export to the same OTLP/gRPC collector with the service
2424
name `openshell-driver-kubernetes`. The driver preserves the gateway trace
2525
context and uses the same compute-driver RPC span names in its in-process and
26-
standalone forms.
26+
standalone forms. Standalone deployments set `--gateway-name` or
27+
`OPENSHELL_GATEWAY_NAME` so exported spans carry the same
28+
`openshell.gateway.name` resource attribute as gateway spans.
2729

2830
When it creates an Agent Sandbox resource, the driver serializes the active W3C
2931
trace context into the controller-reserved `opentelemetry.io/trace-context`

crates/openshell-driver-kubernetes/src/main.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ struct Args {
4242
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
4343
otlp_endpoint: Option<String>,
4444

45+
#[arg(long, env = "OPENSHELL_GATEWAY_NAME")]
46+
gateway_name: Option<String>,
47+
4548
#[arg(long, env = "OPENSHELL_WORKSPACE_MODE", default_value = "shared")]
4649
workspace_mode: WorkspaceMode,
4750

@@ -215,8 +218,10 @@ async fn shutdown_signal() {
215218
#[tokio::main]
216219
async fn main() -> Result<()> {
217220
let args = Args::parse();
218-
let (tracer_provider, setup_error) =
219-
openshell_driver_kubernetes::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
221+
let (tracer_provider, setup_error) = openshell_driver_kubernetes::otel_tracing::provider_for(
222+
args.otlp_endpoint.as_deref(),
223+
args.gateway_name.as_deref(),
224+
);
220225
tracing_subscriber::registry()
221226
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
222227
.with(tracing_subscriber::fmt::layer())
@@ -349,17 +354,20 @@ mod tests {
349354
use super::*;
350355

351356
#[test]
352-
fn accepts_gateway_otlp_endpoint() {
357+
fn accepts_gateway_otlp_configuration() {
353358
let args = Args::try_parse_from([
354359
"openshell-driver-kubernetes",
355360
"--otlp-endpoint",
356361
"http://collector.example:4317",
362+
"--gateway-name",
363+
"kubernetes-dev",
357364
])
358365
.expect("OTLP endpoint should parse");
359366

360367
assert_eq!(
361368
args.otlp_endpoint.as_deref(),
362369
Some("http://collector.example:4317")
363370
);
371+
assert_eq!(args.gateway_name.as_deref(), Some("kubernetes-dev"));
364372
}
365373
}

crates/openshell-driver-kubernetes/src/otel_tracing.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,28 @@ const SERVICE_NAME: &str = "openshell-driver-kubernetes";
1212
const INSTRUMENTATION_SCOPE: &str = "openshell-driver-kubernetes";
1313
pub const IN_PROCESS_TARGET_PREFIX: &str = "openshell_driver_kubernetes";
1414

15+
/// Build a tracer provider for the configured OTLP/gRPC endpoint and gateway.
1516
#[must_use]
16-
pub fn provider_for(endpoint: Option<&str>) -> (Option<SdkTracerProvider>, Option<SetupError>) {
17-
openshell_otel::provider_for(endpoint.map(|endpoint| OtlpTraceConfig {
18-
endpoint,
19-
service_name: ServiceName::Fixed(SERVICE_NAME),
20-
service_version: Some(openshell_core::VERSION),
21-
resource_attributes: Vec::new(),
17+
pub fn provider_for(
18+
endpoint: Option<&str>,
19+
gateway_name: Option<&str>,
20+
) -> (Option<SdkTracerProvider>, Option<SetupError>) {
21+
openshell_otel::provider_for(endpoint.map(|endpoint| {
22+
OtlpTraceConfig {
23+
endpoint,
24+
service_name: ServiceName::Fixed(SERVICE_NAME),
25+
service_version: Some(openshell_core::VERSION),
26+
resource_attributes: gateway_name
27+
.map(str::trim)
28+
.filter(|name| !name.is_empty())
29+
.map(|name| {
30+
vec![opentelemetry::KeyValue::new(
31+
"openshell.gateway.name",
32+
name.to_string(),
33+
)]
34+
})
35+
.unwrap_or_default(),
36+
}
2237
}))
2338
}
2439

@@ -75,10 +90,11 @@ mod tests {
7590
}
7691

7792
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
78-
async fn tracing_kubernetes_driver_spans_reach_otlp_collector_with_distinct_service_name() {
93+
async fn tracing_kubernetes_driver_spans_reach_otlp_collector_with_resource_identity() {
7994
let _tracing_lock = super::test_lock().await;
8095
let collector = OtlpTestServer::start().await;
81-
let (provider, error) = super::provider_for(Some(collector.endpoint()));
96+
let (provider, error) =
97+
super::provider_for(Some(collector.endpoint()), Some("kubernetes-dev"));
8298
assert!(error.is_none());
8399
let provider = provider.expect("provider");
84100
let subscriber = tracing_subscriber::registry().with(super::layer(&provider));
@@ -98,6 +114,7 @@ mod tests {
98114
.iter()
99115
.any(|span| span.name == "kubernetes.create_sandbox")
100116
);
117+
assert_eq!(received.gateway_names, ["kubernetes-dev"]);
101118
assert!(
102119
received
103120
.service_names

crates/openshell-driver-podman/src/main.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ struct Args {
4040
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
4141
otlp_endpoint: Option<String>,
4242

43+
#[arg(long, env = "OPENSHELL_GATEWAY_NAME")]
44+
gateway_name: Option<String>,
45+
4346
/// Path to the Podman API Unix socket.
4447
#[arg(long, env = "OPENSHELL_PODMAN_SOCKET")]
4548
podman_socket: Option<PathBuf>,
@@ -174,8 +177,10 @@ struct Args {
174177
#[tokio::main]
175178
async fn main() -> Result<()> {
176179
let args = Args::parse();
177-
let (tracer_provider, setup_error) =
178-
openshell_driver_podman::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
180+
let (tracer_provider, setup_error) = openshell_driver_podman::otel_tracing::provider_for(
181+
args.otlp_endpoint.as_deref(),
182+
args.gateway_name.as_deref(),
183+
);
179184
tracing_subscriber::registry()
180185
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
181186
.with(tracing_subscriber::fmt::layer())
@@ -306,17 +311,20 @@ mod tests {
306311
}
307312

308313
#[test]
309-
fn accepts_gateway_otlp_endpoint() {
314+
fn accepts_gateway_otlp_configuration() {
310315
let args = Args::try_parse_from([
311316
"openshell-driver-podman",
312317
"--otlp-endpoint",
313318
"http://collector.internal:4317",
319+
"--gateway-name",
320+
"production-us-west",
314321
])
315-
.expect("OTLP endpoint should be accepted");
322+
.expect("OTLP configuration should be accepted");
316323

317324
assert_eq!(
318325
args.otlp_endpoint.as_deref(),
319326
Some("http://collector.internal:4317")
320327
);
328+
assert_eq!(args.gateway_name.as_deref(), Some("production-us-west"));
321329
}
322330
}

0 commit comments

Comments
 (0)