Skip to main content

bpf_tracing/
lib.rs

1//! Rich and event-based diagnostic information for eBPF.
2//!
3//! It exports a set of macros that can be used to emit
4//! diagnostic events from eBPF programs. The events are
5//! efficiently copied to user space via a ring buffer
6//! and integrated into the [`tracing`] infrastructure.
7//!
8//! # Example
9//!
10//! ```no_run
11//! # use std::mem::MaybeUninit;
12//! # mod tracing_subscriber {
13//! #     pub struct Fmt;
14//! #     pub struct EnvFilter;
15//! #
16//! #     pub fn fmt() -> Fmt {
17//! #         Fmt
18//! #     }
19//! #
20//! #     impl EnvFilter {
21//! #         pub fn from_default_env() -> Self {
22//! #             EnvFilter
23//! #         }
24//! #     }
25//! #
26//! #     impl Fmt {
27//! #         pub fn with_env_filter(self, _filter: EnvFilter) -> Self {
28//! #             self
29//! #         }
30//! #
31//! #         pub fn with_file(self, _with_file: bool) -> Self {
32//! #             self
33//! #         }
34//! #
35//! #         pub fn with_line_number(self, _with_line_number: bool) -> Self {
36//! #             self
37//! #         }
38//! #
39//! #         pub fn init(self) {}
40//! #     }
41//! # }
42//! # struct SkelBuilder;
43//! # struct OpenSkel;
44//! # struct Skel;
45//! #
46//! # impl Default for SkelBuilder {
47//! #     fn default() -> Self {
48//! #         Self
49//! #     }
50//! # }
51//! #
52//! # impl SkelBuilder {
53//! #     fn open(&self, _open_obj: &mut MaybeUninit<()>) -> libbpf_rs::Result<OpenSkel> {
54//! #         unimplemented!()
55//! #     }
56//! # }
57//! #
58//! # impl OpenSkel {
59//! #     fn load(self) -> libbpf_rs::Result<Skel> {
60//! #         unimplemented!()
61//! #     }
62//! # }
63//! #
64//! # impl Skel {
65//! #     fn object(&self) -> &libbpf_rs::Object {
66//! #         unimplemented!()
67//! #     }
68//! # }
69//! #
70//! # fn main() -> libbpf_rs::Result<()> {
71//!
72//! tracing_subscriber::fmt()
73//!     .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
74//!     .with_file(true)
75//!     .with_line_number(true)
76//!     .init();
77//!
78//! let mut open_obj = MaybeUninit::uninit();
79//! let skel_builder = SkelBuilder::default();
80//! let open_skel = skel_builder.open(&mut open_obj)?;
81//! let skel = open_skel.load()?;
82//!
83//! bpf_tracing::try_init(skel.object());
84//! # Ok(())
85//! # }
86//! ```
87//!
88//! And in your eBPF program:
89//!
90//! ```custom,{.language-c}
91//! bpf_info("Established socket [%pI4:%u->%pI4:%u]", &skey.local.ip4, skey.local.port, &skey.remote.ip4, skey.remote.port);
92//! ```
93//!
94//! [`tracing`]: https://github.com/tokio-rs/tracing
95use bpf_tracing_include::event::{CallsiteKey, Event, Kind};
96use libbpf_rs::{MapCore, MapHandle};
97use std::{
98    cell::RefCell,
99    collections::{HashMap, VecDeque},
100    path::{Component, Path, PathBuf},
101    thread::{self},
102};
103use tracing::{self, metadata::Metadata, span::EnteredSpan};
104
105const TARGET: &str = "bpf";
106
107type Spans = Vec<VecDeque<(String, EnteredSpan)>>;
108
109thread_local! {
110    static CALLSITES: RefCell<HashMap<CallsiteKey, &'static Metadata<'static>>> = RefCell::new(HashMap::new());
111    static SPANS: RefCell<Spans> = {
112        let cpus = thread::available_parallelism().unwrap().get();
113        let mut spans: Spans = Vec::new();
114        for _ in 0..cpus {
115            spans.push(VecDeque::new());
116        }
117        RefCell::new(spans)
118    };
119}
120
121/// Initializes a ring buffer reader that continuously observes and
122/// emits tracing events.
123///
124/// # Errors
125/// Returns an Error if the `trace_pipe` file cannot be opened
126/// or found.
127pub fn try_init(obj: &libbpf_rs::Object) -> libbpf_rs::Result<()> {
128    let mut builder = libbpf_rs::RingBufferBuilder::new();
129    let mut events: Option<MapHandle> = None;
130
131    for map in obj.maps() {
132        if map.name().eq("bpf_tracing_events") {
133            let map_id = map.info()?.info.id;
134            events = Some(MapHandle::from_map_id(map_id)?);
135        }
136    }
137
138    let Some(events) = events else {
139        return Err(libbpf_rs::Error::from(std::io::Error::new(
140            std::io::ErrorKind::NotFound,
141            "event ring buffer not found",
142        )));
143    };
144
145    builder.add(&events, |ev| process(ev)).unwrap();
146    let ringbuf = builder.build().unwrap();
147
148    thread::spawn(move || {
149        loop {
150            if let Err(_) = ringbuf.poll(std::time::Duration::from_millis(1)) {
151                continue;
152            }
153        }
154    });
155
156    Ok(())
157}
158
159fn process(event: &[u8]) -> i32 {
160    let Ok(event) = Event::try_from(event) else {
161        return -1;
162    };
163
164    emit(event);
165
166    0
167}
168
169fn strip_matching_prefix_components(full: &Path, base: &Path) -> PathBuf {
170    let mut full_it = full.components().peekable();
171    let mut base_it = base.components().peekable();
172
173    while let (Some(f), Some(b)) = (full_it.peek(), base_it.peek()) {
174        if f == b {
175            full_it.next();
176            base_it.next();
177        } else {
178            break;
179        }
180    }
181
182    let mut out = PathBuf::new();
183    for c in full_it {
184        match c {
185            Component::Normal(s) => out.push(s),
186            Component::CurDir => out.push("."),
187            Component::ParentDir => out.push(".."),
188            Component::RootDir => out.push(Path::new("/")),
189            Component::Prefix(p) => out.push(p.as_os_str()),
190        }
191    }
192    out
193}
194
195fn get_callsite(key: CallsiteKey) -> &'static Metadata<'static> {
196    CALLSITES.with_borrow_mut(|cs| {
197        if let Some(meta) = cs.get(&key) {
198            *meta
199        } else {
200            let (file, line, is_span, level) = key;
201
202            let callsite = if is_span {
203                tracing::callsite!(name: "fake", kind: tracing::metadata::Kind::EVENT, fields: &[])
204            } else {
205                tracing::callsite!(name: "fake", kind: tracing::metadata::Kind::SPAN, fields: &[])
206            };
207
208            let static_file: Option<&'static str> = if let Some(ref file) = file {
209                let path = Path::new(&file);
210                let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
211                let rel = strip_matching_prefix_components(path, manifest)
212                    .to_string_lossy()
213                    .to_string();
214
215                Some(Box::leak(rel.into_boxed_str()) as &'static str)
216            } else {
217                None
218            };
219
220            let meta = Box::leak(Box::new(Metadata::new(
221                "",
222                TARGET,
223                level,
224                static_file,
225                line,
226                None,
227                tracing::field::FieldSet::new(
228                    &["message"],
229                    tracing::callsite::Identifier(callsite),
230                ),
231                if is_span {
232                    tracing::metadata::Kind::SPAN
233                } else {
234                    tracing::metadata::Kind::EVENT
235                },
236            )));
237
238            let key = (file, line, is_span, level);
239            cs.insert(key, meta);
240
241            let meta: &'static Metadata = meta;
242            meta
243        }
244    })
245}
246
247fn emit(event: Event) {
248    let cpu = event.cpu;
249    SPANS.with_borrow_mut(|spans| match &event.kind {
250        Kind::Message(lvl) => {
251            if *lvl <= tracing::metadata::LevelFilter::current() {
252                let content = event.content.clone();
253                let meta = get_callsite(event.try_into().unwrap());
254                let parent = spans[cpu].back().and_then(|(_, p)| p.id());
255
256                tracing::Event::child_of(
257                    parent,
258                    meta,
259                    &tracing::valueset_all!(meta.fields(), "{}", content),
260                );
261            }
262        }
263        Kind::StartSpan(lvl) => {
264            if *lvl <= tracing::metadata::LevelFilter::current() {
265                let content = event.content.clone();
266                let meta = get_callsite(event.try_into().unwrap());
267                let parent = spans[cpu].back().and_then(|(_, p)| p.id());
268
269                let span = tracing::Span::child_of(
270                    parent,
271                    meta,
272                    &tracing::valueset_all!(meta.fields(), "{}", content),
273                );
274                spans[cpu].push_back((content, span.entered()));
275            }
276        }
277        Kind::EndSpan => {
278            let content = event.content;
279            while let Some((n, _)) = spans[cpu].pop_back() {
280                if n == content {
281                    break;
282                }
283            }
284        }
285    });
286}
287
288#[cfg(test)]
289mod tests {
290    use tracing::Level;
291
292    use super::*;
293
294    #[test]
295    fn leaks_one_callsite_per_level_and_kind() {
296        fn callsite_len() -> usize {
297            CALLSITES.with_borrow(|cs| cs.len())
298        }
299
300        let event_msg_info1 = Event {
301            kind: Kind::Message(Level::INFO),
302            content: "event 1".to_string(),
303            cpu: 1,
304            file: None,
305            line: None,
306        };
307
308        let event_msg_info2 = Event {
309            kind: Kind::Message(Level::INFO),
310            content: "event 2".to_string(),
311            cpu: 9,
312            file: None,
313            line: None,
314        };
315
316        let _callsite1 = get_callsite(event_msg_info1.try_into().unwrap());
317        let _callsite2 = get_callsite(event_msg_info2.try_into().unwrap());
318        assert_eq!(callsite_len(), 1);
319
320        let event_span_info3 = Event {
321            kind: Kind::StartSpan(Level::INFO),
322            content: "event 3".to_string(),
323            cpu: 29,
324            file: None,
325            line: None,
326        };
327        let _callsite3 = get_callsite(event_span_info3.try_into().unwrap());
328        assert_eq!(callsite_len(), 2);
329
330        let event_span_info4 = Event {
331            kind: Kind::StartSpan(Level::INFO),
332            content: "event 4".to_string(),
333            cpu: 29,
334            file: Some(String::from("this/is/a/test_file.rs")),
335            line: Some(12),
336        };
337        let _callsite4 = get_callsite(event_span_info4.try_into().unwrap());
338        assert_eq!(callsite_len(), 3);
339
340        let event_span_info5 = Event {
341            kind: Kind::StartSpan(Level::INFO),
342            content: "event 5".to_string(),
343            cpu: 29,
344            file: Some(String::from("this/is/a/test_file.rs")),
345            line: Some(12),
346        };
347        let _callsite5 = get_callsite(event_span_info5.try_into().unwrap());
348        assert_eq!(callsite_len(), 3);
349    }
350}