Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ tracing-appender = "0.2"
opentelemetry = "0.32"
opentelemetry_sdk = { version = "0.32", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.32", default-features = false, features = ["grpc-tonic", "trace"] }
opentelemetry-proto = { version = "0.32", default-features = false, features = ["gen-tonic", "trace", "with-serde"] }
opentelemetry-proto = { version = "0.32", default-features = false, features = ["gen-tonic", "logs", "trace", "with-serde"] }
tracing-opentelemetry = { version = "0.33", default-features = false, features = ["tracing-log"] }

# Metrics
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-core/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ pub mod test {
pub use super::generated::openshell::test::v1::*;
}

/// Supervisor capability for relaying OTLP logs.
pub const OTEL_EXPORT_LOGS_V1_CAPABILITY: &str = "otel_export_logs_v1";

pub mod middleware {
pub use super::generated::openshell::middleware::v1;
}
Expand Down
17 changes: 16 additions & 1 deletion crates/openshell-core/src/sandbox_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ pub const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_TRACES_
/// Trace-specific OpenTelemetry OTLP exporter protocol.
pub const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";

/// Log-specific OpenTelemetry OTLP exporter endpoint.
pub const OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT";

/// Log-specific OpenTelemetry OTLP exporter protocol.
pub const OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL";

/// Reserved destination agent processes export OTLP to.
///
/// This address is never routed. A workload `connect()` to any non-loopback
Expand All @@ -297,6 +303,9 @@ pub const OTLP_RELAY_ENDPOINT: &str = "http://192.0.0.8:4318";
/// Trace-specific HTTP ingestion URL for the sandbox relay.
pub const OTLP_RELAY_TRACES_ENDPOINT: &str = "http://192.0.0.8:4318/v1/traces";

/// Log-specific HTTP ingestion URL for the sandbox relay.
pub const OTLP_RELAY_LOGS_ENDPOINT: &str = "http://192.0.0.8:4318/v1/logs";

// The corporate upstream-proxy configuration deliberately has no reserved
// environment variables: it travels on the supervisor's argv
// (`--upstream-proxy` and friends), which a sandbox image cannot forge the
Expand All @@ -306,7 +315,9 @@ pub const OTLP_RELAY_TRACES_ENDPOINT: &str = "http://192.0.0.8:4318/v1/traces";
mod otlp_relay_address_tests {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use super::{OTLP_RELAY_ADDR, OTLP_RELAY_ENDPOINT, OTLP_RELAY_TRACES_ENDPOINT};
use super::{
OTLP_RELAY_ADDR, OTLP_RELAY_ENDPOINT, OTLP_RELAY_LOGS_ENDPOINT, OTLP_RELAY_TRACES_ENDPOINT,
};

#[test]
fn relay_address_is_an_unroutable_non_loopback_label() {
Expand All @@ -330,6 +341,10 @@ mod otlp_relay_address_tests {
OTLP_RELAY_TRACES_ENDPOINT,
format!("{OTLP_RELAY_ENDPOINT}/v1/traces")
);
assert_eq!(
OTLP_RELAY_LOGS_ENDPOINT,
format!("{OTLP_RELAY_ENDPOINT}/v1/logs")
);
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-gateway/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,7 @@ mod tests {
service_name: Some("custom-gateway".to_string()),
agent_endpoint: None,
sandbox_relay_enabled: false,
agent_signals: Vec::default(),
}),
"production-us-west",
);
Expand Down
32 changes: 32 additions & 0 deletions crates/openshell-server/src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,21 @@ pub struct OtlpConfig {
/// self-observability does not implicitly authorize workload telemetry.
#[serde(default)]
pub sandbox_relay_enabled: bool,

/// Signals exposed to sandbox workloads when the relay is enabled.
#[serde(default = "default_agent_signals")]
pub agent_signals: Vec<OtlpAgentSignal>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OtlpAgentSignal {
Traces,
Logs,
}

fn default_agent_signals() -> Vec<OtlpAgentSignal> {
vec![OtlpAgentSignal::Traces]
}

impl OtlpConfig {
Expand Down Expand Up @@ -946,6 +961,7 @@ service_name = "openshell-gateway-dev"
assert_eq!(otlp.service_name.as_deref(), Some("openshell-gateway-dev"));
assert_eq!(otlp.agent_lane_endpoint(), otlp.endpoint);
assert!(!otlp.sandbox_relay_enabled);
assert_eq!(otlp.agent_signals, vec![OtlpAgentSignal::Traces]);
}

#[test]
Expand Down Expand Up @@ -976,6 +992,22 @@ sandbox_relay_enabled = true
assert!(otlp.sandbox_relay_enabled);
}

#[test]
fn parses_explicit_agent_signals() {
let toml = r#"
[openshell.gateway.otlp]
endpoint = "http://infra-collector:4317"
sandbox_relay_enabled = true
agent_signals = ["traces", "logs"]
"#;
let tmp = write_tmp(toml);
let file = load(tmp.path()).expect("valid otlp config parses");
assert_eq!(
file.openshell.gateway.otlp.unwrap().agent_signals,
vec![OtlpAgentSignal::Traces, OtlpAgentSignal::Logs]
);
}

#[test]
fn gateway_accepts_a_single_ocsf_log_destination() {
let tmp = write_tmp("[openshell.gateway.ocsf_log]\npath = 'events.jsonl'\n");
Expand Down
103 changes: 75 additions & 28 deletions crates/openshell-server/src/grpc/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,15 @@ async fn handle_create_sandbox_inner(
// Point the workload's OTel SDK at the supervisor relay when this gateway
// can accept relayed telemetry. The values persist in the stored spec, so
// restarts inherit them and `sandbox get` shows where traces go.
inject_otel_relay_environment(&mut spec.environment, state.otel_relay_exporter.is_some());
inject_otel_relay_environment(
&mut spec.environment,
state
.otel_relay_exporter
.as_ref()
.map_or_else(crate::otel_relay::RelaySignals::none, |exporter| {
exporter.enabled_signals()
}),
);

// Process identity and MCP default materialization can increase the
// protobuf size. Recheck the exact canonical spec before any middleware or
Expand Down Expand Up @@ -794,40 +802,54 @@ fn validate_create_sandbox_request_pre_io(
/// or the unsupported gRPC protocol.
fn inject_otel_relay_environment(
environment: &mut HashMap<String, String>,
otel_relay_enabled: bool,
relay_signals: impl Into<crate::otel_relay::RelaySignals>,
) -> bool {
use openshell_core::sandbox_env::{
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, OTLP_RELAY_TRACES_ENDPOINT,
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, OTLP_RELAY_LOGS_ENDPOINT, OTLP_RELAY_TRACES_ENDPOINT,
};
let has_nonempty_endpoint = |key: &str| {
let relay_signals = relay_signals.into();
let has_nonempty_endpoint = |environment: &HashMap<String, String>, key: &str| {
environment
.get(key)
.is_some_and(|endpoint| !endpoint.trim().is_empty())
};
let configured_traces_protocol = environment
.get(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL)
.map(|protocol| protocol.trim().to_ascii_lowercase());
let traces_protocol = match configured_traces_protocol.as_deref() {
Some("grpc") => return false,
Some(protocol @ ("http/protobuf" | "http/json")) => protocol.to_string(),
_ => "http/protobuf".to_string(),
};
if !otel_relay_enabled
|| has_nonempty_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
|| has_nonempty_endpoint(OTEL_EXPORTER_OTLP_ENDPOINT)
{
if !relay_signals.any() || has_nonempty_endpoint(environment, OTEL_EXPORTER_OTLP_ENDPOINT) {
return false;
}
environment.insert(
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT.to_string(),
OTLP_RELAY_TRACES_ENDPOINT.to_string(),
);
environment.insert(
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL.to_string(),
traces_protocol,
);
true

let mut injected = false;
for (enabled, endpoint_key, protocol_key, relay_endpoint) in [
(
relay_signals.traces(),
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTLP_RELAY_TRACES_ENDPOINT,
),
(
relay_signals.logs(),
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
OTLP_RELAY_LOGS_ENDPOINT,
),
] {
if !enabled || has_nonempty_endpoint(environment, endpoint_key) {
continue;
}
let protocol = environment
.get(protocol_key)
.map(|protocol| protocol.trim().to_ascii_lowercase());
let protocol = match protocol.as_deref() {
Some("grpc") => continue,
Some(protocol @ ("http/protobuf" | "http/json")) => protocol.to_string(),
_ => "http/protobuf".to_string(),
};
environment.insert(endpoint_key.to_string(), relay_endpoint.to_string());
environment.insert(protocol_key.to_string(), protocol);
injected = true;
}
injected
}

fn validate_template_create_governance_spec(spec: &SandboxSpec) -> Result<(), Status> {
Expand Down Expand Up @@ -3990,9 +4012,10 @@ mod tests {
GpuResourceRequirements, SandboxServiceExposure, ServiceAuthorizationMode, ServiceEndpoint,
};
use openshell_core::sandbox_env::{
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTLP_RELAY_TRACES_ENDPOINT,
OTLP_RELAY_LOGS_ENDPOINT, OTLP_RELAY_TRACES_ENDPOINT,
};

#[tokio::test]
Expand Down Expand Up @@ -4154,6 +4177,30 @@ mod tests {
assert_eq!(env.get("HOME").map(String::as_str), Some("/home/user"));
}

#[test]
fn inject_otel_relay_environment_adds_logs_only_when_enabled() {
let mut env = HashMap::new();
let signals = crate::otel_relay::RelaySignals::from_config(&[
crate::config_file::OtlpAgentSignal::Traces,
crate::config_file::OtlpAgentSignal::Logs,
]);
assert!(inject_otel_relay_environment(&mut env, signals));
assert_eq!(
env.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
.map(String::as_str),
Some(OTLP_RELAY_LOGS_ENDPOINT)
);
assert_eq!(
env.get(OTEL_EXPORTER_OTLP_LOGS_PROTOCOL)
.map(String::as_str),
Some("http/protobuf")
);

let mut trace_only = HashMap::new();
assert!(inject_otel_relay_environment(&mut trace_only, true));
assert!(!trace_only.contains_key(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT));
}

#[test]
fn inject_otel_relay_environment_is_a_noop_without_a_relay() {
let mut env = HashMap::new();
Expand Down
Loading
Loading