use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use opentelemetry::metrics::Histogram;
use opentelemetry::{KeyValue, global};
use opentelemetry_http::{Bytes, HttpClient, HttpError, Request, Response};
use opentelemetry_otlp::{ExporterBuildError, MetricExporter, WithExportConfig, WithHttpConfig};
use opentelemetry_sdk::{
Resource,
metrics::{Aggregation, Instrument, InstrumentKind, SdkMeterProvider, Stream},
};
use crate::config::OtelConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OtelSuppression {
Disabled,
TestEnvironmentFlag,
TestHarness,
}
pub(crate) fn otel_suppression_reason(
config: &OtelConfig,
is_test_build: bool,
running_under_cargo_test: bool,
) -> Option<OtelSuppression> {
if !config.enabled {
return Some(OtelSuppression::Disabled);
}
if config.is_test_environment {
return Some(OtelSuppression::TestEnvironmentFlag);
}
if is_test_build || running_under_cargo_test {
return Some(OtelSuppression::TestHarness);
}
None
}
pub(crate) fn resolve_metrics_endpoint(
cfg_endpoint: Option<&str>,
metrics_env: Option<&str>,
generic_env: Option<&str>,
) -> Option<String> {
let is_set = |v: Option<&str>| v.is_some_and(|s| !s.trim().is_empty());
if is_set(metrics_env) || is_set(generic_env) {
return None;
}
let base = cfg_endpoint.map(str::trim).filter(|s| !s.is_empty())?;
Some(format!("{}/v1/metrics", base.trim_end_matches('/')))
}
fn env_endpoint() -> Option<String> {
[
opentelemetry_otlp::OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
opentelemetry_otlp::OTEL_EXPORTER_OTLP_ENDPOINT,
]
.iter()
.find_map(|var| {
let value = std::env::var(var).ok()?;
let value = value.trim();
(!value.is_empty()).then(|| value.to_owned())
})
}
fn env_otlp_headers() -> Option<String> {
[
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
"OTEL_EXPORTER_OTLP_HEADERS",
]
.iter()
.find_map(|var| {
let value = std::env::var(var).ok()?;
let value = value.trim();
(!value.is_empty()).then(|| value.to_owned())
})
}
fn parse_otlp_headers(raw: &str) -> Vec<(String, String)> {
raw.split(',')
.filter_map(|pair| pair.split_once('='))
.map(|(name, value)| (name.trim().to_ascii_lowercase(), value.trim().to_owned()))
.filter(|(name, value)| !name.is_empty() && !value.is_empty())
.collect()
}
fn credential_safe(uri: &http::Uri) -> bool {
if uri.scheme_str() == Some("https") {
return true;
}
let Some(host) = uri
.host()
.map(|h| h.trim_start_matches('[').trim_end_matches(']'))
else {
return false;
};
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
}
fn redact_endpoint(endpoint: &str) -> std::borrow::Cow<'_, str> {
let (prefix, rest) = match endpoint.split_once("://") {
Some((scheme, rest)) => (format!("{scheme}://"), rest),
None => (String::new(), endpoint),
};
let authority_end = rest.find('/').unwrap_or(rest.len());
match rest[..authority_end].rfind('@') {
None => std::borrow::Cow::Borrowed(endpoint),
Some(at) => std::borrow::Cow::Owned(format!("{prefix}[redacted]@{}", &rest[at + 1..])),
}
}
fn redact_uri(uri: &http::Uri) -> String {
let Some(authority) = uri.authority() else {
return uri.to_string();
};
let Some((_, host_port)) = authority.as_str().split_once('@') else {
return uri.to_string();
};
let scheme = uri
.scheme_str()
.map(|s| format!("{s}://"))
.unwrap_or_default();
format!("{scheme}[redacted]@{host_port}{}", uri.path())
}
fn credential_that_would_be_refused(
auth_mode: crate::config::OtelAuthMode,
headers: Option<&str>,
endpoint: Option<&str>,
) -> Option<String> {
let endpoint = endpoint?;
let unsafe_endpoint = endpoint
.parse::<http::Uri>()
.is_ok_and(|uri| !credential_safe(&uri));
if !unsafe_endpoint {
return None;
}
if matches!(auth_mode, crate::config::OtelAuthMode::Freenet) {
return Some("the freenet bearer token (otel-auth-mode)".to_owned());
}
headers
.map(parse_otlp_headers)
.and_then(|declared| declared.into_iter().next())
.map(|(name, _)| format!("the `{name}` header from OTEL_EXPORTER_OTLP_HEADERS"))
}
fn endpoint_problem(endpoint: &str) -> Option<&'static str> {
match endpoint.parse::<http::Uri>() {
Err(_) => Some(
"not a valid URL; include the scheme, e.g. http://collector:4318. \
From the config file this fails the exporter build; from \
OTEL_EXPORTER_OTLP_* the SDK swallows it and exports to \
http://localhost:4318 instead",
),
Ok(uri) if !matches!(uri.scheme_str(), Some("http" | "https")) => {
Some("missing an http:// or https:// scheme; every export will fail to build a request")
}
Ok(_) => None,
}
}
pub(crate) const REPLAY_WINDOW: std::time::Duration = std::time::Duration::from_secs(300);
pub(crate) fn bearer_token(
signer: &xeddsa::xed25519::PrivateKey,
pubkey_b58: &str,
audience: &str,
body: &[u8],
) -> String {
use sha2::{Digest, Sha256};
use xeddsa::xeddsa::Sign;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_default();
let signed_payload = format!("freenet/{pubkey_b58}/{audience}/{timestamp}");
let body_hash = bs58::encode(Sha256::digest(body)).into_string();
let signed_payload_with_body = format!("{signed_payload}/{body_hash}");
let signature: [u8; 64] = signer.sign(
signed_payload_with_body.as_bytes(),
rand_core10::UnwrapErr(rand10::rngs::SysRng),
);
let signature = bs58::encode(signature).into_string();
format!("{signed_payload}/{signature}")
}
fn audience_of(uri: &http::Uri) -> String {
use sha2::{Digest, Sha256};
let Some(host) = uri.host() else {
return String::new();
};
let port = uri.port_u16().unwrap_or(match uri.scheme_str() {
Some("https") => 443,
_ => 80,
});
let canonical = format!("{}:{port}{}", host.to_ascii_lowercase(), uri.path());
bs58::encode(&Sha256::digest(canonical.as_bytes())[..16]).into_string()
}
struct OtlpHttpClient {
inner: reqwest::blocking::Client,
signer: Option<xeddsa::xed25519::PrivateKey>,
pubkey_b58: String,
operator_credentials: Vec<(String, String)>,
}
impl OtlpHttpClient {
fn credential_header_name(&self, headers: &http::HeaderMap) -> Option<String> {
if headers.contains_key(http::header::AUTHORIZATION) {
return Some("Authorization".to_owned());
}
self.operator_credentials
.iter()
.map(|(name, _)| name)
.find(|name| headers.keys().any(|h| h.as_str() == name.as_str()))
.cloned()
}
fn redaction_needles(&self) -> Vec<&str> {
let mut needles = Vec::new();
for (_, value) in &self.operator_credentials {
for candidate in [
value.as_str(),
value
.split_once(' ')
.filter(|(scheme, _)| {
scheme.eq_ignore_ascii_case("bearer")
|| scheme.eq_ignore_ascii_case("basic")
})
.map(|(_, token)| token)
.unwrap_or_default(),
] {
if candidate.len() >= MIN_REDACTION_NEEDLE {
needles.push(candidate);
}
}
}
needles.sort_by_key(|n| std::cmp::Reverse(n.len()));
needles
}
fn safe_body(&self, body: &[u8]) -> String {
let text = String::from_utf8_lossy(body);
let mut text = redact_keyword_lines(&text);
for needle in self.redaction_needles() {
if text.contains(needle) {
text = text.replace(needle, "[redacted]");
}
}
truncate_for_log(&text)
}
}
impl std::fmt::Debug for OtlpHttpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OtlpHttpClient").finish_non_exhaustive()
}
}
static EXPORT_FAILING: AtomicBool = AtomicBool::new(false);
fn report_export_failure(uri: &http::Uri, detail: &str) {
if !EXPORT_FAILING.swap(true, Ordering::Relaxed) {
tracing::warn!(
uri = redact_uri(uri),
detail,
"OTel metrics export is failing; metrics are not reaching the collector"
);
}
}
fn report_export_success() {
if EXPORT_FAILING.swap(false, Ordering::Relaxed) {
tracing::info!("OTel metrics export recovered");
}
}
const MIN_REDACTION_NEEDLE: usize = 8;
const MAX_RESPONSE_BODY: u64 = 1024 * 1024;
static BODY_CAP_REPORTED: std::sync::Once = std::sync::Once::new();
fn truncate_for_log(text: &str) -> String {
match text.char_indices().nth(256) {
None => text.to_owned(),
Some((at, _)) => text[..at].to_owned(),
}
}
fn redact_keyword_lines(text: &str) -> String {
text.split_inclusive('\n')
.map(|line| {
let lower = line.to_ascii_lowercase();
match ["authorization", "bearer ", "freenet/"]
.iter()
.filter_map(|needle| lower.find(needle))
.min()
{
None => line.to_owned(),
Some(_) => {
let mut redacted = String::from("[redacted]");
if line.ends_with('\n') {
redacted.push('\n');
}
redacted
}
}
})
.collect()
}
#[async_trait::async_trait]
impl HttpClient for OtlpHttpClient {
async fn send_bytes(&self, mut request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
if let Some(signer) = self.signer.as_ref() {
if !request.headers().contains_key(http::header::AUTHORIZATION) {
let audience = audience_of(request.uri());
if audience.is_empty() {
report_export_failure(
request.uri(),
"endpoint has no host, so no collector audience can be \
derived; check otel-endpoint",
);
return Err(
"cannot derive a collector audience from an endpoint with no host".into(),
);
}
let token = bearer_token(
signer,
&self.pubkey_b58,
&audience,
request.body(),
);
request.headers_mut().insert(
http::header::AUTHORIZATION,
http::HeaderValue::from_str(&format!("Bearer {token}"))?,
);
}
}
let credential = self.credential_header_name(request.headers()).or_else(|| {
request
.uri()
.authority()
.filter(|a| a.as_str().contains('@'))
.map(|_| "endpoint userinfo".to_owned())
});
if let Some(name) = credential {
if !credential_safe(request.uri()) {
let uri = redact_uri(request.uri());
report_export_failure(
request.uri(),
&format!(
"refusing to send the `{name}` credential over plaintext http to a \
non-loopback collector; use an https:// endpoint or a collector on \
loopback (see docs/otel-metrics.md)"
),
);
return Err(format!("refusing to send a credential in cleartext to {uri}").into());
}
}
let uri = request.uri().clone();
let request: reqwest::blocking::Request = request
.map(|body| body.to_vec())
.try_into()
.inspect_err(|error| {
report_export_failure(&uri, &format!("invalid request URL: {error}"))
})?;
let mut response = self
.inner
.execute(request)
.inspect_err(|error| report_export_failure(&uri, &format!("{error}")))?;
let status = response.status();
let headers = std::mem::take(response.headers_mut());
let mut buf = Vec::new();
std::io::Read::read_to_end(
&mut std::io::Read::take(&mut response, MAX_RESPONSE_BODY),
&mut buf,
)
.inspect_err(|error| report_export_failure(&uri, &format!("{error}")))?;
if buf.len() as u64 > MAX_RESPONSE_BODY {
buf.truncate(MAX_RESPONSE_BODY as usize);
}
if buf.len() as u64 == MAX_RESPONSE_BODY {
BODY_CAP_REPORTED.call_once(|| {
tracing::warn!(
uri = redact_uri(&uri),
cap = MAX_RESPONSE_BODY,
"collector response reached the read cap; if it was larger, \
partial-success detail for this batch is incomplete"
);
});
}
let body = Bytes::from(buf);
if status.is_success() {
report_export_success();
} else {
report_export_failure(&uri, &format!("HTTP {status}: {}", self.safe_body(&body)));
}
let mut http_response = Response::builder().status(status).body(body)?;
*http_response.headers_mut() = headers;
Ok(http_response)
}
}
fn export_http_client() -> Result<reqwest::blocking::Client, reqwest::Error> {
reqwest::blocking::Client::builder()
.timeout(export_timeout())
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()
}
const METER_NAME: &str = "freenet";
pub(crate) fn init(
config: &OtelConfig,
keypair: &crate::transport::TransportKeypair,
) -> Option<OtelSuppression> {
if let Some(reason) = otel_suppression_reason(
config,
cfg!(test),
super::telemetry::running_under_cargo_test(),
) {
if config.enabled && reason != OtelSuppression::Disabled {
tracing::warn!(
?reason,
"otel-telemetry-enabled is set but the OTel metrics exporter was suppressed"
);
} else {
tracing::debug!(?reason, "OTel metrics exporter not started");
}
return Some(reason);
}
let env_endpoint = env_endpoint();
let endpoint = resolve_metrics_endpoint(
config.endpoint.as_deref(),
std::env::var(opentelemetry_otlp::OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)
.ok()
.as_deref(),
std::env::var(opentelemetry_otlp::OTEL_EXPORTER_OTLP_ENDPOINT)
.ok()
.as_deref(),
);
if let (Some(env_endpoint), Some(cfg_endpoint)) =
(env_endpoint.as_deref(), config.endpoint.as_deref())
{
tracing::warn!(
env_endpoint = %redact_endpoint(env_endpoint),
cfg_endpoint = %redact_endpoint(cfg_endpoint),
"OTEL_EXPORTER_OTLP_* overrides the configured otel-endpoint"
);
}
let effective_endpoint = endpoint.as_deref().or(env_endpoint.as_deref());
if let Some(effective) = effective_endpoint {
if let Some(problem) = endpoint_problem(effective) {
tracing::warn!(
endpoint = %redact_endpoint(effective),
problem,
"OTLP endpoint is unusable"
);
}
}
if let Some(credential) = credential_that_would_be_refused(
config.auth_mode,
env_otlp_headers().as_deref(),
effective_endpoint,
) {
tracing::warn!(
endpoint = %redact_endpoint(effective_endpoint.unwrap_or_default()),
credential,
"the collector endpoint is plaintext http and not loopback, so every \
export will be refused rather than send this credential in cleartext; \
use an https:// endpoint or a collector on loopback (see \
docs/otel-metrics.md)"
);
}
let (pubkey, fingerprint) = identity_attributes(keypair);
let auth_signer = match config.auth_mode {
crate::config::OtelAuthMode::Freenet => Some(keypair.auth_token_signer()),
crate::config::OtelAuthMode::Disabled => None,
};
match build_provider(endpoint.as_deref(), pubkey, fingerprint, auth_signer) {
Ok(provider) => {
global::set_meter_provider(provider);
register_metrics();
tracing::info!(
endpoint = %redact_endpoint(
endpoint
.as_deref()
.or(env_endpoint.as_deref())
.unwrap_or("http://localhost:4318 (SDK default)")
),
auth_mode = ?config.auth_mode,
replay_window_secs = matches!(
config.auth_mode,
crate::config::OtelAuthMode::Freenet
)
.then(|| REPLAY_WINDOW.as_secs()),
"OTel metrics exporter started"
);
}
Err(error) => {
tracing::warn!(
%error,
"OTel metrics exporter failed to start; node continues without metrics"
);
}
}
None
}
fn identity_attributes(keypair: &crate::transport::TransportKeypair) -> (String, String) {
(
bs58::encode(keypair.public_key_bytes()).into_string(),
keypair.public().to_string(),
)
}
pub(crate) fn build_provider(
endpoint: Option<&str>,
pubkey: String,
fingerprint: String,
auth_signer: Option<xeddsa::xed25519::PrivateKey>,
) -> Result<SdkMeterProvider, ExporterBuildError> {
std::thread::scope(|scope| {
scope
.spawn(move || build_provider_blocking(endpoint, pubkey, fingerprint, auth_signer))
.join()
.unwrap_or_else(|panic| {
let msg = panic
.downcast_ref::<&str>()
.map(ToString::to_string)
.or_else(|| panic.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "non-string panic payload".to_owned());
Err(ExporterBuildError::InternalFailure(format!(
"otel provider build thread panicked: {msg}"
)))
})
})
}
fn export_timeout() -> std::time::Duration {
for var in [
opentelemetry_otlp::OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
opentelemetry_otlp::OTEL_EXPORTER_OTLP_TIMEOUT,
] {
let Ok(raw) = std::env::var(var) else {
continue;
};
let raw = raw.trim();
if raw.is_empty() {
continue;
}
match raw.parse::<u64>() {
Ok(ms) => {
if ms < 100 {
tracing::warn!(
var,
ms,
"OTLP export timeout is in MILLISECONDS and this value is very small; \
a seconds value was probably intended"
);
}
return std::time::Duration::from_millis(ms);
}
Err(error) => tracing::warn!(
var,
raw,
%error,
"ignoring unparseable OTLP export timeout (expected whole milliseconds)"
),
}
}
opentelemetry_otlp::OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT
}
fn env_declared_resource_keys() -> Vec<String> {
let mut keys = Vec::new();
if std::env::var("OTEL_SERVICE_NAME").is_ok_and(|v| !v.trim().is_empty()) {
keys.push("service.name".to_owned());
}
if let Ok(attrs) = std::env::var("OTEL_RESOURCE_ATTRIBUTES") {
keys.extend(attrs.split(',').filter_map(|pair| {
let key = pair.split('=').next()?.trim();
(!key.is_empty()).then(|| key.to_owned())
}));
}
keys
}
fn resource_attributes(
pubkey: String,
fingerprint: String,
declared: &[String],
) -> Vec<(&'static str, String)> {
let mut attributes = vec![
("freenet.node.pubkey", pubkey),
("freenet.node.fingerprint", fingerprint),
];
for (key, _) in &attributes {
if declared.iter().any(|declared| declared == key) {
tracing::warn!(
key,
"the node identity resource attribute cannot be overridden by \
OTEL_RESOURCE_ATTRIBUTES; the signed value is exported instead"
);
}
}
attributes.extend(
[
("service.name", "freenet-node".to_owned()),
("service.version", env!("CARGO_PKG_VERSION").to_owned()),
("os.type", std::env::consts::OS.to_owned()),
("host.arch", std::env::consts::ARCH.to_owned()),
]
.into_iter()
.filter(|(key, _)| !declared.iter().any(|declared| declared == key)),
);
attributes
}
fn build_provider_blocking(
endpoint: Option<&str>,
pubkey: String,
fingerprint: String,
auth_signer: Option<xeddsa::xed25519::PrivateKey>,
) -> Result<SdkMeterProvider, ExporterBuildError> {
let mut builder = MetricExporter::builder().with_http();
if let Some(endpoint) = endpoint {
builder = builder.with_endpoint(endpoint);
}
let http_client = export_http_client().map_err(|error| {
ExporterBuildError::InternalFailure(format!("otel http client build failed: {error}"))
})?;
builder = builder.with_http_client(OtlpHttpClient {
inner: http_client,
signer: auth_signer,
pubkey_b58: pubkey.clone(),
operator_credentials: env_otlp_headers()
.as_deref()
.map(parse_otlp_headers)
.unwrap_or_default(),
});
let exporter = builder.build()?;
let mut resource = Resource::builder();
for (key, value) in resource_attributes(pubkey, fingerprint, &env_declared_resource_keys()) {
resource = resource.with_attribute(KeyValue::new(key, value));
}
let resource = resource.build();
Ok(SdkMeterProvider::builder()
.with_periodic_exporter(exporter)
.with_resource(resource)
.with_view(|instrument: &Instrument| {
(instrument.kind() == InstrumentKind::Histogram)
.then(|| {
Stream::builder()
.with_aggregation(Aggregation::Base2ExponentialHistogram {
max_size: 160,
max_scale: 20,
record_min_max: true,
})
.build()
.ok()
})
.flatten()
})
.build())
}
struct Instruments {
rtt: Histogram<f64>,
cwnd: Histogram<u64>,
}
static INSTRUMENTS: OnceLock<Instruments> = OnceLock::new();
pub(crate) fn record_rtt_ms(rtt_ms: f64) {
if let Some(i) = INSTRUMENTS.get() {
i.rtt.record(rtt_ms, &[]);
}
}
pub(crate) fn record_cwnd(cwnd_bytes: u64) {
if let Some(i) = INSTRUMENTS.get() {
i.cwnd.record(cwnd_bytes, &[]);
}
}
fn register_metrics() {
let meter = global::meter(METER_NAME);
if INSTRUMENTS.set(build_instruments(&meter)).is_err() {
tracing::warn!("OTel instruments already registered; keeping the first set");
}
register_observables(&meter, RingSources::live());
}
fn build_instruments(meter: &opentelemetry::metrics::Meter) -> Instruments {
Instruments {
rtt: meter
.f64_histogram("freenet.transport.rtt")
.with_unit("ms")
.with_description("Round-trip time observed on transport connections")
.build(),
cwnd: meter
.u64_histogram("freenet.transport.cwnd")
.with_unit("By")
.with_description("Congestion window samples")
.build(),
}
}
fn register_observables(meter: &opentelemetry::metrics::Meter, sources: RingSources) {
let _rss = meter
.u64_observable_gauge("freenet.process.memory.rss")
.with_unit("By")
.with_description("Resident set size of the freenet process")
.with_callback(|observer| {
if let Some(rss) = crate::node::resource_metrics::rss_bytes() {
observer.observe(rss, &[]);
}
})
.build();
register_transport_metrics(meter);
register_ring_metrics(meter, sources);
register_queue_metrics(meter);
}
fn register_transport_metrics(meter: &opentelemetry::metrics::Meter) {
use crate::transport::TRANSPORT_METRICS;
let _bytes = meter
.u64_observable_counter("freenet.transport.bytes")
.with_unit("By")
.with_description(
"Wire bytes. Sent is metered at the socket (includes keep-alives, ACKs and \
NAT probes); received is metered post-authentication, so the two directions \
are deliberately not symmetric.",
)
.with_callback(|observer| {
observer.observe(
TRANSPORT_METRICS.cumulative_bytes_sent(),
&[KeyValue::new("direction", "sent")],
);
observer.observe(
TRANSPORT_METRICS.cumulative_bytes_received(),
&[KeyValue::new("direction", "received")],
);
})
.build();
let _packets = meter
.u64_observable_counter("freenet.transport.packets")
.with_description("UDP datagrams, metered at the same sites as freenet.transport.bytes")
.with_callback(|observer| {
let (sent, received) = TRANSPORT_METRICS.cumulative_packets();
observer.observe(sent, &[KeyValue::new("direction", "sent")]);
observer.observe(received, &[KeyValue::new("direction", "received")]);
})
.build();
let _transfers = meter
.u64_observable_counter("freenet.transport.transfers")
.with_description("Stream transfers by outcome")
.with_callback(|observer| {
let (completed, failed) = TRANSPORT_METRICS.cumulative_transfers();
observer.observe(completed, &[KeyValue::new("result", "completed")]);
observer.observe(failed, &[KeyValue::new("result", "failed")]);
})
.build();
let _nat = meter
.u64_observable_counter("freenet.transport.nat_traversal")
.with_description("Outbound NAT traversal attempts by outcome")
.with_callback(|observer| {
let (attempt, established, failed_error, failed_version) =
TRANSPORT_METRICS.cumulative_nat_traversal();
for (result, value) in [
("attempt", attempt),
("established", established),
("failed_error", failed_error),
("failed_version", failed_version),
] {
observer.observe(value, &[KeyValue::new("result", result)]);
}
})
.build();
}
#[derive(Clone, Copy)]
struct RingSources {
ring_stats: fn() -> Option<crate::node::network_status::RingStatsSnapshot>,
hosting_reasons: fn() -> Option<crate::ring::HostingReasonStats>,
status_scalars: fn() -> Option<crate::node::network_status::OtelStatusScalars>,
}
impl RingSources {
fn live() -> Self {
use crate::node::network_status::{
otel_hosting_reasons, otel_ring_stats, otel_status_scalars,
};
Self {
ring_stats: otel_ring_stats,
hosting_reasons: otel_hosting_reasons,
status_scalars: otel_status_scalars,
}
}
}
fn register_ring_metrics(meter: &opentelemetry::metrics::Meter, sources: RingSources) {
use crate::ring::HostingReason;
let _connections = meter
.u64_observable_gauge("freenet.ring.connections")
.with_description("Active ring connections")
.with_callback(move |observer| {
if let Some(ring) = (sources.ring_stats)() {
observer.observe(ring.connection_count as u64, &[]);
}
})
.build();
let _hosted = meter
.u64_observable_gauge("freenet.node.contracts.hosted")
.with_description(
"Contracts currently hosted by this node, partitioned by why each one is held",
)
.with_callback(move |observer| {
if let Some(reasons) = (sources.hosting_reasons)() {
for reason in HostingReason::ALL {
observer.observe(
reasons.count(reason),
&[KeyValue::new("reason", reason.as_str())],
);
}
}
})
.build();
let _hosted_bytes = meter
.u64_observable_gauge("freenet.node.contracts.hosted.bytes")
.with_unit("By")
.with_description(
"Contract state bytes hosted by this node, partitioned by why each contract is \
held. State only — WASM code blobs and database overhead are excluded, matching \
what the hosting cache's byte budget measures.",
)
.with_callback(move |observer| {
if let Some(reasons) = (sources.hosting_reasons)() {
for reason in HostingReason::ALL {
observer.observe(
reasons.bytes(reason),
&[KeyValue::new("reason", reason.as_str())],
);
}
}
})
.build();
let _gateway_failures = meter
.u64_observable_counter("freenet.connect.gateway_failures")
.with_description("Gateway connection failures since startup")
.with_callback(move |observer| {
if let Some(status) = (sources.status_scalars)() {
observer.observe(status.connection_attempts as u64, &[]);
}
})
.build();
let _operations = meter
.u64_observable_counter("freenet.operation.results")
.with_description("Completed operations by type and outcome")
.with_callback(move |observer| {
let Some(status) = (sources.status_scalars)() else {
return;
};
for (op, (success, failure)) in [
("get", status.op_stats.gets),
("put", status.op_stats.puts),
("update", status.op_stats.updates),
("subscribe", status.op_stats.subscribes),
] {
observer.observe(
success as u64,
&[KeyValue::new("op", op), KeyValue::new("result", "success")],
);
observer.observe(
failure as u64,
&[KeyValue::new("op", op), KeyValue::new("result", "failure")],
);
}
})
.build();
let _lattice = meter
.u64_observable_gauge("freenet.ring.lattice.neighbor")
.with_description(
"1 when this node holds its closest connected ring neighbor on that side. \
Held does not mean tight — compare distances across nodes.",
)
.with_callback(move |observer| {
if let Some(ring) = (sources.ring_stats)() {
observer.observe(
ring.lattice_has_successor as u64,
&[KeyValue::new("position", "successor")],
);
observer.observe(
ring.lattice_has_predecessor as u64,
&[KeyValue::new("position", "predecessor")],
);
}
})
.build();
let _distance = meter
.f64_observable_gauge("freenet.ring.lattice.neighbor.distance")
.with_description("Ring distance to each held lattice edge; absent when unheld")
.with_callback(move |observer| {
if let Some(ring) = (sources.ring_stats)() {
if let Some(d) = ring.lattice_successor_distance {
observer.observe(d, &[KeyValue::new("position", "successor")]);
}
if let Some(d) = ring.lattice_predecessor_distance {
observer.observe(d, &[KeyValue::new("position", "predecessor")]);
}
}
})
.build();
let _probes = meter
.u64_observable_counter("freenet.ring.lattice.probes")
.with_description(
"Route-to-self probes fired, and lattice improvements observed. Counted \
independently — an improvement lands some ticks after the probe that caused \
it, so the ratio is a convergence gauge, not a success rate.",
)
.with_callback(move |observer| {
if let Some(ring) = (sources.ring_stats)() {
observer.observe(
ring.lattice_probes_issued,
&[KeyValue::new("result", "issued")],
);
observer.observe(
ring.lattice_probe_improvements,
&[KeyValue::new("result", "improvement")],
);
}
})
.build();
let _updates = meter
.u64_observable_counter("freenet.contract.updates")
.with_description("Relayed UPDATEs by admission outcome")
.with_callback(move |observer| {
if let Some(ring) = (sources.ring_stats)() {
observer.observe(
ring.updates_accepted,
&[KeyValue::new("result", "accepted")],
);
observer.observe(
ring.updates_rate_limited,
&[KeyValue::new("result", "rate_limited")],
);
observer.observe(
ring.updates_capacity_dropped,
&[KeyValue::new("result", "capacity_dropped")],
);
}
})
.build();
}
fn register_queue_metrics(meter: &opentelemetry::metrics::Meter) {
use crate::contract::fair_queue_stats as snapshot;
let _depth = meter
.u64_observable_gauge("freenet.contract.queue.depth")
.with_description(
"Current fair-queue occupancy, per tier. No `total` series: it would \
double-count under `sum by (queue)` — sum the tiers instead.",
)
.with_callback(|observer| {
let q = snapshot();
for (tier, depth) in [
("client_local", q.depth_client_local),
("network_relay", q.depth_network_relay),
("background", q.depth_background),
] {
observer.observe(depth as u64, &[KeyValue::new("queue", tier)]);
}
})
.build();
let _high_water = meter
.u64_observable_gauge("freenet.contract.queue.depth.high_water")
.with_description("Highest fair-queue occupancy reached since startup")
.with_callback(|observer| observer.observe(snapshot().high_water as u64, &[]))
.build();
let _rejected = meter
.u64_observable_counter("freenet.contract.queue.rejected")
.with_description(
"Fair-queue admission rejections. global_capacity is node-wide saturation; \
per_contract is one noisy contract hitting its own cap.",
)
.with_callback(|observer| {
let q = snapshot();
observer.observe(
q.rejected_global_capacity,
&[KeyValue::new("reason", "global_capacity")],
);
observer.observe(
q.rejected_per_contract,
&[KeyValue::new("reason", "per_contract")],
);
})
.build();
let _shed = meter
.u64_observable_counter("freenet.contract.queue.background_shed")
.with_description("Background events shed to make room for higher-priority work")
.with_callback(|observer| observer.observe(snapshot().background_shed, &[]))
.build();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::OtelConfig;
fn test_audience() -> String {
audience_of(
&"http://collector.example:4318/v1/metrics"
.parse::<http::Uri>()
.unwrap(),
)
}
fn enabled_config() -> OtelConfig {
OtelConfig {
enabled: true,
endpoint: None,
auth_mode: Default::default(),
is_test_environment: false,
}
}
const TEST_BODY: &[u8] = b"export-payload";
fn signing_input(payload: &str, body: &[u8]) -> String {
use sha2::Digest;
format!(
"{payload}/{}",
bs58::encode(sha2::Sha256::digest(body)).into_string()
)
}
fn token_fixture() -> (crate::transport::TransportKeypair, String) {
let keypair = crate::transport::TransportKeypair::new();
let pubkey_b58 = bs58::encode(keypair.public_key_bytes()).into_string();
let token = bearer_token(
&keypair.auth_token_signer(),
&pubkey_b58,
&test_audience(),
TEST_BODY,
);
(keypair, token)
}
#[test]
fn bearer_token_has_the_documented_shape_and_verifies() {
use xeddsa::xeddsa::Verify;
let (keypair, token) = token_fixture();
let parts: Vec<&str> = token.split('/').collect();
let [scheme, pubkey, audience, timestamp, signature] = parts[..] else {
panic!("expected 5 /-separated parts, got {token}");
};
assert_eq!(scheme, "freenet");
assert_eq!(
pubkey,
bs58::encode(keypair.public_key_bytes()).into_string(),
"pubkey part must be the full base58 x25519 transport public key"
);
assert_eq!(
audience,
test_audience(),
"the token must name the collector it was minted for, or it can be \
replayed to any other collector accepting this scheme"
);
let ts: u64 = timestamp.parse().expect("timestamp is epoch seconds");
assert!(
ts > 1_700_000_000,
"timestamp must be current epoch seconds"
);
use sha2::Digest as _;
let body_hash = bs58::encode(sha2::Sha256::digest(TEST_BODY)).into_string();
let signed_payload = format!("freenet/{pubkey}/{audience}/{timestamp}/{body_hash}");
let sig_bytes: [u8; 64] = bs58::decode(signature)
.into_vec()
.unwrap()
.try_into()
.expect("64-byte signature");
xeddsa::xed25519::PublicKey(keypair.public_key_bytes())
.verify(signed_payload.as_bytes(), &sig_bytes)
.expect("XEdDSA signature must verify against the transport pubkey");
assert!(
xeddsa::xed25519::PublicKey(keypair.public_key_bytes())
.verify(b"freenet/forged", &sig_bytes)
.is_err()
);
}
#[test]
fn node_pubkey_is_verifiable_with_stock_ed25519() {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let (keypair, token) = token_fixture();
let (payload, sig_b58) = token.rsplit_once('/').unwrap();
let sig_bytes: [u8; 64] = bs58::decode(sig_b58)
.into_vec()
.unwrap()
.try_into()
.unwrap();
let edwards = curve25519_dalek::montgomery::MontgomeryPoint(keypair.public_key_bytes())
.to_edwards(0)
.expect("transport pubkey must map to Edwards")
.compress()
.to_bytes();
VerifyingKey::from_bytes(&edwards)
.unwrap()
.verify(
signing_input(payload, TEST_BODY).as_bytes(),
&Signature::from_bytes(&sig_bytes),
)
.expect("stock ed25519 verify after Montgomery->Edwards conversion");
}
fn oneshot_collector() -> (std::net::SocketAddr, std::thread::JoinHandle<String>) {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut raw = Vec::new();
let mut buf = [0u8; 1024];
while !raw.windows(14).any(|w| w == b"export-payload") {
let n = stream.read(&mut buf).unwrap();
if n == 0 {
break;
}
raw.extend_from_slice(&buf[..n]);
}
stream
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n")
.unwrap();
String::from_utf8_lossy(&raw).into_owned()
});
(addr, server)
}
fn wire_auth_header(raw: &str) -> Option<String> {
raw.lines()
.find(|l| l.to_ascii_lowercase().starts_with("authorization:"))
.map(|l| l.split_once(':').unwrap().1.trim().to_owned())
}
fn post_export(client: &OtlpHttpClient, addr: std::net::SocketAddr, auth: Option<&str>) {
let mut request = Request::builder()
.method("POST")
.uri(format!("http://{addr}/v1/metrics"));
if let Some(auth) = auth {
request = request.header(http::header::AUTHORIZATION, auth);
}
let request = request.body(Bytes::from_static(b"export-payload")).unwrap();
let response = futures::executor::block_on(client.send_bytes(request)).unwrap();
assert!(response.status().is_success());
}
fn test_client(keypair: &crate::transport::TransportKeypair, signed: bool) -> OtlpHttpClient {
OtlpHttpClient {
inner: export_http_client().expect("export client must build"),
signer: signed.then(|| keypair.auth_token_signer()),
pubkey_b58: bs58::encode(keypair.public_key_bytes()).into_string(),
operator_credentials: Vec::new(),
}
}
fn fixture_sources() -> RingSources {
use crate::node::network_status::{OtelStatusScalars, RingStatsSnapshot};
fn ring() -> Option<RingStatsSnapshot> {
Some(RingStatsSnapshot {
connection_count: 3,
lattice_successor_distance: Some(0.25),
lattice_predecessor_distance: Some(0.5),
..Default::default()
})
}
fn reasons() -> Option<crate::ring::HostingReasonStats> {
Some(crate::ring::HostingReasonStats::default())
}
fn scalars() -> Option<OtelStatusScalars> {
Some(OtelStatusScalars::default())
}
RingSources {
ring_stats: ring,
hosting_reasons: reasons,
status_scalars: scalars,
}
}
#[test]
fn send_bytes_puts_a_verifiable_bearer_header_on_the_wire() {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let (addr, server) = oneshot_collector();
let keypair = crate::transport::TransportKeypair::new();
let pubkey_b58 = bs58::encode(keypair.public_key_bytes()).into_string();
post_export(&test_client(&keypair, true), addr, None);
let raw = server.join().unwrap();
let token = wire_auth_header(&raw)
.expect("Authorization header must reach the wire")
.strip_prefix("Bearer ")
.expect("Bearer scheme")
.to_owned();
let (payload, sig_b58) = token.rsplit_once('/').unwrap();
let audience = audience_of(
&format!("http://{addr}/v1/metrics")
.parse::<http::Uri>()
.unwrap(),
);
assert!(
payload.starts_with(&format!("freenet/{pubkey_b58}/{audience}/")),
"wire token must carry this node's pubkey and the audience hash of \
the URL it was actually sent to: {token}"
);
let sig_bytes: [u8; 64] = bs58::decode(sig_b58)
.into_vec()
.unwrap()
.try_into()
.unwrap();
let edwards = curve25519_dalek::montgomery::MontgomeryPoint(keypair.public_key_bytes())
.to_edwards(0)
.unwrap()
.compress()
.to_bytes();
VerifyingKey::from_bytes(&edwards)
.unwrap()
.verify(
signing_input(payload, b"export-payload").as_bytes(),
&Signature::from_bytes(&sig_bytes),
)
.expect("wire bearer token must verify with stock ed25519");
assert!(
raw.contains("export-payload"),
"body must survive the http -> reqwest conversion"
);
}
#[test]
fn an_operator_supplied_authorization_header_is_never_overwritten() {
let (addr, server) = oneshot_collector();
let keypair = crate::transport::TransportKeypair::new();
post_export(
&test_client(&keypair, true),
addr,
Some("Basic b3BlcmF0b3I="),
);
assert_eq!(
wire_auth_header(&server.join().unwrap()).as_deref(),
Some("Basic b3BlcmF0b3I="),
"the operator's own credentials must reach the collector"
);
}
#[test]
fn disabled_auth_mode_sends_no_authorization_header() {
let (addr, server) = oneshot_collector();
let keypair = crate::transport::TransportKeypair::new();
post_export(&test_client(&keypair, false), addr, None);
assert_eq!(
wire_auth_header(&server.join().unwrap()),
None,
"auth-mode disabled must put no Authorization header on the wire"
);
}
#[test]
fn fingerprint_attr_is_recomputable_from_the_pubkey_attr() {
let keypair = crate::transport::TransportKeypair::new();
let (pubkey_attr, fingerprint_attr) = identity_attributes(&keypair);
let decoded = bs58::decode(&pubkey_attr).into_vec().unwrap();
assert_eq!(
bs58::encode(&decoded[..12]).into_string(),
fingerprint_attr,
"fingerprint must be a pure function of pubkey, or the collector \
cannot validate the UI-facing id"
);
}
#[test]
fn bearer_tokens_are_unique_per_request() {
let keypair = crate::transport::TransportKeypair::new();
let pubkey = bs58::encode(keypair.public_key_bytes()).into_string();
let signer = keypair.auth_token_signer();
assert_ne!(
bearer_token(&signer, &pubkey, &test_audience(), TEST_BODY),
bearer_token(&signer, &pubkey, &test_audience(), TEST_BODY)
);
}
#[test]
fn a_token_for_one_collector_does_not_verify_at_another() {
use xeddsa::xeddsa::Verify;
let keypair = crate::transport::TransportKeypair::new();
let (pubkey, _) = identity_attributes(&keypair);
let token = bearer_token(
&keypair.auth_token_signer(),
&pubkey,
&test_audience(),
TEST_BODY,
);
let (payload, sig_b58) = token.rsplit_once('/').unwrap();
let sig_bytes: [u8; 64] = bs58::decode(sig_b58)
.into_vec()
.unwrap()
.try_into()
.unwrap();
let other = audience_of(
&"http://other-collector:4318/v1/metrics"
.parse::<http::Uri>()
.unwrap(),
);
let replayed = payload.replace(&test_audience(), &other);
assert_ne!(replayed, payload, "audience must appear in the payload");
assert!(
xeddsa::xed25519::PublicKey(keypair.public_key_bytes())
.verify(signing_input(&replayed, TEST_BODY).as_bytes(), &sig_bytes)
.is_err(),
"a token re-aimed at another collector must fail verification"
);
}
#[test]
fn the_audience_hash_is_reproducible_from_the_documented_canonical_url() {
use sha2::{Digest, Sha256};
let expect = |canonical: &str| {
bs58::encode(&Sha256::digest(canonical.as_bytes())[..16]).into_string()
};
for (uri, canonical) in [
(
"http://collector.example:4318/v1/metrics",
"collector.example:4318/v1/metrics",
),
(
"https://Collector.Example/v1/metrics",
"collector.example:443/v1/metrics",
),
(
"http://collector.example/v1/metrics",
"collector.example:80/v1/metrics",
),
(
"https://user:secret@collector.example:4318/v1/metrics",
"collector.example:4318/v1/metrics",
),
("http://[::1]:4318/v1/metrics", "[::1]:4318/v1/metrics"),
(
"http://user:pass@host:1234/path/here",
"host:1234/path/here",
),
] {
let audience = audience_of(&uri.parse::<http::Uri>().unwrap());
assert_eq!(audience, expect(canonical), "audience of {uri}");
assert!(
!audience.contains('/'),
"audience must not contain the token's field separator"
);
}
let of = |u: &str| audience_of(&u.parse::<http::Uri>().unwrap());
assert_ne!(
of("http://c.example:4318/v1/metrics"),
of("http://c.example:4319/v1/metrics"),
"port is bound"
);
assert_ne!(
of("http://c.example:4318/tenant-a/v1/metrics"),
of("http://c.example:4318/tenant-b/v1/metrics"),
"path is bound — two collectors behind one host must differ"
);
assert_eq!(
of("https://c.example:4318/v1/metrics"),
of("http://c.example:4318/v1/metrics"),
"scheme is deliberately not bound"
);
assert_ne!(
of("https://c.example/v1/metrics"),
of("http://c.example/v1/metrics"),
"an omitted port defaults per scheme: 443 vs 80"
);
}
#[test]
fn a_token_does_not_verify_against_a_different_body() {
use xeddsa::xeddsa::Verify;
let (keypair, token) = token_fixture();
let (payload, sig_b58) = token.rsplit_once('/').unwrap();
let sig_bytes: [u8; 64] = bs58::decode(sig_b58)
.into_vec()
.unwrap()
.try_into()
.unwrap();
let pubkey = xeddsa::xed25519::PublicKey(keypair.public_key_bytes());
assert!(
pubkey
.verify(signing_input(payload, TEST_BODY).as_bytes(), &sig_bytes)
.is_ok(),
"the token must verify against the body it was minted for"
);
assert!(
pubkey
.verify(
signing_input(payload, b"substituted-metrics").as_bytes(),
&sig_bytes
)
.is_err(),
"the same token must NOT verify against a substituted body"
);
}
#[test]
fn credential_safe_permits_https_and_loopback_only() {
let of = |u: &str| credential_safe(&u.parse::<http::Uri>().unwrap());
assert!(of("https://collector.example:4318/v1/metrics"), "https");
assert!(of("https://collector.example/v1/metrics"), "https, no port");
assert!(of("http://localhost:4318/v1/metrics"), "loopback by name");
assert!(of("http://127.0.0.1:4318/v1/metrics"), "loopback v4");
assert!(
of("http://127.9.9.9:4318/v1/metrics"),
"all of 127/8 is loopback"
);
assert!(of("http://[::1]:4318/v1/metrics"), "loopback v6, bracketed");
assert!(
!of("http://collector.example:4318/v1/metrics"),
"plaintext remote"
);
assert!(
!of("http://10.0.0.5:4318/v1/metrics"),
"a LAN address is not loopback"
);
assert!(!of("http://[2001:db8::1]:4318/v1/metrics"), "remote v6");
}
fn client_with_operator_header(name: &str, value: &str) -> OtlpHttpClient {
OtlpHttpClient {
inner: export_http_client().expect("export client must build"),
signer: None,
pubkey_b58: String::new(),
operator_credentials: vec![(name.to_ascii_lowercase(), value.to_owned())],
}
}
fn post_to(client: &OtlpHttpClient, uri: &str, header: Option<(&str, &str)>) -> String {
let mut request = Request::builder().method("POST").uri(uri);
if let Some((name, value)) = header {
request = request.header(name, value);
}
let request = request.body(Bytes::from_static(TEST_BODY)).unwrap();
futures::executor::block_on(client.send_bytes(request))
.expect_err("a credential over plaintext http must not be sent")
.to_string()
}
#[test]
fn a_credential_is_refused_over_plaintext_http_to_a_remote_collector() {
let keypair = crate::transport::TransportKeypair::new();
let signed = test_client(&keypair, true);
let error = post_to(
&signed,
"http://collector.example:4318/v1/metrics",
Some(("authorization", "Bearer operator-secret")),
);
assert!(
error.contains("cleartext"),
"the error must say why it refused, got: {error}"
);
}
#[test]
fn an_operator_credential_that_is_not_authorization_is_still_guarded() {
let client = client_with_operator_header("x-honeycomb-team", "hcaik_secret");
let error = post_to(
&client,
"http://collector.example:4318/v1/metrics",
Some(("x-honeycomb-team", "hcaik_secret")),
);
assert!(
error.contains("cleartext"),
"a non-Authorization credential must be guarded too, got: {error}"
);
}
#[test]
fn safe_body_redacts_a_credential_the_collector_names_without_a_keyword() {
let client = client_with_operator_header("x-api-key", "sk-abc123");
let redacted =
client.safe_body(b"{\"message\":\"unauthorized: key 'sk-abc123' rejected\"}");
assert!(
!redacted.contains("sk-abc123"),
"the credential value must not reach the log: {redacted}"
);
assert!(
redacted.contains("unauthorized"),
"the rejection detail this body exists for must survive: {redacted}"
);
}
#[test]
fn redact_keyword_lines_removes_echoed_credentials() {
let redacted = redact_keyword_lines(
"rejected\nauthorization: Bearer operator-secret\ntrailing detail\n",
);
assert!(
!redacted.contains("operator-secret"),
"an echoed credential must not reach the log: {redacted}"
);
assert!(
redacted.contains("rejected") && redacted.contains("trailing detail"),
"the rejection detail must survive: {redacted}"
);
assert!(!redact_keyword_lines("bad token freenet/aaa/bbb/1/ccc").contains("ccc"));
assert_eq!(
redact_keyword_lines("quota exceeded"),
"quota exceeded",
"an ordinary body must pass through untouched"
);
let wide = "é".repeat(400);
assert_eq!(truncate_for_log(&wide).chars().count(), 256);
}
#[test]
fn safe_body_redacts_a_credential_that_straddles_the_truncation_boundary() {
let client = client_with_operator_header("x-api-key", "sk-LIVE-abcdefghijklmnop");
let mut body = "x".repeat(240).into_bytes();
body.extend_from_slice(b" key sk-LIVE-abcdefghijklmnop rejected");
let redacted = client.safe_body(&body);
assert!(
!redacted.contains("sk-LIVE"),
"no part of the credential may survive truncation: {redacted}"
);
}
#[test]
fn safe_body_redacts_the_token_inside_a_scheme_prefixed_header_value() {
let client = client_with_operator_header("authorization", "Bearer sk-abc12345");
let redacted = client.safe_body(b"{\"message\":\"rejected key 'sk-abc12345'\"}");
assert!(
!redacted.contains("sk-abc12345"),
"the token alone must be redacted, not just the whole header value: {redacted}"
);
}
#[test]
fn safe_body_leaves_short_non_secret_values_alone() {
let client = client_with_operator_header("x-scope-orgid", "prod");
let body = client.safe_body(b"tenant prod rejected: bad timestamp");
assert!(
body.contains("prod rejected"),
"a short, non-secret value must not be scrubbed out of the detail: {body}"
);
}
#[test]
fn redact_uri_strips_userinfo() {
let of = |u: &str| redact_uri(&u.parse::<http::Uri>().unwrap());
assert_eq!(
of("http://user:pass@collector.example:4318/v1/metrics"),
"http://[redacted]@collector.example:4318/v1/metrics"
);
assert!(!of("http://user:pass@c.example/x").contains("pass"));
assert_eq!(
of("https://collector.example:4318/v1/metrics"),
"https://collector.example:4318/v1/metrics",
"a URL with no userinfo must be unchanged"
);
let of = |e: &str| redact_endpoint(e).into_owned();
assert_eq!(
of("http://user:pass@collector.example:4318"),
"http://[redacted]@collector.example:4318"
);
assert!(!of("https://user:hunter2@c.example/v1/metrics").contains("hunter2"));
assert_eq!(
of("http://collector.example:4318"),
"http://collector.example:4318",
"no userinfo, unchanged"
);
assert_eq!(
of("http://localhost:4318 (SDK default)"),
"http://localhost:4318 (SDK default)",
"the default placeholder must survive verbatim"
);
assert_eq!(
of("http://collector.example/v1/a@b"),
"http://collector.example/v1/a@b"
);
assert_eq!(of("u:secret@collector:4318"), "[redacted]@collector:4318");
assert!(!of("user:hunter2@collector:4318/v1/metrics").contains("hunter2"));
}
#[test]
fn nothing_is_exported_anywhere_until_an_operator_asks() {
let cfg = OtelConfig::default();
assert!(!cfg.enabled, "the exporter must be OFF unless enabled");
assert_eq!(cfg.endpoint, None, "no endpoint may be baked in");
assert_eq!(
cfg.auth_mode,
crate::config::OtelAuthMode::Disabled,
"a node must not assert a signed identity to a collector unasked"
);
assert_eq!(resolve_metrics_endpoint(None, None, None), None);
}
#[test]
fn a_credential_bound_for_a_plaintext_remote_endpoint_is_diagnosed_at_startup() {
use crate::config::OtelAuthMode;
let remote = Some("http://collector.example:4318");
assert!(
credential_that_would_be_refused(OtelAuthMode::Freenet, None, remote).is_some(),
"auth-mode=freenet over plaintext remote must be diagnosed"
);
assert!(
credential_that_would_be_refused(
OtelAuthMode::Disabled,
Some("x-api-key=secret"),
remote
)
.is_some()
);
assert!(
credential_that_would_be_refused(
OtelAuthMode::Freenet,
None,
Some("https://collector.example:4318")
)
.is_none()
);
assert!(
credential_that_would_be_refused(
OtelAuthMode::Freenet,
None,
Some("http://127.0.0.1:4318")
)
.is_none()
);
assert!(credential_that_would_be_refused(OtelAuthMode::Disabled, None, remote).is_none());
assert!(credential_that_would_be_refused(OtelAuthMode::Freenet, None, None).is_none());
}
#[test]
fn the_export_client_keeps_its_credential_safety_policies() {
let body = top_level_fn_body(include_str!("otel.rs"), "fn export_http_client()");
assert!(
body.contains("Client::builder()"),
"the pin must have scoped to the builder, or it proves nothing: {body}"
);
assert!(
body.contains("redirect(reqwest::redirect::Policy::none())"),
"redirects must stay disabled: reqwest replays a 30x with the \
original headers, carrying a credential to an http Location"
);
assert!(
body.contains(".no_proxy()"),
"ambient proxies must stay disabled: reqwest defaults to \
auto_sys_proxy with no loopback exemption, so HTTP_PROXY would \
take an export aimed at localhost off the machine"
);
}
#[test]
fn init_refuses_to_start_from_a_test_process() {
assert_eq!(
init(
&enabled_config(),
&crate::transport::TransportKeypair::new()
),
Some(OtelSuppression::TestHarness),
"init must consult otel_suppression_reason and return before \
building anything"
);
assert!(
INSTRUMENTS.get().is_none(),
"a suppressed init must not have registered instruments"
);
}
fn top_level_fn_body<'a>(src: &'a str, signature: &str) -> &'a str {
let at = src
.find(signature)
.unwrap_or_else(|| panic!("definition not found: {signature}"));
let tests_at = src
.find("\n#[cfg(test)]\nmod ")
.map(|i| i + 1)
.expect("test module not located — this guard cannot verify anything");
assert!(
at < tests_at,
"`{signature}` matched inside the test module — this pin is \
scraping its own source and would pass vacuously"
);
let after = &src[at + signature.len()..];
let (body, _) = after
.split_once("\n}\n")
.unwrap_or_else(|| panic!("could not locate end of: {signature}"));
body
}
fn method_body<'a>(src: &'a str, signature: &str) -> &'a str {
let at = src
.find(signature)
.unwrap_or_else(|| panic!("definition not found: {signature}"));
let tests_at = src
.find("\n#[cfg(test)]\nmod ")
.map(|i| i + 1)
.expect("test module not located — this guard cannot verify anything");
assert!(
at < tests_at,
"`{signature}` matched inside the test module — this pin is \
scraping its own source and would pass vacuously"
);
let after = &src[at + signature.len()..];
let (body, _) = after
.split_once("\n }\n")
.unwrap_or_else(|| panic!("could not locate end of: {signature}"));
assert!(
!body.contains("\n#[cfg(test)]\nmod "),
"scoped region for `{signature}` escaped into the test module — \
this pin would pass vacuously"
);
body
}
#[test]
fn every_sync_instrument_still_has_its_hot_path_mirror() {
let source = include_str!("../transport/metrics.rs");
for (signature, call) in [
(
" pub(crate) fn record_cwnd_sample(&self, cwnd_bytes: u32) {",
"crate::tracing::otel::record_cwnd(",
),
(
" pub(crate) fn record_rtt_sample(&self, rtt_us: u64) {",
"crate::tracing::otel::record_rtt_ms(",
),
] {
assert!(
method_body(source, signature).contains(call),
"`{signature}` no longer calls `{call}`: the histogram it \
feeds would report nothing forever"
);
}
}
#[test]
fn production_shaped_input_is_not_suppressed() {
assert_eq!(
otel_suppression_reason(&enabled_config(), false, false),
None,
"a real release binary with the flag on must export"
);
}
#[test]
fn every_test_signal_suppresses() {
let disabled = OtelConfig {
enabled: false,
..enabled_config()
};
assert_eq!(
otel_suppression_reason(&disabled, false, false),
Some(OtelSuppression::Disabled)
);
let test_env = OtelConfig {
is_test_environment: true,
..enabled_config()
};
assert_eq!(
otel_suppression_reason(&test_env, false, false),
Some(OtelSuppression::TestEnvironmentFlag)
);
assert_eq!(
otel_suppression_reason(&enabled_config(), true, false),
Some(OtelSuppression::TestHarness),
"cfg(test) build"
);
assert_eq!(
otel_suppression_reason(&enabled_config(), false, true),
Some(OtelSuppression::TestHarness),
"running from a cargo deps/ harness"
);
}
#[test]
fn metrics_env_wins_over_generic_env_and_config() {
assert_eq!(
resolve_metrics_endpoint(
Some("http://from-config:4318"),
Some("http://from-metrics-env:4318/v1/metrics"),
Some("http://from-generic-env:4318"),
),
None
);
assert_eq!(
resolve_metrics_endpoint(
Some("http://from-config:4318"),
None,
Some("http://from-generic-env:4318"),
),
None
);
}
#[test]
fn config_endpoint_gets_the_signal_path_appended() {
assert_eq!(
resolve_metrics_endpoint(Some("http://collector:4318"), None, None),
Some("http://collector:4318/v1/metrics".to_string())
);
assert_eq!(
resolve_metrics_endpoint(Some("http://collector:4318/"), None, None),
Some("http://collector:4318/v1/metrics".to_string()),
"trailing slash must not double up"
);
}
#[test]
fn nothing_configured_defers_to_the_sdk_default() {
assert_eq!(resolve_metrics_endpoint(None, None, None), None);
assert_eq!(
resolve_metrics_endpoint(Some(" "), None, None),
None,
"a blank endpoint is not a configuration"
);
}
#[test]
fn node_pubkey_attr_matches_the_bearer_token_pubkey() {
let (keypair, token) = token_fixture();
let pubkey_attr = bs58::encode(keypair.public_key_bytes()).into_string();
assert_eq!(
token.split('/').nth(1),
Some(pubkey_attr.as_str()),
"token <pubkey> must equal freenet.node.pubkey, or the collector \
cannot self-validate the node id against the signing key"
);
}
#[test]
fn instance_id_carries_no_network_address() {
let keypair = crate::transport::TransportKeypair::new();
let (pubkey_attr, fingerprint_attr) = identity_attributes(&keypair);
for instance_id in [pubkey_attr, fingerprint_attr] {
assert!(!instance_id.is_empty());
assert!(
!instance_id.contains('@') && !instance_id.contains(':'),
"identity attribute must not embed an address, got {instance_id}"
);
}
let peer_id = crate::node::PeerId::new(
keypair.public().clone(),
"203.0.113.7:31337".parse().expect("valid addr"),
);
assert!(
peer_id.to_string().contains("203.0.113.7"),
"guard is meaningless if PeerId stops embedding the address"
);
}
#[test]
fn record_helpers_are_inert_without_a_pipeline() {
record_rtt_ms(12.5);
record_cwnd(4096);
assert!(
INSTRUMENTS.get().is_none(),
"recording must not lazily bind instruments to the no-op provider"
);
}
#[test]
fn env_declared_attributes_cannot_shadow_the_node_identity() {
let declared = [
"freenet.node.pubkey".to_owned(),
"freenet.node.fingerprint".to_owned(),
"service.name".to_owned(),
];
let attributes = resource_attributes("REAL-PK".into(), "REAL-FP".into(), &declared);
let value = |key: &str| {
attributes
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| v.as_str())
};
assert_eq!(
value("freenet.node.pubkey"),
Some("REAL-PK"),
"the signed pubkey must be exported even when the environment declares it"
);
assert_eq!(value("freenet.node.fingerprint"), Some("REAL-FP"));
assert_eq!(
value("service.name"),
None,
"a declared service.name must be left to the environment"
);
assert!(
value("service.version").is_some(),
"undeclared descriptive attributes are still filled in"
);
}
#[test]
fn unusable_endpoints_are_diagnosed() {
assert!(
endpoint_problem("collector.example:4318").is_some(),
"a schemeless authority must be reported"
);
assert!(endpoint_problem("collector.example:4318/v1/metrics").is_some());
assert!(endpoint_problem("not a url").is_some());
assert!(endpoint_problem("ftp://collector.example:4318/v1/metrics").is_some());
assert_eq!(
endpoint_problem("http://collector.example:4318/v1/metrics"),
None
);
assert_eq!(
endpoint_problem("https://collector.example/v1/metrics"),
None
);
}
#[test]
fn instrument_callbacks_export_named_datapoints() {
use opentelemetry::metrics::MeterProvider;
use opentelemetry_sdk::metrics::{InMemoryMetricExporter, PeriodicReader};
let exporter = InMemoryMetricExporter::default();
let provider = SdkMeterProvider::builder()
.with_reader(PeriodicReader::builder(exporter.clone()).build())
.build();
let meter = provider.meter(METER_NAME);
let instruments = build_instruments(&meter);
register_observables(&meter, fixture_sources());
instruments.rtt.record(12.5, &[]);
instruments.cwnd.record(4096, &[]);
provider.force_flush().expect("collection must not fail");
let exported = exporter.get_finished_metrics().expect("exported batches");
let mut seen: Vec<(String, Vec<String>)> = Vec::new();
for resource_metric in &exported {
for scope in resource_metric.scope_metrics() {
for metric in scope.metrics() {
use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData};
let render = |kv: &KeyValue| format!("{}={}", kv.key, kv.value.as_str());
#[allow(clippy::wildcard_enum_match_arm)]
let attributes: Vec<String> = match metric.data() {
AggregatedMetrics::U64(MetricData::Sum(sum)) => sum
.data_points()
.flat_map(|p| p.attributes())
.map(render)
.collect(),
AggregatedMetrics::U64(MetricData::Gauge(gauge)) => gauge
.data_points()
.flat_map(|p| p.attributes())
.map(render)
.collect(),
AggregatedMetrics::F64(MetricData::Gauge(gauge)) => gauge
.data_points()
.flat_map(|p| p.attributes())
.map(render)
.collect(),
_ => Vec::new(),
};
seen.push((metric.name().to_string(), attributes));
}
}
}
let names: Vec<&str> = seen.iter().map(|(name, _)| name.as_str()).collect();
for expected in [
"freenet.transport.rtt",
"freenet.transport.cwnd",
"freenet.transport.bytes",
"freenet.transport.packets",
"freenet.transport.transfers",
"freenet.transport.nat_traversal",
"freenet.contract.queue.depth",
"freenet.contract.queue.depth.high_water",
"freenet.contract.queue.rejected",
"freenet.contract.queue.background_shed",
"freenet.ring.connections",
"freenet.node.contracts.hosted",
"freenet.node.contracts.hosted.bytes",
"freenet.connect.gateway_failures",
"freenet.operation.results",
"freenet.ring.lattice.neighbor",
"freenet.ring.lattice.neighbor.distance",
"freenet.ring.lattice.probes",
"freenet.contract.updates",
] {
assert!(
names.contains(&expected),
"instrument `{expected}` produced no datapoint; exported: {names:?}"
);
}
let attributes_of = |name: &str| {
seen.iter()
.find(|(n, _)| n == name)
.map(|(_, a)| a.clone())
.unwrap_or_default()
};
for (name, attribute) in [
("freenet.transport.bytes", "direction=sent"),
("freenet.transport.bytes", "direction=received"),
("freenet.transport.transfers", "result=completed"),
("freenet.transport.transfers", "result=failed"),
("freenet.transport.nat_traversal", "result=attempt"),
("freenet.transport.nat_traversal", "result=failed_version"),
("freenet.contract.queue.depth", "queue=client_local"),
("freenet.contract.queue.depth", "queue=network_relay"),
("freenet.contract.queue.depth", "queue=background"),
("freenet.contract.queue.rejected", "reason=per_contract"),
("freenet.contract.queue.rejected", "reason=global_capacity"),
("freenet.transport.packets", "direction=sent"),
("freenet.transport.packets", "direction=received"),
("freenet.transport.nat_traversal", "result=established"),
("freenet.transport.nat_traversal", "result=failed_error"),
("freenet.node.contracts.hosted", "reason=local_client"),
("freenet.node.contracts.hosted", "reason=downstream"),
("freenet.node.contracts.hosted", "reason=subscribed"),
("freenet.node.contracts.hosted", "reason=local_access"),
("freenet.node.contracts.hosted", "reason=abandoned"),
("freenet.node.contracts.hosted", "reason=restored"),
("freenet.node.contracts.hosted", "reason=routed"),
("freenet.node.contracts.hosted.bytes", "reason=local_client"),
("freenet.node.contracts.hosted.bytes", "reason=downstream"),
("freenet.node.contracts.hosted.bytes", "reason=subscribed"),
("freenet.node.contracts.hosted.bytes", "reason=local_access"),
("freenet.node.contracts.hosted.bytes", "reason=abandoned"),
("freenet.node.contracts.hosted.bytes", "reason=restored"),
("freenet.node.contracts.hosted.bytes", "reason=routed"),
("freenet.operation.results", "op=get"),
("freenet.operation.results", "op=put"),
("freenet.operation.results", "op=update"),
("freenet.operation.results", "op=subscribe"),
("freenet.operation.results", "result=success"),
("freenet.operation.results", "result=failure"),
("freenet.ring.lattice.neighbor", "position=successor"),
("freenet.ring.lattice.neighbor", "position=predecessor"),
(
"freenet.ring.lattice.neighbor.distance",
"position=successor",
),
(
"freenet.ring.lattice.neighbor.distance",
"position=predecessor",
),
("freenet.ring.lattice.probes", "result=issued"),
("freenet.ring.lattice.probes", "result=improvement"),
("freenet.contract.updates", "result=accepted"),
("freenet.contract.updates", "result=rate_limited"),
("freenet.contract.updates", "result=capacity_dropped"),
] {
assert!(
attributes_of(name).iter().any(|a| a == attribute),
"`{name}` is missing attribute `{attribute}`; has {:?}",
attributes_of(name)
);
}
}
#[tokio::test]
async fn provider_builds_with_auth_disabled() {
let provider = build_provider(
Some("http://127.0.0.1:1/v1/metrics"),
"pubkey-under-test".to_string(),
"fingerprint-under-test".to_string(),
None,
)
.expect("exporter build must succeed with auth disabled");
tokio::task::spawn_blocking(move || provider.shutdown().expect("clean shutdown"))
.await
.expect("shutdown thread panicked");
}
#[tokio::test]
async fn provider_builds_inside_a_tokio_runtime() {
let provider = build_provider(
Some("http://127.0.0.1:1/v1/metrics"),
"pubkey-under-test".to_string(),
"fingerprint-under-test".to_string(),
Some(crate::transport::TransportKeypair::new().auth_token_signer()),
)
.expect("exporter build must succeed against an unreachable collector");
tokio::task::spawn_blocking(move || provider.shutdown().expect("clean shutdown"))
.await
.expect("shutdown thread panicked");
}
}