//! Telemetry shipping to an s10 ingest endpoint.
//!
//! One wire type — the s10 `SignalEnvelope` (`s10/2`) — posted as NDJSON to
//! `{S10_INGEST_URL}/v2/envelopes` with a bearer key, batched over a bounded
//! channel. Configuration comes from the environment; when `S10_INGEST_URL`
//! or `S10_INGEST_KEY` is unset, every handle is a silent no-op.
//!
//! # Privacy invariant
//!
//! Envelopes carry ids, slugs, labels, and counts — never file content,
//! commit messages, rationale, statements, or repository paths. s10's PII
//! scrub does not run on ordinary event attributes, and jacquard's own
//! posture is that only sketches leave the org; telemetry does not get a
//! wider channel than the registry.
//!
//! # Shipper invariant
//!
//! The shipper never propagates failures and never logs at error level: a
//! telemetry outage must not become an outage, and an error-shipping loop
//! must not feed itself.
mod envelope;
mod rfc3339;
mod shipper;
pub use envelope::{Envelope, SpanStatus};
/// Where telemetry goes: an s10 ingest endpoint and its bearer key.
#[derive(Debug, Clone)]
pub struct S10Config {
/// Base URL, e.g. `http://localhost:8080`.
pub url: String,
/// The bearer key; the tenant is derived from it server-side.
pub key: String,
}
impl S10Config {
/// Reads `S10_INGEST_URL` / `S10_INGEST_KEY`. `None` when either is
/// unset or empty — the conventional "telemetry off" state.
#[must_use]
pub fn from_env() -> Option<Self> {
let url = std::env::var("S10_INGEST_URL")
.ok()
.filter(|s| !s.is_empty())?;
let key = std::env::var("S10_INGEST_KEY")
.ok()
.filter(|s| !s.is_empty())?;
Some(Self { url, key })
}
}
/// A cheap-to-clone telemetry handle. Disabled handles drop everything.
#[derive(Debug, Clone)]
pub struct Telemetry {
tx: Option<tokio::sync::mpsc::Sender<Envelope>>,
service: &'static str,
}
impl Telemetry {
/// Starts the background shipper. Must be called inside a Tokio
/// runtime. With `config: None`, returns a no-op handle.
#[must_use]
pub fn start(config: Option<S10Config>, service: &'static str) -> Self {
let tx = config.map(|config| {
let (tx, rx) = tokio::sync::mpsc::channel(shipper::QUEUE_CAP);
tokio::spawn(shipper::run(config, rx));
tx
});
Self { tx, service }
}
/// A handle that ships nothing, for tests.
#[must_use]
pub const fn disabled() -> Self {
Self {
tx: None,
service: "disabled",
}
}
/// Whether envelopes actually leave the process.
#[must_use]
pub const fn enabled(&self) -> bool {
self.tx.is_some()
}
/// Enqueues an envelope. Fire-and-forget: a full queue drops the
/// envelope rather than blocking the caller.
pub fn emit(&self, envelope: Envelope) {
if let Some(tx) = &self.tx {
let _ = tx.try_send(envelope.resource(self.service));
}
}
}