Skip to content

Commit 53673b0

Browse files
committed
feat(datadog): create exporter module for datadog
Signed-off-by: Jérémie Drouet <[email protected]>
1 parent be5c3a3 commit 53673b0

File tree

3 files changed

+271
-0
lines changed

3 files changed

+271
-0
lines changed

Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,26 @@ readme = "README.md"
1111

1212
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313

14+
[features]
15+
default = ["datadog"]
16+
datadog = ["datadog-client"]
17+
1418
[dependencies]
1519
loggerv = "0.7.2"
1620
log = "0.4"
1721
clap = "2.33.3"
1822
regex = "1"
1923
procfs = "0.8.1"
2024
actix-web = "3"
25+
futures = "0.3"
2126
riemann_client = "0.9.0"
2227
hostname = "0.3.1"
2328
protobuf = "2.20.0"
2429
serde = { version = "1.0", features = ["derive"] }
2530
serde_json = "1.0"
2631

32+
datadog-client = { version = "0.1", optional = true }
33+
2734
[profile.release]
2835
lto = true
2936
debug = true

src/exporters/datadog.rs

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
use crate::exporters::*;
2+
use crate::sensors::{Record, Sensor, Topology};
3+
use datadog_client::client::{Client, Config};
4+
use datadog_client::metrics::{Point, Serie, Type};
5+
use std::collections::HashMap;
6+
use std::thread;
7+
use std::time::{Duration, Instant};
8+
9+
fn merge<A>(first: Vec<A>, second: Vec<A>) -> Vec<A> {
10+
second.into_iter().fold(first, |mut res, item| {
11+
res.push(item);
12+
res
13+
})
14+
}
15+
16+
fn get_domain_name(index: usize) -> Option<&'static str> {
17+
match index {
18+
0 => Some("core"),
19+
1 => Some("uncore"),
20+
2 => Some("dram"),
21+
_ => None,
22+
}
23+
}
24+
25+
/// An Exporter that displays power consumption data of the host
26+
/// and its processes on the standard output of the terminal.
27+
pub struct DatadogExporter {
28+
topology: Topology,
29+
hostname: String,
30+
}
31+
32+
impl Exporter for DatadogExporter {
33+
/// Lanches runner()
34+
fn run(&mut self, parameters: ArgMatches) {
35+
self.runner(&parameters);
36+
}
37+
38+
/// Returns options needed for that exporter, as a HashMap
39+
fn get_options() -> HashMap<String, ExporterOption> {
40+
let mut options = HashMap::new();
41+
options.insert(
42+
String::from("host"),
43+
ExporterOption {
44+
default_value: Some(String::from("https://api.datadoghq.eu")),
45+
long: String::from("host"),
46+
short: String::from("h"),
47+
required: true,
48+
takes_value: true,
49+
help: String::from("The domain of the datadog instance."),
50+
},
51+
);
52+
options.insert(
53+
String::from("api_key"),
54+
ExporterOption {
55+
default_value: None,
56+
long: String::from("api_key"),
57+
short: String::from("k"),
58+
required: true,
59+
takes_value: true,
60+
help: String::from("Api key to authenticate with datadog."),
61+
},
62+
);
63+
options
64+
}
65+
}
66+
67+
impl DatadogExporter {
68+
/// Instantiates and returns a new DatadogExporter
69+
pub fn new(mut sensor: Box<dyn Sensor>) -> DatadogExporter {
70+
let some_topology = *sensor.get_topology();
71+
72+
DatadogExporter {
73+
topology: some_topology.unwrap(),
74+
hostname: hostname::get()
75+
.expect("unable to get hostname")
76+
.to_str()
77+
.unwrap()
78+
.to_string(),
79+
}
80+
}
81+
82+
fn build_client(parameters: &ArgMatches) -> Client {
83+
let config = Config::new(
84+
parameters.value_of("host").unwrap().to_string(),
85+
parameters.value_of("api_key").unwrap().to_string(),
86+
);
87+
Client::new(config)
88+
}
89+
90+
fn runner(&mut self, parameters: &ArgMatches) {
91+
if let Some(timeout) = parameters.value_of("timeout") {
92+
let now = Instant::now();
93+
let timeout = timeout
94+
.parse::<u64>()
95+
.expect("Wrong timeout value, should be a number of seconds");
96+
97+
// We have a default value of 2s so it is safe to unwrap the option
98+
// Panic if a non numerical value is passed
99+
let step_duration: u64 = parameters
100+
.value_of("step_duration")
101+
.unwrap()
102+
.parse::<u64>()
103+
.expect("Wrong step_duration value, should be a number of seconds");
104+
let step_duration_nano: u32 = parameters
105+
.value_of("step_duration_nano")
106+
.unwrap()
107+
.parse::<u32>()
108+
.expect("Wrong step_duration_nano value, should be a number of nano seconds");
109+
110+
info!("Measurement step is: {}s", step_duration);
111+
112+
while now.elapsed().as_secs() <= timeout {
113+
self.iterate(parameters);
114+
thread::sleep(Duration::new(step_duration, step_duration_nano));
115+
}
116+
} else {
117+
self.iterate(parameters);
118+
}
119+
}
120+
121+
fn iterate(&mut self, parameters: &ArgMatches) {
122+
self.topology.refresh();
123+
let _series = self.collect_series();
124+
let _client = Self::build_client(parameters);
125+
}
126+
127+
fn create_consumption_serie(&self) -> Serie {
128+
Serie::new("consumption", Type::Gauge)
129+
.set_host(self.hostname.as_str())
130+
.add_tag(format!("hostname:{}", self.hostname))
131+
}
132+
133+
fn collect_process_series(&mut self) -> Vec<Serie> {
134+
let record = match self.topology.get_records_diff_power_microwatts() {
135+
Some(item) => item,
136+
None => return vec![],
137+
};
138+
let host_stat = match self.topology.get_stats_diff() {
139+
Some(item) => item,
140+
None => return vec![],
141+
};
142+
let host_power_ts = record.timestamp.as_secs();
143+
let host_power = record.value.parse::<u64>().unwrap_or(0) as f32;
144+
145+
let ticks_per_second = procfs::ticks_per_second().unwrap() as f32;
146+
147+
let consumers = self.topology.proc_tracker.get_top_consumers(10);
148+
consumers
149+
.iter()
150+
.map(|item| {
151+
let host_time = host_stat.total_time_jiffies();
152+
let consumption = (item.1 as f32 / (host_time * ticks_per_second)) * host_power;
153+
let exe = item
154+
.0
155+
.exe()
156+
.ok()
157+
.and_then(|v| v.to_str().map(|s| s.to_string()))
158+
.unwrap_or_default();
159+
let point = Point::new(host_power_ts, consumption as f64);
160+
self.create_consumption_serie()
161+
.add_point(point)
162+
.add_tag(format!("process.exe:{}", exe))
163+
.add_tag(format!("process.pid:{}", item.0.pid()))
164+
})
165+
.collect::<Vec<_>>()
166+
}
167+
168+
fn get_domains_power(&self, socket_id: u16) -> Vec<Option<Record>> {
169+
let socket_present = self
170+
.topology
171+
.get_sockets_passive()
172+
.iter()
173+
.find(move |x| x.id == socket_id);
174+
175+
if let Some(socket) = socket_present {
176+
let mut domains_power: Vec<Option<Record>> = vec![];
177+
for d in socket.get_domains_passive() {
178+
domains_power.push(d.get_records_diff_power_microwatts());
179+
}
180+
domains_power
181+
} else {
182+
vec![None, None, None]
183+
}
184+
}
185+
186+
fn collect_socket_series(&mut self) -> Vec<Serie> {
187+
self.topology
188+
.get_sockets_passive()
189+
.iter()
190+
.fold(Vec::new(), |mut res, socket| {
191+
let socket_record = match socket.get_records_diff_power_microwatts() {
192+
Some(item) => item,
193+
None => return res,
194+
};
195+
let socket_power = socket_record.value.parse::<u64>().unwrap_or(0);
196+
res.push(
197+
self.create_consumption_serie()
198+
.add_point(Point::new(
199+
socket_record.timestamp.as_secs(),
200+
socket_power as f64,
201+
))
202+
.add_tag(format!("socket.id:{}", socket.id)),
203+
);
204+
let domains_power = self.get_domains_power(socket.id);
205+
domains_power
206+
.iter()
207+
.enumerate()
208+
.filter_map(|(index, record)| {
209+
let name = match get_domain_name(index) {
210+
Some(name) => name,
211+
None => return None,
212+
};
213+
let record = match record {
214+
Some(item) => item,
215+
None => return None,
216+
};
217+
Some((
218+
name,
219+
Point::new(
220+
record.timestamp.as_secs(),
221+
record.value.parse::<u64>().unwrap_or(0) as f64,
222+
),
223+
))
224+
})
225+
.for_each(|(name, point)| {
226+
res.push(
227+
self.create_consumption_serie()
228+
.add_point(point)
229+
.add_tag(format!("socket.id:{}", socket.id))
230+
.add_tag(format!("socket.domain:{}", name)),
231+
);
232+
});
233+
res
234+
})
235+
}
236+
237+
fn collect_series(&mut self) -> Vec<Serie> {
238+
let processes = self.collect_process_series();
239+
let sockets = self.collect_socket_series();
240+
merge(processes, sockets)
241+
}
242+
}
243+
244+
#[cfg(test)]
245+
mod tests {
246+
//#[test]
247+
//fn get_cons_socket0() {}
248+
}
249+
250+
// Copyright 2020 The scaphandre authors.
251+
//
252+
// Licensed under the Apache License, Version 2.0 (the "License");
253+
// you may not use this file except in compliance with the License.
254+
// You may obtain a copy of the License at
255+
//
256+
// http://www.apache.org/licenses/LICENSE-2.0
257+
//
258+
// Unless required by applicable law or agreed to in writing, software
259+
// distributed under the License is distributed on an "AS IS" BASIS,
260+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
261+
// See the License for the specific language governing permissions and
262+
// limitations under the License.

src/exporters/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "datadog")]
2+
pub mod datadog;
13
pub mod json;
24
pub mod prometheus;
35
pub mod qemu;

0 commit comments

Comments
 (0)