Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
dd5e57d
docs(otel): add design spec and implementation plan for the SDK metri…
skandragon Aug 1, 2026
94ba512
feat(otel): add isolated otel-telemetry config
skandragon Aug 1, 2026
e2c2f76
feat(otel): add suppression and endpoint-precedence logic
skandragon Aug 1, 2026
d43f0a9
feat(otel): export metrics through the OpenTelemetry SDK
skandragon Aug 1, 2026
4e3a4f5
docs(otel): document the two independent telemetry pipelines
skandragon Aug 1, 2026
d6ce281
fix(otel): flatten otel config keys and trim the OTLP feature set
skandragon Aug 1, 2026
36000bd
docs(otel): correct the OTLP feature-trim claim
skandragon Aug 1, 2026
991a48b
feat(otel): export node, ring, transport, and queue metrics
skandragon Aug 2, 2026
bf33c5d
feat(otel): authenticate collector exports with XEdDSA bearer tokens
skandragon Aug 4, 2026
d233ca3
feat(otel): drop the nonce from the bearer token format
skandragon Aug 4, 2026
6cb17a7
fix(otel): build the exporter off the async runtime
skandragon Aug 5, 2026
2621cdd
fix(otel): address rule-review findings
skandragon Aug 5, 2026
4ce4cc3
fix(otel): shrink the dep graph and default collector auth off
skandragon Aug 6, 2026
4a78d3b
fix(otel): bind the bearer token to a hash of the full target URL
skandragon Aug 6, 2026
f94e0a7
fix(otel): drop the scheme from the audience canonical form
skandragon Aug 6, 2026
f891539
fix(otel): make export failures and identity spoofing visible
skandragon Aug 7, 2026
cf6fb39
feat(otel): break out hosted contracts by why they are held
skandragon Aug 8, 2026
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
42 changes: 42 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,48 @@ until a runtime path actually reads it.
Operator-facing documentation (encryption model, migration matrix,
`freenet secrets` CLI) lives in [`docs/secrets-at-rest.md`](docs/secrets-at-rest.md).

## Two independent telemetry pipelines

`telemetry-enabled` / `telemetry-endpoint` feed the project's central dashboard
(`tracing/telemetry.rs`). `otel-telemetry-enabled` / `otel-endpoint` are a
**separate, unrelated** OpenTelemetry SDK metrics pipeline (`tracing/otel.rs`):
no shared config, no shared endpoint, no fallback in either direction, and
`otel-endpoint` must never default to the dashboard collector.

Rules when touching `tracing/otel.rs`:

- Observable instruments must read **cumulative, never-reset** values.
`TransportSnapshot` fields are period accumulators that `take_snapshot`
zeroes for the legacy telemetry worker, so observing one as a counter yields
a non-monotonic series whenever `telemetry-enabled` is also on.
- Never export a `PeerId`, socket address, or any attribute identifying the
remote end of a connection. `PeerId` renders as `{pub_key}@{addr}`, which
leaks our address and re-identifies the node whenever it changes. This node's
identity is two resource attributes (`freenet.node.*`, one per export batch),
not a per-datapoint attribute. "Peer" means the *other* end of a connection.
- Histograms get their base-2 exponential aggregation from one `with_view` in
`build_provider_blocking`. Do not add explicit bucket boundaries per
instrument.
- Export outcomes must be logged by `OtlpHttpClient::send_bytes`. The SDK will
not do it: `opentelemetry-otlp` logs network errors and non-2xx at DEBUG on
the stated grounds that `PeriodicReader` re-logs them at error level, which
is true for the batch log/span processors and false for metrics. Deleting
that logging makes a dead collector produce no output at all.
- The `freenet.node.*` resource attributes are always emitted and must never
be made deferrable to `OTEL_RESOURCE_ATTRIBUTES`. They are what the
collector checks the bearer-token signature against; an override would
export an identity that does not match the signing key.
- The exporter always installs its own `HttpClient`, so `opentelemetry-otlp`
needs none of its `reqwest-*`/TLS features — enabling one pulls a second
reqwest major, a second TLS stack, and a C/asm aws-lc build into every
release target.

Everything else — the bearer-token format, endpoint precedence, the
`OTEL_*` variables honored, and per-instrument notes — is in
[`docs/design/otel-metrics-exporter.md`](docs/design/otel-metrics-exporter.md),
and operator-facing configuration is in
[`docs/otel-metrics.md`](docs/otel-metrics.md).

## External Resources

- API docs: https://docs.rs/freenet
Expand Down
74 changes: 38 additions & 36 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,21 @@ chrono = { version = "0.4", default-features = true }
# Random
rand = "0.9"

# XEdDSA (Signal's construction): signs with the x25519 transport key so the
# OTel collector can verify node identity against the SAME public key peers
# and UIs see. Its rand 0.10 dep is exposed as `rand10` for the sign nonce.
xeddsa = "1.1"
rand10 = { package = "rand", version = "0.10", default-features = false, features = ["sys_rng"] }
rand_core10 = { package = "rand_core", version = "0.10" }

