233 lines
8.9 KiB
Rust
233 lines
8.9 KiB
Rust
//! Integration test for the external OTEL stream against an in-process OTLP
|
|
//! collector: wire payloads, delta temporality, gates-off canary absence at
|
|
//! the wire layer, flush-on-shutdown ≤ 2 s, and post-shutdown silence.
|
|
|
|
mod otlp_collector;
|
|
|
|
use otlp_collector as col;
|
|
|
|
const CANARY_MODEL: &str = "sk-CANARYabcdefghij1234567890";
|
|
const CANARY_PROMPT: &str = "CANARY_PROMPT_TEXT do not export";
|
|
const CANARY_MCP: &str = "canary-internal-mcp-server";
|
|
|
|
#[test]
|
|
fn external_stream_end_to_end() {
|
|
let collected = col::Collected::default();
|
|
let endpoint = col::start_collector(collected.clone());
|
|
|
|
// Resolve through the real config path (double opt-in, gates off).
|
|
let mut cfg = xai_grok_telemetry::external::ExternalOtelConfig::resolve_with(
|
|
|name| match name {
|
|
"GROK_EXTERNAL_OTEL" => Some("1".into()),
|
|
"OTEL_LOGS_EXPORTER" | "OTEL_METRICS_EXPORTER" => Some("otlp".into()),
|
|
"OTEL_EXPORTER_OTLP_ENDPOINT" => Some(endpoint.clone()),
|
|
// Keep intervals short so the test is fast; flush() forces anyway.
|
|
"OTEL_METRIC_EXPORT_INTERVAL" => Some("200".into()),
|
|
"OTEL_BLRP_SCHEDULE_DELAY" => Some("100".into()),
|
|
_ => None,
|
|
},
|
|
None,
|
|
)
|
|
.expect("double opt-in must resolve");
|
|
cfg.client = xai_grok_telemetry::external::config::ExternalClientInfo {
|
|
service_version: "0.0.0-test".into(),
|
|
client_version: "0.0.0-test".into(),
|
|
app_entrypoint: "cli".into(),
|
|
};
|
|
|
|
xai_grok_telemetry::external::init(Some(cfg));
|
|
assert!(xai_grok_telemetry::external::is_active());
|
|
|
|
// Emit through the same funnel production uses — with the product events client
|
|
// never initialized (TelemetryMode effectively Disabled) and no auth at
|
|
// all, pinning the Disabled half of the G7 independence matrix at the
|
|
// funnel level: the external sink fires anyway.
|
|
assert!(!xai_grok_telemetry::is_enabled());
|
|
xai_grok_telemetry::log_event(xai_grok_telemetry::events::SessionNew {
|
|
session_id: "sess-int-1".into(),
|
|
client_identifier: None,
|
|
client_version: None,
|
|
is_git_repo: true,
|
|
permission_mode: xai_grok_telemetry::enums::PermissionMode::Ask,
|
|
});
|
|
xai_grok_telemetry::log_event(xai_grok_telemetry::events::SessionHarness {
|
|
session_id: "sess-int-1".into(),
|
|
client_identifier: Some("grok-pager".into()),
|
|
model_id: "grok-4".into(),
|
|
agent_name: "grok-build-plan".into(),
|
|
permission_mode: xai_grok_telemetry::enums::PermissionMode::Ask,
|
|
mcp_server_names: vec![CANARY_MCP.into()],
|
|
plugin_names: vec![],
|
|
skill_names: vec![],
|
|
lsp_server_names: vec![],
|
|
hook_names: vec![],
|
|
agents_md_dir_names: vec![],
|
|
memory_enabled: false,
|
|
is_git_repo: true,
|
|
auto_update: None,
|
|
});
|
|
xai_grok_telemetry::log_event(xai_grok_telemetry::events::PromptSubmitted {
|
|
prompt_length: CANARY_PROMPT.len(),
|
|
model_id: "grok-4".into(),
|
|
client_identifier: None,
|
|
screen_mode: None,
|
|
prompt_text: Some(CANARY_PROMPT.into()),
|
|
});
|
|
// Model-id canary for the metrics body (increment-time scrub).
|
|
xai_grok_telemetry::log_event(xai_grok_telemetry::events::ModelResponseReceived {
|
|
model_id: CANARY_MODEL.into(),
|
|
duration_ms: 5,
|
|
stop_reason: Some("stop".into()),
|
|
prompt_tokens: Some(11),
|
|
completion_tokens: Some(7),
|
|
reasoning_tokens: None,
|
|
cached_prompt_tokens: None,
|
|
});
|
|
|
|
xai_grok_telemetry::external::flush();
|
|
|
|
assert!(
|
|
col::wait_until(std::time::Duration::from_secs(10), || {
|
|
collected.logs_len() > 0 && collected.metrics_len() > 0
|
|
}),
|
|
"collector must receive both signals"
|
|
);
|
|
|
|
// ── Logs payload ────────────────────────────────────────────────────
|
|
let logs = col::decode_logs(&collected);
|
|
let mut event_names: Vec<String> = vec![];
|
|
let mut resource_service_name = None;
|
|
for req in &logs {
|
|
for rl in &req.resource_logs {
|
|
if let Some(resource) = &rl.resource {
|
|
for kv in &resource.attributes {
|
|
if kv.key == "service.name"
|
|
&& let Some(v) = &kv.value
|
|
{
|
|
resource_service_name = Some(format!("{v:?}"));
|
|
}
|
|
}
|
|
}
|
|
for sl in &rl.scope_logs {
|
|
assert_eq!(
|
|
sl.scope.as_ref().map(|s| s.name.as_str()),
|
|
Some("ai.xai.grok_code")
|
|
);
|
|
for record in &sl.log_records {
|
|
event_names.push(record.event_name.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(
|
|
resource_service_name
|
|
.as_deref()
|
|
.is_some_and(|s| s.contains("grok-cli")),
|
|
"service.name=grok-cli is a wire commitment: {resource_service_name:?}"
|
|
);
|
|
for expected in [
|
|
"grok_code.session_start",
|
|
"grok_code.user_prompt",
|
|
"grok_code.api_request",
|
|
] {
|
|
assert!(
|
|
event_names.iter().any(|n| n == expected),
|
|
"missing {expected} in {event_names:?}"
|
|
);
|
|
}
|
|
// session_start arrives exactly once per emission (no double-send from
|
|
// the funnel).
|
|
assert_eq!(
|
|
event_names
|
|
.iter()
|
|
.filter(|n| *n == "grok_code.session_start")
|
|
.count(),
|
|
1
|
|
);
|
|
|
|
// ── Metrics payload: names + Delta temporality + session.count == 1 ──
|
|
let metrics = col::decode_metrics(&collected);
|
|
let mut metric_names = vec![];
|
|
let mut session_count_total = 0u64;
|
|
for req in &metrics {
|
|
for rm in &req.resource_metrics {
|
|
for sm in &rm.scope_metrics {
|
|
for metric in &sm.metrics {
|
|
metric_names.push(metric.name.clone());
|
|
use opentelemetry_proto::tonic::metrics::v1::metric::Data;
|
|
if let Some(Data::Sum(sum)) = &metric.data {
|
|
assert_eq!(
|
|
sum.aggregation_temporality,
|
|
opentelemetry_proto::tonic::metrics::v1::AggregationTemporality::Delta
|
|
as i32,
|
|
"default temporality must be Delta (CC parity)"
|
|
);
|
|
if metric.name == "grok_code.session.count" {
|
|
for dp in &sum.data_points {
|
|
if let Some(
|
|
opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsInt(v),
|
|
) = dp.value
|
|
{
|
|
session_count_total += v as u64;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(
|
|
metric_names.iter().any(|n| n == "grok_code.session.count"),
|
|
"missing session.count in {metric_names:?}"
|
|
);
|
|
assert!(metric_names.iter().any(|n| n == "grok_code.token.usage"));
|
|
assert_eq!(
|
|
session_count_total, 1,
|
|
"session.count must increment exactly once per SessionNew"
|
|
);
|
|
|
|
// ── Canary absence at the HTTP layer (raw bytes, both signals) ──────
|
|
let raw_logs = collected.raw_logs();
|
|
let raw_metrics = collected.raw_metrics();
|
|
for (label, raw) in [("logs", &raw_logs), ("metrics", &raw_metrics)] {
|
|
let haystack = String::from_utf8_lossy(raw);
|
|
assert!(
|
|
!haystack.contains("CANARY"),
|
|
"canary reached the {label} wire: gates are off / scrub failed"
|
|
);
|
|
assert!(
|
|
!haystack.contains(CANARY_MCP),
|
|
"MCP server name reached the {label} wire"
|
|
);
|
|
}
|
|
// Prompt length exported, text not (already covered by the canary scan).
|
|
|
|
// ── Shutdown: ≤ 2 s + post-shutdown silence ─────────────────────────
|
|
let start = std::time::Instant::now();
|
|
xai_grok_telemetry::external::shutdown();
|
|
assert!(
|
|
start.elapsed() <= std::time::Duration::from_millis(2500),
|
|
"shutdown watchdog must bound exit at ~2s (took {:?})",
|
|
start.elapsed()
|
|
);
|
|
assert!(!xai_grok_telemetry::external::is_active());
|
|
|
|
let logs_before = collected.logs_len();
|
|
xai_grok_telemetry::log_event(xai_grok_telemetry::events::PromptSubmitted {
|
|
prompt_length: 1,
|
|
model_id: "grok-4".into(),
|
|
client_identifier: None,
|
|
screen_mode: None,
|
|
prompt_text: None,
|
|
});
|
|
std::thread::sleep(std::time::Duration::from_millis(400));
|
|
assert_eq!(
|
|
collected.logs_len(),
|
|
logs_before,
|
|
"no exports after shutdown"
|
|
);
|
|
|
|
// Idempotent shutdown: second call is a no-op, not an error/panic.
|
|
xai_grok_telemetry::external::shutdown();
|
|
}
|