binaryninja/tracing.rs
1//! Integration with the [`tracing`](https://docs.rs/tracing) ecosystem. Send logs to and from Binary Ninja.
2//!
3//! This module allows you to use standard Rust `tracing` macros (like `info!`, `warn!`, `error!`)
4//! and have them get sent to Binary Ninja, as well as the other way around with [`TracingLogListener`].
5//!
6//! ## For Plugins
7//!
8//! When writing a plugin, and you want your Rust logs to appear in the Binary Ninja UI, use the
9//! [`crate::tracing_init`] macro at the start of your plugin's init function.
10//!
11//! ## For Headless
12//!
13//! When running headless (standalone executables), and you want internal Binary Ninja logs to appear
14//! in your terminal's standard output, register the [`TracingLogListener`] after initializing your
15//! tracing subscriber.
16//!
17//! ## Never use both [`BinaryNinjaLayer`] and [`TracingLogListener`] simultaneously
18//!
19//! Enabling both creates an infinite feedback loop where a log triggers a log, deadlocking the application.
20
21use crate::file_metadata::SessionId;
22use crate::logger::{
23 bn_log_with_session, BnLogLevel, LogContext, LogGuard, LogListener, LOGGER_DEFAULT_SESSION_ID,
24};
25use tracing::{Event, Id, Level, Subscriber};
26use tracing_subscriber::layer::Context;
27use tracing_subscriber::prelude::*;
28use tracing_subscriber::registry::LookupSpan;
29use tracing_subscriber::Layer;
30
31/// Helper macro to initialize the [`BinaryNinjaLayer`] tracing layer for plugins.
32///
33/// Maps the current crate name to the provided display name and enables target formatting.
34/// Use [`init_with_layer`] if you intend on registering a [`BinaryNinjaLayer`] with non-default values.
35///
36/// ## Note for Plugins
37///
38/// This should **only** be called once, at the start of plugins.
39///
40/// ```no_run
41/// #[unsafe(no_mangle)]
42/// pub unsafe extern "C" fn CorePluginInit() -> bool {
43/// binaryninja::tracing_init!("MyPlugin");
44/// tracing::info!("Core plugin initialized");
45/// true
46/// }
47/// ```
48///
49/// ## Note for Headless
50///
51/// This should **never** be called if you are running headlessly and as you will likely be
52/// registering a [`LogListener`], which will possibly round-trip back to tracing logs and deadlock
53/// the program.
54#[macro_export]
55macro_rules! tracing_init {
56 () => {
57 let layer = $crate::tracing::BinaryNinjaLayer::default();
58 $crate::tracing::init_with_layer(layer);
59 };
60 ($name:expr) => {
61 let layer = $crate::tracing::BinaryNinjaLayer::default()
62 .with_target_mapping(env!("CARGO_CRATE_NAME"), $name);
63 $crate::tracing::init_with_layer(layer);
64 };
65}
66
67/// Initialize the core tracing subscriber with the given [`BinaryNinjaLayer`]. Collects and sends logs
68/// to the core.
69pub fn init_with_layer(layer: BinaryNinjaLayer) {
70 let subscriber = tracing_subscriber::registry().with(layer);
71 let _ = tracing::subscriber::set_global_default(subscriber);
72}
73
74/// Subscribes to all spans and events emitted by `tracing` and forwards them to the core logging API.
75#[derive(Clone)]
76pub struct BinaryNinjaLayer {
77 /// Rewrite mappings for the default target.
78 ///
79 /// # Example
80 ///
81 /// Given the target "my_crate::commands::analyze" and the mapping ("my_crate", "MyPlugin") the
82 /// target will be rewritten to "MyPlugin::commands::analyze", assuming no other rewrites occur.
83 pub target_mappings: Vec<(String, String)>,
84 /// Whether the default target should be formatted to be displayed in the "common" logger name
85 /// format for Binary Ninja.
86 ///
87 /// This formatting will only be applied when the target name is implicit. Explicitly provided
88 /// target names will be preserved as-is.
89 ///
90 /// # Example
91 ///
92 /// Given the target "my_crate::commands::analyze" the target will be rewritten to "MyCrate.Commands.Analyze".
93 pub format_target: bool,
94}
95
96impl BinaryNinjaLayer {
97 pub fn new() -> Self {
98 Self::default()
99 }
100
101 /// Add a target mapping which will rewrite the `old` in the default target to `new`.
102 ///
103 /// This is typically done when you have a plugin name that is verbose, and you wish to display
104 /// it in the logs as something else.
105 pub fn with_target_mapping(mut self, old: &str, new: &str) -> Self {
106 self.target_mappings
107 .push((old.to_string(), new.to_string()));
108 self
109 }
110
111 /// Whether formatting of the default target should be applied when sending logs to the core.
112 pub fn with_format_target(mut self, formatted: bool) -> Self {
113 self.format_target = formatted;
114 self
115 }
116}
117
118impl BinaryNinjaLayer {
119 /// Rewrite the target so that the logger name will be displayed in the common Binary Ninja format.
120 pub fn rewrite_target(&self, mut target: String) -> String {
121 // Formats the target such that "my_crate::commands::analyze" becomes "MyPlugin.Commands.Analyze".
122 let format_target = |target: &str| -> String {
123 let mut result = String::with_capacity(target.len());
124 let mut capitalize_next = true;
125 let mut chars = target.chars().peekable();
126
127 while let Some(c) = chars.next() {
128 match c {
129 ':' if chars.peek() == Some(&':') => {
130 // Found "::", consume the second ':' and treat as a dot separator
131 chars.next();
132 result.push('.');
133 capitalize_next = true;
134 }
135 '.' => {
136 result.push('.');
137 capitalize_next = true;
138 }
139 '_' => {
140 // Treat underscore as a separator: strip it and capitalize next
141 capitalize_next = true;
142 }
143 _ if capitalize_next => {
144 for upper in c.to_uppercase() {
145 result.push(upper);
146 }
147 capitalize_next = false;
148 }
149 _ => {
150 result.push(c);
151 }
152 }
153 }
154
155 result
156 };
157
158 // Perform "my_crate" -> "MyPlugin" rewrite rules.
159 for (old, new) in &self.target_mappings {
160 target = target.replace(old, new);
161 }
162
163 if self.format_target {
164 target = format_target(&target);
165 }
166
167 target
168 }
169}
170
171impl Default for BinaryNinjaLayer {
172 fn default() -> Self {
173 Self {
174 target_mappings: vec![("binaryninja".to_string(), "Binary Ninja".to_string())],
175 format_target: true,
176 }
177 }
178}
179
180impl<S> Layer<S> for BinaryNinjaLayer
181where
182 S: Subscriber + for<'a> LookupSpan<'a>,
183{
184 fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
185 let span = ctx.span(id).expect("Span not found in registry");
186
187 let mut visitor = BnFieldVisitor::default();
188 attrs.record(&mut visitor);
189
190 // If this span has a session_id, store it in the span's extensions
191 if let Some(session_id) = visitor.session_id {
192 span.extensions_mut().insert(session_id);
193 }
194 }
195
196 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
197 let level = match *event.metadata().level() {
198 Level::ERROR => BnLogLevel::ErrorLog,
199 Level::WARN => BnLogLevel::WarningLog,
200 Level::INFO => BnLogLevel::InfoLog,
201 Level::DEBUG | Level::TRACE => BnLogLevel::DebugLog,
202 };
203
204 let mut visitor = BnFieldVisitor::default();
205 event.record(&mut visitor);
206
207 // Walk up the span tree to find the session id.
208 // NOTE: Inserted by `BinaryNinjaLayer::on_new_span`.
209 let session_from_scope = || {
210 ctx.event_scope(event).and_then(|scope| {
211 scope
212 .from_root()
213 .find_map(|span| span.extensions().get::<SessionId>().copied())
214 })
215 };
216
217 // First we check the log event itself, then we check the scope context.
218 let session_id = visitor
219 .session_id
220 .or_else(session_from_scope)
221 .unwrap_or(LOGGER_DEFAULT_SESSION_ID);
222
223 let mut logger_name = event.metadata().target().to_string();
224 // Target is not overridden, we should try and apply mapping.
225 if let Some(module_path) = event.metadata().module_path() {
226 if module_path == logger_name {
227 // Target is the default module path, rewrite it into a more friendly format.
228 logger_name = self.rewrite_target(logger_name);
229 }
230 }
231
232 bn_log_with_session(session_id, &logger_name, level, &visitor.message);
233 }
234}
235
236#[derive(Default)]
237struct BnFieldVisitor {
238 message: String,
239 session_id: Option<SessionId>,
240}
241
242impl tracing::field::Visit for BnFieldVisitor {
243 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
244 if field.name() == "session_id" {
245 self.session_id = Some(SessionId(value as usize));
246 }
247 }
248
249 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
250 if field.name() == "session_id" {
251 self.session_id = Some(SessionId(value as usize));
252 }
253 }
254
255 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
256 if field.name() == "message" {
257 use std::fmt::Write;
258 let _ = write!(self.message, "{:?}", value);
259 }
260 }
261}
262
263/// A [`LogListener`] that forwards logs to the registered [`Subscriber`].
264///
265/// This should **never** be registered if the [`BinaryNinjaLayer`] is active. The [`BinaryNinjaLayer`]
266/// will consume our events we send in this log listener and send them back to the core, causing a
267/// never-ending cycle of sending and receiving the same logs.
268///
269/// Typically, you will register this listener for headless applications. You can technically use this
270/// in a plugin, but it is likely that you are sending tracing logs to the core, in which case you will
271/// run into the problem above.
272///
273/// ```no_run
274/// use binaryninja::tracing::TracingLogListener;
275/// use binaryninja::logger::{register_log_listener, BnLogLevel, bn_log};
276/// use binaryninja::headless::Session;
277///
278/// pub fn main() {
279/// // Register our tracing subscriber, this will send tracing events to stdout.
280/// tracing_subscriber::fmt::init();
281/// // Register our log listener, this will send logs from the core to our tracing subscriber.
282/// let _listener = TracingLogListener::new().register();
283/// // Should see logs from the core in regard to initialization show up.
284/// let _session = Session::new().expect("Failed to create session");
285/// bn_log("Test", BnLogLevel::DebugLog, "Hello, world!");
286/// }
287/// ```
288pub struct TracingLogListener {
289 minimum_level: BnLogLevel,
290}
291
292impl TracingLogListener {
293 /// Create a [`TracingLogListener`] with the minimum log level set to [`BnLogLevel::InfoLog`].
294 pub fn new() -> Self {
295 Self::default()
296 }
297
298 pub fn new_with_lvl(minimum_level: BnLogLevel) -> Self {
299 Self { minimum_level }
300 }
301
302 /// Register the [`TracingLogListener`] and send logs to the registered tracing subscriber until
303 /// the [`LogGuard`] is dropped, make sure to register your tracing subscriber before registering.
304 #[must_use]
305 pub fn register(self) -> LogGuard<Self> {
306 crate::logger::register_log_listener(self)
307 }
308}
309
310impl Default for TracingLogListener {
311 fn default() -> Self {
312 Self {
313 minimum_level: BnLogLevel::InfoLog,
314 }
315 }
316}
317
318impl LogListener for TracingLogListener {
319 fn log(&self, ctx: &LogContext, level: BnLogLevel, message: &str) {
320 let session = ctx.session_id.map(|s| s.0);
321 match level {
322 BnLogLevel::ErrorLog | BnLogLevel::AlertLog => {
323 tracing::error!(
324 target: "binaryninja",
325 session_id = session,
326 logger = %ctx.logger_name,
327 "{}",
328 message
329 )
330 }
331 BnLogLevel::WarningLog => {
332 tracing::warn!(
333 target: "binaryninja",
334 session_id = session,
335 logger = %ctx.logger_name,
336 "{}",
337 message
338 )
339 }
340 BnLogLevel::InfoLog => {
341 tracing::info!(
342 target: "binaryninja",
343 session_id = session,
344 logger = %ctx.logger_name,
345 "{}",
346 message
347 )
348 }
349 BnLogLevel::DebugLog => {
350 tracing::debug!(
351 target: "binaryninja",
352 session_id = session,
353 logger = %ctx.logger_name,
354 "{}",
355 message
356 )
357 }
358 };
359 }
360
361 fn level(&self) -> BnLogLevel {
362 self.minimum_level
363 }
364}