Skip to content

Commit 39bdedc

Browse files
committed
Add telemetry-rs for exporting otel data into mdm
1 parent abb627d commit 39bdedc

6 files changed

Lines changed: 269 additions & 0 deletions

File tree

scripts/rs/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "telemetry_rs"
3+
version = "1.0.0"
4+
edition = "2021"
5+
description = "Daemon to export its cpu usage via OTEL"
6+
license = "MIT"
7+
8+
[dependencies]
9+
opentelemetry = { version = "0.21", features = ["metrics"] }
10+
opentelemetry_sdk = { version = "0.21", features = ["metrics", "rt-tokio"] }
11+
opentelemetry-otlp = { version = "0.14", features = ["tonic", "metrics"] }
12+
sysinfo = "0.30"
13+
tokio = { version = "1", features = ["full"] }
14+
15+
# config + errors
16+
anyhow = "1"
17+
serde = { version = "1", features = ["derive"] }
18+
toml = "0.8"
19+
20+
[lib]
21+
name = "otel_lib_rs"
22+
path = "src/lib.rs"
23+
24+
[[bin]]
25+
name = "telemetry_rs"
26+
path = "src/main.rs"

scripts/rs/otel.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[otel]
2+
endpoint = "http://localhost:4317"
3+
service_name = "rust-otel-service"
4+
export_interval_secs = 10
5+
set_global = true
6+
7+
[otel.resource]
8+
env = "dev"
9+
region = "mel"

scripts/rs/rs.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
OTEL_CONFIG_FILE=./otel.toml cargo run --bin telemetry_rs

scripts/rs/src/config.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use serde::Deserialize;
2+
use std::{collections::HashMap, fs, time::Duration};
3+
4+
#[derive(Debug, Deserialize)]
5+
pub struct OtelFileConfig {
6+
pub otel: OtelSection,
7+
}
8+
9+
#[derive(Debug, Deserialize)]
10+
pub struct OtelSection {
11+
pub endpoint: String,
12+
pub service_name: String,
13+
pub export_interval_secs: u64,
14+
15+
#[serde(default = "default_true")]
16+
pub set_global: bool,
17+
18+
#[serde(default)]
19+
pub resource: HashMap<String, String>,
20+
}
21+
22+
fn default_true() -> bool {
23+
true
24+
}
25+
26+
#[derive(Clone, Debug)]
27+
pub struct OtelConfig {
28+
pub endpoint: String,
29+
pub service_name: String,
30+
pub export_interval: Duration,
31+
pub set_global: bool,
32+
pub resource_kvs: Vec<(String, String)>,
33+
}
34+
35+
impl OtelConfig {
36+
pub fn from_toml_file(path: &str) -> anyhow::Result<Self> {
37+
let content = fs::read_to_string(path)?;
38+
let file_cfg: OtelFileConfig = toml::from_str(&content)?;
39+
40+
Ok(Self {
41+
endpoint: file_cfg.otel.endpoint,
42+
service_name: file_cfg.otel.service_name,
43+
export_interval: Duration::from_secs(file_cfg.otel.export_interval_secs),
44+
set_global: file_cfg.otel.set_global,
45+
resource_kvs: file_cfg.otel.resource.into_iter().collect(),
46+
})
47+
}
48+
49+
pub fn validate(&self) -> anyhow::Result<()> {
50+
if self.service_name.trim().is_empty() {
51+
anyhow::bail!("otel.service_name cannot be empty");
52+
}
53+
if !self.endpoint.starts_with("http://") && !self.endpoint.starts_with("https://") {
54+
anyhow::bail!(
55+
"otel.endpoint must start with http:// or https:// (got: {})",
56+
self.endpoint
57+
);
58+
}
59+
Ok(())
60+
}
61+
}

