diff --git a/Cargo.lock b/Cargo.lock index e505921b..03443d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,29 +237,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-rs" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - [[package]] name = "axum" version = "0.8.9" @@ -457,8 +434,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -602,31 +577,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "compression-codecs" version = "0.4.38" @@ -666,18 +622,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "const-hex" -version = "1.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -966,12 +910,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "dyn-clone" version = "1.0.20" @@ -1167,7 +1105,7 @@ dependencies = [ "base64ct", "bytes", "futures", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "soft_assert", @@ -1188,12 +1126,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futf" version = "0.1.5" @@ -1473,7 +1405,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "reqwest 0.12.28", + "reqwest", "rmcp", "rusqlite", "schemars", @@ -1525,7 +1457,7 @@ dependencies = [ "listenfd", "petgraph", "problem_details", - "reqwest 0.12.28", + "reqwest", "rusqlite", "serde", "serde_json", @@ -1555,7 +1487,7 @@ dependencies = [ "anyhow", "clap", "forgejo-api", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "time", @@ -1572,7 +1504,7 @@ dependencies = [ "matrix-sdk", "mime", "mime_guess", - "reqwest 0.12.28", + "reqwest", "rmcp", "schemars", "serde", @@ -1582,17 +1514,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "hive-metric" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", -] - [[package]] name = "hive-priv" version = "0.1.0" @@ -2068,65 +1989,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.2", - "libc", -] - [[package]] name = "js-sys" version = "0.3.99" @@ -2415,7 +2277,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "reqwest 0.12.28", + "reqwest", "ruma", "serde", "serde_html_form", @@ -2708,7 +2570,7 @@ dependencies = [ "getrandom 0.2.17", "http", "rand 0.8.6", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "serde_path_to_error", @@ -2741,80 +2603,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "opentelemetry" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest 0.13.4", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" -dependencies = [ - "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost 0.14.4", - "reqwest 0.13.4", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" -dependencies = [ - "base64", - "const-hex", - "opentelemetry", - "opentelemetry_sdk", - "prost 0.14.4", - "serde", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "portable-atomic", - "rand 0.9.4", - "thiserror 2.0.18", -] - [[package]] name = "parking" version = "2.2.1" @@ -3046,21 +2834,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bitflags", - "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "unarray", -] - [[package]] name = "prost" version = "0.13.5" @@ -3068,17 +2841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive 0.13.5", -] - -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive 0.14.4", + "prost-derive", ] [[package]] @@ -3094,19 +2857,6 @@ dependencies = [ "syn", ] -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pulldown-cmark" version = "0.13.4" @@ -3151,7 +2901,6 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -3278,15 +3027,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -3416,43 +3156,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "ring" version = "0.17.14" @@ -3754,7 +3457,6 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -3785,40 +3487,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3836,15 +3510,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.29" @@ -4097,22 +3762,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "siphasher" version = "1.0.3" @@ -4703,12 +4352,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicase" version = "2.9.0" @@ -4842,7 +4485,7 @@ dependencies = [ "hkdf", "hmac", "matrix-pickle", - "prost 0.13.5", + "prost", "rand 0.8.6", "serde", "serde_bytes", @@ -4854,16 +4497,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -5019,15 +4652,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webpki-roots" version = "1.0.7" @@ -5059,15 +4683,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index c9b9238d..b06e72c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "hive-claude", "hive-forge", "hive-matrix-mcp", - "hive-metric", "hive-priv", "hive-sh4re", ] diff --git a/docs/observability.md b/docs/observability.md index c95d5d52..25e7e358 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -123,55 +123,7 @@ Every agent's export includes these resource attributes automatically: | `hive` | hive display name (`services.hyperhive.hiveName`) | | `swarm` | swarm display name (`services.hyperhive.swarmName`, if set) | -Additional labels can be appended via `extraResourceAttributes` (see option -reference above); custom per-data-point labels can be passed with -`hive-metric --labels` (see below). - -## Agent-emitted custom metrics (`hive-metric`) - -Agents can push arbitrary labeled metrics to the same OTEL collector via the -`hive-metric` CLI tool, available in every agent container when -`services.hyperhive.otel.enable = true`. - -### Usage - -```text -hive-metric [--type counter|gauge] [--labels key=value...] -``` - -- `` — metric name (e.g. `tasks_completed`, `latency_ms`). -- `` — numeric value (f64; integers and floats both accepted). -- `--type counter|gauge` — metric kind: `counter` (cumulative sum, default) or - `gauge` (instantaneous point-in-time value). -- `--labels key=value` — extra per-data-point labels. May be repeated. - The resource labels (agent, hive, swarm, service.name) are inherited - automatically from `OTEL_RESOURCE_ATTRIBUTES` — do not re-specify them. - -### Examples - -```text -# Counter: cumulative tasks finished (default type — no --type flag needed) -hive-metric tasks_completed 1 --labels phase=scan - -# Gauge: current queue depth (absolute value — must use --type gauge) -hive-metric queue_depth 17 --type gauge - -# Float gauge with multiple labels (instantaneous measurement) -hive-metric api_latency_ms 142.5 --type gauge --labels model=sonnet --labels tier=api -``` - -### Error when OTEL is not configured - -When `services.hyperhive.otel.enable = false` (the default), the -`OTEL_EXPORTER_OTLP_ENDPOINT` env var is not set and `hive-metric` exits -with an informative error message. No silently-dropped metrics. - -### Wire format - -`hive-metric` always uses **OTLP HTTP/JSON** (`application/json` POST to -`$OTEL_EXPORTER_OTLP_ENDPOINT/v1/metrics`), regardless of the -`OTEL_EXPORTER_OTLP_PROTOCOL` setting. Auth headers from -`OTEL_EXPORTER_OTLP_HEADERS` are forwarded verbatim. +Additional labels can be appended via `extraResourceAttributes`. ## Metrics temporality diff --git a/flake.nix b/flake.nix index a4b9e0e7..8aca61c7 100644 --- a/flake.nix +++ b/flake.nix @@ -141,16 +141,10 @@ # (`hive-matrix-mcp` workspace member) — the # matrix-sdk-sqlite + rusqlite stack links against system # libsqlite3 by default. - # `cmake` builds `aws-lc-sys` (BoringSSL) from source — pulled in by - # the `rustls` (aws-lc-rs) crypto provider under the OTLP/reqwest - # stack in `hive-metric`. Without it the crane deps build fails on - # `cargo-package-reqwest-0.13.4`. Kept in the shared inputs since - # more metric/crypto deps are expected to land in the workspace. nativeBuildInputs = [ pkgs.git pkgs.sqlite pkgs.pkg-config - pkgs.cmake ]; } ); diff --git a/hive-metric/Cargo.toml b/hive-metric/Cargo.toml deleted file mode 100644 index d71614be..00000000 --- a/hive-metric/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "hive-metric" -version.workspace = true -edition.workspace = true - -[[bin]] -name = "hive-metric" -path = "src/main.rs" - -[dependencies] -anyhow.workspace = true -clap.workspace = true -# OTEL SDK — only used by this crate, so kept local rather than in workspace.dependencies. -opentelemetry = "0.32" -opentelemetry_sdk = { version = "0.32", features = ["metrics"] } -# Blocking (not async) reqwest client on purpose: the metrics SDK drives the -# OTLP push from a `PeriodicReader` background thread that has no Tokio runtime, -# so the async client panics there with "no reactor running". The blocking -# client sends on that thread directly — and for a fire-and-forget CLI that -# records one point then `shutdown()`s, a synchronous send is exactly right. -opentelemetry-otlp = { version = "0.32", default-features = false, features = [ - "metrics", - "http-json", - "reqwest-blocking-client", - "reqwest-rustls", -] } diff --git a/hive-metric/src/main.rs b/hive-metric/src/main.rs deleted file mode 100644 index 48e066eb..00000000 --- a/hive-metric/src/main.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! `hive-metric` — push a single labeled metric to the OTEL collector. -//! -//! Uses the OpenTelemetry Rust SDK with the OTLP HTTP/JSON exporter to emit -//! metrics. Standard OTEL env vars are read automatically by the SDK: -//! -//! - `OTEL_EXPORTER_OTLP_ENDPOINT` — collector URL (required) -//! - `OTEL_EXPORTER_OTLP_HEADERS` — auth headers (`Key=Value,...`) -//! - `OTEL_RESOURCE_ATTRIBUTES` — resource labels (`k=v,...`) -//! -//! The harness populates all of these per-agent when -//! `services.hyperhive.otel.enable = true`. -//! -//! # Usage -//! ```text -//! hive-metric [--type counter|gauge] [--labels key=value...] -//! ``` - -use anyhow::{Context, Result, bail}; -use clap::Parser; -use opentelemetry::KeyValue; -use opentelemetry::metrics::MeterProvider; -use opentelemetry_otlp::{Protocol, WithExportConfig}; - -#[derive(Parser)] -#[command( - name = "hive-metric", - about = "Push a labeled metric to the OTEL collector (requires services.hyperhive.otel.enable = true)", - // Let a leading-minus value parse as the `value` positional rather than - // being rejected as an unknown flag — negative gauges are legitimate - // (e.g. a delta), and it lets the counter-sign check below produce a clear - // domain error instead of clap's "unexpected argument '-1'". - allow_negative_numbers = true -)] -struct Cli { - /// Metric name (e.g. `tasks_completed`, `latency_ms`). - name: String, - - /// Numeric value (f64). Floats and integers both accepted. Gauges may be - /// negative; counters must be >= 0 (they only increase). - value: f64, - - /// Metric kind: `counter` (cumulative sum, default) or `gauge` (instantaneous value). - #[arg(long = "type", value_name = "TYPE", default_value = "counter")] - metric_type: MetricKind, - - /// Extra label(s) as `key=value` pairs. May be repeated. - /// Resource labels (agent, hive, swarm) come from `OTEL_RESOURCE_ATTRIBUTES` - /// automatically — do not re-specify them here. - #[arg(long, value_name = "KEY=VALUE")] - labels: Vec, -} - -#[derive(Clone, Debug, clap::ValueEnum)] -enum MetricKind { - /// Counter: monotonically increasing cumulative sum (default). - Counter, - /// Gauge: point-in-time instantaneous value. - Gauge, -} - -fn main() -> Result<()> { - let cli = Cli::parse(); - - // Fail early with a clear message when OTEL is not configured. - if std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_err() { - bail!( - "OTEL_EXPORTER_OTLP_ENDPOINT not set — \ - enable OTEL in NixOS config (services.hyperhive.otel.enable = true)" - ); - } - - let labels = parse_labels(&cli.labels)?; - - // Counters are monotonically non-negative — the OTEL SDK silently drops a - // negative `add`, so a typo like `hive-metric tasks_done -1` would vanish - // with exit 0. Reject it loudly instead. - if matches!(cli.metric_type, MetricKind::Counter) && cli.value < 0.0 { - bail!( - "counter '{}' value must be >= 0 (got {}); counters only increase — \ - use `--type gauge` for a value that can go down", - cli.name, - cli.value - ); - } - - let exporter = opentelemetry_otlp::MetricExporter::builder() - .with_http() - .with_protocol(Protocol::HttpJson) - .build() - .context("failed to build OTLP metric exporter")?; - - // SDK automatically reads OTEL_RESOURCE_ATTRIBUTES for resource labels. - // The metrics SDK has no `with_simple_exporter` (that's a *traces*-only - // API) — a push exporter is always driven by a periodic reader. For this - // fire-and-forget CLI the timer never actually fires: we record one metric - // and immediately `shutdown()`, which force-flushes the pending export - // synchronously before returning. So there's no background-timer flush race - // in practice — the single point is exported exactly once on shutdown. - let provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() - .with_periodic_exporter(exporter) - .build(); - - let meter = provider.meter("hive-metric"); - - match cli.metric_type { - MetricKind::Counter => { - meter.f64_counter(cli.name).build().add(cli.value, &labels); - } - MetricKind::Gauge => { - meter.f64_gauge(cli.name).build().record(cli.value, &labels); - } - } - - // Flush pending metrics and shut down cleanly. - provider - .shutdown() - .context("failed to flush OTLP metrics")?; - - Ok(()) -} - -fn parse_labels(args: &[String]) -> Result> { - args.iter() - .map(|s| { - let (k, v) = s - .split_once('=') - .with_context(|| format!("--labels must be key=value, got {s:?}"))?; - Ok(KeyValue::new(k.trim().to_owned(), v.trim().to_owned())) - }) - .collect() -}