From 6793a51f4e5fe4a39f6640e651fc3f393cd5583a Mon Sep 17 00:00:00 2001 From: Hazel Atkinson Date: Mon, 7 Apr 2025 13:16:43 +0100 Subject: add export interval config --- README.md | 20 +++++++++--- src/config.rs | 5 ++- src/main.rs | 100 ++++++++++++++++++++++++++++++++++------------------------ 3 files changed, 78 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index a3926d2..6676b10 100644 --- a/README.md +++ b/README.md @@ -42,17 +42,27 @@ TODO: will write once it actually works ## How to configure -| `config.json` | env var | description | default | -|-----------------|----------------------|-------------------------------------------------------------------|------------| -| `docker_socket` | `CSPY_DOCKER_SOCKET` | The docker socket / named pipe to connect to | unset | -| `otlp_protocol` | `CSPY_OTLP_PROTO` | Whether to use httpbinary, httpjson, or grpc to send OTLP metrics | httpbinary | +| `config.json` | env var | description | default | +|------------------------|----------------------|-------------------------------------------------------------------|------------------------------------------------------| +| `docker_socket` | `CSPY_DOCKER_SOCKET` | The docker socket / named pipe to connect to | unset | +| `otlp_protocol` | `CSPY_OTLP_PROTO` | Whether to use httpbinary, httpjson, or grpc to send OTLP metrics | httpbinary | +| `otlp_endpoint` | `CSPY_OTLP_ENDPOINT` | Where to post metrics to | unset | +| `otlp_export_interval` | `CSPY_OTLP_INTERVAL` | How often to report metrics, in milliseconds | value of `OTEL_METRIC_EXPORT_INTERVAL` or 60 seconds | You can set configuration in the config file specified in the `CSPY_CONFIG` env variable -(`/etc/containerspy/config.json`) by default, which supports JSON5 syntax, or configure via the `CSPY_` env vars. +(`/etc/containerspy/config.json` by default), which supports JSON5 syntax, or configure via the `CSPY_` env vars. If a docker socket path is not set, containerspy will try to connect to `/var/run/docker.sock` or `//./pipe/docker_engine` depending on host OS. +If an OTLP endpoint is not set, it will try to post to the default ports and endpoints for an OTLP collector running on +the chosen protocol (`http://localhost:4318` for HTTP, `http://localhost:4317` for gRPC, see +[here](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md) and +[here](https://github.com/open-telemetry/opentelemetry-rust/blob/bc82d4f6/opentelemetry-otlp/src/exporter/mod.rs#L60)). + +Note: to send directly to Prometheus (with `--enable-feature=otlp-write-receiver`), use +http://localhost:9090/api/v1/otlp/v1/metrics as your endpoint, swapping `localhost:9090` for your Prometheus `host:port`. + ## Supported metrics This is intended to be a dropin replacement for cAdvisor, which lists its supported metrics diff --git a/src/config.rs b/src/config.rs index 153c170..52cb69c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,11 +14,14 @@ pub struct CspyConfig { #[config(env = "CSPY_OTLP_ENDPOINT")] pub otlp_endpoint: Option, + + #[config(env = "CSPY_OTLP_INTERVAL")] + pub otlp_export_interval: Option, } pub static CONFIG: LazyLock = LazyLock::new(|| { let cfg_loc = std::env::var("CSPY_CONFIG"); - let cfg_loc = cfg_loc.as_deref().ok().unwrap_or(&"/etc/containerspy/config.json"); + let cfg_loc = cfg_loc.as_deref().ok().unwrap_or("/etc/containerspy/config.json"); CspyConfig::builder() .env() diff --git a/src/main.rs b/src/main.rs index 2bbfe3f..dd8f6a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,57 +4,67 @@ use anyhow::Result; use bollard::Docker; use config::CONFIG; use opentelemetry_otlp::{MetricExporter, Protocol, WithExportConfig}; -use opentelemetry_sdk::metrics::SdkMeterProvider; +use opentelemetry_sdk::metrics::{PeriodicReader, PeriodicReaderBuilder, SdkMeterProvider}; use tokio::task::JoinHandle; +use tokio::time::interval; use tokio_util::sync::CancellationToken; mod config; mod stats_task; fn setup_otlp() -> Result { - let metric_exporter = - match CONFIG.otlp_protocol { - Protocol::HttpBinary | Protocol::HttpJson => { - let builder = MetricExporter::builder().with_http().with_protocol(CONFIG.otlp_protocol); - let builder = - if let Some(e) = &CONFIG.otlp_endpoint { - builder.with_endpoint(e) - } else { - builder - }; - - builder.build()? - }, - Protocol::Grpc => { - let builder = MetricExporter::builder().with_tonic().with_protocol(Protocol::Grpc); - - let builder = - if let Some(e) = &CONFIG.otlp_endpoint { - builder.with_endpoint(e.as_str()) - } else { - builder - }; - - builder.build()? - }, - }; + let metric_exporter = match CONFIG.otlp_protocol { + Protocol::HttpBinary | Protocol::HttpJson => { + let builder = MetricExporter::builder() + .with_http() + .with_protocol(CONFIG.otlp_protocol); + let builder = if let Some(e) = &CONFIG.otlp_endpoint { + builder.with_endpoint(e) + } else { + builder + }; + + builder.build()? + } + Protocol::Grpc => { + let builder = MetricExporter::builder() + .with_tonic() + .with_protocol(Protocol::Grpc); + + let builder = if let Some(e) = &CONFIG.otlp_endpoint { + builder.with_endpoint(e.as_str()) + } else { + builder + }; + + builder.build()? + } + }; + + // if we have a CSPY_OTLP_INTERVAL, apply that, + // else use default behaviour which reads OTEL_METRIC_EXPORT_INTERVAL else uses one minute as the interval + // note that a PeriodicReader without setting .with_interval is equivalent to using .with_periodic_exporter + + let reader_builder = PeriodicReader::builder(metric_exporter); + let reader_builder = if let Some(interval) = CONFIG.otlp_export_interval { + reader_builder.with_interval(Duration::from_millis(interval)) + } else { + reader_builder + }; Ok(SdkMeterProvider::builder() - .with_periodic_exporter(metric_exporter) - .build()) + .with_reader(reader_builder.build()) + .build()) } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { // open a docker connection - let docker = Arc::new( - if let Some(path) = &CONFIG.docker_socket { - Docker::connect_with_socket(path, 60, bollard::API_DEFAULT_VERSION)? - } - else { - Docker::connect_with_local_defaults()? - } - ); + let docker = Arc::new(if let Some(path) = &CONFIG.docker_socket { + Docker::connect_with_socket(path, 60, bollard::API_DEFAULT_VERSION)? + } else { + Docker::connect_with_local_defaults()? + }); // connect the OTLP exporter let meter_provider = Arc::new(setup_otlp()?); @@ -64,7 +74,9 @@ async fn main() -> Result<()> { let st2 = shutdown_token.clone(); // to be moved into the task tokio::spawn(async move { - tokio::signal::ctrl_c().await.expect("Failed to setup ctrl-c handler"); + tokio::signal::ctrl_c() + .await + .expect("Failed to setup ctrl-c handler"); st2.cancel(); }); @@ -87,7 +99,10 @@ async fn main() -> Result<()> { for (cont, handle) in &tasks { // funny O(n^2) loop - if containers.binary_search_by(|c| c.id.as_ref().unwrap().cmp(cont)).is_err() { + if containers + .binary_search_by(|c| c.id.as_ref().unwrap().cmp(cont)) + .is_err() + { handle.abort(); to_remove.push(cont.clone()); } @@ -102,7 +117,10 @@ async fn main() -> Result<()> { let id_string = cont.id.as_ref().unwrap(); if !tasks.contains_key(id_string) { // all this string cloning hurts me - tasks.insert(id_string.clone(), stats_task::launch_stats_task(cont, docker.clone(), meter_provider.clone())); + tasks.insert( + id_string.clone(), + stats_task::launch_stats_task(cont, docker.clone(), meter_provider.clone()), + ); } } } @@ -115,4 +133,4 @@ async fn main() -> Result<()> { println!("clean shutdown."); Ok(()) -} \ No newline at end of file +} -- cgit