Skip to content

Commit b3d9cd0

Browse files
feat(dogstatsd): add shared_runtime buffered sink
1 parent 395fb45 commit b3d9cd0

7 files changed

Lines changed: 246 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libdd-data-pipeline/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ libdd-trace-stats = { version = "6.0.0", path = "../libdd-trace-stats", default-
4141
libdd-trace-utils = { version = "9.0.0", path = "../libdd-trace-utils", default-features = false }
4242
libdd-trace-obfuscation = { version = "5.0.0", path = "../libdd-trace-obfuscation", default-features = false, optional = true }
4343
libdd-ddsketch = { version = "1.1.0", path = "../libdd-ddsketch" }
44-
libdd-dogstatsd-client = { version = "4.0.0", path = "../libdd-dogstatsd-client", default-features = false }
44+
libdd-dogstatsd-client = { version = "4.0.0", path = "../libdd-dogstatsd-client", default-features = false, features = ["shared-runtime"] }
4545
libdd-tinybytes = { version = "1.1.1", path = "../libdd-tinybytes", features = [
4646
"bytes_string",
4747
"serialization",

libdd-data-pipeline/src/trace_exporter/builder.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,13 @@ impl<R: SharedRuntime> TraceExporterBuilder<R> {
575575
})?),
576576
};
577577

578-
let dogstatsd = self
578+
let (dogstatsd, dogstatsd_handle) = self
579579
.dogstatsd_url
580-
.and_then(|u| DogStatsDClient::new(Endpoint::from_slice(&u)).ok());
580+
.and_then(|u| {
581+
DogStatsDClient::new_with_shared_runtime(Endpoint::from_slice(&u), &*shared_runtime)
582+
.ok()
583+
})
584+
.unzip();
581585

582586
let base_url = self.url.as_deref().unwrap_or(DEFAULT_AGENT_URL);
583587

@@ -836,6 +840,7 @@ impl<R: SharedRuntime> TraceExporterBuilder<R> {
836840
capabilities,
837841
#[cfg(not(target_arch = "wasm32"))]
838842
workers: TraceExporterWorkers {
843+
dogstatsd: dogstatsd_handle,
839844
info_fetcher: info_fetcher_handle,
840845
#[cfg(feature = "telemetry")]
841846
telemetry: telemetry_handle,

libdd-data-pipeline/src/trace_exporter/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ pub use libdd_trace_utils::tracer_metadata::TracerMetadata;
189189
#[cfg(not(target_arch = "wasm32"))]
190190
#[derive(Debug)]
191191
pub(crate) struct TraceExporterWorkers {
192+
dogstatsd: Option<WorkerHandle>,
192193
/// `None` when no background `/info` fetcher is started (agentless trace
193194
/// export mode, log-export mode).
194195
info_fetcher: Option<WorkerHandle>,
@@ -345,6 +346,10 @@ impl<
345346
join_set.spawn(async move { handle.stop().await });
346347
}
347348

349+
if let Some(dogstatsd) = self.workers.dogstatsd {
350+
join_set.spawn(async move { dogstatsd.stop().await });
351+
}
352+
348353
if let Some(info_fetcher) = self.workers.info_fetcher {
349354
join_set.spawn(async move { info_fetcher.stop().await });
350355
}

libdd-dogstatsd-client/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ serde = { version = "1.0", features = ["derive", "rc"] }
1818
tracing = { version = "0.1", default-features = false }
1919
anyhow = { version = "1.0" }
2020
http = "1.1"
21+
libdd-shared-runtime = { version = "2.0.0", path = "../libdd-shared-runtime", default-features = false, optional = true }
22+
tokio = { version = "1.23", features = ["sync"], optional = true }
23+
async-trait = { version = "0.1", optional = true }
2124

2225
[features]
2326
default = ["https"]
2427
https = ["libdd-common/https"]
2528
fips = ["libdd-common/fips"]
26-
29+
shared-runtime = ["dep:libdd-shared-runtime", "dep:tokio", "dep:async-trait"]
2730

2831
[dev-dependencies]
2932
tokio = {version = "1.23", features = ["rt", "time", "test-util", "rt-multi-thread"], default-features = false}

libdd-dogstatsd-client/src/client/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use cadence::prelude::*;
66
use cadence::{Metric, MetricBuilder, QueuingMetricSink, StatsdClient};
77
use libdd_common::tag::Tag;
88
use libdd_common::Endpoint;
9+
#[cfg(feature = "shared-runtime")]
10+
use libdd_shared_runtime::{SharedRuntime, WorkerHandle};
911
use sink::create_udp_sink;
1012
#[cfg(unix)]
1113
use sink::create_unix_sink;
@@ -15,6 +17,10 @@ use tracing::error;
1517
/// Provides transport sink which can be wrapped in buffered sink
1618
mod sink;
1719

20+
/// Provides a buffered sink running on a [libdd_shared_runtime::SharedRuntime]
21+
#[cfg(feature = "shared-runtime")]
22+
mod shared_runtime_sink;
23+
1824
// Queue with a maximum capacity of 32K elements
1925
const QUEUE_SIZE: usize = 32 * 1024;
2026

@@ -47,6 +53,31 @@ impl DogStatsDClient {
4753
})
4854
}
4955