scripts/rs/src/lib.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
mod config;
2+
pub use config::OtelConfig;
3+
4+
use opentelemetry::{global, KeyValue};
5+
use opentelemetry::metrics::{Meter, MeterProvider as _};
6+
use opentelemetry_otlp::WithExportConfig;
7+
8+
use opentelemetry_sdk::{
9+
metrics::{
10+
reader::{AggregationSelector, TemporalitySelector},
11+
Aggregation, InstrumentKind, MeterProviderBuilder, PeriodicReader,
12+
},
13+
runtime::Tokio,
14+
Resource,
15+
};
16+
17+
pub struct OtelRuntime {
18+
meter: Meter,
19+
// Keep provider alive (dropping it can stop exports)
20+
_provider: opentelemetry_sdk::metrics::SdkMeterProvider,
21+
}
22+
23+
struct LastValueCumulative;
24+
25+
impl AggregationSelector for LastValueCumulative {
26+
fn aggregation(&self, _kind: InstrumentKind) -> Aggregation {
27+
Aggregation::LastValue
28+
}
29+
}
30+
31+
impl TemporalitySelector for LastValueCumulative {
32+
fn temporality(&self, _kind: InstrumentKind) -> opentelemetry_sdk::metrics::data::Temporality {
33+
opentelemetry_sdk::metrics::data::Temporality::Cumulative
34+
}
35+
}
36+
37+
impl OtelRuntime {
38+
pub fn init(cfg: OtelConfig) -> anyhow::Result<Self> {
39+
cfg.validate()?;
40+
41+
let exporter = opentelemetry_otlp::new_exporter()
42+
.tonic()
43+
.with_endpoint(cfg.endpoint)
44+
.build_metrics_exporter(Box::new(LastValueCumulative), Box::new(LastValueCumulative))
45+
.map_err(|e| anyhow::anyhow!("Failed to build OTLP metrics exporter: {e}"))?;
46+
47+
let reader = PeriodicReader::builder(exporter, Tokio)
48+
.with_interval(cfg.export_interval)
49+
.build();
50+
51+
let mut attrs = vec![KeyValue::new("service.name", cfg.service_name)];
52+
for (k, v) in cfg.resource_kvs {
53+
attrs.push(KeyValue::new(k, v));
54+
}
55+
56+
let provider = MeterProviderBuilder::default()
57+
.with_resource(Resource::new(attrs))
58+
.with_reader(reader)
59+
.build();
60+
61+
if cfg.set_global {
62+
global::set_meter_provider(provider.clone());
63+
}
64+
65+
let meter = provider.meter("telemetry_rs");
66+
67+
Ok(Self {
68+
meter,
69+
_provider: provider,
70+
})
71+
}
72+
73+
pub fn meter(&self) -> &Meter {
74+
&self.meter
75+
}
76+
}

scripts/rs/src/main.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use opentelemetry::KeyValue;
2+
use sysinfo::{CpuRefreshKind, ProcessRefreshKind, RefreshKind, System};
3+
use tokio::time::{sleep, Duration};
4+
5+
fn find_main_dockerd_pid(system: &System) -> Option<u32> {
6+
// Prefer dockerd whose parent PID is 1 (your original heuristic)
7+
for (pid, process) in system.processes() {
8+
if process.name() == "dockerd"
9+
&& process.parent().map(|pp| pp.as_u32()).unwrap_or(0) == 1
10+
{
11+
return Some(pid.as_u32());
12+
}
13+
}
14+
// Fallback: first dockerd found
15+
for (pid, process) in system.processes() {
16+
if process.name() == "dockerd" {
17+
return Some(pid.as_u32());
18+
}
19+
}
20+
None
21+
}
22+
23+
#[tokio::main]
24+
async fn main() -> anyhow::Result<()> {
25+
// Config path: env override + default
26+
let cfg_path =
27+
std::env::var("OTEL_CONFIG_FILE").unwrap_or_else(|_| "otel.toml".to_string());
28+
29+
let cfg = otel_lib_rs::OtelConfig::from_toml_file(&cfg_path)?;
30+
let otel = otel_lib_rs::OtelRuntime::init(cfg)?;
31+
32+
let meter = otel.meter();
33+
34+
let gauge = meter
35+
.f64_observable_gauge("dockerd.cpu.usage.percent")
36+
.with_description("Total CPU usage of dockerd process and its children (%)")
37+
.init();
38+
39+
meter
40+
.register_callback(&[gauge.as_any()], move |observer| {
41+
let mut system = System::new_with_specifics(
42+
RefreshKind::new()
43+
.with_processes(ProcessRefreshKind::everything())
44+
.with_cpu(CpuRefreshKind::everything()),
45+
);
46+
47+
// sysinfo CPU% is based on two samples
48+
system.refresh_processes();
49+
system.refresh_cpu();
50+
51+
std::thread::sleep(std::time::Duration::from_millis(500));
52+
53+
system.refresh_processes();
54+
system.refresh_cpu();
55+
56+
let mut total_cpu = 0.0;
57+
58+
if let Some(main_pid) = find_main_dockerd_pid(&system) {
59+
for (pid, process) in system.processes() {
60+
if process.name() == "dockerd" {
61+
let p = pid.as_u32();
62+
let parent = process.parent().map(|pp| pp.as_u32()).unwrap_or(0);
63+
if p == main_pid || parent == main_pid {
64+
total_cpu += process.cpu_usage() as f64;
65+
}
66+
}
67+
}
68+
69+
observer.observe_f64(
70+
&gauge,
71+
total_cpu,
72+
&[
73+
KeyValue::new("process", "dockerd"),
74+
KeyValue::new("scope", "main+children"),
75+
],
76+
);
77+
} else {
78+
observer.observe_f64(
79+
&gauge,
80+
0.0,
81+
&[
82+
KeyValue::new("process", "dockerd"),
83+
KeyValue::new("status", "not_found"),
84+
],
85+
);
86+
}
87+
})
88+
.expect("Failed to register callback");
89+
90+
println!("Loaded OTEL config from: {}", cfg_path);
91+
println!("Exporting dockerd CPU usage...");
92+
93+
loop {
94+
sleep(Duration::from_secs(600)).await;
95+
}
96+
}

0 commit comments

Comments
 (0)