Skip to main content

pamoja_ffi/
telemetry.rs

1//! The C ABI for device-side telemetry.
2//!
3//! These functions wrap [`pamoja_telemetry`] for callers that reach the SDK
4//! through the flat C boundary: a reporter that ships the events worth their
5//! bytes, counts every event it sees whether it ships or not, and moves its own
6//! bar as the link gets more expensive.
7//!
8//! A reporter holds counters across calls, so it crosses as an opaque handle.
9//! Only the level of an event crosses with it, because the level is the whole of
10//! what the reporter decides on: the code and the optional value belong to the
11//! caller, which keeps them alongside.
12
13use pamoja_telemetry::{Event, Level, LinkCost, Reporter};
14
15/// The number of severity levels, which is the width of a snapshot.
16pub const PAMOJA_TELEMETRY_LEVEL_COUNT: usize = 5;
17
18/// How urgent an event is.
19///
20/// A reporter ships an event whose level is at or above its threshold and drops
21/// anything below it, so the order of these values is what the filter compares.
22#[repr(C)]
23#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
24pub enum PamojaTelemetryLevel {
25    /// Fine-grained detail, useful only when chasing a specific problem.
26    Trace = 0,
27    /// Diagnostic detail for development.
28    Debug = 1,
29    /// A normal, noteworthy event.
30    Info = 2,
31    /// Something unexpected that the node recovered from.
32    Warn = 3,
33    /// A failure that needs attention.
34    Error = 4,
35}
36
37/// What the link back to the network currently costs.
38#[repr(C)]
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum PamojaLinkCost {
41    /// Bytes are effectively free, such as on wired power and ethernet.
42    Free = 0,
43    /// Bytes are paid for, such as on a cellular plan.
44    Metered = 1,
45    /// Bytes are scarce, such as on a satellite or long-range radio link.
46    Expensive = 2,
47    /// Nothing can be shipped at all.
48    Offline = 3,
49}
50
51/// A count of everything a reporter has seen, cheap enough to ship anywhere.
52///
53/// This is what a node sends in place of the event stream when the link cannot
54/// carry the detail: the shape of what happened survives even though the
55/// individual events did not.
56#[repr(C)]
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct PamojaTelemetrySnapshot {
59    /// How many events were seen at each level, indexed by
60    /// [`PamojaTelemetryLevel`].
61    pub by_level: [u32; PAMOJA_TELEMETRY_LEVEL_COUNT],
62    /// How many events passed the filter and were shipped.
63    pub emitted: u32,
64    /// How many events the filter dropped.
65    pub dropped: u32,
66}
67
68/// An opaque handle to one reporter and its counters.
69///
70/// Create it with [`pamoja_reporter_new`], feed it with
71/// [`pamoja_reporter_record`], and release it with [`pamoja_reporter_free`].
72pub struct PamojaReporter {
73    reporter: Reporter,
74}
75
76/// Returns the level a link cost calls for.
77///
78/// # Arguments
79///
80/// * `cost` - what the link currently costs.
81///
82/// # Returns
83///
84/// The lowest level still worth its bytes at that cost.
85#[no_mangle]
86pub extern "C" fn pamoja_link_cost_threshold(cost: PamojaLinkCost) -> PamojaTelemetryLevel {
87    level(rust_cost(cost).threshold())
88}
89
90/// Creates a reporter that ships events at or above a level.
91///
92/// # Arguments
93///
94/// * `threshold` - the lowest level to ship.
95///
96/// # Returns
97///
98/// A handle the caller must release with [`pamoja_reporter_free`].
99#[no_mangle]
100pub extern "C" fn pamoja_reporter_new(threshold: PamojaTelemetryLevel) -> *mut PamojaReporter {
101    Box::into_raw(Box::new(PamojaReporter {
102        reporter: Reporter::new(rust_level(threshold)),
103    }))
104}
105
106/// Returns the level a reporter is currently shipping from.
107///
108/// # Arguments
109///
110/// * `reporter` - the reporter.
111///
112/// # Returns
113///
114/// The threshold, or [`PamojaTelemetryLevel::Trace`] if `reporter` is null.
115///
116/// # Safety
117///
118/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
119#[no_mangle]
120pub unsafe extern "C" fn pamoja_reporter_threshold(
121    reporter: *const PamojaReporter,
122) -> PamojaTelemetryLevel {
123    if reporter.is_null() {
124        return PamojaTelemetryLevel::Trace;
125    }
126    level((*reporter).reporter.threshold())
127}
128
129/// Moves the level a reporter ships from.
130///
131/// # Arguments
132///
133/// * `reporter` - the reporter.
134/// * `threshold` - the new lowest level to ship.
135///
136/// # Safety
137///
138/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
139#[no_mangle]
140pub unsafe extern "C" fn pamoja_reporter_set_threshold(
141    reporter: *mut PamojaReporter,
142    threshold: PamojaTelemetryLevel,
143) {
144    if reporter.is_null() {
145        return;
146    }
147    (*reporter).reporter.set_threshold(rust_level(threshold));
148}
149
150/// Moves the threshold to match what the link now costs.
151///
152/// # Arguments
153///
154/// * `reporter` - the reporter.
155/// * `cost` - what the link currently costs.
156///
157/// # Safety
158///
159/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
160#[no_mangle]
161pub unsafe extern "C" fn pamoja_reporter_adapt_to(
162    reporter: *mut PamojaReporter,
163    cost: PamojaLinkCost,
164) {
165    if reporter.is_null() {
166        return;
167    }
168    (*reporter).reporter.adapt_to(rust_cost(cost));
169}
170
171/// Records an event and reports whether it is worth shipping.
172///
173/// Only the level crosses the boundary, because the level is the whole of what
174/// the reporter decides on. The code and the optional value stay with the caller,
175/// which is free to ship its own event when this returns `true`.
176///
177/// # Arguments
178///
179/// * `reporter` - the reporter.
180/// * `level` - the severity of the event that occurred.
181///
182/// # Returns
183///
184/// `true` if the event passed the threshold and should be shipped, or `false` if
185/// it was counted and dropped, or `reporter` is null.
186///
187/// # Safety
188///
189/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
190#[no_mangle]
191pub unsafe extern "C" fn pamoja_reporter_record(
192    reporter: *mut PamojaReporter,
193    level: PamojaTelemetryLevel,
194) -> bool {
195    if reporter.is_null() {
196        return false;
197    }
198    // The code is a borrowed static string on the Rust side and the reporter never
199    // reads it, so nothing is lost by leaving it empty here.
200    (*reporter)
201        .reporter
202        .record(Event::new(rust_level(level), ""))
203        .is_some()
204}
205
206/// Returns how many events a reporter has seen at a level, shipped or not.
207///
208/// # Arguments
209///
210/// * `reporter` - the reporter.
211/// * `level` - the level to count.
212///
213/// # Returns
214///
215/// The count, or 0 if `reporter` is null.
216///
217/// # Safety
218///
219/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
220#[no_mangle]
221pub unsafe extern "C" fn pamoja_reporter_count(
222    reporter: *const PamojaReporter,
223    level: PamojaTelemetryLevel,
224) -> u32 {
225    if reporter.is_null() {
226        return 0;
227    }
228    (*reporter).reporter.count(rust_level(level))
229}
230
231/// Returns how many events a reporter has seen across every level.
232///
233/// # Arguments
234///
235/// * `reporter` - the reporter.
236///
237/// # Returns
238///
239/// The total, or 0 if `reporter` is null.
240///
241/// # Safety
242///
243/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
244#[no_mangle]
245pub unsafe extern "C" fn pamoja_reporter_total(reporter: *const PamojaReporter) -> u32 {
246    if reporter.is_null() {
247        return 0;
248    }
249    (*reporter).reporter.total()
250}
251
252/// Returns how many events passed the threshold and were shipped.
253///
254/// # Arguments
255///
256/// * `reporter` - the reporter.
257///
258/// # Returns
259///
260/// The emitted count, or 0 if `reporter` is null.
261///
262/// # Safety
263///
264/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
265#[no_mangle]
266pub unsafe extern "C" fn pamoja_reporter_emitted(reporter: *const PamojaReporter) -> u32 {
267    if reporter.is_null() {
268        return 0;
269    }
270    (*reporter).reporter.emitted()
271}
272
273/// Returns how many events the threshold dropped.
274///
275/// # Arguments
276///
277/// * `reporter` - the reporter.
278///
279/// # Returns
280///
281/// The dropped count, or 0 if `reporter` is null.
282///
283/// # Safety
284///
285/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
286#[no_mangle]
287pub unsafe extern "C" fn pamoja_reporter_dropped(reporter: *const PamojaReporter) -> u32 {
288    if reporter.is_null() {
289        return 0;
290    }
291    (*reporter).reporter.dropped()
292}
293
294/// Takes a snapshot of the counters to ship in place of the event stream.
295///
296/// # Arguments
297///
298/// * `reporter` - the reporter.
299///
300/// # Returns
301///
302/// The snapshot, or an all-zero snapshot if `reporter` is null.
303///
304/// # Safety
305///
306/// `reporter` must be a live handle from [`pamoja_reporter_new`], or null.
307#[no_mangle]
308pub unsafe extern "C" fn pamoja_reporter_snapshot(
309    reporter: *const PamojaReporter,
310) -> PamojaTelemetrySnapshot {
311    if reporter.is_null() {
312        return PamojaTelemetrySnapshot {
313            by_level: [0; PAMOJA_TELEMETRY_LEVEL_COUNT],
314            emitted: 0,
315            dropped: 0,
316        };
317    }
318    let snapshot = (*reporter).reporter.snapshot();
319    PamojaTelemetrySnapshot {
320        by_level: snapshot.by_level,
321        emitted: snapshot.emitted,
322        dropped: snapshot.dropped,
323    }
324}
325
326/// Releases a reporter handle.
327///
328/// Passing null is a no-op.
329///
330/// # Safety
331///
332/// `reporter` must be a handle from [`pamoja_reporter_new`] that has not already
333/// been freed, or null. After this call it must not be used again.
334#[no_mangle]
335pub unsafe extern "C" fn pamoja_reporter_free(reporter: *mut PamojaReporter) {
336    if !reporter.is_null() {
337        drop(Box::from_raw(reporter));
338    }
339}
340
341/// Maps a Rust level onto the value that crosses the boundary.
342fn level(level: Level) -> PamojaTelemetryLevel {
343    match level {
344        Level::Trace => PamojaTelemetryLevel::Trace,
345        Level::Debug => PamojaTelemetryLevel::Debug,
346        Level::Info => PamojaTelemetryLevel::Info,
347        Level::Warn => PamojaTelemetryLevel::Warn,
348        Level::Error => PamojaTelemetryLevel::Error,
349    }
350}
351
352/// Maps a boundary level back onto the Rust one.
353fn rust_level(level: PamojaTelemetryLevel) -> Level {
354    match level {
355        PamojaTelemetryLevel::Trace => Level::Trace,
356        PamojaTelemetryLevel::Debug => Level::Debug,
357        PamojaTelemetryLevel::Info => Level::Info,
358        PamojaTelemetryLevel::Warn => Level::Warn,
359        PamojaTelemetryLevel::Error => Level::Error,
360    }
361}
362
363/// Maps a boundary link cost back onto the Rust one.
364fn rust_cost(cost: PamojaLinkCost) -> LinkCost {
365    match cost {
366        PamojaLinkCost::Free => LinkCost::Free,
367        PamojaLinkCost::Metered => LinkCost::Metered,
368        PamojaLinkCost::Expensive => LinkCost::Expensive,
369        PamojaLinkCost::Offline => LinkCost::Offline,
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use std::ptr;
376
377    use super::*;
378
379    #[test]
380    fn a_costly_link_raises_the_bar() {
381        unsafe {
382            let reporter = pamoja_reporter_new(PamojaTelemetryLevel::Trace);
383
384            pamoja_reporter_adapt_to(reporter, PamojaLinkCost::Metered);
385            assert!(!pamoja_reporter_record(
386                reporter,
387                PamojaTelemetryLevel::Debug
388            ));
389            assert!(pamoja_reporter_record(reporter, PamojaTelemetryLevel::Warn));
390
391            assert_eq!(pamoja_reporter_total(reporter), 2);
392            assert_eq!(pamoja_reporter_emitted(reporter), 1);
393            assert_eq!(pamoja_reporter_dropped(reporter), 1);
394
395            pamoja_reporter_free(reporter);
396        }
397    }
398
399    #[test]
400    fn dropped_events_are_still_counted() {
401        unsafe {
402            let reporter = pamoja_reporter_new(PamojaTelemetryLevel::Error);
403
404            for _ in 0..3 {
405                pamoja_reporter_record(reporter, PamojaTelemetryLevel::Info);
406            }
407            pamoja_reporter_record(reporter, PamojaTelemetryLevel::Error);
408
409            let snapshot = pamoja_reporter_snapshot(reporter);
410            assert_eq!(snapshot.by_level[PamojaTelemetryLevel::Info as usize], 3);
411            assert_eq!(snapshot.by_level[PamojaTelemetryLevel::Error as usize], 1);
412            assert_eq!(snapshot.emitted, 1);
413            assert_eq!(snapshot.dropped, 3);
414            assert_eq!(
415                pamoja_reporter_count(reporter, PamojaTelemetryLevel::Info),
416                3
417            );
418
419            pamoja_reporter_free(reporter);
420        }
421    }
422
423    #[test]
424    fn each_link_cost_sets_its_own_bar() {
425        assert_eq!(
426            pamoja_link_cost_threshold(PamojaLinkCost::Free),
427            PamojaTelemetryLevel::Trace
428        );
429        assert_eq!(
430            pamoja_link_cost_threshold(PamojaLinkCost::Metered),
431            PamojaTelemetryLevel::Info
432        );
433        assert_eq!(
434            pamoja_link_cost_threshold(PamojaLinkCost::Expensive),
435            PamojaTelemetryLevel::Warn
436        );
437        assert_eq!(
438            pamoja_link_cost_threshold(PamojaLinkCost::Offline),
439            PamojaTelemetryLevel::Error
440        );
441    }
442
443    #[test]
444    fn a_null_reporter_is_inert() {
445        unsafe {
446            assert!(!pamoja_reporter_record(
447                ptr::null_mut(),
448                PamojaTelemetryLevel::Error
449            ));
450            assert_eq!(pamoja_reporter_total(ptr::null()), 0);
451            assert_eq!(pamoja_reporter_snapshot(ptr::null()).emitted, 0);
452            pamoja_reporter_free(ptr::null_mut());
453        }
454    }
455}