56+
/// Create a [`Client`] backed by a [`MetricSinkWorker`] running on the
57+
/// provided [`SharedRuntime`].
58+
///
59+
/// Returns the client and a [`WorkerHandle`] that can be used to stop the
60+
/// worker independently of the runtime.
61+
///
62+
/// # Errors
63+
/// Returns an error if the endpoint is invalid or the worker cannot be spawned.
64+
#[cfg(feature = "shared-runtime")]
65+
pub fn new_with_shared_runtime(
66+
endpoint: Endpoint,
67+
runtime: &impl SharedRuntime,
68+
) -> anyhow::Result<(Self, WorkerHandle)> {
69+
let (sink, handle) = shared_runtime_sink::create_shared_runtime_sink(&endpoint, runtime)?;
70+
71+
let client = Self {
72+
inner: Arc::new(InnerClient {
73+
client: OnceLock::from(StatsdClient::from_sink("", sink)),
74+
endpoint,
75+
}),
76+
};
77+
78+
Ok((client, handle))
79+
}
80+
5081
/// Send a vector of DogStatsDActionOwned, this is the same as `send` except it uses the
5182
/// "owned" version of DogStatsDAction. See the docs for DogStatsDActionOwned for details.
5283
pub fn send_owned(&self, actions: Vec<DogStatsDActionOwned>) {
@@ -239,4 +270,51 @@ mod test {
239270
task.await.unwrap();
240271
}
241272
}
273+
274+
#[test]
275+
#[cfg_attr(miri, ignore)]
276+
#[cfg(feature = "shared-runtime")]
277+
fn test_shared_runtime_flusher() {
278+
use libdd_shared_runtime::{BasicRuntime, BlockingRuntime, SharedRuntime};
279+
280+
let socket = net::UdpSocket::bind("127.0.0.1:0").expect("failed to bind host socket");
281+
let _ = socket.set_read_timeout(Some(Duration::from_millis(500)));
282+
283+
let runtime = BasicRuntime::new().unwrap();
284+
285+
let (flusher, _handle) = DogStatsDClient::new_with_shared_runtime(
286+
Endpoint::from_slice(socket.local_addr().unwrap().to_string().as_str()),
287+
&runtime,
288+
)
289+
.unwrap();
290+
291+
flusher.send(vec![
292+
Count("test_count", 3, &vec![tag!("foo", "bar")]),
293+
Count("test_neg_count", -2, &vec![]),
294+
Distribution("test_distribution", 4.2, &vec![]),
295+
Gauge("test_gauge", 7.6, &vec![]),
296+
Histogram("test_histogram", 8.0, &vec![]),
297+
Set("test_set", 9, &vec![tag!("the", "end")]),
298+
Set("test_neg_set", -1, &vec![]),
299+
]);
300+
301+
runtime
302+
.block_on(runtime.shutdown_async())
303+
.expect("Failed to shutdown runtime");
304+
305+
fn read(socket: &net::UdpSocket) -> String {
306+
let mut buf = [0; 100];
307+
socket.recv(&mut buf).expect("No data");
308+
let datagram = String::from_utf8_lossy(buf.strip_suffix(&[0]).unwrap());
309+
datagram.trim_matches(char::from(0)).to_string()
310+
}
311+
312+
assert_eq!("test_count:3|c|#foo:bar", read(&socket));
313+
assert_eq!("test_neg_count:-2|c", read(&socket));
314+
assert_eq!("test_distribution:4.2|d", read(&socket));
315+
assert_eq!("test_gauge:7.6|g", read(&socket));
316+
assert_eq!("test_histogram:8|h", read(&socket));
317+
assert_eq!("test_set:9|s|#the:end", read(&socket));
318+
assert_eq!("test_neg_set:-1|s", read(&socket));
319+
}
242320
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use anyhow::anyhow;
5+
use async_trait::async_trait;
6+
use cadence::{MetricSink, SinkStats};
7+
use libdd_common::Endpoint;
8+
use libdd_shared_runtime::{worker::Worker, SharedRuntime, WorkerHandle};
9+
use std::fmt;
10+
use std::io;
11+
use std::panic::RefUnwindSafe;
12+
use std::sync::Arc;
13+
use tokio::sync::mpsc;
14+
use tracing::error;
15+
16+
use super::{sink, QUEUE_SIZE};
17+
18+
/// A [`MetricSink`] that offloads sent metrics to a [`SharedRuntime`].
19+
#[derive(Clone)]
20+
pub struct SharedRuntimeMetricSink {
21+
sender: mpsc::Sender<String>,
22+
sink: Arc<dyn MetricSink + Send + Sync + RefUnwindSafe>,
23+
}
24+
25+
impl fmt::Debug for SharedRuntimeMetricSink {
26+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+
write!(f, "SharedRuntimeMetricSink {{ {:?} }}", self.sender)
28+
}
29+
}
30+
31+
impl MetricSink for SharedRuntimeMetricSink {
32+
fn emit(&self, metric: &str) -> io::Result<usize> {
33+
let len = metric.len();
34+
self.sender
35+
.try_send(metric.to_owned())
36+
.map(|_| len)
37+
.map_err(|e| match e {
38+
mpsc::error::TrySendError::Full(_) => {
39+
io::Error::new(io::ErrorKind::WouldBlock, "dogstatsd channel full")
40+
}
41+
mpsc::error::TrySendError::Closed(_) => {
42+
io::Error::new(io::ErrorKind::BrokenPipe, "dogstatsd channel closed")
43+
}
44+
})
45+
}
46+
47+
fn flush(&self) -> Result<(), std::io::Error> {
48+
self.sink.flush()
49+
}
50+
51+
fn stats(&self) -> SinkStats {
52+
self.sink.stats()
53+
}
54+
}
55+
56+
/// A [`Worker`] that drains metrics from a channel and forwards them to a
57+
/// wrapped [`MetricSink`] (e.g. `UdpMetricSink`).
58+
pub struct MetricSinkWorker {
59+
receiver: mpsc::Receiver<String>,
60+
sink: Arc<dyn MetricSink + Send + Sync + RefUnwindSafe>,
61+
pending: Option<String>,
62+
}
63+
64+
impl std::fmt::Debug for MetricSinkWorker {
65+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66+
f.debug_struct("MetricSinkWorker")
67+
.field("pending", &self.pending)
68+
.finish_non_exhaustive()
69+
}
70+
}
71+
72+
#[async_trait]
73+
impl Worker for MetricSinkWorker {
74+
/// Awaits the next metric from the channel, storing it in `pending`.
75+
///
76+
/// If the channel is closed this future never resolves, allowing the
77+
/// SharedRuntime to cancel it cleanly at the next yield point.
78+
async fn trigger(&mut self) {
79+
match self.receiver.recv().await {
80+
Some(metric) => self.pending = Some(metric),
81+
None => {
82+
// Channel closed — park until cancelled by the runtime.
83+
std::future::pending::<()>().await;
84+
}
85+
}
86+
}
87+
88+
/// Forwards the single metric stored by `trigger` to the wrapped sink.
89+
async fn run(&mut self) {
90+
if let Some(metric) = self.pending.take() {
91+
if let Err(e) = self.sink.emit(&metric) {
92+
error!(?e, "MetricSinkWorker: failed to emit metric");
93+
}
94+
}
95+
}
96+
97+
/// Drains any remaining queued metrics and flushes the wrapped sink.
98+
async fn shutdown(&mut self) {
99+
// Forward the metric that was sitting in `pending`, if any.
100+
if let Some(metric) = self.pending.take() {
101+
if let Err(e) = self.sink.emit(&metric) {
102+
error!(?e, "MetricSinkWorker: failed to emit metric on shutdown");
103+
}
104+
}
105+
// Drain the channel.
106+
while let Ok(metric) = self.receiver.try_recv() {
107+
if let Err(e) = self.sink.emit(&metric) {
108+
error!(?e, "MetricSinkWorker: failed to emit metric on shutdown");
109+
}
110+
}
111+
if let Err(e) = self.sink.flush() {
112+
error!(?e, "MetricSinkWorker: failed to flush sink on shutdown");
113+
}
114+
}
115+
116+
/// Reset the worker in the child process after a fork.
117+
fn reset(&mut self) {
118+
self.pending = None;
119+
// Drain the channel
120+
while self.receiver.try_recv().is_ok() {}
121+
}
122+
}
123+
124+
pub fn create_shared_runtime_sink(
125+
endpoint: &Endpoint,
126+
runtime: &impl SharedRuntime,
127+
) -> anyhow::Result<(SharedRuntimeMetricSink, WorkerHandle)> {
128+
let (tx, rx) = mpsc::channel(QUEUE_SIZE);
129+
130+
let sink: Arc<dyn MetricSink + Send + Sync + RefUnwindSafe> = match endpoint.url.scheme_str() {
131+
#[cfg(unix)]
132+
Some("unix") => Arc::new(sink::create_unix_sink(endpoint)?),
133+
_ => Arc::new(sink::create_udp_sink(endpoint)?),
134+
};
135+
136+
let sink_worker = MetricSinkWorker {
137+
receiver: rx,
138+
sink: sink.clone(),
139+
pending: None,
140+
};
141+
142+
let handle = runtime
143+
.spawn_worker(sink_worker, true)
144+
.map_err(|e| anyhow!("failed to spawn MetricSinkWorker: {e}"))?;
145+
146+
let shared_sink = SharedRuntimeMetricSink { sender: tx, sink };
147+
148+
Ok((shared_sink, handle))
149+
}

0 commit comments

Comments
 (0)