Post-1.0: OTLP tracing, broker image + compose service, install rehearsal

- tc-telemetry: fmt subscriber always; with [telemetry] otlp_endpoint
  set, spans batch-export over OTLP/HTTP. Tested against a REAL OTLP
  receiver decoding the actual protobuf (official proto types): the
  emitted span and service.name arrive on the wire. No endpoint = no
  export = no network (air-gap stance). tower-http TraceLayer gives
  every API request a span
- The broker finally has its own image (images/broker.Dockerfile,
  9.5MB from scratch) — the Helm chart referenced one that never
  existed — and the compose deployment now RUNS the broker, sharing a
  socket volume with the server (the unix-socket equivalent of the K8s
  sidecar). Compose secret flows were silently dead before this
- server.Dockerfile fixes surfaced by the rehearsal: the workspace
  build needs tools/ (bundler joined the workspace) and
  images/seccomp/ (include_str! profile) in the build context
- scripts/rehearse-install.sh (plan: clean-VM rehearsal): assembles a
  REAL signed bundle from the built images (server/frontend/broker/
  postgres/socket-proxy), runs the customer path — offline verify,
  docker load, compose up — and asserts /healthz plus the served login
  page before teardown. Passing locally; wired as a release.yml step,
  which also builds/ships the broker + socket-proxy images now

161 Rust tests + 29 journeys; clean-room rehearsal green.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 11:35:32 -05:00
co-authored by Claude Fable 5
parent 05e9612688
commit 8046853feb
17 changed files with 569 additions and 7 deletions
+81
View File
@@ -0,0 +1,81 @@
//! Spans reach a REAL OTLP/HTTP receiver: a live server accepting
//! protobuf at /v1/traces, decoded with the official proto types — the
//! exact wire contract any collector speaks.
use std::sync::Arc;
use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
use prost::Message;
use tokio::sync::Mutex;
type Received = Arc<Mutex<Vec<ExportTraceServiceRequest>>>;
#[tokio::test(flavor = "multi_thread")]
async fn spans_export_to_a_real_otlp_receiver() {
let received: Received = Arc::new(Mutex::new(Vec::new()));
let state = received.clone();
let app = axum::Router::new()
.route(
"/v1/traces",
axum::routing::post(
|axum::extract::State(state): axum::extract::State<Received>,
body: axum::body::Bytes| async move {
let request =
ExportTraceServiceRequest::decode(body.as_ref()).expect("otlp protobuf");
state.lock().await.push(request);
// An empty ExportTraceServiceResponse.
(
[("content-type", "application/x-protobuf")],
Vec::<u8>::new(),
)
},
),
)
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let endpoint = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let guard = tc_telemetry::init("teamclaw-test", Some(&endpoint)).expect("init");
{
let span = tracing::info_span!("approval_decision", approval_id = "ap_123");
let _entered = span.enter();
tracing::info!("decided");
}
guard.flush();
// Find our span among everything exported.
let requests = received.lock().await;
assert!(!requests.is_empty(), "no OTLP export arrived");
let mut span_names = Vec::new();
let mut service_names = Vec::new();
for request in requests.iter() {
for resource in &request.resource_spans {
if let Some(res) = &resource.resource {
for attr in &res.attributes {
if attr.key == "service.name" {
if let Some(value) = &attr.value {
service_names.push(format!("{value:?}"));
}
}
}
}
for scope in &resource.scope_spans {
for span in &scope.spans {
span_names.push(span.name.clone());
}
}
}
}
assert!(
span_names.iter().any(|name| name == "approval_decision"),
"spans: {span_names:?}"
);
assert!(
service_names.iter().any(|s| s.contains("teamclaw-test")),
"services: {service_names:?}"
);
}