From 6163940768e1c92b908d5fc35fc0b7549c41b04c Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Thu, 16 Jul 2026 13:09:35 +0300 Subject: [PATCH 01/12] event-plugin: configure subscriptions from environment --- event-plugin/src/events.rs | 64 +++++++++++++------------------------- event-plugin/src/main.rs | 50 +++++++++++++++++------------ 2 files changed, 52 insertions(+), 62 deletions(-) diff --git a/event-plugin/src/events.rs b/event-plugin/src/events.rs index ac3895e..86c9781 100644 --- a/event-plugin/src/events.rs +++ b/event-plugin/src/events.rs @@ -76,49 +76,29 @@ fn encode_envelope(envelope: EventEnvelope) -> Result> { Ok(buf) } -/// Convenience function to handle events. -macro_rules! define_event_handler { - ($fn_name:ident, $event_type:expr) => { - pub async fn $fn_name(p: Plugin, v: JsonValue) -> Result<()> { - debug!( - "handled event, event_type={}, payload={:?}", - $event_type, &v - ); - let state = p.state(); - state - .publish_bytes(encode_envelope( - new_envelope( - $event_type, - state.source_kind(), - state.source_node_id(), - state.producer_version(), - &v, - ) - .await?, - )?) - .await - } - }; -} +pub async fn on_event(p: Plugin, event_type: String, v: JsonValue) -> Result<()> { + if event_type == "warning" { + return on_warning(p, v).await; + } -// Define event handler. -define_event_handler!(on_connect, "connect"); -define_event_handler!(on_disconnect, "disconnect"); -define_event_handler!(on_invoice_creation, "invoice_creation"); -define_event_handler!(on_invoice_payment, "invoice_payment"); -define_event_handler!(on_channel_opened, "channel_opened"); -define_event_handler!(on_channel_open_failed, "channel_open_failed"); -define_event_handler!(on_channel_state_changed, "channel_state_changed"); -define_event_handler!(on_forward_event, "forward_event"); -define_event_handler!(on_block_added, "block_added"); -define_event_handler!(on_custommsg, "custommsg"); -define_event_handler!(on_sendpay_success, "sendpay_success"); -define_event_handler!(on_sendpay_failure, "sendpay_failure"); -define_event_handler!(on_coin_movement, "coin_movement"); -define_event_handler!(on_openchannel_peer_sigs, "openchannel_peer_sigs"); -define_event_handler!(on_onionmessage_forward_fail, "onionmessage_forward_fail"); -define_event_handler!(on_pay_part_start, "pay_part_start"); -define_event_handler!(on_pay_part_end, "pay_part_end"); + debug!( + "handled event, event_type={}, payload={:?}", + &event_type, &v + ); + let state = p.state(); + state + .publish_bytes(encode_envelope( + new_envelope( + &event_type, + state.source_kind(), + state.source_node_id(), + state.producer_version(), + &v, + ) + .await?, + )?) + .await +} pub async fn on_warning(_p: Plugin, _v: JsonValue) -> Result<()> { // TODO: no-op for now, will eff-up integration test teardown otherwise as diff --git a/event-plugin/src/main.rs b/event-plugin/src/main.rs index a4730fd..f60ebeb 100644 --- a/event-plugin/src/main.rs +++ b/event-plugin/src/main.rs @@ -11,6 +11,7 @@ use lapin::options::{ExchangeDeclareOptions, QueueDeclareOptions}; use lapin::types::FieldTable; use lapin::{Connection, ConnectionProperties, ExchangeKind}; use serde::Deserialize; +use std::env; use std::fmt; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; @@ -22,12 +23,14 @@ use tracing::info; const DEFAULT_EXCHANGE_NAME: &str = "cln.events"; const DEFAULT_QUEUE_NAME: &str = "events"; +const EVENT_TYPES_ENV: &str = "EVENT_PLUGIN_EVENTS"; #[tokio::main] async fn main() -> Result<()> { let amqp_channel: Arc>> = Arc::new(RwLock::new(None)); + let event_types = event_types_from_env(); - let configured = Builder::new(tokio::io::stdin(), tokio::io::stdout()) + let mut builder = Builder::new(tokio::io::stdin(), tokio::io::stdout()) .option(ConfigOption::new_str_no_default( "rabbitmq-url", "AMQP URL: user:pass@host:port/vhost", @@ -45,31 +48,28 @@ async fn main() -> Result<()> { .option(ConfigOption::new_str_no_default( "source-kind", "Event source kind: 'gateway' or 'lsp'", - )) - .subscribe("connect", on_connect) - .subscribe("disconnect", on_disconnect) - .subscribe("invoice_creation", on_invoice_creation) - .subscribe("invoice_payment", on_invoice_payment) - .subscribe("channel_opened", on_channel_opened) - .subscribe("channel_open_failed", on_channel_open_failed) - .subscribe("channel_state_changed", on_channel_state_changed) - .subscribe("forward_event", on_forward_event) - .subscribe("block_added", on_block_added) - .subscribe("custommsg", on_custommsg) - .subscribe("warning", on_warning) - .subscribe("sendpay_success", on_sendpay_success) - .subscribe("sendpay_failure", on_sendpay_failure) - .subscribe("coin_movement", on_coin_movement) - .subscribe("openchannel_peer_sigs", on_openchannel_peer_sigs) - .subscribe("onionmessage_forward_fail", on_onionmessage_forward_fail) - .subscribe("pay_part_start", on_pay_part_start) - .subscribe("pay_part_end", on_pay_part_end) + )); + + for event_type in &event_types { + let handler_event_type = event_type.clone(); + builder = builder.subscribe(event_type, move |p, v| { + on_event(p, handler_event_type.clone(), v) + }); + } + + let configured = builder .dynamic() .configure() .await? // Fail early and loud, we don't want to run the node without this plugin. .context("failed to configure event-plugin")?; + if event_types.is_empty() { + let msg = format!("'{EVENT_TYPES_ENV}' must contain at least one event type"); + _ = configured.disable(&msg).await; + bail!(msg); + } + // Fail if RabbitMQ is not configured, we can not operate without. let Some(url) = get_configured_string_option(&configured, "rabbitmq-url") else { let msg = "'rabbitmq-url' option is required but not set"; @@ -146,6 +146,16 @@ async fn main() -> Result<()> { Ok(()) } +fn event_types_from_env() -> Vec { + env::var(EVENT_TYPES_ENV) + .unwrap_or_default() + .split(',') + .map(str::trim) + .filter(|event_type| !event_type.is_empty()) + .map(str::to_string) + .collect() +} + struct NodeInfo { node_id: Vec, version: String, From 6b2dce95c282e56da8c73515fb387b980259d3c8 Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Thu, 16 Jul 2026 13:14:40 +0300 Subject: [PATCH 02/12] docs: document configurable event subscriptions --- event-plugin/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/event-plugin/README.md b/event-plugin/README.md index 07420ec..c268561 100644 --- a/event-plugin/README.md +++ b/event-plugin/README.md @@ -6,7 +6,7 @@ Core Lightning plugin that collects events and sends them to RabbitMQ ## Events handled -There are 8 events (subscriptions): +The plugin subscribes to the CLN notifications listed in `EVENT_PLUGIN_EVENTS`. Common event types are: - `connect` - A peer connected to the node - `disconnect` - A peer disconnected from the node @@ -32,11 +32,15 @@ RabbitMQ, and then return "continue" so that CLN can continue the operation Set RabbitMQ URL with the plugin option: ``` +EVENT_PLUGIN_EVENTS=connect,disconnect,invoice_creation,invoice_payment,channel_opened,channel_state_changed,forward_event,block_added --rabbitmq-url=amqp://guest:guest@localhost:5672/%2f --rabbitmq-exchange=some_exchange --rabbitmq-queue=some_queue ``` +`EVENT_PLUGIN_EVENTS` is required and must be set in the environment before CLN starts the plugin. Its comma-separated +value defines the notifications advertised to CLN during the plugin handshake. + If RabbitMQ is not available at **startup**, the plugin exits with an error and CLN will not load it. If the connection drops **during operation**, the plugin keeps running but events cannot be published. Each failed From 55b47cca961b39fc50c3725ca4b5ad97c652a96d Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Thu, 16 Jul 2026 13:16:15 +0300 Subject: [PATCH 03/12] neat: update Cargo.lock --- Cargo.lock | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 87388d3..f79f90c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,9 +345,9 @@ checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bitcoin" -version = "0.32.101" +version = "0.32.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed8ccb78a9ff7a6fbb90e2fb9b8588b4a9928d49d8af3cb789108a84ea6b0ce" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" dependencies = [ "base58ck", "bech32", @@ -362,21 +362,20 @@ dependencies = [ [[package]] name = "bitcoin-consensus-encoding" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" dependencies = [ "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", ] [[package]] name = "bitcoin-internals" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" -dependencies = [ - "hex-conservative 0.3.2", -] +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" [[package]] name = "bitcoin-io" @@ -1105,9 +1104,9 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.3.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" dependencies = [ "arrayvec", ] @@ -2587,9 +2586,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", From 283d5676ea14eb617eb1b5427e90f1c99cc1f06a Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Thu, 16 Jul 2026 13:28:48 +0300 Subject: [PATCH 04/12] event-plugin: reject wildcard subscriptions --- event-plugin/src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/event-plugin/src/main.rs b/event-plugin/src/main.rs index f60ebeb..d9f36d9 100644 --- a/event-plugin/src/main.rs +++ b/event-plugin/src/main.rs @@ -70,6 +70,12 @@ async fn main() -> Result<()> { bail!(msg); } + if event_types.iter().any(|event_type| event_type == "*") { + let msg = format!("'{EVENT_TYPES_ENV}' does not support wildcard subscriptions"); + _ = configured.disable(&msg).await; + bail!(msg); + } + // Fail if RabbitMQ is not configured, we can not operate without. let Some(url) = get_configured_string_option(&configured, "rabbitmq-url") else { let msg = "'rabbitmq-url' option is required but not set"; From 261018f2f0ef0259db4d08fc84ff6b321f92a7a7 Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Thu, 16 Jul 2026 13:28:56 +0300 Subject: [PATCH 05/12] docs: clarify wildcard subscription support --- event-plugin/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/event-plugin/README.md b/event-plugin/README.md index c268561..808c9be 100644 --- a/event-plugin/README.md +++ b/event-plugin/README.md @@ -39,7 +39,8 @@ EVENT_PLUGIN_EVENTS=connect,disconnect,invoice_creation,invoice_payment,channel_ ``` `EVENT_PLUGIN_EVENTS` is required and must be set in the environment before CLN starts the plugin. Its comma-separated -value defines the notifications advertised to CLN during the plugin handshake. +value defines the notifications advertised to CLN during the plugin handshake. Wildcard subscriptions are not +supported. If RabbitMQ is not available at **startup**, the plugin exits with an error and CLN will not load it. From 5682bd60bd79cee1de17b1ba767b47d8a67fdf35 Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Fri, 17 Jul 2026 17:59:19 +0300 Subject: [PATCH 06/12] feat: add TOML config and default event subscription list --- Cargo.lock | 61 ++++++++++++++++++ event-plugin/Cargo.toml | 1 + event-plugin/README.md | 20 ++++-- event-plugin/src/config.rs | 124 +++++++++++++++++++++++++++++++++++++ event-plugin/src/main.rs | 28 ++------- 5 files changed, 208 insertions(+), 26 deletions(-) create mode 100644 event-plugin/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index f79f90c..b10afe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -821,6 +821,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "toml", "tonic", "tonic-prost-build", "tracing", @@ -2456,6 +2457,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2821,6 +2831,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tonic" version = "0.14.6" @@ -3263,6 +3312,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "writeable" version = "0.6.3" diff --git a/event-plugin/Cargo.toml b/event-plugin/Cargo.toml index f39b985..0d89b25 100644 --- a/event-plugin/Cargo.toml +++ b/event-plugin/Cargo.toml @@ -19,6 +19,7 @@ prost = { version = "0.14" } prost-types = { version = "0.14" } ferroid = { version = "2", features = ["snowflake", "atomic", "async-tokio"]} hex = { version = "0.4" } +toml = { version = "0.9" } [build-dependencies] tonic-prost-build = { version = "0.14" } diff --git a/event-plugin/README.md b/event-plugin/README.md index 808c9be..5b98f64 100644 --- a/event-plugin/README.md +++ b/event-plugin/README.md @@ -6,7 +6,7 @@ Core Lightning plugin that collects events and sends them to RabbitMQ ## Events handled -The plugin subscribes to the CLN notifications listed in `EVENT_PLUGIN_EVENTS`. Common event types are: +The plugin subscribes to a configurable list of CLN notifications. Common event types are: - `connect` - A peer connected to the node - `disconnect` - A peer disconnected from the node @@ -38,9 +38,21 @@ EVENT_PLUGIN_EVENTS=connect,disconnect,invoice_creation,invoice_payment,channel_ --rabbitmq-queue=some_queue ``` -`EVENT_PLUGIN_EVENTS` is required and must be set in the environment before CLN starts the plugin. Its comma-separated -value defines the notifications advertised to CLN during the plugin handshake. Wildcard subscriptions are not -supported. +The list of notifications advertised to CLN during the plugin handshake is resolved from, in priority order: + +1. `EVENT_PLUGIN_EVENTS` environment variable — a comma-separated list of event types. +2. The TOML config file pointed to by the `EVENT_PLUGIN_CONFIG` environment variable: + + ```toml + [event-plugin] + events_list = ["connect", "disconnect", "invoice_payment"] + ``` + +3. A built-in default list covering the common notifications (connect/disconnect, invoices, channels, forwards, + payments, coin movements, and more). + +Because subscriptions are declared before CLN passes plugin options, both variables must be set in the environment +before CLN starts the plugin. Wildcard subscriptions are not supported. If RabbitMQ is not available at **startup**, the plugin exits with an error and CLN will not load it. diff --git a/event-plugin/src/config.rs b/event-plugin/src/config.rs new file mode 100644 index 0000000..b16c6d8 --- /dev/null +++ b/event-plugin/src/config.rs @@ -0,0 +1,124 @@ +use anyhow::{Context, Result, bail}; +use serde::Deserialize; +use std::env; + +const EVENT_TYPES_ENV: &str = "EVENT_PLUGIN_EVENTS"; +const CONFIG_FILE_ENV: &str = "EVENT_PLUGIN_CONFIG"; + +/// Single source of truth for the events subscribed to when neither +/// `EVENT_PLUGIN_EVENTS` nor the TOML config file specifies a list. +const DEFAULT_EVENTS: &[&str] = &[ + "connect", + "disconnect", + "invoice_creation", + "invoice_payment", + "channel_opened", + "channel_open_failed", + "channel_state_changed", + "forward_event", + "block_added", + "custommsg", + "warning", + "sendpay_success", + "sendpay_failure", + "coin_movement", + "openchannel_peer_sigs", + "onionmessage_forward_fail", + "pay_part_start", + "pay_part_end", +]; + +/// Resolves the list of events to subscribe to, in priority order: +/// 1. `EVENT_PLUGIN_EVENTS` environment variable (comma-separated) +/// 2. `events_list` in the TOML file pointed to by `EVENT_PLUGIN_CONFIG` +/// 3. [`DEFAULT_EVENTS`] +pub fn resolve_event_types() -> Result> { + let event_types = if let Ok(raw) = env::var(EVENT_TYPES_ENV) { + parse_event_list(&raw) + } else if let Some(list) = event_types_from_config_file()? { + list + } else { + DEFAULT_EVENTS.iter().map(|s| s.to_string()).collect() + }; + + if event_types.is_empty() { + bail!("event subscription list must contain at least one event type"); + } + if event_types.iter().any(|event_type| event_type == "*") { + bail!("wildcard event subscriptions are not supported"); + } + + Ok(event_types) +} + +fn parse_event_list(raw: &str) -> Vec { + raw.split(',') + .map(str::trim) + .filter(|event_type| !event_type.is_empty()) + .map(str::to_string) + .collect() +} + +#[derive(Deserialize, Default)] +struct ConfigFile { + #[serde(rename = "event-plugin", default)] + event_plugin: EventPluginSection, +} + +#[derive(Deserialize, Default)] +struct EventPluginSection { + events_list: Option>, +} + +/// Returns the `events_list` from the TOML config file, or `None` when no +/// config file is configured or the file does not set the field. +fn event_types_from_config_file() -> Result>> { + let Ok(path) = env::var(CONFIG_FILE_ENV) else { + return Ok(None); + }; + let raw = std::fs::read_to_string(&path) + .with_context(|| format!("failed to read config file '{path}'"))?; + let config: ConfigFile = + toml::from_str(&raw).with_context(|| format!("failed to parse config file '{path}'"))?; + Ok(config.event_plugin.events_list) +} + +#[cfg(test)] +mod test { + use super::{ConfigFile, parse_event_list}; + + #[test] + fn test_parse_event_list() { + assert_eq!( + parse_event_list(" connect, disconnect ,,invoice_payment"), + vec!["connect", "disconnect", "invoice_payment"] + ); + assert!(parse_event_list("").is_empty()); + } + + #[test] + fn test_config_file_events_list() { + let config: ConfigFile = toml::from_str( + r#" + [event-plugin] + events_list = ["connect", "disconnect", "invoice_payment"] + "#, + ) + .unwrap(); + + assert_eq!( + config.event_plugin.events_list, + Some(vec![ + "connect".to_string(), + "disconnect".to_string(), + "invoice_payment".to_string() + ]) + ); + } + + #[test] + fn test_config_file_without_events_list() { + let config: ConfigFile = toml::from_str("").unwrap(); + assert_eq!(config.event_plugin.events_list, None); + } +} diff --git a/event-plugin/src/main.rs b/event-plugin/src/main.rs index d9f36d9..4b048c1 100644 --- a/event-plugin/src/main.rs +++ b/event-plugin/src/main.rs @@ -1,4 +1,5 @@ mod broker; +mod config; mod events; mod proto; @@ -6,12 +7,12 @@ use anyhow::{Context, Result, bail}; use broker::MessageBroker; use cln_plugin::Builder; use cln_plugin::options::ConfigOption; +use config::resolve_event_types; use events::*; use lapin::options::{ExchangeDeclareOptions, QueueDeclareOptions}; use lapin::types::FieldTable; use lapin::{Connection, ConnectionProperties, ExchangeKind}; use serde::Deserialize; -use std::env; use std::fmt; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; @@ -23,12 +24,11 @@ use tracing::info; const DEFAULT_EXCHANGE_NAME: &str = "cln.events"; const DEFAULT_QUEUE_NAME: &str = "events"; -const EVENT_TYPES_ENV: &str = "EVENT_PLUGIN_EVENTS"; #[tokio::main] async fn main() -> Result<()> { let amqp_channel: Arc>> = Arc::new(RwLock::new(None)); - let event_types = event_types_from_env(); + let event_types = resolve_event_types(); let mut builder = Builder::new(tokio::io::stdin(), tokio::io::stdout()) .option(ConfigOption::new_str_no_default( @@ -50,7 +50,7 @@ async fn main() -> Result<()> { "Event source kind: 'gateway' or 'lsp'", )); - for event_type in &event_types { + for event_type in event_types.iter().flatten() { let handler_event_type = event_type.clone(); builder = builder.subscribe(event_type, move |p, v| { on_event(p, handler_event_type.clone(), v) @@ -64,14 +64,8 @@ async fn main() -> Result<()> { // Fail early and loud, we don't want to run the node without this plugin. .context("failed to configure event-plugin")?; - if event_types.is_empty() { - let msg = format!("'{EVENT_TYPES_ENV}' must contain at least one event type"); - _ = configured.disable(&msg).await; - bail!(msg); - } - - if event_types.iter().any(|event_type| event_type == "*") { - let msg = format!("'{EVENT_TYPES_ENV}' does not support wildcard subscriptions"); + if let Err(e) = &event_types { + let msg = format!("{e:#}"); _ = configured.disable(&msg).await; bail!(msg); } @@ -152,16 +146,6 @@ async fn main() -> Result<()> { Ok(()) } -fn event_types_from_env() -> Vec { - env::var(EVENT_TYPES_ENV) - .unwrap_or_default() - .split(',') - .map(str::trim) - .filter(|event_type| !event_type.is_empty()) - .map(str::to_string) - .collect() -} - struct NodeInfo { node_id: Vec, version: String, From de1199f34f10a1268ff2720bac1a6d5e23e5e7ec Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Tue, 21 Jul 2026 10:28:33 +0300 Subject: [PATCH 07/12] feat: replace configuration with config crate --- Cargo.lock | 49 ++++++---- event-plugin/Cargo.toml | 2 +- event-plugin/README.md | 23 +++-- event-plugin/src/config.rs | 196 ++++++++++++++++++++++++++----------- 4 files changed, 183 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b10afe3..f654ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.15.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" +dependencies = [ + "pathdiff", + "serde_core", + "toml", + "winnow", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -813,6 +825,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cln-plugin", + "config", "ferroid", "hex", "lapin", @@ -821,7 +834,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "toml", "tonic", "tonic-prost-build", "tracing", @@ -1811,6 +1823,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -2833,24 +2851,22 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ - "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", - "toml_writer", - "winnow 0.7.15", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -2861,15 +2877,9 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.4", + "winnow", ] -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - [[package]] name = "tonic" version = "0.14.6" @@ -3312,17 +3322,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - [[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "writeable" diff --git a/event-plugin/Cargo.toml b/event-plugin/Cargo.toml index 0d89b25..34675fc 100644 --- a/event-plugin/Cargo.toml +++ b/event-plugin/Cargo.toml @@ -19,7 +19,7 @@ prost = { version = "0.14" } prost-types = { version = "0.14" } ferroid = { version = "2", features = ["snowflake", "atomic", "async-tokio"]} hex = { version = "0.4" } -toml = { version = "0.9" } +config = { version = "0.15", default-features = false, features = ["toml"] } [build-dependencies] tonic-prost-build = { version = "0.14" } diff --git a/event-plugin/README.md b/event-plugin/README.md index 5b98f64..d4d7a45 100644 --- a/event-plugin/README.md +++ b/event-plugin/README.md @@ -29,13 +29,21 @@ RabbitMQ, and then return "continue" so that CLN can continue the operation ## Configuration -Set RabbitMQ URL with the plugin option: +Set the event subscription source in the environment before starting CLN. For example: +```bash +export EVENT_PLUGIN_EVENTS="connect,disconnect,invoice_creation,invoice_payment,channel_opened,channel_state_changed,forward_event,block_added" ``` -EVENT_PLUGIN_EVENTS=connect,disconnect,invoice_creation,invoice_payment,channel_opened,channel_state_changed,forward_event,block_added ---rabbitmq-url=amqp://guest:guest@localhost:5672/%2f ---rabbitmq-exchange=some_exchange ---rabbitmq-queue=some_queue + +Configure RabbitMQ and the source kind through CLN plugin options. For example, when starting `lightningd`: + +```bash +lightningd \ + --plugin=/path/to/event-plugin \ + --rabbitmq-url=amqp://guest:guest@localhost:5672/%2f \ + --rabbitmq-exchange=some_exchange \ + --rabbitmq-queue=some_queue \ + --source-kind=gateway ``` The list of notifications advertised to CLN during the plugin handshake is resolved from, in priority order: @@ -51,8 +59,9 @@ The list of notifications advertised to CLN during the plugin handshake is resol 3. A built-in default list covering the common notifications (connect/disconnect, invoices, channels, forwards, payments, coin movements, and more). -Because subscriptions are declared before CLN passes plugin options, both variables must be set in the environment -before CLN starts the plugin. Wildcard subscriptions are not supported. +Because subscriptions are declared before CLN passes plugin options, `EVENT_PLUGIN_EVENTS` or `EVENT_PLUGIN_CONFIG`, +when used, must be present in the plugin process environment before CLN starts it. Wildcard subscriptions are not +supported. If RabbitMQ is not available at **startup**, the plugin exits with an error and CLN will not load it. diff --git a/event-plugin/src/config.rs b/event-plugin/src/config.rs index b16c6d8..b51463c 100644 --- a/event-plugin/src/config.rs +++ b/event-plugin/src/config.rs @@ -1,9 +1,12 @@ use anyhow::{Context, Result, bail}; +use config::builder::{ConfigBuilder, DefaultState}; +use config::{Config, Environment, File, FileFormat}; use serde::Deserialize; -use std::env; +use std::path::PathBuf; -const EVENT_TYPES_ENV: &str = "EVENT_PLUGIN_EVENTS"; -const CONFIG_FILE_ENV: &str = "EVENT_PLUGIN_CONFIG"; +const ENV_PREFIX: &str = "EVENT_PLUGIN"; +const ENV_EVENTS_KEY: &str = "events"; +const CONFIG_EVENTS_KEY: &str = "event-plugin.events_list"; /// Single source of truth for the events subscribed to when neither /// `EVENT_PLUGIN_EVENTS` nor the TOML config file specifies a list. @@ -33,92 +36,169 @@ const DEFAULT_EVENTS: &[&str] = &[ /// 2. `events_list` in the TOML file pointed to by `EVENT_PLUGIN_CONFIG` /// 3. [`DEFAULT_EVENTS`] pub fn resolve_event_types() -> Result> { - let event_types = if let Ok(raw) = env::var(EVENT_TYPES_ENV) { - parse_event_list(&raw) - } else if let Some(list) = event_types_from_config_file()? { - list - } else { - DEFAULT_EVENTS.iter().map(|s| s.to_string()).collect() - }; + let environment = read_environment(environment_source())?; + let mut builder = Config::builder().set_default(CONFIG_EVENTS_KEY, DEFAULT_EVENTS.to_vec())?; - if event_types.is_empty() { - bail!("event subscription list must contain at least one event type"); - } - if event_types.iter().any(|event_type| event_type == "*") { - bail!("wildcard event subscriptions are not supported"); + if let Some(path) = environment.config { + builder = builder.add_source(File::from(path).format(FileFormat::Toml)); } - Ok(event_types) + resolve_event_types_from(builder, environment.events) } -fn parse_event_list(raw: &str) -> Vec { - raw.split(',') - .map(str::trim) - .filter(|event_type| !event_type.is_empty()) - .map(str::to_string) - .collect() +fn environment_source() -> Environment { + Environment::with_prefix(ENV_PREFIX) + .try_parsing(true) + .list_separator(",") + .with_list_parse_key(ENV_EVENTS_KEY) } -#[derive(Deserialize, Default)] -struct ConfigFile { - #[serde(rename = "event-plugin", default)] - event_plugin: EventPluginSection, +fn read_environment(environment: Environment) -> Result { + Config::builder() + .add_source(environment) + .build() + .context("failed to read event-plugin environment")? + .try_deserialize() + .context("failed to parse event-plugin environment") } -#[derive(Deserialize, Default)] -struct EventPluginSection { - events_list: Option>, +fn resolve_event_types_from( + builder: ConfigBuilder, + environment_events: Option>, +) -> Result> { + let event_types = builder + .set_override_option(CONFIG_EVENTS_KEY, environment_events)? + .build() + .context("failed to load event-plugin configuration")? + .get::>(CONFIG_EVENTS_KEY) + .context("failed to parse configured event types")? + .into_iter() + .map(|event_type| event_type.trim().to_owned()) + .filter(|event_type| !event_type.is_empty()) + .collect::>(); + + if event_types.is_empty() { + bail!("event subscription list must contain at least one event type"); + } + if event_types.iter().any(|event_type| event_type == "*") { + bail!("wildcard event subscriptions are not supported"); + } + + Ok(event_types) } -/// Returns the `events_list` from the TOML config file, or `None` when no -/// config file is configured or the file does not set the field. -fn event_types_from_config_file() -> Result>> { - let Ok(path) = env::var(CONFIG_FILE_ENV) else { - return Ok(None); - }; - let raw = std::fs::read_to_string(&path) - .with_context(|| format!("failed to read config file '{path}'"))?; - let config: ConfigFile = - toml::from_str(&raw).with_context(|| format!("failed to parse config file '{path}'"))?; - Ok(config.event_plugin.events_list) +#[derive(Deserialize)] +struct EnvironmentSettings { + config: Option, + events: Option>, } #[cfg(test)] mod test { - use super::{ConfigFile, parse_event_list}; + use super::{ + CONFIG_EVENTS_KEY, DEFAULT_EVENTS, environment_source, read_environment, + resolve_event_types_from, + }; + use config::{Environment, File, FileFormat}; + use std::collections::HashMap; + + fn environment(values: &[(&str, &str)]) -> Environment { + environment_source().source(Some( + values + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect::>(), + )) + } #[test] - fn test_parse_event_list() { + fn defaults_when_sources_do_not_configure_events() { assert_eq!( - parse_event_list(" connect, disconnect ,,invoice_payment"), - vec!["connect", "disconnect", "invoice_payment"] + resolve_event_types_from( + config::Config::builder() + .set_default(CONFIG_EVENTS_KEY, DEFAULT_EVENTS.to_vec()) + .unwrap() + .add_source(File::from_str("", FileFormat::Toml)), + None, + ) + .unwrap(), + DEFAULT_EVENTS ); - assert!(parse_event_list("").is_empty()); } #[test] - fn test_config_file_events_list() { - let config: ConfigFile = toml::from_str( + fn reads_config_path_from_environment() { + let settings = read_environment(environment(&[( + "EVENT_PLUGIN_CONFIG", + "/etc/lightning/event-plugin.toml", + )])) + .unwrap(); + + assert_eq!( + settings.config.unwrap(), + std::path::PathBuf::from("/etc/lightning/event-plugin.toml") + ); + } + + #[test] + fn reads_events_from_toml() { + let file = File::from_str( r#" [event-plugin] events_list = ["connect", "disconnect", "invoice_payment"] "#, - ) - .unwrap(); + FileFormat::Toml, + ); + + assert_eq!( + resolve_event_types_from(config::Config::builder().add_source(file), None).unwrap(), + ["connect", "disconnect", "invoice_payment"] + ); + } + + #[test] + fn environment_overrides_toml_and_parses_the_list() { + let file = File::from_str( + r#" + [event-plugin] + events_list = ["connect"] + "#, + FileFormat::Toml, + ); assert_eq!( - config.event_plugin.events_list, - Some(vec![ - "connect".to_string(), - "disconnect".to_string(), - "invoice_payment".to_string() - ]) + resolve_event_types_from( + config::Config::builder().add_source(file), + read_environment(environment(&[( + "EVENT_PLUGIN_EVENTS", + " disconnect, invoice_payment , ,", + )])) + .unwrap() + .events, + ) + .unwrap(), + ["disconnect", "invoice_payment"] ); } #[test] - fn test_config_file_without_events_list() { - let config: ConfigFile = toml::from_str("").unwrap(); - assert_eq!(config.event_plugin.events_list, None); + fn rejects_empty_and_wildcard_event_lists() { + let empty = resolve_event_types_from( + config::Config::builder(), + read_environment(environment(&[("EVENT_PLUGIN_EVENTS", "")])) + .unwrap() + .events, + ) + .unwrap_err(); + assert!(empty.to_string().contains("at least one event type")); + + let wildcard = resolve_event_types_from( + config::Config::builder(), + read_environment(environment(&[("EVENT_PLUGIN_EVENTS", "connect, *")])) + .unwrap() + .events, + ) + .unwrap_err(); + assert!(wildcard.to_string().contains("wildcard")); } } From b2ad616dcff25339a79122a9b822d9a19cb19d0b Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Tue, 21 Jul 2026 10:28:55 +0300 Subject: [PATCH 08/12] neat: fixed formatting --- metrics-plugin/src/main.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/metrics-plugin/src/main.rs b/metrics-plugin/src/main.rs index 61d11bb..98d126f 100644 --- a/metrics-plugin/src/main.rs +++ b/metrics-plugin/src/main.rs @@ -55,11 +55,7 @@ async fn main() -> Result<()> { .subscribe("forward_event", events::on_forward_event) .subscribe("channel_state_changed", events::on_channel_state_changed) .hook("htlc_accepted", events::hook_htlc_accepted) - .rpcmethod( - "metrics-status", - "Get metrics plugin status", - rpc_status, - ) + .rpcmethod("metrics-status", "Get metrics plugin status", rpc_status) .dynamic() .configure() .await?; From cc3fd2311829548eb57ecf5903bd579168cb3587 Mon Sep 17 00:00:00 2001 From: Oleh Fomenko Date: Wed, 22 Jul 2026 23:47:38 +0300 Subject: [PATCH 09/12] Small refactoring of configuration logic. Adding log with events list Co-authored-by: Oleg Fomenko --- event-plugin/README.md | 9 +- event-plugin/src/config.rs | 224 ++++++++++++++----------------------- event-plugin/src/main.rs | 2 + 3 files changed, 92 insertions(+), 143 deletions(-) diff --git a/event-plugin/README.md b/event-plugin/README.md index d4d7a45..150705c 100644 --- a/event-plugin/README.md +++ b/event-plugin/README.md @@ -48,12 +48,13 @@ lightningd \ The list of notifications advertised to CLN during the plugin handshake is resolved from, in priority order: -1. `EVENT_PLUGIN_EVENTS` environment variable — a comma-separated list of event types. +1. `EVENT_PLUGIN_EVENTS` environment variable — a comma-separated list of event types. Example: + ``` + EVENT_PLUGIN_EVENTS=connect,disconnect,invoice_creation + ``` 2. The TOML config file pointed to by the `EVENT_PLUGIN_CONFIG` environment variable: - ```toml - [event-plugin] - events_list = ["connect", "disconnect", "invoice_payment"] + events = ["connect", "disconnect", "invoice_payment"] ``` 3. A built-in default list covering the common notifications (connect/disconnect, invoices, channels, forwards, diff --git a/event-plugin/src/config.rs b/event-plugin/src/config.rs index b51463c..be9f927 100644 --- a/event-plugin/src/config.rs +++ b/event-plugin/src/config.rs @@ -1,12 +1,11 @@ -use anyhow::{Context, Result, bail}; -use config::builder::{ConfigBuilder, DefaultState}; -use config::{Config, Environment, File, FileFormat}; -use serde::Deserialize; +use anyhow::Result; +use config::{Config, Environment, File}; use std::path::PathBuf; const ENV_PREFIX: &str = "EVENT_PLUGIN"; -const ENV_EVENTS_KEY: &str = "events"; -const CONFIG_EVENTS_KEY: &str = "event-plugin.events_list"; +const ENV_CONFIG_VAR: &str = "EVENT_PLUGIN_CONFIG"; +const ENV_EVENTS_VAR: &str = "EVENT_PLUGIN_EVENTS"; +const CFG_EVENTS_KEY: &str = "events"; /// Single source of truth for the events subscribed to when neither /// `EVENT_PLUGIN_EVENTS` nor the TOML config file specifies a list. @@ -32,173 +31,120 @@ const DEFAULT_EVENTS: &[&str] = &[ ]; /// Resolves the list of events to subscribe to, in priority order: -/// 1. `EVENT_PLUGIN_EVENTS` environment variable (comma-separated) -/// 2. `events_list` in the TOML file pointed to by `EVENT_PLUGIN_CONFIG` -/// 3. [`DEFAULT_EVENTS`] +/// +/// 1. `EVENT_PLUGIN_EVENTS`, as a comma-separated list: +/// +/// ```text +/// EVENT_PLUGIN_EVENTS=connect,disconnect,invoice_payment +/// ``` +/// +/// 2. `events` in the TOML file selected by `EVENT_PLUGIN_CONFIG`: +/// +/// ```toml +/// events = [ +/// "connect", +/// "disconnect", +/// "invoice_payment", +/// ] +/// ``` +/// +/// 3. [`DEFAULT_EVENTS`]. pub fn resolve_event_types() -> Result> { - let environment = read_environment(environment_source())?; - let mut builder = Config::builder().set_default(CONFIG_EVENTS_KEY, DEFAULT_EVENTS.to_vec())?; - - if let Some(path) = environment.config { - builder = builder.add_source(File::from(path).format(FileFormat::Toml)); - } - - resolve_event_types_from(builder, environment.events) -} - -fn environment_source() -> Environment { - Environment::with_prefix(ENV_PREFIX) + let env = Environment::with_prefix(ENV_PREFIX) .try_parsing(true) .list_separator(",") - .with_list_parse_key(ENV_EVENTS_KEY) -} + .with_list_parse_key(CFG_EVENTS_KEY) + .ignore_empty(true); -fn read_environment(environment: Environment) -> Result { - Config::builder() - .add_source(environment) - .build() - .context("failed to read event-plugin environment")? - .try_deserialize() - .context("failed to parse event-plugin environment") -} + let mut builder = Config::builder().set_default(CFG_EVENTS_KEY, DEFAULT_EVENTS.to_vec())?; -fn resolve_event_types_from( - builder: ConfigBuilder, - environment_events: Option>, -) -> Result> { - let event_types = builder - .set_override_option(CONFIG_EVENTS_KEY, environment_events)? - .build() - .context("failed to load event-plugin configuration")? - .get::>(CONFIG_EVENTS_KEY) - .context("failed to parse configured event types")? - .into_iter() - .map(|event_type| event_type.trim().to_owned()) - .filter(|event_type| !event_type.is_empty()) - .collect::>(); - - if event_types.is_empty() { - bail!("event subscription list must contain at least one event type"); + if let Some(path) = std::env::var_os(ENV_CONFIG_VAR) { + builder = builder.add_source(File::from(PathBuf::from(path)).required(false)); } - if event_types.iter().any(|event_type| event_type == "*") { - bail!("wildcard event subscriptions are not supported"); - } - - Ok(event_types) -} -#[derive(Deserialize)] -struct EnvironmentSettings { - config: Option, - events: Option>, + let events: Vec = builder.add_source(env).build()?.get(CFG_EVENTS_KEY)?; + Ok(events) } #[cfg(test)] mod test { - use super::{ - CONFIG_EVENTS_KEY, DEFAULT_EVENTS, environment_source, read_environment, - resolve_event_types_from, - }; - use config::{Environment, File, FileFormat}; - use std::collections::HashMap; - - fn environment(values: &[(&str, &str)]) -> Environment { - environment_source().source(Some( - values - .iter() - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect::>(), - )) + use super::{DEFAULT_EVENTS, ENV_CONFIG_VAR, ENV_EVENTS_VAR, resolve_event_types}; + use std::fs; + use std::path::PathBuf; + fn clear_environment() { + // SAFETY: these tests are expected to run serially. + unsafe { + std::env::remove_var(ENV_CONFIG_VAR); + std::env::remove_var(ENV_EVENTS_VAR); + } } - #[test] - fn defaults_when_sources_do_not_configure_events() { - assert_eq!( - resolve_event_types_from( - config::Config::builder() - .set_default(CONFIG_EVENTS_KEY, DEFAULT_EVENTS.to_vec()) - .unwrap() - .add_source(File::from_str("", FileFormat::Toml)), - None, - ) - .unwrap(), - DEFAULT_EVENTS - ); + fn set_environment(key: &str, value: impl AsRef) { + // SAFETY: these tests are expected to run serially. + unsafe { std::env::set_var(key, value) } + } + + fn test_config_file(contents: &str) -> PathBuf { + let path = std::env::temp_dir().join("event-plugin-config-test.toml"); + fs::write(&path, contents).unwrap(); + path } #[test] - fn reads_config_path_from_environment() { - let settings = read_environment(environment(&[( - "EVENT_PLUGIN_CONFIG", - "/etc/lightning/event-plugin.toml", - )])) - .unwrap(); + fn defaults_when_sources_do_not_configure_events() { + clear_environment(); - assert_eq!( - settings.config.unwrap(), - std::path::PathBuf::from("/etc/lightning/event-plugin.toml") - ); + assert_eq!(resolve_event_types().unwrap(), DEFAULT_EVENTS); } #[test] - fn reads_events_from_toml() { - let file = File::from_str( - r#" - [event-plugin] - events_list = ["connect", "disconnect", "invoice_payment"] - "#, - FileFormat::Toml, - ); + fn reads_events_from_toml_file() { + clear_environment(); + let file = test_config_file(r#"events = ["connect", "disconnect", "invoice_payment"]"#); + set_environment(ENV_CONFIG_VAR, &file); assert_eq!( - resolve_event_types_from(config::Config::builder().add_source(file), None).unwrap(), + resolve_event_types().unwrap(), ["connect", "disconnect", "invoice_payment"] ); + clear_environment(); + fs::remove_file(file).unwrap(); } #[test] fn environment_overrides_toml_and_parses_the_list() { - let file = File::from_str( - r#" - [event-plugin] - events_list = ["connect"] - "#, - FileFormat::Toml, - ); + clear_environment(); + let file = test_config_file(r#"events = ["connect"]"#); + set_environment(ENV_CONFIG_VAR, &file); + set_environment(ENV_EVENTS_VAR, "disconnect,invoice_payment"); assert_eq!( - resolve_event_types_from( - config::Config::builder().add_source(file), - read_environment(environment(&[( - "EVENT_PLUGIN_EVENTS", - " disconnect, invoice_payment , ,", - )])) - .unwrap() - .events, - ) - .unwrap(), + resolve_event_types().unwrap(), ["disconnect", "invoice_payment"] ); + clear_environment(); + fs::remove_file(file).unwrap(); + } + + #[test] + fn ignores_an_empty_events_environment_variable() { + clear_environment(); + let file = test_config_file(r#"events = ["connect", "warning"]"#); + set_environment(ENV_CONFIG_VAR, &file); + set_environment(ENV_EVENTS_VAR, ""); + + assert_eq!(resolve_event_types().unwrap(), ["connect", "warning"]); + clear_environment(); + fs::remove_file(file).unwrap(); } #[test] - fn rejects_empty_and_wildcard_event_lists() { - let empty = resolve_event_types_from( - config::Config::builder(), - read_environment(environment(&[("EVENT_PLUGIN_EVENTS", "")])) - .unwrap() - .events, - ) - .unwrap_err(); - assert!(empty.to_string().contains("at least one event type")); - - let wildcard = resolve_event_types_from( - config::Config::builder(), - read_environment(environment(&[("EVENT_PLUGIN_EVENTS", "connect, *")])) - .unwrap() - .events, - ) - .unwrap_err(); - assert!(wildcard.to_string().contains("wildcard")); + fn missing_optional_config_file_falls_back_to_defaults() { + clear_environment(); + let missing_path = std::env::temp_dir().join("missing-event-plugin-config.toml"); + set_environment(ENV_CONFIG_VAR, missing_path); + + assert_eq!(resolve_event_types().unwrap(), DEFAULT_EVENTS); + clear_environment(); } } diff --git a/event-plugin/src/main.rs b/event-plugin/src/main.rs index 4b048c1..c4b6cce 100644 --- a/event-plugin/src/main.rs +++ b/event-plugin/src/main.rs @@ -70,6 +70,8 @@ async fn main() -> Result<()> { bail!(msg); } + info!("Collecting events={}", event_types?.join(",")); + // Fail if RabbitMQ is not configured, we can not operate without. let Some(url) = get_configured_string_option(&configured, "rabbitmq-url") else { let msg = "'rabbitmq-url' option is required but not set"; From dcc6af4230f00f92ddd5b1eb70f722c65a55b4ae Mon Sep 17 00:00:00 2001 From: Oleh Fomenko Date: Wed, 22 Jul 2026 23:52:02 +0300 Subject: [PATCH 10/12] Bump crate version Co-authored-by: Oleg Fomenko --- Cargo.lock | 2 +- event-plugin/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f654ce7..0b3fcfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -821,7 +821,7 @@ dependencies = [ [[package]] name = "event-plugin" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "cln-plugin", diff --git a/event-plugin/Cargo.toml b/event-plugin/Cargo.toml index 34675fc..d155e2e 100644 --- a/event-plugin/Cargo.toml +++ b/event-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "event-plugin" -version = "0.1.0" +version = "0.2.0" edition = "2024" description = "CLN plugin for collecting events" license.workspace = true From de9b8a5523815ca26999aaa195c77fc33fac66da Mon Sep 17 00:00:00 2001 From: Oleh Fomenko Date: Thu, 23 Jul 2026 14:41:40 +0300 Subject: [PATCH 11/12] [events] Refactoring config, using config trait to get file path env as well. Removed unused env key constant. Making env keys consistent. Co-authored-by: Oleg Fomenko --- event-plugin/src/config.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/event-plugin/src/config.rs b/event-plugin/src/config.rs index be9f927..80045a1 100644 --- a/event-plugin/src/config.rs +++ b/event-plugin/src/config.rs @@ -3,9 +3,8 @@ use config::{Config, Environment, File}; use std::path::PathBuf; const ENV_PREFIX: &str = "EVENT_PLUGIN"; -const ENV_CONFIG_VAR: &str = "EVENT_PLUGIN_CONFIG"; -const ENV_EVENTS_VAR: &str = "EVENT_PLUGIN_EVENTS"; const CFG_EVENTS_KEY: &str = "events"; +const CFG_CONFIG_KEY: &str = "config"; /// Single source of truth for the events subscribed to when neither /// `EVENT_PLUGIN_EVENTS` nor the TOML config file specifies a list. @@ -50,27 +49,37 @@ const DEFAULT_EVENTS: &[&str] = &[ /// /// 3. [`DEFAULT_EVENTS`]. pub fn resolve_event_types() -> Result> { - let env = Environment::with_prefix(ENV_PREFIX) - .try_parsing(true) - .list_separator(",") - .with_list_parse_key(CFG_EVENTS_KEY) - .ignore_empty(true); - + let env = Environment::with_prefix(ENV_PREFIX).ignore_empty(true); let mut builder = Config::builder().set_default(CFG_EVENTS_KEY, DEFAULT_EVENTS.to_vec())?; - if let Some(path) = std::env::var_os(ENV_CONFIG_VAR) { + if let Ok(path) = Config::builder() + .add_source(env.clone()) + .build()? + .get_string(CFG_CONFIG_KEY) + { builder = builder.add_source(File::from(PathBuf::from(path)).required(false)); } - let events: Vec = builder.add_source(env).build()?.get(CFG_EVENTS_KEY)?; + let events: Vec = builder + .add_source( + env.try_parsing(true) + .list_separator(",") + .with_list_parse_key(CFG_EVENTS_KEY), + ) + .build()? + .get(CFG_EVENTS_KEY)?; Ok(events) } #[cfg(test)] mod test { - use super::{DEFAULT_EVENTS, ENV_CONFIG_VAR, ENV_EVENTS_VAR, resolve_event_types}; + use super::{DEFAULT_EVENTS, resolve_event_types}; use std::fs; use std::path::PathBuf; + + const ENV_CONFIG_VAR: &str = "EVENT_PLUGIN_CONFIG"; + const ENV_EVENTS_VAR: &str = "EVENT_PLUGIN_EVENTS"; + fn clear_environment() { // SAFETY: these tests are expected to run serially. unsafe { From 2e4fd53f1ebe175d1feb489f56313c3f68f79afd Mon Sep 17 00:00:00 2001 From: Oleh Fomenko Date: Thu, 23 Jul 2026 14:49:41 +0300 Subject: [PATCH 12/12] [events] Adding comment about tests Co-authored-by: Oleg Fomenko --- event-plugin/src/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/event-plugin/src/config.rs b/event-plugin/src/config.rs index 80045a1..f49cbf7 100644 --- a/event-plugin/src/config.rs +++ b/event-plugin/src/config.rs @@ -71,6 +71,8 @@ pub fn resolve_event_types() -> Result> { Ok(events) } +/// Run tests as follows: +/// cargo test -- --test-threads=1 #[cfg(test)] mod test { use super::{DEFAULT_EVENTS, resolve_event_types};