From 8510f335fe7f8c7a76ea5c7652bab82e93e1e6b6 Mon Sep 17 00:00:00 2001 From: Nicolas Grunwald Date: Fri, 8 Dec 2023 12:25:46 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 ++ Cargo.toml | 10 ++++++ README.md | 27 ++++++++++++++ src/main.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..161aded --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rusty-json-prom-exporter" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.11.22", features = ["blocking"] } +serde_json = "1.0.108" diff --git a/README.md b/README.md new file mode 100644 index 0000000..183a538 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Rustbased exporter that converts arbitrary JSON to the .prom format. + +This small application works great in tandem with the Prometheus [node-exporter](https://github.com/prometheus/node_exporter). +If you place the converted JSON -> prom data in the /var/lib/prometheus/node-exporter/ directory, the metrics will be added to the node-exporter output. + +Build the project using Cargo. + +Run the binary as + +``` +./rusty-json-prom-exporter [url] [filename] +``` + +or better yet, set up a linux systemd service: + +``` +[Unit] +Description=Exporter that converts arbitrary JSON to the .prom data format - written in Rust. + +[Service] +ExecStart= .prom +Restart=always +User=root + +[Install] +WantedBy=multi-user.target +``` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5c05c48 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,101 @@ +use serde_json::Value; +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::{thread, time, process}; + +const FIVE_SECONDS: time::Duration = time::Duration::from_secs(5); +const THIRTY_SECONDS: time::Duration = time::Duration::from_secs(30); +const MAX_RETRIES: usize = 5; + +fn main() { + let args: Vec = env::args().collect(); + let mut data; + + if args.len() != 3 { + eprintln!("Usage: rusty-json-prom-exporter [url] [filename]"); + std::process::exit(1); + } + + loop { + match send_request_with_retry(&args[1], MAX_RETRIES) { + Ok(response_data) => { + data = response_data; + } + Err(err) => { + eprintln!("Failed to send the request ({})", err); + process::exit(1); // Is this bad? + } + } + let parsed_data: Value = match serde_json::from_str(&data) { + Ok(data) => data, + Err(err) => { + eprintln!("Could not parse JSON data ({})", err); + std::process::exit(1); + } + }; + let mut file = File::create(&args[2]).expect("Could not create file {&args[2]}"); + unpack_dict(&parsed_data, "", &mut file); + thread::sleep(FIVE_SECONDS); + } +} + + +fn send_request_with_retry(url: &str, max_retries: usize) -> Result { + let mut retries = 0; + loop { + match reqwest::blocking::get(url) { + Ok(response) => { + return response.text(); + } + Err(_) => { + retries += 1; + if retries >= max_retries { + eprintln!("Exceeded maximum retries - Exiting."); + process::exit(1); + } + eprintln!("Connection refused - Retrying in 30 seconds... (Attempt {} of {})", retries, MAX_RETRIES); + thread::sleep(THIRTY_SECONDS); + } + } + } +} + + +fn unpack_dict(data: &Value, path: &str, file: &mut File) { + + /* Help-text should be somewhat customizable in + * the future, maybe by including a dictionary + * that can substitue values based on metric name? */ + + let formatted_path = str::replace(path, "-", "_"); + match data { + Value::Number(num) => { + let line = format!( + "# HELP {}\n# TYPE gauge\n{} {}\n", + formatted_path, formatted_path, num + ); + file.write_all(line.as_bytes()).unwrap(); + } + Value::Bool(boolean) => { + let numeric_bool: i8 = if *boolean { 1 } else { 0 }; + let line: String = format!( + "# HELP {}_bool\n# TYPE gauge\n{} {}\n", + formatted_path, formatted_path, numeric_bool + ); + file.write_all(line.as_bytes()).unwrap(); + } + Value::Object(map) => { + for (key, value) in map { + if path == "" { + let new_path = format!("{}{}", formatted_path, key); + unpack_dict(value, &new_path, file); + } else { + let new_path = format!("{}_{}", formatted_path, key); + unpack_dict(value, &new_path, file); + } + } + } + _ => (), // Do not include in output, if not Value::Number or Value::Bool. + } +}