# Observability
async-trait = "0.1"
opentelemetry = "0.32"
# Trait/types only (HttpClient for the per-request auth wrapper); its reqwest
# integration targets reqwest 0.13 and the workspace is on 0.12, so no features.
opentelemetry-http = "0.32"
opentelemetry-jaeger = "0.22"
opentelemetry-otlp = "0.32.0"
opentelemetry-otlp = { version = "0.32.0", default-features = false }
opentelemetry_sdk = { version = "0.32", features = ["rt-tokio"] }
tracing = "0.1"
# Bumping this? Re-read `rolling.rs`'s `impl io::Write for
Expand Down
43 changes: 39 additions & 4 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ wasmtime = { workspace = true, optional = true }
# PUT time must run regardless of which WASM execution backend is compiled in.
wasmparser = { workspace = true }
xz2 = { workspace = true }
reqwest = { workspace = true }
# `blocking` named explicitly (not just inherited via feature unification from
# opentelemetry-http) because tracing::otel constructs reqwest::blocking::Client.
reqwest = { workspace = true, features = ["blocking"] }
# Hidden (no-echo) prompt for the `freenet secrets export/import` passphrase, so
# secrets don't have to be passed on argv (where they leak via `ps`/history).
rpassword = "7"
Expand All @@ -126,8 +128,38 @@ tracing-core = { workspace = true }
tracing-opentelemetry = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, features = ["json"], optional = true }
tracing-appender = { workspace = true, optional = true }
opentelemetry-otlp = { workspace = true, optional = true }
opentelemetry_sdk = { workspace = true, optional = true }
# Non-optional: the SDK metrics pipeline (tracing::otel) ships in every build.
# default-features = false + explicit list drops the logs exporter; only metrics
# is a goal here (see docs/design/otel-metrics-exporter.md, non-goals). It does
# NOT drop trace: `http-proto` mandates `trace`, `prost` and
# opentelemetry-proto, so those stay in the graph regardless.
# NO reqwest-*-client / reqwest-rustls feature on purpose: tracing::otel always
# supplies its own HttpClient, so the exporter never builds one (it only does
# when none was given — opentelemetry-otlp src/exporter/http/mod.rs). Enabling
# them would pull opentelemetry-http's reqwest 0.13, whose `rustls` feature is
# hardwired to aws-lc-rs + rustls-platform-verifier: a second reqwest major, a
# second TLS root store, a C/asm build (aws-lc-sys via cc+cmake) on every
# release target, and two crypto providers in one rustls, which makes
# `CryptoProvider::get_default_or_install_from_crate_features` panic.
# Our client rides the workspace reqwest 0.12, which already has rustls-tls, so
# https:// collector endpoints work with nothing new in the graph.
# Direct dep so tracing::otel can implement HttpClient (per-request auth
# headers); the crate is already in the graph via opentelemetry-otlp.
opentelemetry-http = { workspace = true }
async-trait = { workspace = true }
http = { workspace = true }
# XEdDSA collector auth: sign with the x25519 transport key (see
# transport::crypto::TransportKeypair::auth_token_signer). rand10/rand_core10
# exist only because xeddsa's rng bound is rand 0.10 (workspace rand is 0.9).
xeddsa = { workspace = true }
rand10 = { workspace = true }
rand_core10 = { workspace = true }
opentelemetry-otlp = { workspace = true, features = [
"http-proto",
"metrics",
"internal-logs",
] }
opentelemetry_sdk = { workspace = true }

hkdf = { workspace = true }
keyring = { workspace = true }
Expand Down Expand Up @@ -172,6 +204,9 @@ criterion = { workspace = true }
freenet-stdlib = { features = ["net", "testing"], workspace = true }
freenet-macros = { path = "../freenet-macros" }
httptest = { workspace = true }
# Test-only: proves the collector can verify XEdDSA tokens with a stock
# ed25519 library after Montgomery->Edwards conversion (4.1.3 = xeddsa's).
curve25519-dalek = "4.1.3"
libc = { workspace = true } # For sendmmsg syscall batching benchmarks
parking_lot = { workspace = true, features = ["deadlock_detection"] }
# Used by tests/in_process_restart.rs to directly probe the redb file lock as a
Expand Down Expand Up @@ -230,7 +265,7 @@ winres = "0.1"
default = ["redb", "trace", "websocket", "wasmtime-backend"]
sqlite = ["sqlx"]
trace = ["tracing-subscriber", "tracing-appender"]
trace-ot = ["opentelemetry-jaeger", "trace", "tracing-opentelemetry", "opentelemetry-otlp"]
trace-ot = ["opentelemetry-jaeger", "trace", "tracing-opentelemetry"]
websocket = ["axum/ws"]
testing = ["freenet-stdlib/testing", "parking_lot/deadlock_detection"]
console-subscriber = ["dep:console-subscriber"]
Expand Down
Loading
Loading