Initial generalization of source workers
This commit is contained in:
parent
82906adca2
commit
ed3cb89325
43
Cargo.lock
generated
43
Cargo.lock
generated
@ -120,6 +120,12 @@ version = "3.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -142,6 +148,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
name = "chimemon"
|
name = "chimemon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"chrony-candm",
|
"chrony-candm",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@ -152,7 +159,9 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
"sysctl",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1331,6 +1340,15 @@ version = "1.0.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@ -1514,6 +1532,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sysctl"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f99d037b2bef227ab8963f4b0acc33ecbb1f9a2e7365add7789372b387ec19e1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"byteorder",
|
||||||
|
"libc",
|
||||||
|
"thiserror",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1645,6 +1676,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1812,6 +1844,17 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
influxdb2 = "0.3"
|
influxdb2 = "0.3.3"
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
@ -14,6 +14,9 @@ figment = { version = "0.10", features = ["toml"] }
|
|||||||
gethostname = "0.3"
|
gethostname = "0.3"
|
||||||
env_logger = "0.9.1"
|
env_logger = "0.9.1"
|
||||||
futures = "0.3.24"
|
futures = "0.3.24"
|
||||||
|
sysctl = "0.5.2"
|
||||||
|
async-trait = "0.1.58"
|
||||||
|
tokio-stream = { version = "0.1.11", features = ["sync"] }
|
||||||
|
|
||||||
[dependencies.chrony-candm]
|
[dependencies.chrony-candm]
|
||||||
git = "https://github.com/aws/chrony-candm"
|
git = "https://github.com/aws/chrony-candm"
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
[sources.chrony]
|
[sources]
|
||||||
enabled = true
|
[sources.chrony]
|
||||||
poll_interval = 10
|
enabled = true
|
||||||
timeout = 1
|
# host = "127.0.0.1:323" # default; port is required
|
||||||
measurement_prefix = "chrony."
|
tracking_interval = 10
|
||||||
tracking_measurement = "tracking"
|
sources_interval = 60
|
||||||
sources_measurement = "sources"
|
timeout = 1
|
||||||
|
measurement_prefix = "chrony."
|
||||||
|
tracking_measurement = "tracking"
|
||||||
|
sources_measurement = "sources"
|
||||||
|
|
||||||
|
[sources.hwmon]
|
||||||
|
enabled = true
|
||||||
|
interval = 60
|
||||||
|
measurement_prefix = "hwmon."
|
||||||
|
|
||||||
|
[sources.hwmon.sensors.temp1]
|
||||||
|
name = "hwmon0"
|
||||||
|
sensor = "temp1_input"
|
||||||
|
|
||||||
[influxdb]
|
[influxdb]
|
||||||
# url = "http://localhost:8086" # defaults to localhost
|
url = "http://localhost:8086"
|
||||||
org = "default"
|
org = "default"
|
||||||
bucket = "default"
|
bucket = "default"
|
||||||
token = ""
|
token = ""
|
||||||
|
|
||||||
[influxdb.tags]
|
[influxdb.tags]
|
||||||
# host = "localhost" # defaults to gethostname()
|
# host = "qubit" # default is the local hostname
|
||||||
|
# arbitrary = "tags" # are allowed
|
200
src/chrony.rs
200
src/chrony.rs
@ -1,18 +1,114 @@
|
|||||||
use chrony_candm::reply::{self, ReplyBody};
|
use async_trait::async_trait;
|
||||||
|
use chimemon::{ChimemonSource, ChimemonSourceChannel, Config};
|
||||||
|
use chrony_candm::reply::{self, ReplyBody, SourceMode};
|
||||||
use chrony_candm::request::{self, RequestBody};
|
use chrony_candm::request::{self, RequestBody};
|
||||||
use std::net::SocketAddr;
|
use influxdb2::models::DataPoint;
|
||||||
use tokio::time::timeout;
|
use log::{info, warn};
|
||||||
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
use tokio::{join, time::timeout};
|
||||||
|
|
||||||
pub struct ChronyClient {
|
pub struct ChronyClient {
|
||||||
pub server: SocketAddr,
|
pub server: SocketAddr,
|
||||||
|
config: Config,
|
||||||
client: chrony_candm::Client,
|
client: chrony_candm::Client,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn datapoint_from_tracking(
|
||||||
|
t: &reply::Tracking,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<DataPoint, Box<dyn std::error::Error>> {
|
||||||
|
let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
||||||
|
let measurement = config.sources.chrony.measurement_prefix.to_owned()
|
||||||
|
+ &config.sources.chrony.tracking_measurement;
|
||||||
|
let mut builder =
|
||||||
|
DataPoint::builder(&measurement).timestamp(now.as_nanos().try_into().unwrap());
|
||||||
|
for (key, value) in &config.influxdb.tags {
|
||||||
|
builder = builder.tag(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = builder
|
||||||
|
.field("ref_id", t.ref_id as i64)
|
||||||
|
.field("ref_ip_addr", t.ip_addr.to_string())
|
||||||
|
.field("stratum", t.stratum as i64)
|
||||||
|
.field("leap_status", t.leap_status as i64)
|
||||||
|
.field("current_correction", f64::from(t.current_correction))
|
||||||
|
.field("last_offset", f64::from(t.last_offset))
|
||||||
|
.field("rms_offset", f64::from(t.rms_offset))
|
||||||
|
.field("freq_ppm", f64::from(t.freq_ppm))
|
||||||
|
.field("resid_freq_ppm", f64::from(t.resid_freq_ppm))
|
||||||
|
.field("skew_ppm", f64::from(t.skew_ppm))
|
||||||
|
.field("root_delay", f64::from(t.root_delay))
|
||||||
|
.field("root_dispersion", f64::from(t.root_dispersion))
|
||||||
|
.field("last_update_interval", f64::from(t.last_update_interval))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
Ok(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn datapoint_from_sourcedata(
|
||||||
|
d: &reply::SourceData,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<DataPoint, Box<dyn std::error::Error>> {
|
||||||
|
let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
||||||
|
let measurement = config.sources.chrony.measurement_prefix.to_owned()
|
||||||
|
+ &config.sources.chrony.sources_measurement;
|
||||||
|
let mut builder =
|
||||||
|
DataPoint::builder(&measurement).timestamp(now.as_nanos().try_into().unwrap());
|
||||||
|
for (key, value) in &config.influxdb.tags {
|
||||||
|
builder = builder.tag(key, value)
|
||||||
|
}
|
||||||
|
builder = builder
|
||||||
|
.tag("ref_id", d.ip_addr.to_string())
|
||||||
|
.tag(
|
||||||
|
"mode",
|
||||||
|
match d.mode {
|
||||||
|
SourceMode::Client => String::from("server"),
|
||||||
|
SourceMode::Peer => String::from("peer"),
|
||||||
|
SourceMode::Ref => String::from("refclock"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.tag(
|
||||||
|
"state",
|
||||||
|
match d.state {
|
||||||
|
reply::SourceState::Selected => String::from("best"),
|
||||||
|
reply::SourceState::NonSelectable => String::from("unusable"),
|
||||||
|
reply::SourceState::Falseticker => String::from("falseticker"),
|
||||||
|
reply::SourceState::Jittery => String::from("jittery"),
|
||||||
|
reply::SourceState::Unselected => String::from("unused"),
|
||||||
|
reply::SourceState::Selectable => String::from("combined"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.field("poll", d.poll as i64)
|
||||||
|
.field("stratum", d.stratum as i64)
|
||||||
|
.field("flags", d.flags.bits() as i64)
|
||||||
|
.field("reachability", d.reachability.count_ones() as i64)
|
||||||
|
.field("since_sample", d.since_sample as i64)
|
||||||
|
.field("orig_latest_meas", f64::from(d.orig_latest_meas))
|
||||||
|
.field("latest_meas", f64::from(d.latest_meas))
|
||||||
|
.field("latest_meas_err", f64::from(d.latest_meas_err));
|
||||||
|
let point = builder.build()?;
|
||||||
|
|
||||||
|
Ok(point)
|
||||||
|
}
|
||||||
|
|
||||||
impl ChronyClient {
|
impl ChronyClient {
|
||||||
pub fn new(client: chrony_candm::Client, server: SocketAddr, timeout: std::time::Duration) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
Self {
|
let server = config
|
||||||
|
.sources
|
||||||
|
.chrony
|
||||||
|
.host
|
||||||
|
.to_socket_addrs()
|
||||||
|
.unwrap()
|
||||||
|
.next()
|
||||||
|
.expect("Unable to parse host:port:");
|
||||||
|
let client = chrony_candm::Client::spawn(&Handle::current(), Default::default());
|
||||||
|
let timeout = Duration::from_secs(config.sources.chrony.timeout);
|
||||||
|
ChronyClient {
|
||||||
server,
|
server,
|
||||||
|
config,
|
||||||
client,
|
client,
|
||||||
timeout,
|
timeout,
|
||||||
}
|
}
|
||||||
@ -79,6 +175,100 @@ impl ChronyClient {
|
|||||||
)),
|
)),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
// if sourcedata.mode == SourceMode::Ref {
|
||||||
|
// // Get the name if it's a refclock
|
||||||
|
// let reply = timeout(
|
||||||
|
// self.timeout,
|
||||||
|
// self.client.query(
|
||||||
|
// RequestBody::NtpSourceName(request::NtpSourceName { ip_addr: sourcedata.ip_addr }),
|
||||||
|
// self.server,
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// .await??;
|
||||||
|
|
||||||
|
// let sourcename = match reply.body {
|
||||||
|
// ReplyBody::NtpSourceName(sourcename) => Ok(sourcename),
|
||||||
|
// _ => Err(std::io::Error::new(
|
||||||
|
// std::io::ErrorKind::InvalidData,
|
||||||
|
// "Invalid response",
|
||||||
|
// )),
|
||||||
|
// }?;
|
||||||
|
|
||||||
|
// sourcedata.ip_addr = sourcename;
|
||||||
|
// }
|
||||||
|
|
||||||
Ok(sourcedata)
|
Ok(sourcedata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn tracking_poll(
|
||||||
|
&self,
|
||||||
|
chan: &ChimemonSourceChannel,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let tracking = self.get_tracking().await?;
|
||||||
|
|
||||||
|
let tracking_data = datapoint_from_tracking(&tracking, &self.config)?;
|
||||||
|
|
||||||
|
info!("Writing tracking data: {:?}", tracking_data);
|
||||||
|
|
||||||
|
chan.send(tracking_data)
|
||||||
|
.expect("Unable to send tracking data to targets");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sources_poll(
|
||||||
|
&self,
|
||||||
|
chan: &ChimemonSourceChannel,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let sources = self.get_sources().await?;
|
||||||
|
for ds in sources {
|
||||||
|
let source_data = datapoint_from_sourcedata(&ds, &self.config)?;
|
||||||
|
info!("Writing source data: {:?}", source_data);
|
||||||
|
chan.send(source_data)
|
||||||
|
.expect("Unable to send source data to targets");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ChimemonSource for ChronyClient {
|
||||||
|
async fn run(self, chan: tokio::sync::broadcast::Sender<DataPoint>) {
|
||||||
|
info!("Chrony task started");
|
||||||
|
|
||||||
|
let mut t_interval = tokio::time::interval(Duration::from_secs(
|
||||||
|
self.config.sources.chrony.tracking_interval,
|
||||||
|
));
|
||||||
|
let mut s_interval = tokio::time::interval(Duration::from_secs(
|
||||||
|
self.config.sources.chrony.sources_interval,
|
||||||
|
));
|
||||||
|
|
||||||
|
let t_future = async {
|
||||||
|
let lchan = chan.clone();
|
||||||
|
loop {
|
||||||
|
t_interval.tick().await;
|
||||||
|
|
||||||
|
match self.tracking_poll(&lchan).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error in chrony task: {}", e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let s_future = async {
|
||||||
|
let lchan = chan.clone();
|
||||||
|
loop {
|
||||||
|
s_interval.tick().await;
|
||||||
|
|
||||||
|
match self.sources_poll(&lchan).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error in chrony task: {}", e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
join!(t_future, s_future);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
75
src/hwmon.rs
Normal file
75
src/hwmon.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use chimemon::{ChimemonSource, ChimemonSourceChannel, Config};
|
||||||
|
use futures::{stream, StreamExt};
|
||||||
|
use influxdb2::models::DataPoint;
|
||||||
|
use log::{info, debug};
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HwmonSource {
|
||||||
|
config: Config,
|
||||||
|
sensors: Vec<HwmonSensor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HwmonSensor {
|
||||||
|
path: PathBuf,
|
||||||
|
device: String,
|
||||||
|
sensor: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const HWMON_ROOT: &str = "/sys/class/hwmon";
|
||||||
|
|
||||||
|
impl HwmonSource {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let sensors = config
|
||||||
|
.sources
|
||||||
|
.hwmon
|
||||||
|
.sensors
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| HwmonSensor {
|
||||||
|
name: k.into(),
|
||||||
|
device: v.name.clone(),
|
||||||
|
sensor: v.sensor.clone(),
|
||||||
|
path: PathBuf::from(HWMON_ROOT).join(&v.name).join(&v.sensor),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
HwmonSource { config, sensors }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_raw_value(sensor: &HwmonSensor) -> Result<String, std::io::Error> {
|
||||||
|
tokio::fs::read_to_string(&sensor.path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ChimemonSource for HwmonSource {
|
||||||
|
async fn run(self, chan: tokio::sync::broadcast::Sender<DataPoint>) {
|
||||||
|
info!("hwmon task started");
|
||||||
|
let mut interval =
|
||||||
|
tokio::time::interval(Duration::from_secs(self.config.sources.hwmon.interval));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
stream::iter(&self.sensors).for_each_concurrent(None, |s| async {
|
||||||
|
let sensor_val = HwmonSource::get_raw_value(s)
|
||||||
|
.await
|
||||||
|
.expect("Unable to read sensor");
|
||||||
|
debug!("hwmon {} raw value {}", s.path.to_string_lossy(), sensor_val);
|
||||||
|
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||||
|
let mut builder =
|
||||||
|
DataPoint::builder(&s.device).timestamp(now.as_nanos().try_into().unwrap());
|
||||||
|
for (key, value) in &self.config.influxdb.tags {
|
||||||
|
builder = builder.tag(key, value)
|
||||||
|
}
|
||||||
|
builder = builder.field(&s.name, sensor_val.trim().parse::<f64>().unwrap());
|
||||||
|
let dp = builder.build().unwrap();
|
||||||
|
|
||||||
|
info!("Writing hwmon data: {:?}", dp);
|
||||||
|
chan.send(dp).unwrap();
|
||||||
|
}).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
118
src/lib.rs
118
src/lib.rs
@ -1,4 +1,4 @@
|
|||||||
use chrony_candm::reply;
|
use async_trait::async_trait;
|
||||||
use figment::{
|
use figment::{
|
||||||
providers::{Format, Serialized, Toml},
|
providers::{Format, Serialized, Toml},
|
||||||
util::map,
|
util::map,
|
||||||
@ -8,11 +8,8 @@ use figment::{
|
|||||||
use gethostname::gethostname;
|
use gethostname::gethostname;
|
||||||
use influxdb2::models::DataPoint;
|
use influxdb2::models::DataPoint;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::path::Path;
|
||||||
use std::{
|
use tokio::sync::broadcast::*;
|
||||||
net::{IpAddr, Ipv4Addr},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct InfluxConfig {
|
pub struct InfluxConfig {
|
||||||
@ -40,12 +37,12 @@ impl Default for InfluxConfig {
|
|||||||
pub struct ChronyConfig {
|
pub struct ChronyConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub timeout: u64,
|
pub timeout: u64,
|
||||||
pub poll_interval: u64,
|
pub tracking_interval: u64,
|
||||||
|
pub sources_interval: u64,
|
||||||
pub measurement_prefix: String,
|
pub measurement_prefix: String,
|
||||||
pub tracking_measurement: String,
|
pub tracking_measurement: String,
|
||||||
pub sources_measurement: String,
|
pub sources_measurement: String,
|
||||||
pub host: IpAddr,
|
pub host: String,
|
||||||
pub port: u16,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ChronyConfig {
|
impl Default for ChronyConfig {
|
||||||
@ -53,12 +50,37 @@ impl Default for ChronyConfig {
|
|||||||
ChronyConfig {
|
ChronyConfig {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
poll_interval: 60,
|
tracking_interval: 60,
|
||||||
|
sources_interval: 300,
|
||||||
measurement_prefix: "chrony.".into(),
|
measurement_prefix: "chrony.".into(),
|
||||||
tracking_measurement: "tracking".into(),
|
tracking_measurement: "tracking".into(),
|
||||||
sources_measurement: "sources".into(),
|
sources_measurement: "sources".into(),
|
||||||
host: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
host: "127.0.0.1:323".into(),
|
||||||
port: 323,
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct HwmonSensorConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub sensor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct HwmonConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub interval: u64,
|
||||||
|
pub measurement_prefix: String,
|
||||||
|
pub sensors: Map<String, HwmonSensorConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HwmonConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
HwmonConfig {
|
||||||
|
enabled: false,
|
||||||
|
interval: 60,
|
||||||
|
measurement_prefix: "hwmon.".into(),
|
||||||
|
sensors: map! {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,6 +88,7 @@ impl Default for ChronyConfig {
|
|||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct SourcesConfig {
|
pub struct SourcesConfig {
|
||||||
pub chrony: ChronyConfig,
|
pub chrony: ChronyConfig,
|
||||||
|
pub hwmon: HwmonConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
@ -74,70 +97,19 @@ pub struct Config {
|
|||||||
pub sources: SourcesConfig,
|
pub sources: SourcesConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config(filename: &Path) -> Result<Config, Box<dyn std::error::Error>> {
|
pub fn load_config(filename: &Path) -> Figment {
|
||||||
let config = Figment::from(Serialized::defaults(Config::default()))
|
Figment::from(Serialized::defaults(Config::default())).merge(Toml::file(filename))
|
||||||
.merge(Toml::file(filename))
|
|
||||||
.extract()?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn datapoint_from_tracking(
|
pub type ChimemonSourceChannel = Sender<DataPoint>;
|
||||||
t: &reply::Tracking,
|
pub type ChimemonTargetChannel = Receiver<DataPoint>;
|
||||||
config: &Config,
|
|
||||||
) -> Result<DataPoint, Box<dyn std::error::Error>> {
|
|
||||||
let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
|
||||||
let measurement = config.sources.chrony.measurement_prefix.to_owned()
|
|
||||||
+ &config.sources.chrony.tracking_measurement;
|
|
||||||
let mut builder =
|
|
||||||
DataPoint::builder(&measurement).timestamp(now.as_nanos().try_into().unwrap());
|
|
||||||
for (key, value) in &config.influxdb.tags {
|
|
||||||
builder = builder.tag(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let point = builder
|
#[async_trait]
|
||||||
.field("ref_id", t.ref_id as i64)
|
pub trait ChimemonSource {
|
||||||
.field("ref_ip_addr", t.ip_addr.to_string())
|
async fn run(self, chan: ChimemonSourceChannel);
|
||||||
.field("stratum", t.stratum as i64)
|
|
||||||
.field("leap_status", t.leap_status as i64)
|
|
||||||
.field("current_correction", f64::from(t.current_correction))
|
|
||||||
.field("last_offset", f64::from(t.last_offset))
|
|
||||||
.field("rms_offset", f64::from(t.rms_offset))
|
|
||||||
.field("freq_ppm", f64::from(t.freq_ppm))
|
|
||||||
.field("resid_freq_ppm", f64::from(t.resid_freq_ppm))
|
|
||||||
.field("skew_ppm", f64::from(t.skew_ppm))
|
|
||||||
.field("root_delay", f64::from(t.root_delay))
|
|
||||||
.field("root_dispersion", f64::from(t.root_dispersion))
|
|
||||||
.field("last_update_interval", f64::from(t.last_update_interval))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
Ok(point)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn datapoint_from_sourcedata(
|
#[async_trait]
|
||||||
d: &reply::SourceData,
|
pub trait ChimemonTarget {
|
||||||
config: &Config,
|
async fn run(self, chan: ChimemonTargetChannel);
|
||||||
) -> Result<DataPoint, Box<dyn std::error::Error>> {
|
|
||||||
let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
|
|
||||||
let measurement = config.sources.chrony.measurement_prefix.to_owned()
|
|
||||||
+ &config.sources.chrony.sources_measurement;
|
|
||||||
let mut builder =
|
|
||||||
DataPoint::builder(&measurement).timestamp(now.as_nanos().try_into().unwrap());
|
|
||||||
for (key, value) in &config.influxdb.tags {
|
|
||||||
builder = builder.tag(key, value)
|
|
||||||
}
|
|
||||||
builder = builder
|
|
||||||
.tag("ref_id", d.ip_addr.to_string())
|
|
||||||
.tag("mode", (d.mode as i64).to_string())
|
|
||||||
.tag("state", (d.state as i64).to_string())
|
|
||||||
.field("poll", d.poll as i64)
|
|
||||||
.field("stratum", d.stratum as i64)
|
|
||||||
.field("flags", d.flags.bits() as i64)
|
|
||||||
.field("reachability", d.reachability as i64)
|
|
||||||
.field("since_sample", d.since_sample as i64)
|
|
||||||
.field("orig_latest_meas", f64::from(d.orig_latest_meas))
|
|
||||||
.field("latest_meas", f64::from(d.latest_meas))
|
|
||||||
.field("latest_meas_err", f64::from(d.latest_meas_err));
|
|
||||||
let point = builder.build()?;
|
|
||||||
|
|
||||||
Ok(point)
|
|
||||||
}
|
}
|
||||||
|
144
src/main.rs
144
src/main.rs
@ -1,63 +1,55 @@
|
|||||||
mod chrony;
|
mod chrony;
|
||||||
|
mod hwmon;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Parser, ValueEnum};
|
||||||
use env_logger;
|
use env_logger::{self, Env};
|
||||||
use futures::{future::join_all, prelude::*};
|
use futures::{future::join_all, prelude::*};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::{path::Path, time::Duration};
|
use std::path::Path;
|
||||||
use std::net::SocketAddr;
|
use tokio::sync::broadcast;
|
||||||
use tokio::runtime::Handle;
|
|
||||||
|
|
||||||
use crate::chrony::*;
|
use crate::{chrony::*, hwmon::HwmonSource};
|
||||||
use chimemon::*;
|
use chimemon::*;
|
||||||
|
|
||||||
const PROGRAM_NAME: &str = "chimemon";
|
const PROGRAM_NAME: &str = "chimemon";
|
||||||
const VERSION: &str = "0.0.1";
|
const VERSION: &str = "0.0.1";
|
||||||
|
|
||||||
const DEFAULT_RETRY_DELAY: Duration = Duration::new(60, 0);
|
#[derive(ValueEnum, Clone)]
|
||||||
const MAX_RETRY_DELAY: Duration = Duration::new(1800, 0);
|
enum Level {
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// TOML configuration to load
|
/// TOML configuration to load
|
||||||
#[arg(short, long, default_value_t = String::from("config.toml"))]
|
#[arg(short, long, default_value_t = String::from("config.toml"))]
|
||||||
config_file: String,
|
config_file: String,
|
||||||
}
|
#[arg(value_enum, default_value_t = Level::Warn)]
|
||||||
|
log_level: Level,
|
||||||
async fn do_chrony_poll(
|
|
||||||
config: &Config,
|
|
||||||
chrony: &ChronyClient,
|
|
||||||
influx: &influxdb2::Client,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let tracking = chrony.get_tracking().await?;
|
|
||||||
let sources = chrony.get_sources().await?;
|
|
||||||
|
|
||||||
let mut frame = Vec::new();
|
|
||||||
|
|
||||||
let tracking_data = datapoint_from_tracking(&tracking, &config)?;
|
|
||||||
|
|
||||||
info!(target: "chrony", "Writing tracking data: {:?}", tracking_data);
|
|
||||||
|
|
||||||
frame.push(tracking_data);
|
|
||||||
for ds in sources {
|
|
||||||
let source_data = datapoint_from_sourcedata(&ds, &config)?;
|
|
||||||
info!(target: "chrony", "Writing source data: {:?}", source_data);
|
|
||||||
frame.push(source_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
influx
|
|
||||||
.write(&config.influxdb.bucket, stream::iter(frame))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
env_logger::init();
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
let loglevel = args
|
||||||
|
.log_level
|
||||||
|
.to_possible_value()
|
||||||
|
.unwrap()
|
||||||
|
.get_name()
|
||||||
|
.to_owned();
|
||||||
|
env_logger::Builder::from_env(Env::default().default_filter_or(loglevel)).init();
|
||||||
|
|
||||||
info!("{} v{} starting...", PROGRAM_NAME, VERSION);
|
info!("{} v{} starting...", PROGRAM_NAME, VERSION);
|
||||||
let config = load_config(Path::new(&args.config_file))?;
|
let fig = load_config(Path::new(&args.config_file));
|
||||||
|
warn!("{:?}", fig);
|
||||||
|
let config: Config = fig.extract()?;
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
let (tx, _) = broadcast::channel(16);
|
||||||
|
let sourcechan: ChimemonSourceChannel = tx;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Connecting to influxdb {} org: {} using token",
|
"Connecting to influxdb {} org: {} using token",
|
||||||
@ -69,48 +61,48 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
&config.influxdb.token,
|
&config.influxdb.token,
|
||||||
);
|
);
|
||||||
|
|
||||||
if config.sources.chrony.enabled {
|
let chrony = if config.sources.chrony.enabled {
|
||||||
let chrony = ChronyClient::new(
|
Some(ChronyClient::new(config.to_owned()))
|
||||||
chrony_candm::Client::spawn(&Handle::current(), Default::default()),
|
} else {
|
||||||
SocketAddr::new(
|
None
|
||||||
config.sources.chrony.host,
|
|
||||||
config.sources.chrony.port,
|
|
||||||
),
|
|
||||||
Duration::from_secs(config.sources.chrony.timeout),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut retry_delay = DEFAULT_RETRY_DELAY;
|
|
||||||
let mut fail_count = 0;
|
|
||||||
|
|
||||||
let interval = Duration::new(config.sources.chrony.poll_interval, 0);
|
|
||||||
let mut interval = tokio::time::interval(interval);
|
|
||||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
info!(target: "chrony", "Chrony task started");
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
match do_chrony_poll(&config, &chrony, &influx.to_owned()).await {
|
|
||||||
Ok(_) => {
|
|
||||||
fail_count = 0;
|
|
||||||
retry_delay = DEFAULT_RETRY_DELAY;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!(target: "chrony", "Error in chrony task: {}", e.to_string());
|
|
||||||
if fail_count % 4 == 0 {
|
|
||||||
// exponential backoff, every 4 failures double the retry timer, cap at max
|
|
||||||
retry_delay = std::cmp::min(MAX_RETRY_DELAY, retry_delay * 2);
|
|
||||||
}
|
|
||||||
fail_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fail_count > 0 {
|
|
||||||
tokio::time::sleep(retry_delay).await;
|
|
||||||
interval.reset();
|
|
||||||
};
|
};
|
||||||
|
match chrony {
|
||||||
|
Some(c) => {
|
||||||
|
tasks.push(tokio::spawn(c.run(sourcechan.clone())));
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
let hwmon = if config.sources.hwmon.enabled {
|
||||||
|
Some(HwmonSource::new(config.to_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match hwmon {
|
||||||
|
Some(hwmon) => {
|
||||||
|
tasks.push(tokio::spawn(hwmon.run(sourcechan.clone())));
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut influxrx = sourcechan.subscribe();
|
||||||
|
tasks.push(tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let dp = influxrx.recv().await.unwrap();
|
||||||
|
influx
|
||||||
|
.write(&config.influxdb.bucket, stream::iter([dp]))
|
||||||
|
.await
|
||||||
|
.expect("Error writing to influxdb");
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
// let mut debugrx = sourcechan.subscribe();
|
||||||
|
// tasks.push(tokio::spawn(async move {
|
||||||
|
// loop {
|
||||||
|
// let v = debugrx.recv().await;
|
||||||
|
// warn!("streamed: {:?}", v.unwrap());
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
|
||||||
join_all(tasks).await;
|
join_all(tasks).await;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user