ray/
origin.rs

1use gethostname::gethostname;
2use serde::Serialize;
3use std::path::{Path, PathBuf};
4
5#[cfg(feature = "origin-backtrace")]
6use backtrace::{Backtrace, BacktraceSymbol};
7
8/// Callsite metadata attached to each payload.
9///
10/// ```rust
11/// use ray::Origin;
12///
13/// let origin = Origin::from_callsite(file!(), line!(), module_path!());
14/// assert!(!origin.file.is_empty());
15/// ```
16#[derive(Debug, Clone, Serialize)]
17pub struct Origin {
18    pub function_name: String,
19    pub file: String,
20    pub line_number: u32,
21    pub hostname: String,
22}
23
24impl Default for Origin {
25    fn default() -> Self {
26        Self {
27            function_name: "Unknown".to_string(),
28            file: "Unknown".to_string(),
29            line_number: 0,
30            hostname: gethostname().to_string_lossy().to_string(),
31        }
32    }
33}
34
35impl Origin {
36    /// Build origin metadata from a callsite.
37    pub fn from_callsite(file: &str, line_number: u32, module_path: &str) -> Self {
38        Self::from_callsite_with_options(file, line_number, module_path, false)
39    }
40
41    /// Build origin metadata from a callsite with extra options.
42    pub fn from_callsite_with_options(
43        file: &str,
44        line_number: u32,
45        module_path: &str,
46        canonicalize_paths: bool,
47    ) -> Self {
48        let base_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
49        Self::from_callsite_with_base(
50            file,
51            line_number,
52            module_path,
53            canonicalize_paths,
54            &base_dir,
55        )
56    }
57
58    /// Build origin metadata from a callsite with an explicit base directory.
59    pub fn from_callsite_with_base(
60        file: &str,
61        line_number: u32,
62        module_path: &str,
63        canonicalize_paths: bool,
64        base_dir: &Path,
65    ) -> Self {
66        let file = absolutize(file, canonicalize_paths, base_dir)
67            .to_string_lossy()
68            .to_string();
69
70        Self {
71            function_name: module_path.to_string(),
72            file,
73            line_number,
74            hostname: gethostname().to_string_lossy().to_string(),
75        }
76    }
77
78    #[cfg(feature = "origin-backtrace")]
79    /// Capture origin metadata from the current backtrace.
80    pub fn capture(place_in_stack: Option<usize>) -> Self {
81        let place_in_stack = place_in_stack.unwrap_or(6);
82
83        if let Some(symbol) = get_symbol(place_in_stack) {
84            return Self::from(symbol);
85        }
86
87        Self::default()
88    }
89
90    /// Override the function name.
91    pub fn with_function_name(mut self, function_name: impl Into<String>) -> Self {
92        self.function_name = function_name.into();
93        self
94    }
95
96    /// Override the file path.
97    pub fn with_file(mut self, file: impl Into<String>) -> Self {
98        self.file = file.into();
99        self
100    }
101}
102
103#[cfg(feature = "origin-backtrace")]
104impl From<BacktraceSymbol> for Origin {
105    fn from(symbol: BacktraceSymbol) -> Self {
106        let function_name = symbol
107            .name()
108            .map(|v| v.to_string())
109            .unwrap_or_else(|| "Unknown".to_string());
110
111        let file = symbol
112            .filename()
113            .map(|v| v.to_string_lossy().to_string())
114            .unwrap_or_else(|| "Unknown".to_string());
115
116        let line_number = symbol.lineno().unwrap_or(0) as u32;
117
118        Self {
119            function_name,
120            file,
121            line_number,
122            hostname: gethostname().to_string_lossy().to_string(),
123        }
124    }
125}
126
127#[cfg(feature = "origin-backtrace")]
128fn get_symbol(place_in_stack: usize) -> Option<BacktraceSymbol> {
129    let trace = Backtrace::new();
130
131    let mut found = 0;
132    for frame in trace.frames() {
133        for symbol in frame.symbols() {
134            if let Some(file_path) = symbol.filename() {
135                let as_string = file_path.to_string_lossy();
136                if is_user_frame(as_string.as_ref()) {
137                    found += 1;
138                    if found == place_in_stack {
139                        return Some(symbol.to_owned());
140                    }
141                }
142            }
143        }
144    }
145
146    None
147}
148
149#[cfg(feature = "origin-backtrace")]
150fn is_user_frame(path: &str) -> bool {
151    !path.contains("/.cargo/registry") && !path.contains("/rustc/")
152}
153
154fn absolutize(file: &str, canonicalize_paths: bool, base_dir: &Path) -> PathBuf {
155    let p = Path::new(file);
156
157    if p.is_absolute() {
158        return p.to_path_buf();
159    }
160
161    let joined = base_dir.join(p);
162
163    if canonicalize_paths {
164        joined.canonicalize().unwrap_or(joined)
165    } else if joined.exists() {
166        joined
167    } else {
168        p.to_path_buf()
169    }
170}