pyroscope 1.0.9-aarch64-linux → 1.1.0-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/ext/rbspy/Cargo.lock +201 -292
- data/ext/rbspy/Cargo.toml +10 -3
- data/ext/rbspy/include/rbspy.h +2 -0
- data/ext/rbspy/src/backend/backend.rs +134 -0
- data/ext/rbspy/src/backend/mod.rs +8 -0
- data/ext/rbspy/src/backend/ruleset.rs +82 -0
- data/ext/rbspy/src/backend/tests.rs +378 -0
- data/ext/rbspy/src/backend/types.rs +303 -0
- data/ext/rbspy/src/encode/gen/google.rs +233 -0
- data/ext/rbspy/src/encode/gen/mod.rs +3 -0
- data/ext/rbspy/src/encode/gen/push.rs +31 -0
- data/ext/rbspy/src/encode/gen/types.rs +11 -0
- data/ext/rbspy/src/encode/mod.rs +2 -0
- data/ext/rbspy/src/encode/pprof.rs +159 -0
- data/ext/rbspy/src/error.rs +67 -0
- data/ext/rbspy/src/ffikit.rs +76 -0
- data/ext/rbspy/src/lib.rs +41 -15
- data/ext/rbspy/src/pyroscope.rs +726 -0
- data/ext/rbspy/src/{backend.rs → rbspy_backend.rs} +3 -3
- data/ext/rbspy/src/session.rs +275 -0
- data/ext/rbspy/src/timer/epoll.rs +268 -0
- data/ext/rbspy/src/timer/kqueue.rs +191 -0
- data/ext/rbspy/src/timer/mod.rs +38 -0
- data/ext/rbspy/src/timer/sleep.rs +114 -0
- data/ext/rbspy/src/utils.rs +141 -0
- data/lib/pyroscope/version.rb +1 -1
- data/lib/pyroscope.rb +11 -1
- data/lib/rbspy/rbspy.so +0 -0
- data/pyroscope.gemspec +1 -3
- metadata +22 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
|
|
3
|
+
use crate::backend::types::Report;
|
|
4
|
+
use crate::encode::gen::google::{Function, Label, Line, Location, Profile, Sample, ValueType};
|
|
5
|
+
|
|
6
|
+
struct PProfBuilder {
|
|
7
|
+
profile: Profile,
|
|
8
|
+
strings: HashMap<String, i64>,
|
|
9
|
+
functions: HashMap<FunctionMirror, u64>,
|
|
10
|
+
locations: HashMap<LocationMirror, u64>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Hash, PartialEq, Eq, Clone)]
|
|
14
|
+
pub struct LocationMirror {
|
|
15
|
+
pub function_id: u64,
|
|
16
|
+
pub line: i64,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Hash, PartialEq, Eq, Clone)]
|
|
20
|
+
pub struct FunctionMirror {
|
|
21
|
+
pub name: i64,
|
|
22
|
+
pub filename: i64,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl PProfBuilder {
|
|
26
|
+
fn add_string(&mut self, s: &String) -> i64 {
|
|
27
|
+
let v = self.strings.get(s);
|
|
28
|
+
if let Some(v) = v {
|
|
29
|
+
return *v;
|
|
30
|
+
}
|
|
31
|
+
assert_ne!(self.strings.len(), self.profile.string_table.len() + 1);
|
|
32
|
+
let id: i64 = self.strings.len() as i64;
|
|
33
|
+
self.strings.insert(s.to_owned(), id);
|
|
34
|
+
self.profile.string_table.push(s.to_owned());
|
|
35
|
+
id
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn add_function(&mut self, fm: FunctionMirror) -> u64 {
|
|
39
|
+
let v = self.functions.get(&fm);
|
|
40
|
+
if let Some(v) = v {
|
|
41
|
+
return *v;
|
|
42
|
+
}
|
|
43
|
+
assert_ne!(self.functions.len(), self.profile.function.len() + 1);
|
|
44
|
+
let id: u64 = self.functions.len() as u64 + 1;
|
|
45
|
+
let f = Function {
|
|
46
|
+
id,
|
|
47
|
+
name: fm.name,
|
|
48
|
+
system_name: 0,
|
|
49
|
+
filename: fm.filename,
|
|
50
|
+
start_line: 0,
|
|
51
|
+
};
|
|
52
|
+
self.functions.insert(fm, id);
|
|
53
|
+
self.profile.function.push(f);
|
|
54
|
+
id
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn add_location(&mut self, lm: LocationMirror) -> u64 {
|
|
58
|
+
let v = self.locations.get(&lm);
|
|
59
|
+
if let Some(v) = v {
|
|
60
|
+
return *v;
|
|
61
|
+
}
|
|
62
|
+
assert_ne!(self.locations.len(), self.profile.location.len() + 1);
|
|
63
|
+
let id: u64 = self.locations.len() as u64 + 1;
|
|
64
|
+
let l = Location {
|
|
65
|
+
id,
|
|
66
|
+
mapping_id: 0,
|
|
67
|
+
address: 0,
|
|
68
|
+
line: vec![Line {
|
|
69
|
+
function_id: lm.function_id,
|
|
70
|
+
line: lm.line,
|
|
71
|
+
}],
|
|
72
|
+
is_folded: false,
|
|
73
|
+
};
|
|
74
|
+
self.locations.insert(lm, id);
|
|
75
|
+
self.profile.location.push(l);
|
|
76
|
+
id
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn encode(
|
|
81
|
+
reports: &Vec<Report>,
|
|
82
|
+
sample_rate: u32,
|
|
83
|
+
start_time_nanos: u64,
|
|
84
|
+
duration_nanos: u64,
|
|
85
|
+
) -> Profile {
|
|
86
|
+
let mut b = PProfBuilder {
|
|
87
|
+
strings: HashMap::new(),
|
|
88
|
+
functions: HashMap::new(),
|
|
89
|
+
locations: HashMap::new(),
|
|
90
|
+
profile: Profile {
|
|
91
|
+
sample_type: vec![],
|
|
92
|
+
sample: vec![],
|
|
93
|
+
mapping: vec![],
|
|
94
|
+
location: vec![],
|
|
95
|
+
function: vec![],
|
|
96
|
+
string_table: vec![],
|
|
97
|
+
drop_frames: 0,
|
|
98
|
+
keep_frames: 0,
|
|
99
|
+
time_nanos: start_time_nanos as i64,
|
|
100
|
+
duration_nanos: duration_nanos as i64,
|
|
101
|
+
period_type: None,
|
|
102
|
+
period: 0,
|
|
103
|
+
comment: vec![],
|
|
104
|
+
default_sample_type: 0,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
b.add_string(&"".to_string());
|
|
108
|
+
{
|
|
109
|
+
let cpu = b.add_string(&"cpu".to_string());
|
|
110
|
+
let nanoseconds = b.add_string(&"nanoseconds".to_string());
|
|
111
|
+
b.profile.sample_type.push(ValueType {
|
|
112
|
+
r#type: cpu,
|
|
113
|
+
unit: nanoseconds,
|
|
114
|
+
});
|
|
115
|
+
b.profile.period = 1_000_000_000 / sample_rate as i64;
|
|
116
|
+
b.profile.period_type = Some(ValueType {
|
|
117
|
+
r#type: cpu,
|
|
118
|
+
unit: nanoseconds,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
for report in reports {
|
|
122
|
+
for (stacktrace, value) in &report.data {
|
|
123
|
+
let mut sample = Sample {
|
|
124
|
+
location_id: vec![],
|
|
125
|
+
value: vec![*value as i64 * b.profile.period],
|
|
126
|
+
label: vec![],
|
|
127
|
+
};
|
|
128
|
+
for sf in &stacktrace.frames {
|
|
129
|
+
let name = b.add_string(sf.name.as_ref().unwrap_or(&"".to_string()));
|
|
130
|
+
let filename = b.add_string(sf.filename.as_ref().unwrap_or(&"".to_string()));
|
|
131
|
+
let line = sf.line.unwrap_or(0) as i64;
|
|
132
|
+
let function_id = b.add_function(FunctionMirror { name, filename });
|
|
133
|
+
let location_id = b.add_location(LocationMirror { function_id, line });
|
|
134
|
+
sample.location_id.push(location_id);
|
|
135
|
+
}
|
|
136
|
+
let mut labels = HashMap::new();
|
|
137
|
+
for l in &stacktrace.metadata.tags {
|
|
138
|
+
let k = b.add_string(&l.key);
|
|
139
|
+
let v = b.add_string(&l.value);
|
|
140
|
+
labels.insert(k, v);
|
|
141
|
+
}
|
|
142
|
+
for l in &report.metadata.tags {
|
|
143
|
+
let k = b.add_string(&l.key);
|
|
144
|
+
let v = b.add_string(&l.value);
|
|
145
|
+
labels.insert(k, v);
|
|
146
|
+
}
|
|
147
|
+
for (k, v) in &labels {
|
|
148
|
+
sample.label.push(Label {
|
|
149
|
+
key: *k,
|
|
150
|
+
str: *v,
|
|
151
|
+
num: 0,
|
|
152
|
+
num_unit: 0,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
b.profile.sample.push(sample);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
b.profile
|
|
159
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
pub type Result<T> = std::result::Result<T, PyroscopeError>;
|
|
2
|
+
|
|
3
|
+
/// Error type of Pyroscope
|
|
4
|
+
#[non_exhaustive]
|
|
5
|
+
#[derive(thiserror::Error, Debug)]
|
|
6
|
+
pub enum PyroscopeError {
|
|
7
|
+
#[error("Other: {}", &.0)]
|
|
8
|
+
AdHoc(String),
|
|
9
|
+
|
|
10
|
+
#[error("{msg}: {source:?}")]
|
|
11
|
+
Compat {
|
|
12
|
+
msg: String,
|
|
13
|
+
#[source]
|
|
14
|
+
source: Box<dyn std::error::Error + Send + Sync + 'static>,
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
#[error("BackendImpl error")]
|
|
18
|
+
BackendImpl,
|
|
19
|
+
|
|
20
|
+
#[error(transparent)]
|
|
21
|
+
Reqwest(#[from] reqwest::Error),
|
|
22
|
+
|
|
23
|
+
#[error(transparent)]
|
|
24
|
+
ParseError(#[from] url::ParseError),
|
|
25
|
+
|
|
26
|
+
#[error(transparent)]
|
|
27
|
+
TimeSource(#[from] std::time::SystemTimeError),
|
|
28
|
+
|
|
29
|
+
#[error(transparent)]
|
|
30
|
+
Io(#[from] std::io::Error),
|
|
31
|
+
|
|
32
|
+
#[error(transparent)]
|
|
33
|
+
Json(#[from] serde_json::Error),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl PyroscopeError {
|
|
37
|
+
/// Create a new instance of PyroscopeError
|
|
38
|
+
pub fn new(msg: &str) -> Self {
|
|
39
|
+
PyroscopeError::AdHoc(msg.to_string())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Create a new instance of PyroscopeError with source
|
|
43
|
+
pub fn new_with_source<E>(msg: &str, source: E) -> Self
|
|
44
|
+
where
|
|
45
|
+
E: std::error::Error + Send + Sync + 'static,
|
|
46
|
+
{
|
|
47
|
+
PyroscopeError::Compat {
|
|
48
|
+
msg: msg.to_string(),
|
|
49
|
+
source: Box::new(source),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl<T> From<std::sync::PoisonError<T>> for PyroscopeError {
|
|
55
|
+
fn from(_err: std::sync::PoisonError<T>) -> Self {
|
|
56
|
+
PyroscopeError::AdHoc("Poison Error".to_owned())
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl<T: 'static + Send + Sync> From<std::sync::mpsc::SendError<T>> for PyroscopeError {
|
|
61
|
+
fn from(err: std::sync::mpsc::SendError<T>) -> Self {
|
|
62
|
+
PyroscopeError::Compat {
|
|
63
|
+
msg: String::from("SendError Error"),
|
|
64
|
+
source: Box::new(err),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
use crate::backend::Tag;
|
|
2
|
+
use crate::error::{PyroscopeError, Result};
|
|
3
|
+
use crate::pyroscope::{PyroscopeAgentBuilder, PyroscopeAgentRunning};
|
|
4
|
+
use crate::{PyroscopeAgent, ThreadId};
|
|
5
|
+
use lazy_static::lazy_static;
|
|
6
|
+
use std::sync::{
|
|
7
|
+
mpsc::{self, Receiver, Sender},
|
|
8
|
+
Mutex,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, PartialEq, Clone)]
|
|
12
|
+
pub enum Signal {
|
|
13
|
+
Kill,
|
|
14
|
+
AddThreadTag(ThreadId, Tag),
|
|
15
|
+
RemoveThreadTag(ThreadId, Tag),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TAG: &str = "pyroscope::ffikit";
|
|
19
|
+
|
|
20
|
+
lazy_static! {
|
|
21
|
+
static ref SENDER: Mutex<Option<Sender<Signal>>> = Mutex::new(None);
|
|
22
|
+
}
|
|
23
|
+
pub fn run(agent: PyroscopeAgentBuilder) -> Result<()> {
|
|
24
|
+
let mut sender_holder = SENDER.lock()?;
|
|
25
|
+
if (*sender_holder).is_some() {
|
|
26
|
+
return Err(PyroscopeError::new("FFI channel already initialized"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let agent = agent.build()?;
|
|
30
|
+
|
|
31
|
+
let agent = agent.start()?;
|
|
32
|
+
|
|
33
|
+
let (sender, receiver): (Sender<Signal>, Receiver<Signal>) = mpsc::channel();
|
|
34
|
+
|
|
35
|
+
*sender_holder = Some(sender);
|
|
36
|
+
|
|
37
|
+
std::thread::spawn(move || {
|
|
38
|
+
while let Ok(signal) = receiver.recv() {
|
|
39
|
+
match signal {
|
|
40
|
+
Signal::Kill => {
|
|
41
|
+
if let Err(err) = stop(agent) {
|
|
42
|
+
log::error!(target: TAG, "failed to stop agent {err}");
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
Signal::AddThreadTag(thread_id, tag) => {
|
|
47
|
+
if let Err(err) = agent.add_thread_tag(thread_id, tag) {
|
|
48
|
+
log::error!(target: TAG, "failed to add tag {err}");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
Signal::RemoveThreadTag(thread_id, tag) => {
|
|
52
|
+
if let Err(err) = agent.remove_thread_tag(thread_id, tag) {
|
|
53
|
+
log::error!(target: TAG, "failed to remove tag {err}");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
Ok(())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn send(signal: Signal) -> Result<()> {
|
|
64
|
+
if let Some(sender) = &*SENDER.lock()? {
|
|
65
|
+
sender.send(signal)?;
|
|
66
|
+
} else {
|
|
67
|
+
return Err(PyroscopeError::new("FFI channel not initialized"));
|
|
68
|
+
}
|
|
69
|
+
Ok(())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn stop(agent: PyroscopeAgent<PyroscopeAgentRunning>) -> Result<()> {
|
|
73
|
+
agent.stop()?;
|
|
74
|
+
*SENDER.lock()? = None;
|
|
75
|
+
Ok(())
|
|
76
|
+
}
|
data/ext/rbspy/src/lib.rs
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
pub use crate::pyroscope::PyroscopeAgent;
|
|
2
|
+
pub use error::{PyroscopeError, Result};
|
|
3
|
+
|
|
4
|
+
pub mod backend;
|
|
5
|
+
pub mod encode;
|
|
6
|
+
pub mod error;
|
|
7
|
+
pub mod ffikit;
|
|
8
|
+
pub mod pyroscope;
|
|
9
|
+
pub mod session;
|
|
10
|
+
pub mod timer;
|
|
11
|
+
|
|
12
|
+
mod rbspy_backend;
|
|
13
|
+
mod utils;
|
|
14
|
+
pub use utils::ThreadId;
|
|
2
15
|
|
|
3
16
|
use rbspy::sampler::Sampler;
|
|
4
17
|
use remoteprocess::Pid;
|
|
@@ -6,9 +19,9 @@ use std::env;
|
|
|
6
19
|
use std::ffi::CStr;
|
|
7
20
|
use std::os::raw::c_char;
|
|
8
21
|
|
|
9
|
-
use crate::backend::
|
|
10
|
-
use pyroscope::
|
|
11
|
-
use
|
|
22
|
+
use crate::backend::{BackendConfig, BackendImpl, Report, StackFrame, Tag};
|
|
23
|
+
use crate::pyroscope::PyroscopeAgentBuilder;
|
|
24
|
+
use crate::rbspy_backend::Rbspy;
|
|
12
25
|
|
|
13
26
|
const LOG_TAG: &str = "Pyroscope::rbspy::ffi";
|
|
14
27
|
const RBSPY_NAME: &str = "rbspy";
|
|
@@ -110,6 +123,8 @@ pub unsafe extern "C" fn initialize_agent(
|
|
|
110
123
|
oncpu: bool,
|
|
111
124
|
report_pid: bool,
|
|
112
125
|
report_thread_id: bool,
|
|
126
|
+
runtime_name: *const c_char,
|
|
127
|
+
runtime_version: *const c_char,
|
|
113
128
|
tags: *const c_char,
|
|
114
129
|
tenant_id: *const c_char,
|
|
115
130
|
http_headers_json: *const c_char,
|
|
@@ -134,6 +149,16 @@ pub unsafe extern "C" fn initialize_agent(
|
|
|
134
149
|
.unwrap()
|
|
135
150
|
.to_string();
|
|
136
151
|
|
|
152
|
+
let runtime_name = unsafe { CStr::from_ptr(runtime_name) }
|
|
153
|
+
.to_str()
|
|
154
|
+
.unwrap()
|
|
155
|
+
.to_string();
|
|
156
|
+
|
|
157
|
+
let runtime_version = unsafe { CStr::from_ptr(runtime_version) }
|
|
158
|
+
.to_str()
|
|
159
|
+
.unwrap()
|
|
160
|
+
.to_string();
|
|
161
|
+
|
|
137
162
|
let tags_string = unsafe { CStr::from_ptr(tags) }
|
|
138
163
|
.to_str()
|
|
139
164
|
.unwrap()
|
|
@@ -172,7 +197,8 @@ pub unsafe extern "C" fn initialize_agent(
|
|
|
172
197
|
rbspy,
|
|
173
198
|
)
|
|
174
199
|
.func(transform_report_with_current_dir)
|
|
175
|
-
.tags(tags)
|
|
200
|
+
.tags(tags)
|
|
201
|
+
.runtime(runtime_name, runtime_version);
|
|
176
202
|
|
|
177
203
|
if !basic_auth_user.is_empty() && !basic_auth_password.is_empty() {
|
|
178
204
|
agent_builder = agent_builder.basic_auth(basic_auth_user, basic_auth_password);
|
|
@@ -182,28 +208,28 @@ pub unsafe extern "C" fn initialize_agent(
|
|
|
182
208
|
agent_builder = agent_builder.tenant_id(tenant_id);
|
|
183
209
|
}
|
|
184
210
|
|
|
185
|
-
let http_headers =
|
|
211
|
+
let http_headers = crate::pyroscope::parse_http_headers_json(http_headers_json);
|
|
186
212
|
match http_headers {
|
|
187
213
|
Ok(http_headers) => {
|
|
188
214
|
agent_builder = agent_builder.http_headers(http_headers);
|
|
189
215
|
}
|
|
190
216
|
Err(e) => match e {
|
|
191
|
-
|
|
217
|
+
crate::PyroscopeError::Json(e) => {
|
|
192
218
|
log::error!(target: LOG_TAG, "parse_http_headers_json error {}", e);
|
|
193
219
|
}
|
|
194
|
-
|
|
220
|
+
crate::PyroscopeError::AdHoc(e) => {
|
|
195
221
|
log::error!(target: LOG_TAG, "parse_http_headers_json {}", e);
|
|
196
222
|
}
|
|
197
223
|
_ => {}
|
|
198
224
|
},
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
|
|
227
|
+
crate::ffikit::run(agent_builder).is_ok()
|
|
202
228
|
}
|
|
203
229
|
|
|
204
230
|
#[no_mangle]
|
|
205
231
|
pub extern "C" fn drop_agent() -> bool {
|
|
206
|
-
|
|
232
|
+
crate::ffikit::send(crate::ffikit::Signal::Kill).is_ok()
|
|
207
233
|
}
|
|
208
234
|
|
|
209
235
|
#[no_mangle]
|
|
@@ -216,8 +242,8 @@ pub unsafe extern "C" fn add_thread_tag(key: *const c_char, value: *const c_char
|
|
|
216
242
|
.unwrap()
|
|
217
243
|
.to_owned();
|
|
218
244
|
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
crate::ffikit::send(crate::ffikit::Signal::AddThreadTag(
|
|
246
|
+
rbspy_backend::self_thread_id(),
|
|
221
247
|
Tag { key, value },
|
|
222
248
|
))
|
|
223
249
|
.is_ok()
|
|
@@ -233,8 +259,8 @@ pub unsafe extern "C" fn remove_thread_tag(key: *const c_char, value: *const c_c
|
|
|
233
259
|
.unwrap()
|
|
234
260
|
.to_owned();
|
|
235
261
|
|
|
236
|
-
|
|
237
|
-
|
|
262
|
+
crate::ffikit::send(crate::ffikit::Signal::RemoveThreadTag(
|
|
263
|
+
rbspy_backend::self_thread_id(),
|
|
238
264
|
Tag { key, value },
|
|
239
265
|
))
|
|
240
266
|
.is_ok()
|
|
@@ -260,7 +286,7 @@ fn string_to_tags(tags: &str) -> Vec<(&str, &str)> {
|
|
|
260
286
|
#[cfg(test)]
|
|
261
287
|
mod tests {
|
|
262
288
|
use super::*;
|
|
263
|
-
use
|
|
289
|
+
use crate::backend::{Report, StackFrame, StackTrace};
|
|
264
290
|
use std::collections::HashMap;
|
|
265
291
|
|
|
266
292
|
fn report_with_filename(filename: &str) -> Report {
|