Skip to content

Commit 70710a7

Browse files
feat(discovery): Rust-to-Go logging bridge for libdd_discovery
1 parent 56888f8 commit 70710a7

File tree

5 files changed

+165
-0
lines changed

5 files changed

+165
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025-present Datadog, Inc.
5+
6+
//go:build dd_discovery_rust && cgo
7+
8+
package module
9+
10+
/*
11+
#cgo CFLAGS: -I${SRCDIR}/rust/include
12+
#cgo LDFLAGS: -L${SRCDIR}/rust -L${SRCDIR}/rust/target/release -ldd_discovery
13+
14+
#include "dd_discovery.h"
15+
16+
extern void goDiscoveryLogCallback(uint32_t level, const char* msg, size_t msg_len);
17+
18+
// cgo does not allow passing the address of an exported Go function (goDiscoveryLogCallback)
19+
// directly as a function-pointer argument in the cgo preamble. This thin C wrapper returns
20+
// the pointer so we can pass it to dd_discovery_init_logger without violating that rule.
21+
static dd_log_fn getGoLogCallback(void) {
22+
return goDiscoveryLogCallback;
23+
}
24+
*/
25+
import "C"
26+
27+
func init() {
28+
// All Rust log records are forwarded to the Go logger, which applies its own
29+
// level filter. See dd_discovery_init_logger in ffi.rs for details.
30+
C.dd_discovery_init_logger(C.getGoLogCallback())
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025-present Datadog, Inc.
5+
6+
//go:build dd_discovery_rust && cgo
7+
8+
package module
9+
10+
/*
11+
#cgo CFLAGS: -I${SRCDIR}/rust/include
12+
#include "dd_discovery.h"
13+
*/
14+
import "C"
15+
16+
import (
17+
"math"
18+
19+
"github.com/DataDog/datadog-agent/pkg/util/log"
20+
)
21+
22+
//export goDiscoveryLogCallback
23+
func goDiscoveryLogCallback(level C.uint32_t, msg *C.char, msgLen C.size_t) {
24+
// Reject nil pointers, empty messages (nothing useful to log), and messages
25+
// larger than MaxInt32 to prevent overflow when casting msgLen to C.int below.
26+
if msg == nil || msgLen == 0 || msgLen > math.MaxInt32 {
27+
return
28+
}
29+
message := C.GoStringN(msg, C.int(msgLen))
30+
// Level mapping mirrors dd_log_fn in dd_discovery.h: 1=Error, 2=Warn, 3=Info, 4=Debug, 5+=Trace.
31+
switch level {
32+
case 1:
33+
_ = log.Errorf("[dd_discovery] %s", message)
34+
case 2:
35+
_ = log.Warnf("[dd_discovery] %s", message)
36+
case 3:
37+
log.Infof("[dd_discovery] %s", message)
38+
case 4:
39+
log.Debugf("[dd_discovery] %s", message)
40+
default:
41+
log.Tracef("[dd_discovery] %s", message)
42+
}
43+
}

pkg/discovery/module/rust/cbindgen.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ include = [
2929
"dd_tracer_metadata_slice",
3030
"dd_service",
3131
"dd_discovery_result",
32+
"dd_log_fn",
3233
]
3334

3435
[fn]

pkg/discovery/module/rust/include/dd_discovery.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
#include <stddef.h>
1313
#include <stdint.h>
1414

15+
/**
16+
* Callback type used to forward Rust log records to the Go logging subsystem.
17+
*
18+
* Parameters:
19+
* - `level`: severity — 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Trace.
20+
* - `msg` / `msg_len`: UTF-8 log message (NOT NUL-terminated).
21+
*
22+
* The pointer is only valid for the duration of the call; do not retain it.
23+
*/
24+
typedef void (*dd_log_fn)(uint32_t level, const char *msg, size_t msg_len);
25+
1526
/**
1627
* Length-delimited byte string — avoids NUL-termination so the Go caller
1728
* can use `C.GoStringN(data, len)` directly without `strlen`.
@@ -89,6 +100,19 @@ struct dd_discovery_result {
89100
size_t gpu_pids_len;
90101
};
91102

103+
/**
104+
* Register a Go logging callback. Idempotent: only the first call has effect;
105+
* subsequent calls (e.g. from racing goroutines at start-up) are ignored.
106+
*
107+
* All log levels are forwarded to the callback; the Go side applies its own
108+
* level filter so no records are emitted above the configured level.
109+
*
110+
* # Safety
111+
* `callback` must be a valid function pointer that remains valid for the
112+
* lifetime of the process.
113+
*/
114+
void dd_discovery_init_logger(dd_log_fn callback);
115+
92116
/**
93117
* Run service discovery and return a heap-allocated result.
94118
*

pkg/discovery/module/rust/src/ffi.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,72 @@ fn services_response_to_result(resp: ServicesResponse) -> dd_discovery_result {
290290
}
291291
}
292292

293+
// ---------------------------------------------------------------------------
294+
// Logger bridge
295+
// ---------------------------------------------------------------------------
296+
297+
/// Callback type used to forward Rust log records to the Go logging subsystem.
298+
///
299+
/// Parameters:
300+
/// - `level`: severity — 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Trace.
301+
/// - `msg` / `msg_len`: UTF-8 log message (NOT NUL-terminated).
302+
///
303+
/// The pointer is only valid for the duration of the call; do not retain it.
304+
pub type dd_log_fn = unsafe extern "C" fn(level: u32, msg: *const c_char, msg_len: usize);
305+
306+
/// Stores the Go log callback set via `dd_discovery_init_logger`. Written once.
307+
static LOGGER_CALLBACK: std::sync::OnceLock<dd_log_fn> = std::sync::OnceLock::new();
308+
309+
/// Logger backend that forwards records to the registered Go callback.
310+
struct GoLogger;
311+
312+
impl log::Log for GoLogger {
313+
fn enabled(&self, _metadata: &log::Metadata) -> bool {
314+
true // All records pass through; Go-side filtering applies.
315+
}
316+
317+
fn log(&self, record: &log::Record) {
318+
let Some(&callback) = LOGGER_CALLBACK.get() else {
319+
return;
320+
};
321+
let level: u32 = match record.level() {
322+
log::Level::Error => 1,
323+
log::Level::Warn => 2,
324+
log::Level::Info => 3,
325+
log::Level::Debug => 4,
326+
log::Level::Trace => 5,
327+
};
328+
let message = record.args().to_string();
329+
// SAFETY: `callback` was registered by the Go runtime and is valid for
330+
// the process lifetime. `message` is live for the duration of this call.
331+
unsafe { callback(level, message.as_ptr() as *const c_char, message.len()) };
332+
}
333+
334+
fn flush(&self) {}
335+
}
336+
337+
static GO_LOGGER: GoLogger = GoLogger;
338+
339+
/// Register a Go logging callback. Idempotent: only the first call has effect;
340+
/// subsequent calls (e.g. from racing goroutines at start-up) are ignored.
341+
///
342+
/// All log levels are forwarded to the callback; the Go side applies its own
343+
/// level filter so no records are emitted above the configured level.
344+
///
345+
/// # Safety
346+
/// `callback` must be a valid function pointer that remains valid for the
347+
/// lifetime of the process.
348+
#[unsafe(no_mangle)]
349+
pub unsafe extern "C" fn dd_discovery_init_logger(callback: dd_log_fn) {
350+
if LOGGER_CALLBACK.set(callback).is_err() {
351+
return;
352+
}
353+
// `set_logger` returns Err if another logger was already registered.
354+
if log::set_logger(&GO_LOGGER).is_ok() {
355+
log::set_max_level(log::LevelFilter::Trace);
356+
}
357+
}
358+
293359
// ---------------------------------------------------------------------------
294360
// Exported C ABI functions
295361
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)