hyperhive/hive-metric/src/main.rs
2026-07-10 21:27:56 +02:00

108 lines
3.4 KiB
Rust

//! `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 <name> <value> [--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)"
)]
struct Cli {
/// Metric name (e.g. `tasks_completed`, `latency_ms`).
name: String,
/// Numeric value (f64). Floats and integers both accepted.
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<String>,
}
#[derive(Clone, Debug, clap::ValueEnum)]
enum MetricKind {
/// Counter: monotonically increasing cumulative sum (default).
Counter,
/// Gauge: point-in-time instantaneous value.
Gauge,
}
#[tokio::main]
async 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)?;
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.
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<Vec<KeyValue>> {
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()
}