Skip to main content

pact_plugin_driver/
plugin_log_sink.rs

1//! Driver-level plugin log sink abstraction
2
3use std::sync::RwLock;
4
5use lazy_static::lazy_static;
6use tracing::{debug, error, info, warn};
7
8/// Source of a plugin log entry
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum PluginLogSource {
11  /// Raw line read from the plugin process stderr
12  Stderr,
13  /// Structured record received via the PluginHost Log RPC
14  LogRpc,
15}
16
17/// Structured log entry produced by a running plugin
18#[derive(Debug, Clone)]
19pub struct PluginLogEntry {
20  /// Plugin name from its manifest
21  pub plugin_name: String,
22  /// UUID assigned by the driver when this plugin instance was started
23  pub plugin_instance_id: String,
24  /// Test run ID extracted from testContext, if available
25  pub test_run_id: Option<String>,
26  /// Log level string: TRACE, DEBUG, INFO, WARN, ERROR
27  pub level: String,
28  /// Human-readable log message
29  pub message: String,
30  /// Logger name / module path, if known
31  pub target: Option<String>,
32  /// Unix epoch milliseconds
33  pub timestamp_ms: i64,
34  /// Where this entry originated
35  pub source: PluginLogSource,
36}
37
38/// Receives structured log entries from running plugin processes.
39///
40/// Register a custom implementation with [`set_plugin_log_sink`] to intercept plugin log
41/// output. The built-in `DefaultPluginLogSink` is a no-op for [`PluginLogSource::Stderr`]
42/// entries (those are already written to the per-instance log file) and forwards
43/// [`PluginLogSource::LogRpc`] entries into the `tracing` subscriber.
44pub trait PluginLogSink: Send + Sync {
45  fn log(&self, entry: &PluginLogEntry);
46}
47
48struct DefaultPluginLogSink;
49
50fn is_transport_target(target: &str) -> bool {
51  target.starts_with("h2::") || target.starts_with("tower::") ||
52  target.starts_with("tonic::") || target.starts_with("hyper_util::") ||
53  target.starts_with("hyper::")
54}
55
56impl PluginLogSink for DefaultPluginLogSink {
57  fn log(&self, entry: &PluginLogEntry) {
58    if entry.source != PluginLogSource::LogRpc {
59      return;
60    }
61    if entry.level.to_uppercase() == "TRACE" {
62      return;
63    }
64    if entry.target.as_deref().map(is_transport_target).unwrap_or(false) {
65      return;
66    }
67    let plugin = &entry.plugin_name;
68    let instance = &entry.plugin_instance_id;
69    let msg = &entry.message;
70    let run_id = entry.test_run_id.as_deref().unwrap_or("");
71    match entry.level.to_uppercase().as_str() {
72      "DEBUG" => debug!(plugin_name = %plugin, plugin_instance = %instance, test_run_id = %run_id, "{}", msg),
73      "INFO"  => info!(plugin_name = %plugin, plugin_instance = %instance, test_run_id = %run_id, "{}", msg),
74      "WARN"  => warn!(plugin_name = %plugin, plugin_instance = %instance, test_run_id = %run_id, "{}", msg),
75      _       => error!(plugin_name = %plugin, plugin_instance = %instance, test_run_id = %run_id, "{}", msg),
76    }
77  }
78}
79
80lazy_static! {
81  static ref PLUGIN_LOG_SINK: RwLock<Box<dyn PluginLogSink>> =
82    RwLock::new(Box::new(DefaultPluginLogSink));
83}
84
85/// Replace the active plugin log sink. Should be called once at startup before any plugins load.
86pub fn set_plugin_log_sink(sink: Box<dyn PluginLogSink>) {
87  *PLUGIN_LOG_SINK.write().unwrap() = sink;
88}
89
90/// Forward a log entry to the registered sink. Called by driver internals.
91pub(crate) fn emit_plugin_log(entry: &PluginLogEntry) {
92  PLUGIN_LOG_SINK.read().unwrap().log(entry);
93}