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