Log from Radicle native CI

Request message

{
  "request": "trigger",
  "version": 1,
  "event_type": "push",
  "repository": {
    "id": "rad:zwTxygwuz5LDGBq255RA2CbNGrz8",
    "name": "radicle-ci-broker",
    "description": "Radicle CI broker",
    "private": false,
    "default_branch": "main",
    "delegates": [
      "did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV",
      "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
    ]
  },
  "pusher": {
    "id": "did:key:z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV",
    "alias": "liw"
  },
  "before": "f0a995900e57c181e879d6520e47d69d5d6ee891",
  "after": "85414e8673da912b6370f9bf0e82b71046680581",
  "branch": "main",
  "commits": [
    "85414e8673da912b6370f9bf0e82b71046680581"
  ]
}

.radicle/native.yaml

shell: |
  cargo --version
  rustc --version

  cargo fmt --check
  cargo clippy --all-targets --workspace -- -Dwarnings
  cargo build --all-targets --workspace
  cargo doc --workspace
  cargo test --workspace --no-fail-fast

  subplot docgen ci-broker.subplot -o doc/ci-broker.html
  make -C doc publish

Table of contents

Run: git clone /home/_rad/.radicle/storage/zwTxygwuz5LDGBq255RA2CbNGrz8 /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src

Command arguments:

In directory: /

Exit code: 0

Output (stdout and stderr):

Cloning into '/srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src'...
done.

Run: git config advice.detachedHead false

Command arguments:

In directory: /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src

Exit code: 0

Run: git checkout 85414e8673da912b6370f9bf0e82b71046680581

Command arguments:

In directory: /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src

Exit code: 0

Output (stdout and stderr):

HEAD is now at 85414e8 feat: switch to structured logging only

Run: git show 85414e8673da912b6370f9bf0e82b71046680581

Command arguments:

In directory: /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src

Exit code: 0

Output (stdout and stderr):

commit 85414e8673da912b6370f9bf0e82b71046680581
Author: Lars Wirzenius <liw@liw.fi>
Date:   Fri Aug 23 09:50:57 2024 +0300

    feat: switch to structured logging only
    
    Replace all use of log and env-logger crates with structured logging
    using slog and slog-json.
    
    Change the recently introduced logger module to use the slog-scope
    crate. This avoids having to pass a reference to logger::Logging to
    every place that needs to do any logging.
    
    Signed-off-by: Lars Wirzenius <liw@liw.fi>

diff --git a/Cargo.lock b/Cargo.lock
index 0c37144..1e35935 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -178,6 +178,12 @@ version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
 [[package]]
 name = "arraydeque"
 version = "0.5.1"
@@ -500,6 +506,16 @@ version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df"
 
+[[package]]
+name = "ctor"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
+dependencies = [
+ "quote",
+ "syn 2.0.75",
+]
+
 [[package]]
 name = "ctr"
 version = "0.9.2"
@@ -702,16 +718,6 @@ dependencies = [
  "cfg-if",
 ]
 
-[[package]]
-name = "env_filter"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
-dependencies = [
- "log",
- "regex",
-]
-
 [[package]]
 name = "env_logger"
 version = "0.10.2"
@@ -725,19 +731,6 @@ dependencies = [
  "termcolor",
 ]
 
-[[package]]
-name = "env_logger"
-version = "0.11.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
-dependencies = [
- "anstream",
- "anstyle",
- "env_filter",
- "humantime",
- "log",
-]
-
 [[package]]
 name = "equivalent"
 version = "1.0.1"
@@ -1729,10 +1722,9 @@ version = "0.4.0"
 dependencies = [
  "anyhow",
  "clap",
+ "ctor",
  "culpa",
- "env_logger 0.11.5",
  "html-page",
- "log",
  "radicle",
  "radicle-git-ext",
  "radicle-surf",
@@ -1742,6 +1734,7 @@ dependencies = [
  "serde_yml",
  "slog",
  "slog-json",
+ "slog-scope",
  "sqlite",
  "sqlite3-sys",
  "subplot-build",
@@ -2214,6 +2207,17 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "slog-scope"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786"
+dependencies = [
+ "arc-swap",
+ "lazy_static",
+ "slog",
+]
+
 [[package]]
 name = "slug"
 version = "0.1.6"
@@ -2371,7 +2375,7 @@ dependencies = [
  "anyhow",
  "base64 0.22.1",
  "clap",
- "env_logger 0.10.2",
+ "env_logger",
  "file_diff",
  "git-testament",
  "html-escape",
diff --git a/Cargo.toml b/Cargo.toml
index efa093d..c1590af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,9 +13,7 @@ categories = ["development-tools::build-utils"]
 [dependencies]
 anyhow = "1.0.86"
 clap = { version = "4.5.11", features = ["derive", "wrap_help"] }
-env_logger = "0.11.5"
 html-page = "0.4.0"
-log = "0.4.22"
 radicle-git-ext = "0.7.0"
 radicle-surf = { version = "0.18.0", default-features = false, features = ["serde"] }
 regex = "1.10.5"
@@ -24,6 +22,7 @@ serde_json = "1.0.121"
 serde_yml = "0.0.11"
 slog = "2.7.0"
 slog-json = "2.6.1"
+slog-scope = "4.4.0"
 sqlite = "0.32.0"
 sqlite3-sys = "0.15.0"
 subplotlib = "0.10.0"
@@ -36,6 +35,7 @@ version = "0.12.0"
 features = ["default", "test"]
 
 [dev-dependencies]
+ctor = "0.2.8"
 culpa = "1.0.2"
 tempfile = { version = "3.10.1" }
 
diff --git a/src/adapter.rs b/src/adapter.rs
index 46089db..79487ee 100644
--- a/src/adapter.rs
+++ b/src/adapter.rs
@@ -15,10 +15,9 @@ use std::{
     process::{Command, Stdio},
 };
 
-use log::{debug, error};
-
 use crate::{
     db::{Db, DbError},
+    logger,
     msg::{MessageError, Request, Response},
     run::{Run, RunState},
 };
@@ -52,8 +51,6 @@ impl Adapter {
     }
 
     pub fn run(&self, trigger: &Request, run: &mut Run, db: &Db) -> Result<(), AdapterError> {
-        debug!("running adapter");
-
         run.set_state(RunState::Triggered);
         db.update_run(run).map_err(AdapterError::UpdateRun)?;
 
@@ -69,7 +66,6 @@ impl Adapter {
         assert!(matches!(trigger, Request::Trigger { .. }));
 
         // Spawn the adapter sub-process.
-        debug!("spawn adapter sub-process: {:?}", self.bin);
         let mut child = Command::new(&self.bin)
             .stdin(Stdio::piped())
             .stdout(Stdio::piped())
@@ -81,7 +77,6 @@ impl Adapter {
         // Write the request to trigger a run to the child's stdin.
         // Then close the pipe to prevent the child from trying to
         // read another message that will never be sent.
-        debug!("send request to sub-process stdin");
         {
             let stdin = child.stdin.take().ok_or(AdapterError::StdinHandle)?;
             trigger
@@ -98,7 +93,6 @@ impl Adapter {
         if let Some(line) = lines.next() {
             let line = line.map_err(AdapterError::ReadLine)?;
             let resp = Response::from_str(&line).map_err(AdapterError::ParseResponse)?;
-            debug!("first response {resp:#?}");
             match resp {
                 Response::Triggered { run_id, info_url } => {
                     run.set_state(RunState::Running);
@@ -111,13 +105,12 @@ impl Adapter {
                 _ => return Err(AdapterError::NotTriggered(resp)),
             }
         } else {
-            debug!("no first response message");
+            logger::adapter_no_first_response();
         }
 
         if let Some(line) = lines.next() {
             let line = line.map_err(AdapterError::ReadLine)?;
             let resp = Response::from_str(&line).map_err(AdapterError::ParseResponse)?;
-            debug!("second response {resp:#?}");
             match resp {
                 Response::Finished { result } => {
                     run.set_result(result);
@@ -126,22 +119,18 @@ impl Adapter {
                 _ => return Err(AdapterError::NotFinished(resp)),
             }
         } else {
-            debug!("no second response message");
+            logger::adapter_no_second_response();
         }
 
         if let Some(line) = lines.next() {
             let line = line.map_err(AdapterError::ReadLine)?;
             let resp = Response::from_str(&line).map_err(AdapterError::ParseResponse)?;
-            debug!("third response is too many {resp:#?}");
+            logger::adapter_too_many_responses();
             return Err(AdapterError::TooMany(resp));
-        } else {
-            debug!("no further response messages");
         }
 
         let wait = child.wait().map_err(AdapterError::Wait)?;
-        debug!("adapter wait result {wait:#?}");
         if let Some(exit) = wait.code() {
-            debug!("adapter exit was {exit}");
             if exit != 0 {
                 let mut stderr = child.stderr.take().ok_or(AdapterError::StderrHandle)?;
                 let mut buf = vec![];
@@ -149,11 +138,11 @@ impl Adapter {
                     .read_to_end(&mut buf)
                     .map_err(AdapterError::ReadStderr)?;
                 let stderr = String::from_utf8_lossy(&buf);
-                debug!("adapter stderr: {stderr:?}");
+                logger::adapter_result(exit, &stderr.to_string());
                 return Err(AdapterError::Failed(exit));
             }
         } else {
-            error!("adapter sub-process did not exit voluntarily");
+            logger::adapter_did_not_exit_voluntarioly();
             return Err(AdapterError::Failed(NOT_EXITED));
         }
 
@@ -233,7 +222,7 @@ mod test {
         adapter::AdapterError,
         msg::{MessageError, Response, RunResult},
         run::Whence,
-        test::{log_in_tests, mock_adapter, trigger_request, TestResult},
+        test::{mock_adapter, trigger_request, TestResult},
     };
 
     fn db() -> anyhow::Result<Db> {
@@ -257,8 +246,6 @@ mod test {
 
     #[test]
     fn adapter_reports_success() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -279,8 +266,6 @@ echo '{"response":"finished","result":"success"}'
 
     #[test]
     fn adapter_reports_failure() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -306,8 +291,6 @@ echo '{"response":"finished","result":"failure"}'
 
     #[test]
     fn adapter_exits_nonzero() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -332,8 +315,6 @@ exit 1
 
     #[test]
     fn adapter_is_killed_before_any_messages() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 kill -9 $BASHPID
 "#;
@@ -356,8 +337,6 @@ kill -9 $BASHPID
 
     #[test]
     fn adapter_is_killed_after_first_message() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -379,8 +358,6 @@ kill -9 $BASHPID
 
     #[test]
     fn adapter_is_killed_after_second_message() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -403,8 +380,6 @@ kill -9 $BASHPID
 
     #[test]
     fn adapter_produces_as_bad_message() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -429,8 +404,6 @@ echo '{"response":"finished","result":"success","bad":"field"}'
 
     #[test]
     fn adapter_first_message_isnt_triggered() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"finished","result":"success"}'
@@ -456,8 +429,6 @@ echo '{"response":"finished","result":"success"}'
 
     #[test]
     fn adapter_outputs_too_many_messages() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -485,8 +456,6 @@ echo '{"response":"finished","result":"success"}'
 
     #[test]
     fn adapter_does_not_exist() -> TestResult<()> {
-        log_in_tests();
-
         let tmp = tempdir()?;
         let bin = tmp.path().join("adapter.sh");
 
@@ -507,8 +476,6 @@ echo '{"response":"finished","result":"success"}'
 
     #[test]
     fn adapter_is_not_executable() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
@@ -536,8 +503,6 @@ echo '{"response":"finished","result":"success"}'
 
     #[test]
     fn adapter_has_bad_interpreter() -> TestResult<()> {
-        log_in_tests();
-
         // We test this with a shebang. However, the same kind of code
         // paths and errors should happen when a binary can't be
         // loaded due to missing dynamic linker or library or such.
diff --git a/src/bin/cib.rs b/src/bin/cib.rs
index f8685cb..c027ad0 100644
--- a/src/bin/cib.rs
+++ b/src/bin/cib.rs
@@ -3,14 +3,12 @@
 #![allow(clippy::result_large_err)]
 
 use std::{
-    error::Error,
     fs::write,
     path::{Path, PathBuf},
     process::exit,
 };
 
 use clap::Parser;
-use log::{debug, error, info};
 
 use radicle::Profile;
 
@@ -19,7 +17,7 @@ use radicle_ci_broker::{
     broker::{Broker, BrokerError},
     config::{Config, ConfigError},
     db::{Db, DbError},
-    logger::Logger,
+    logger,
     notif::NotificationChannel,
     pages::{PageBuilder, PageError},
     queueadd::{AdderError, QueueAdderBuilder},
@@ -27,31 +25,22 @@ use radicle_ci_broker::{
 };
 
 fn main() {
-    let logger = Logger::default();
-    logger.start_cib();
+    let _logger = logger::open();
 
+    logger::start_cib();
     if let Err(e) = fallible_main() {
-        error!("ERROR: {}", e);
-        let mut e = e.source();
-        while let Some(source) = e {
-            error!("caused by: {}", source);
-            e = source.source();
-        }
-        logger.end_cib_in_error();
+        logger::error("ERROR", &e);
+        logger::end_cib_in_error();
         exit(1);
     }
-
-    logger.end_cib_successfully();
+    logger::end_cib_successfully();
 }
 
 fn fallible_main() -> Result<(), CibError> {
     let args = Args::parse();
 
-    env_logger::init_from_env("RADICLE_CI_BROKER_LOG");
-    info!("Radicle CI broker starts");
-
     let config = Config::load(&args.config).map_err(|e| CibError::read_config(&args.config, e))?;
-    debug!("loaded configuration: {:#?}", config);
+    logger::loaded_config(&config);
 
     args.run(&config)?;
 
@@ -131,7 +120,6 @@ impl InsertCmd {
             .join()
             .expect("wait for thread to finish")
             .map_err(CibError::add_events)?;
-        debug!("cib ends");
         Ok(())
     }
 }
@@ -153,7 +141,7 @@ impl QueuedCmd {
         let adapter = Adapter::new(&spec.command)
             .with_environment(spec.envs())
             .with_environment(spec.sensitive_envs());
-        debug!("default adapter: {adapter:?}");
+        logger::debug2(format!("default adapter: {adapter:?}"));
         broker.set_default_adapter(&adapter);
 
         let mut event_notifications = NotificationChannel::default();
@@ -174,7 +162,6 @@ impl QueuedCmd {
             .expect("wait for thread to finish")
             .map_err(CibError::process_queue)?;
 
-        debug!("cib ends");
         Ok(())
     }
 }
@@ -221,7 +208,7 @@ impl ProcessEventsCmd {
         let adapter = Adapter::new(&spec.command)
             .with_environment(spec.envs())
             .with_environment(spec.sensitive_envs());
-        debug!("default adapter: {adapter:?}");
+        logger::debug2(format!("default adapter: {adapter:?}"));
         broker.set_default_adapter(&adapter);
 
         let processor = QueueProcessorBuilder::default()
@@ -236,18 +223,14 @@ impl ProcessEventsCmd {
             .join()
             .expect("wait for processor thread to finish")
             .map_err(CibError::process_queue)?;
-        info!("event processing thread has ended");
 
         // The page updating thread ends when the channel for run
         // notifications is closed by the processor thread ending.
-        info!("wait for page updater thread to end");
         page_updater
             .join()
             .expect("wait for page updater thread to finish")
             .expect("page updater thread succeeded");
-        info!("page updater thread has ended");
 
-        debug!("cib ends");
         Ok(())
     }
 }
diff --git a/src/bin/cibtool.rs b/src/bin/cibtool.rs
index 209e98a..5500ae9 100644
--- a/src/bin/cibtool.rs
+++ b/src/bin/cibtool.rs
@@ -54,11 +54,8 @@ fn main() {
 
 #[allow(clippy::result_large_err)]
 fn fallible_main() -> Result<(), CibToolError> {
-    env_logger::init_from_env("RADICLE_CI_BROKER_LOG");
-
     let args = Args::parse();
     args.cmd.run(&args)?;
-
     Ok(())
 }
 
diff --git a/src/broker.rs b/src/broker.rs
index a97d350..e524691 100644
--- a/src/broker.rs
+++ b/src/broker.rs
@@ -5,11 +5,9 @@
 
 use std::{
     collections::HashMap,
-    error::Error,
     path::{Path, PathBuf},
 };
 
-use log::{debug, error, info};
 use time::{macros::format_description, OffsetDateTime};
 
 use radicle::prelude::RepoId;
@@ -17,6 +15,7 @@ use radicle::prelude::RepoId;
 use crate::{
     adapter::Adapter,
     db::{Db, DbError},
+    logger,
     msg::{PatchEvent, PushEvent, Request},
     run::{Run, Whence},
 };
@@ -35,7 +34,7 @@ pub struct Broker {
 impl Broker {
     #[allow(clippy::result_large_err)]
     pub fn new(db_filename: &Path) -> Result<Self, BrokerError> {
-        debug!("broker database in {}", db_filename.display());
+        logger::broker_db(db_filename);
         Ok(Self {
             default_adapter: None,
             adapters: HashMap::new(),
@@ -66,8 +65,7 @@ impl Broker {
 
     #[allow(clippy::result_large_err)]
     pub fn execute_ci(&mut self, trigger: &Request) -> Result<Run, BrokerError> {
-        info!("Start CI run");
-        debug!("Start CI run on {trigger:#?}");
+        logger::broker_start_run(trigger);
         let run = match trigger {
             Request::Trigger {
                 common,
@@ -104,24 +102,17 @@ impl Broker {
                     // We run the adapter, but if that fails, we just
                     // log the error. The `Run` value records the
                     // result of the run.
-                    debug!("broker runs adapter");
                     if let Err(e) = adapter.run(trigger, &mut run, &self.db) {
-                        error!("failed to run adapter or it failed to run CI: {e}");
-                        let mut e = e.source();
-                        while let Some(source) = e {
-                            error!("caused by: {}", source);
-                            e = source.source();
-                        }
+                        logger::error("failed to run adapter or it failed to run CI", &e);
                     }
 
-                    debug!("broker run {run:#?}");
                     run
                 } else {
                     return Err(BrokerError::NoAdapter(*rid));
                 }
             }
         };
-        info!("Finish CI run: {run:?}");
+        logger::broker_end_run(&run);
 
         self.db.update_run(&run)?;
 
@@ -188,7 +179,7 @@ mod test {
     use crate::{
         msg::{RunId, RunResult},
         run::RunState,
-        test::{log_in_tests, mock_adapter, trigger_request, TestResult},
+        test::{mock_adapter, trigger_request, TestResult},
     };
 
     fn broker(filename: &Path) -> anyhow::Result<Broker> {
@@ -309,8 +300,6 @@ echo '{"response":"finished","result":"success"}'
 
     #[test]
     fn adapter_fails() -> TestResult<()> {
-        log_in_tests();
-
         const ADAPTER: &str = r#"#!/bin/bash
 read
 echo '{"response":"triggered","run_id":{"id":"xyzzy"}}'
diff --git a/src/db.rs b/src/db.rs
index 954f508..42d0a91 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -16,7 +16,6 @@ use std::{
     time::Duration,
 };
 
-use log::{debug, trace};
 use sqlite::{Connection, State, Statement};
 use time::{macros::format_description, OffsetDateTime};
 use uuid::Uuid;
@@ -38,7 +37,6 @@ impl Db {
     /// If it is created, tables are created.
     pub fn new<P: AsRef<Path>>(filename: P) -> Result<Self, DbError> {
         let filename = filename.as_ref();
-        debug!("open database {}", filename.display());
         let mut db = Db {
             filename: filename.into(),
             conn: sqlite::open(filename).map_err(|e| DbError::open(filename, e))?,
@@ -46,7 +44,6 @@ impl Db {
 
         debug_assert!(MAX_WAIT.as_millis() < usize::MAX as u128); // safe conversion
         let ms = MAX_WAIT.as_millis() as usize;
-        trace!("set busy timeout to {ms} milliseconds");
         db.conn
             .set_busy_timeout(ms)
             .map_err(|e| DbError::busy_timer(filename, e))?;
@@ -96,7 +93,6 @@ impl Db {
 
     // Prepare a statement for execution.
     fn prepare<'a>(&'a self, sql: &str) -> Result<Stmt<'a>, DbError> {
-        trace!("prepare {}", sql);
         match self.conn.prepare(sql) {
             Ok(stmt) => Ok(Stmt::new(sql, stmt)),
             Err(e) => Err(DbError::prepare(sql, &self.filename, e)),
@@ -106,7 +102,6 @@ impl Db {
     // Execute a statement that doesn't return any rows with values.
     // This means basically any statement except SELECT.
     fn execute_valueless(stmt: &mut Stmt) -> Result<(), DbError> {
-        trace!("execute {}", stmt.sql);
         stmt.stmt.reset().map_err(DbError::reset)?;
         match stmt.stmt.next() {
             Ok(_) => Ok(()),
diff --git a/src/event.rs b/src/event.rs
index 9abf1c6..67ebb0d 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -27,7 +27,6 @@
 //! let e = Filters::try_from(filters).unwrap();
 //! ```
 
-use log::{debug, error, info, trace};
 use radicle::{
     node::{Event, Handle, NodeId},
     prelude::RepoId,
@@ -43,6 +42,8 @@ use std::{
     time,
 };
 
+use crate::logger;
+
 /// Source of events from the local Radicle node.
 ///
 /// The events are filtered. Only events allowed by at least one
@@ -57,30 +58,18 @@ impl NodeEventSource {
     /// Create a new source of node events, for a given Radicle
     /// profile.
     pub fn new(profile: &Profile) -> Result<Self, NodeEventError> {
-        info!("subscribing to local node events");
         let socket = profile.socket();
-        info!(
-            "profile socket exists? {:#?} {}",
-            socket.display(),
-            socket.exists()
-        );
         if !socket.exists() {
             return Err(NodeEventError::NoControlSocket(socket));
         }
-        debug!("about to Node::new()");
         let node = radicle::Node::new(socket.clone());
-        debug!("{node:#?}");
-        debug!("about to node.subscribe()");
         match node.subscribe(time::Duration::MAX) {
-            Ok(events) => {
-                trace!("subscribed OK");
-                Ok(Self {
-                    events: Box::new(events.into_iter()),
-                    allowed: vec![],
-                })
-            }
+            Ok(events) => Ok(Self {
+                events: Box::new(events.into_iter()),
+                allowed: vec![],
+            }),
             Err(err) => {
-                info!("failed to subscribe to node events");
+                logger::error("failed to subscribe to node events", &err);
                 Err(NodeEventError::CannotSubscribe(socket.clone(), err))
             }
         }
@@ -94,11 +83,9 @@ impl NodeEventSource {
     fn allowed(&self, event: &BrokerEvent) -> Result<bool, NodeEventError> {
         for filter in self.allowed.iter() {
             if !event.is_allowed(filter)? {
-                trace!("event is not allowed");
                 return Ok(false);
             }
         }
-        trace!("event is allowed");
         Ok(true)
     }
 
@@ -107,45 +94,34 @@ impl NodeEventSource {
     /// no more events from this source, or there's an error.
     pub fn event(&mut self) -> Result<Vec<BrokerEvent>, NodeEventError> {
         loop {
-            debug!("getting next event from local node");
             if let Some(event) = self.events.next() {
-                debug!("got event from local node");
                 match event {
                     Ok(event) => {
-                        debug!("got node event {:#?}", event);
                         let mut result = vec![];
                         if let Some(broker_events) = BrokerEvent::from_event(&event) {
-                            info!("node event became {} broker events", broker_events.len());
                             for e in broker_events {
                                 if self.allowed(&e)? {
-                                    info!("allowed event {e:?}");
                                     result.push(e);
                                 }
                             }
                             if !result.is_empty() {
-                                info!("got {} allowed broker events from node event", result.len());
                                 return Ok(result);
-                            } else {
-                                info!("got no allowed broker events from node event, ignoring it");
                             }
                         }
-                        debug!(
-                            "got event, but did not result in broker events, or none were allowed"
-                        );
                     }
                     Err(radicle::node::Error::Io(err))
                         if err.kind() == std::io::ErrorKind::ConnectionReset =>
                     {
-                        error!("connection to node control socket broke");
+                        logger::event_disconnected();
                         return Err(NodeEventError::BrokenConnection);
                     }
                     Err(err) => {
-                        error!("error reading event from node: {err}");
+                        logger::error("error reading event from node", &err);
                         return Err(NodeEventError::Node(err));
                     }
                 }
             } else {
-                info!("no more node events from control socket: iterator ended");
+                logger::event_end();
                 return Ok(vec![]);
             }
         }
@@ -353,15 +329,12 @@ impl BrokerEvent {
                     RefUpdate::Skipped { name, oid }
                         if name.as_str() == "shutdown" && oid.is_zero() =>
                     {
-                        info!("received shutdown event from node");
                         events.push(Self::shutdown());
                     }
                     RefUpdate::Created { name, oid } => {
-                        info!("ref created {name} {oid}");
                         events.push(Self::new(rid, name, oid, None));
                     }
                     RefUpdate::Updated { name, old, new } => {
-                        info!("ref updated {name} {old} => {new}");
                         events.push(Self::new(rid, name, new, Some(*old)));
                     }
                     _ => (),
@@ -375,37 +348,20 @@ impl BrokerEvent {
 
     /// Is this broker event allowed by a filter?
     fn is_allowed(&self, filter: &EventFilter) -> Result<bool, NodeEventError> {
-        debug!("is_allowed called: filter={filter:?}");
-        let res = self.is_allowed_helper(filter, 0)?;
-        debug!("is_allowed: res={res}");
+        let res = self.is_allowed_helper(filter)?;
         Ok(res)
     }
 
-    fn is_allowed_helper(
-        &self,
-        filter: &EventFilter,
-        level: usize,
-    ) -> Result<bool, NodeEventError> {
-        let prefix = format!("{:width$}", " ", width = level * 4);
-
-        trace!("is_allowed: {prefix} called {self:?}");
-
+    fn is_allowed_helper(&self, filter: &EventFilter) -> Result<bool, NodeEventError> {
         let allowed = match self {
             Self::Shutdown => true,
             Self::RefChanged {
                 rid,
                 name,
-                oid,
-                old,
+                oid: _,
+                old: _,
             } => {
-                trace!("is_allowed: {prefix} rid={rid:?}");
-                trace!("is_allowed: {prefix} name={name:?}");
-                trace!("is_allowed: {prefix} oid={oid:?}");
-                trace!("is_allowed: {prefix} old={old:?}");
-                trace!("is_allowed: {prefix} filter={filter:?}");
-
                 let parsed = parse_ref(name)?;
-                trace!("is_allowed: {prefix} parsed={parsed:?}");
 
                 match filter {
                     EventFilter::Repository(wanted) => rid == wanted,
@@ -426,18 +382,17 @@ impl BrokerEvent {
                     }
                     EventFilter::And(conds) => conds
                         .iter()
-                        .all(|cond| self.is_allowed_helper(cond, level + 1).unwrap_or(false)),
+                        .all(|cond| self.is_allowed_helper(cond).unwrap_or(false)),
                     EventFilter::Or(conds) => conds
                         .iter()
-                        .any(|cond| self.is_allowed_helper(cond, level + 1).unwrap_or(false)),
+                        .any(|cond| self.is_allowed_helper(cond).unwrap_or(false)),
                     EventFilter::Not(conds) => !conds
                         .iter()
-                        .any(|cond| self.is_allowed_helper(cond, level + 1).unwrap_or(false)),
+                        .any(|cond| self.is_allowed_helper(cond).unwrap_or(false)),
                 }
             }
         };
 
-        trace!("is_allowed: {prefix} allowed={allowed}");
         Ok(allowed)
     }
 
@@ -564,18 +519,13 @@ pub enum ParsedRef {
 /// }
 /// ```
 pub fn parse_ref(s: &str) -> Result<Option<ParsedRef>, NodeEventError> {
-    trace!("parse_ref: s={s:?}");
-
     const PAT_PATCH: &str = r"^refs/namespaces/[^/]+/refs/heads/patches/([^/]+)$";
     let patch_re = Regex::new(PAT_PATCH).map_err(|e| NodeEventError::Regex(PAT_PATCH, e))?;
     if let Some(patch_captures) = patch_re.captures(s) {
-        trace!("parse_ref: patch_captures={patch_captures:?}");
         if let Some(patch_id) = patch_captures.get(1) {
-            trace!("parse_ref: patch_id={patch_id:?}");
             let patch_id_str = patch_id.as_str();
             let oid = Oid::try_from(patch_id_str)
                 .map_err(|e| NodeEventError::ParseOid(patch_id_str.into(), e))?;
-            trace!("parse_ref: patch oid={oid:?}");
             return Ok(Some(ParsedRef::Patch(oid)));
         }
     }
@@ -583,40 +533,26 @@ pub fn parse_ref(s: &str) -> Result<Option<ParsedRef>, NodeEventError> {
     const PAT_BRANCH: &str = r"^refs/namespaces/[^/]+/refs/heads/(.+)$";
     let push_re = Regex::new(PAT_BRANCH).map_err(|e| NodeEventError::Regex(PAT_BRANCH, e))?;
     if let Some(push_captures) = push_re.captures(s) {
-        trace!("parse_ref: push_captures={push_captures:?}");
         if let Some(branch) = push_captures.get(1) {
-            trace!("parse_ref: branch={branch:?}");
             return Ok(Some(ParsedRef::Push(branch.as_str().to_string())));
         }
     }
 
-    trace!("parse_ref: neither push nor patch");
     Ok(None)
 }
 
-#[cfg(test)]
-fn log_init() {
-    let _ = env_logger::builder()
-        .is_test(true)
-        .format_timestamp(None)
-        .filter_level(log::LevelFilter::Trace)
-        .try_init();
-}
-
 #[cfg(test)]
 mod test_parse_ref {
-    use super::{log_init, parse_ref, Oid, ParsedRef};
+    use super::{parse_ref, Oid, ParsedRef};
 
     #[test]
     fn plain_branch_name_is_none() -> anyhow::Result<()> {
-        log_init();
         assert_eq!(parse_ref("main")?, None);
         Ok(())
     }
 
     #[test]
     fn namespaced_branch() -> anyhow::Result<()> {
-        log_init();
         assert_eq!(
             parse_ref(
                 "refs/namespaces/z6MkfBU2cwcZfaE6Z1dLqb7Ve7u4pdgbSo9tP6qUVsqFn2xv/refs/heads/main"
@@ -628,7 +564,6 @@ mod test_parse_ref {
 
     #[test]
     fn namespaced_branch_with_slashes() -> anyhow::Result<()> {
-        log_init();
         assert_eq!(
             parse_ref(
                 "refs/namespaces/z6MkfBU2cwcZfaE6Z1dLqb7Ve7u4pdgbSo9tP6qUVsqFn2xv/refs/heads/liw/cob/draft/v2"
@@ -640,7 +575,6 @@ mod test_parse_ref {
 
     #[test]
     fn namespaced_patch() -> anyhow::Result<()> {
-        log_init();
         const SHA: &str = "0a4c69183fc8b8d849f5ab977d70f2a1f4788bca";
         assert_eq!(
             parse_ref(&format!("refs/namespaces/NID/refs/heads/patches/{SHA}"))?,
@@ -672,11 +606,10 @@ pub fn push_branch(name: &str) -> String {
 
 #[cfg(test)]
 mod test {
-    use super::{is_patch_update, log_init, parse_ref, push_branch, Oid, ParsedRef};
+    use super::{is_patch_update, parse_ref, push_branch, Oid, ParsedRef};
 
     #[test]
     fn test_parse_patch_ref() -> anyhow::Result<()> {
-        log_init();
         let patch_ref =
             "refs/namespaces/NID/refs/heads/patches/9183ed6232687d3105482960cecb01a53018b80a";
 
@@ -691,7 +624,6 @@ mod test {
 
     #[test]
     fn test_parse_push_ref() -> anyhow::Result<()> {
-        log_init();
         let push_ref =
             "refs/namespaces/z6MkuhvCnrcow7vzkyQzkuFixzpTa42iC2Cfa4DA8HRLCmys/refs/heads/main";
         let parsed_ref = parse_ref(push_ref)?;
@@ -708,7 +640,6 @@ mod test {
 
     #[test]
     fn test_parse_invalid_ref() -> anyhow::Result<()> {
-        log_init();
         let invalid_ref = "invalid_ref";
         let parsed_ref = parse_ref(invalid_ref)?;
         assert!(parsed_ref.is_none());
@@ -717,7 +648,6 @@ mod test {
 
     #[test]
     fn branch_is_not_patch_update() {
-        log_init();
         assert_eq!(
             is_patch_update(
                 "refs/namespaces/z6MkuhvCnrcow7vzkyQzkuFixzpTa42iC2Cfa4DA8HRLCmys/refs/heads/main"
@@ -728,7 +658,6 @@ mod test {
 
     #[test]
     fn patch_branch_is_not_patch_update() {
-        log_init();
         assert_eq!(
             is_patch_update(
                 "refs/namespaces/z6MkuhvCnrcow7vzkyQzkuFixzpTa42iC2Cfa4DA8HRLCmys/refs/heads/patches/bbb54a2c9314a528a4fff9d6c2aae874ed098433"
@@ -739,7 +668,6 @@ mod test {
 
     #[test]
     fn patch_update() {
-        log_init();
         assert_eq!(
             is_patch_update(
                 "refs/namespaces/z6MkuhvCnrcow7vzkyQzkuFixzpTa42iC2Cfa4DA8HRLCmys/refs/cobs/xyz.radicle.patch/bbb54a2c9314a528a4fff9d6c2aae874ed098433"
@@ -750,7 +678,6 @@ mod test {
 
     #[test]
     fn get_push_branch() {
-        log_init();
         assert_eq!(
             push_branch(
                 "refs/namespaces/z6MkuhvCnrcow7vzkyQzkuFixzpTa42iC2Cfa4DA8HRLCmys/refs/heads/branch_name"
@@ -761,7 +688,6 @@ mod test {
 
     #[test]
     fn get_no_push_branch() {
-        log_init();
         assert_eq!(
             push_branch(
                 "refs/namespaces/z6MkuhvCnrcow7vzkyQzkuFixzpTa42iC2Cfa4DA8HRLCmys/refs/rad/sigrefs"
diff --git a/src/logger.rs b/src/logger.rs
index 04a89b9..f331f23 100644
--- a/src/logger.rs
+++ b/src/logger.rs
@@ -1,34 +1,224 @@
 //! A logger abstraction for the CI broker.
 
-use std::sync::Mutex;
+#[cfg(test)]
+use std::sync::Once;
+use std::{path::Path, sync::Mutex, time::Duration};
 
-use slog::{info, o, Drain};
+use radicle::{git::raw::Oid, identity::RepoId};
+use slog::{debug, error, info, o, warn, Drain};
+use slog_scope::GlobalLoggerGuard;
 
-pub struct Logger {
-    root: slog::Logger,
+use crate::{config::Config, db::QueueId, event::BrokerEvent, msg::Request, run::Run};
+
+pub fn open() -> GlobalLoggerGuard {
+    let logger = slog_json::Json::new(std::io::stderr())
+        .add_default_keys()
+        .set_flush(true)
+        .set_newlines(true)
+        .build();
+    let log = slog::Logger::root(Mutex::new(logger).fuse(), o!());
+    slog_scope::set_global_logger(log)
 }
 
-impl Default for Logger {
-    fn default() -> Self {
-        Self {
-            root: slog::Logger::root(
-                Mutex::new(slog_json::Json::default(std::io::stderr())).map(slog::Fuse),
-                o!(),
-            ),
-        }
-    }
+// Set up structured logging for tests.
+//
+// We have to keep the GlobalLoggerGuard we get when we initialize
+// `slog-scope` alive as long as we may need to do any logging. We can
+// only create that once per process. For tests, we do that here.
+//
+// We can't do the same thing for non-test processes, as that would
+// interfere with use of `radicle-ci-broker` as a library. Libraries
+// should not interfere with global state, unless they're specifically
+// intended to do that.
+//
+// This is for tests only: otherwise
+
+// We use this to make sure we initialize the logger only once.
+#[cfg(test)]
+static INIT: Once = Once::new();
+
+// This is the actual logger for tests.
+#[cfg(test)]
+static mut LOGGER: Option<GlobalLoggerGuard> = None;
+
+#[cfg(test)]
+#[ctor::ctor]
+fn open_for_tests() {
+    INIT.call_once(|| unsafe {
+        LOGGER = Some(open());
+    });
 }
 
-impl Logger {
-    pub fn start_cib(&self) {
-        info!(&self.root, "CI broker starts");
-    }
+pub fn start_cib() {
+    info!(slog_scope::logger(), "CI broker starts");
+}
 
-    pub fn end_cib_successfully(&self) {
-        info!(&self.root, "CI broker ends successfully");
-    }
+pub fn end_cib_successfully() {
+    info!(slog_scope::logger(), "CI broker ends successfully");
+}
+
+pub fn end_cib_in_error() {
+    info!(
+        slog_scope::logger(),
+        "CI broker ends in unrecoverable error"
+    );
+}
+
+pub fn loaded_config(config: &Config) {
+    debug!(slog_scope::logger(), "loaded configuration {config:#?}");
+}
+
+pub fn queueproc_start() {
+    info!(
+        slog_scope::logger(),
+        "start thread to process events until a shutdown event"
+    );
+}
+
+pub fn queueproc_end() {
+    info!(slog_scope::logger(), "start thread to process events ends");
+}
+
+pub fn queueproc_channel_disconnect() {
+    info!(
+        slog_scope::logger(),
+        "event notification channel disconnected"
+    );
+}
+
+pub fn queueproc_remove_event(id: &QueueId) {
+    info!(slog_scope::logger(), "remove event from queue: {id}");
+}
+
+pub fn queueproc_action_run(rid: &RepoId, oid: &Oid) {
+    info!(slog_scope::logger(), "Action: run: {rid} {oid}");
+}
+
+pub fn queueproc_action_shutdown() {
+    info!(slog_scope::logger(), "Action: shutdown");
+}
+
+pub fn queueadd_start() {
+    info!(
+        slog_scope::logger(),
+        "start thread to add events from node to event queue"
+    );
+}
+
+pub fn queueadd_control_socket_close() {
+    info!(
+        slog_scope::logger(),
+        "no more events from node control sockets"
+    );
+}
+
+pub fn queueadd_push_event(e: &BrokerEvent) {
+    debug!(
+        slog_scope::logger(),
+        "insert broker event into queue: {e:?}"
+    );
+}
+
+pub fn queueadd_end() {
+    info!(slog_scope::logger(), "start thread to process events ends");
+}
+
+pub fn pages_directory_unset() {
+    warn!(
+        slog_scope::logger(),
+        "not writing HTML report pages as output directory has not been set"
+    );
+}
+
+pub fn pages_interval(interval: Duration) {
+    info!(
+        slog_scope::logger(),
+        "wait about {} seconds to update HTML report pages again",
+        interval.as_secs()
+    );
+}
+
+pub fn pages_disconnected() {
+    info!(
+        slog_scope::logger(),
+        "page updater: run notification channel disconnected"
+    );
+}
+
+pub fn pages_start() {
+    info!(slog_scope::logger(), "start page updater thread");
+}
+
+pub fn pages_end() {
+    info!(slog_scope::logger(), "end page updater thread");
+}
+
+pub fn event_disconnected() {
+    info!(
+        slog_scope::logger(),
+        "connection to node control socket broke"
+    );
+}
+
+pub fn event_end() {
+    info!(
+        slog_scope::logger(),
+        "no more node events from control socket: iterator ended"
+    );
+}
+
+pub fn broker_db(filename: &Path) {
+    info!(
+        slog_scope::logger(),
+        "broker database: {}",
+        filename.display()
+    );
+}
+
+pub fn broker_start_run(trigger: &Request) {
+    info!(slog_scope::logger(), "start CI run");
+    debug!(slog_scope::logger(), "trigger event: {trigger:#?}");
+}
+
+pub fn broker_end_run(run: &Run) {
+    info!(slog_scope::logger(), "Finish CI run");
+    debug!(slog_scope::logger(), "finished CI run: {run:#?}");
+}
+
+pub fn adapter_no_first_response() {
+    error!(slog_scope::logger(), "no first response message");
+}
+
+pub fn adapter_no_second_response() {
+    error!(slog_scope::logger(), "no second response message");
+}
+
+pub fn adapter_too_many_responses() {
+    error!(slog_scope::logger(), "too many response messages");
+}
+
+pub fn adapter_result(exit: i32, stderr: &String) {
+    debug!(slog_scope::logger(), "adapter exit code: {exit}");
+    debug!(slog_scope::logger(), "adapter stderr: {stderr}");
+}
+
+pub fn adapter_did_not_exit_voluntarioly() {
+    warn!(slog_scope::logger(), "adapter did not exit voluntarily");
+}
+
+pub fn debug(msg: &str) {
+    debug!(slog_scope::logger(), "{msg}");
+}
+
+pub fn debug2(msg: String) {
+    debug!(slog_scope::logger(), "{msg}");
+}
 
-    pub fn end_cib_in_error(&self) {
-        info!(&self.root, "CI broker ends in unrecoverable error");
+pub fn error(msg: &str, e: &impl std::error::Error) {
+    error!(slog_scope::logger(), "{msg}: {e}");
+    let mut e = e.source();
+    while let Some(source) = e {
+        error!(slog_scope::logger(), "caused by: {}", source);
+        e = source.source();
     }
 }
diff --git a/src/pages.rs b/src/pages.rs
index e07058b..b3b43f2 100644
--- a/src/pages.rs
+++ b/src/pages.rs
@@ -17,7 +17,6 @@ use std::{
 };
 
 use html_page::{Element, HtmlPage, Tag};
-use log::{debug, info, trace, warn};
 use serde::Serialize;
 use time::{macros::format_description, OffsetDateTime};
 
@@ -30,6 +29,7 @@ use radicle::{
 use crate::{
     db::{Db, DbError, QueuedEvent},
     event::{parse_ref, BrokerEvent, ParsedRef},
+    logger,
     msg::RunId,
     notif::NotificationReceiver,
     run::{Run, RunState, Whence},
@@ -96,7 +96,7 @@ impl PageBuilder {
         for run in self.runs.iter() {
             runs.insert(run.adapter_run_id().unwrap().clone(), run.clone());
         }
-        debug!("broker database has {} CI runs", runs.len());
+        logger::debug2(format!("broker database has {} CI runs", runs.len()));
 
         Ok(StatusPage::new())
     }
@@ -525,15 +525,15 @@ impl StatusPage {
         db: Db,
         once: bool,
     ) -> JoinHandle<Result<(), PageError>> {
+        logger::pages_start();
+
         if self.dirname.is_none() {
-            warn!("not writing HTML report pages as output directory has not been set");
+            logger::pages_directory_unset();
         }
 
         self.node_alias = profile.config.alias().to_string();
-        info!(
-            "wait about {} seconds to update HTML report pages again",
-            UPDATE_INTERVAL.as_secs()
-        );
+        logger::pages_interval(UPDATE_INTERVAL);
+
         spawn(move || {
             'processing_loop: loop {
                 self.update_and_write(&profile, &db)?;
@@ -541,35 +541,26 @@ impl StatusPage {
                     return Ok(());
                 }
 
-                trace!("wait for run notification or timeout");
                 match run_rx.recv_timeout(UPDATE_INTERVAL) {
-                    Ok(_) => trace!("page updater: got a run notification"),
-                    Err(RecvTimeoutError::Timeout) => {
-                        trace!("page updater: timeout on run notification channel")
-                    }
+                    Ok(_) => (),
+                    Err(RecvTimeoutError::Timeout) => (),
                     Err(RecvTimeoutError::Disconnected) => {
-                        debug!("page updater: run notification channel disconnected");
+                        logger::pages_disconnected();
                         break 'processing_loop;
                     }
                 }
             }
 
             // Make sure we update reports and status JSON at least once.
-            info!("make sure we update reports and status JSON at least once");
             self.update_and_write(&profile, &db)?;
 
-            info!("end page updater thread");
+            logger::pages_end();
             Ok(())
         })
     }
 
     fn update_and_write(&mut self, profile: &Profile, db: &Db) -> Result<(), PageError> {
         if let Some(dirname) = &self.dirname {
-            info!(
-                "write HTML report pages and {STATUS_JSON} to {}",
-                dirname.display()
-            );
-
             let runs = db.get_all_runs()?;
 
             // Create list of events, except ones for private
@@ -655,7 +646,6 @@ impl StatusPage {
     }
 
     fn write_file(filename: &Path, text: &str) -> Result<(), PageError> {
-        debug!("write file {}", filename.display());
         write(filename, text).map_err(|e| PageError::Write(filename.into(), e))?;
         Ok(())
     }
diff --git a/src/queueadd.rs b/src/queueadd.rs
index ab50928..8eaf77e 100644
--- a/src/queueadd.rs
+++ b/src/queueadd.rs
@@ -2,11 +2,10 @@ use std::thread::{spawn, JoinHandle};
 
 use radicle::Profile;
 
-use log::{debug, info};
-
 use crate::{
     db::{Db, DbError},
     event::{BrokerEvent, EventFilter, NodeEventError, NodeEventSource},
+    logger,
     notif::NotificationSender,
 };
 
@@ -62,31 +61,26 @@ impl QueueAdder {
     }
 
     pub fn add_events(&self) -> Result<(), AdderError> {
-        info!("start thread to add events from node to event queue");
+        logger::queueadd_start();
 
         let profile = Profile::load()?;
-        debug!("loaded profile {profile:#?}");
 
         let mut source = NodeEventSource::new(&profile)?;
-        debug!("created node event source");
 
         for filter in self.filters.iter() {
             source.allow(filter.clone());
         }
-        debug!("added filters to node event source");
 
         // This loop ends when there's an error, e.g., failure to read an
         // event from the node.
         'event_loop: loop {
-            debug!("waiting for event from node");
             let events = source.event()?;
             if events.is_empty() {
-                info!("no more events from node control sockets");
+                logger::queueadd_control_socket_close();
                 break 'event_loop;
             } else {
                 for e in events {
-                    debug!("got event {e:#?}");
-                    info!("insert broker event into queue: {e:?}");
+                    logger::queueadd_push_event(&e);
                     self.push_event(e)?;
                 }
             }
@@ -95,10 +89,10 @@ impl QueueAdder {
         // Add a shutdown event to the queue so that the queue
         // processing thread knows to stop.
         if self.push_shutdown {
-            info!("push a shutdown event into queue");
             self.push_event(BrokerEvent::Shutdown)?;
         }
 
+        logger::queueadd_end();
         Ok(())
     }
 
diff --git a/src/queueproc.rs b/src/queueproc.rs
index ef5e809..81233b6 100644
--- a/src/queueproc.rs
+++ b/src/queueproc.rs
@@ -8,13 +8,13 @@ use std::{
     time::Duration,
 };
 
-use log::{debug, info};
 use radicle::Profile;
 
 use crate::{
     broker::{Broker, BrokerError},
     db::{Db, DbError, QueueId, QueuedEvent},
     event::BrokerEvent,
+    logger,
     msg::{MessageError, RequestBuilder},
     notif::{NotificationReceiver, NotificationSender},
 };
@@ -75,38 +75,30 @@ impl QueueProcessor {
     }
 
     fn process_until_shutdown(&mut self) -> Result<(), QueueError> {
-        info!("start thread to process events until a shutdown event");
+        logger::queueproc_start();
         let mut done = false;
         while !done {
-            info!("Looking for an event to process");
             while let Some(qe) = self.pick_event()? {
-                info!("picked event from queue: {}: {:?}", qe.id(), qe.event());
                 done = self.process_event(qe.event())?;
                 self.drop_event(qe.id())?;
                 self.run_tx.send(()).map_err(|_| QueueError::NotifyRun)?;
             }
-            info!("waiting for next event from node");
             match self.events_rx.recv_timeout(RECV_TIMEOUT) {
-                Ok(_) => {
-                    info!("got event notification");
-                }
-                Err(RecvTimeoutError::Timeout) => {
-                    info!("timed out on event notification");
-                }
+                Ok(_) => {}
+                Err(RecvTimeoutError::Timeout) => {}
                 Err(RecvTimeoutError::Disconnected) => {
-                    info!("event notification channel disconnected");
+                    logger::queueproc_channel_disconnect();
                     done = true;
                 }
             }
         }
 
-        info!("thread to process events ends");
+        logger::queueproc_end();
         Ok(())
     }
 
     fn pick_event(&self) -> Result<Option<QueuedEvent>, QueueError> {
         let ids = self.db.queued_events().map_err(QueueError::db)?;
-        debug!("event queue has {} events", ids.len());
 
         let mut queue = vec![];
         for id in ids.iter() {
@@ -131,7 +123,7 @@ impl QueueProcessor {
                 oid,
                 old: _,
             } => {
-                info!("Action: run: {rid} {oid}");
+                logger::queueproc_action_run(rid, oid);
 
                 let trigger = RequestBuilder::default()
                     .profile(&self.profile)
@@ -144,14 +136,14 @@ impl QueueProcessor {
                 Ok(false)
             }
             BrokerEvent::Shutdown => {
-                info!("Action: shutdown");
+                logger::queueproc_action_shutdown();
                 Ok(true)
             }
         }
     }
 
     fn drop_event(&mut self, id: &QueueId) -> Result<(), QueueError> {
-        debug!("remove event {id}");
+        logger::queueproc_remove_event(id);
         self.db.remove_queued_event(id).map_err(QueueError::db)?;
         Ok(())
     }
diff --git a/src/test.rs b/src/test.rs
index 2e248e3..facb7ae 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -90,11 +90,3 @@ pub fn mock_adapter(filename: &Path, script: &str) -> TestResult<Adapter> {
 
     Ok(Adapter::new(filename))
 }
-
-pub fn log_in_tests() {
-    env_logger::builder()
-        .filter_level(log::LevelFilter::Debug)
-        .is_test(true)
-        .try_init()
-        .ok();
-}

Run: timeout 600 bash -c set -xeuo pipefail cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- -Dwarnings cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast subplot docgen ci-broker.subplot -o doc/ci-broker.html make -C doc publish

Command arguments:

In directory: /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src

Exit code: 101

Output (stdout and stderr):

+ cargo --version
cargo 1.77.0 (3fe68eabf 2024-02-29)
+ rustc --version
rustc 1.77.0 (aedd173a2 2024-03-17)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- -Dwarnings
   Compiling proc-macro2 v1.0.86
   Compiling unicode-ident v1.0.12
   Compiling version_check v0.9.5
   Compiling libc v0.2.158
   Compiling typenum v1.17.0
    Checking cfg-if v1.0.0
   Compiling serde v1.0.208
   Compiling shlex v1.3.0
   Compiling thiserror v1.0.63
   Compiling once_cell v1.19.0
   Compiling memchr v2.7.4
   Compiling byteorder v1.5.0
   Compiling syn v1.0.109
   Compiling bitflags v2.6.0
    Checking subtle v2.6.1
   Compiling pkg-config v0.3.30
   Compiling crossbeam-utils v0.8.20
   Compiling rustix v0.38.34
   Compiling itoa v1.0.11
   Compiling anyhow v1.0.86
   Compiling linux-raw-sys v0.4.14
   Compiling allocator-api2 v0.2.18
   Compiling regex-syntax v0.8.4
   Compiling generic-array v0.14.7
   Compiling ahash v0.8.11
    Checking cpufeatures v0.2.13
   Compiling log v0.4.22
   Compiling num-conv v0.1.0
   Compiling time-core v0.1.2
   Compiling same-file v1.0.6
   Compiling time-macros v0.2.18
   Compiling aho-corasick v1.1.3
   Compiling walkdir v2.5.0
   Compiling utf8parse v0.2.2
   Compiling ucd-trie v0.1.6
   Compiling anstyle-parse v0.2.5
   Compiling bstr v1.10.0
   Compiling serde_json v1.0.125
   Compiling quote v1.0.36
   Compiling heck v0.5.0
   Compiling ryu v1.0.18
   Compiling is_terminal_polyfill v1.70.1
   Compiling doc-comment v0.3.3
   Compiling syn v2.0.75
   Compiling jobserver v0.1.32
   Compiling getrandom v0.2.15
   Compiling crossbeam-epoch v0.9.18
   Compiling anstyle v1.0.8
   Compiling unicode-width v0.1.13
   Compiling rand_core v0.6.4
   Compiling powerfmt v0.2.0
   Compiling unic-common v0.9.0
   Compiling vcpkg v0.2.15
   Compiling cc v1.1.13
   Compiling unic-char-range v0.9.0
   Compiling anstyle-query v1.1.1
   Compiling libm v0.2.8
   Compiling colorchoice v1.0.2
   Compiling deranged v0.3.11
   Compiling unic-char-property v0.9.0
   Compiling anstream v0.6.15
   Compiling crossbeam-deque v0.8.5
   Compiling unic-ucd-version v0.9.0
   Compiling proc-macro-error-attr v1.0.4
   Compiling unicase v2.7.0
   Compiling encoding_rs v0.8.34
   Compiling fastrand v2.1.0
    Checking tinyvec_macros v0.1.1
   Compiling arraydeque v0.5.1
   Compiling strsim v0.11.1
   Compiling clap_lex v0.7.2
    Checking tinyvec v1.8.0
   Compiling clap_builder v4.5.15
    Checking crypto-common v0.1.6
    Checking block-buffer v0.10.4
    Checking block-padding v0.3.3
    Checking inout v0.1.3
    Checking digest v0.10.7
    Checking universal-hash v0.5.1
   Compiling unic-ucd-segment v0.9.0
    Checking cipher v0.4.4
   Compiling proc-macro-error v1.0.4
   Compiling deunicode v1.6.0
    Checking opaque-debug v0.3.1
   Compiling regex-automata v0.4.7
   Compiling tempfile v3.12.0
   Compiling time v0.3.36
   Compiling pulldown-cmark v0.9.6
   Compiling amplify_syn v2.0.1
    Checking signature v1.6.4
   Compiling unicode-linebreak v0.1.5
   Compiling equivalent v1.0.1
   Compiling smawk v0.3.2
    Checking ed25519 v1.5.3
   Compiling textwrap v0.16.1
    Checking unicode-normalization v0.1.23
   Compiling unic-segment v0.9.0
   Compiling humansize v2.1.3
   Compiling slug v0.1.6
   Compiling getopts v0.2.21
   Compiling is-terminal v0.4.13
   Compiling subplot v0.10.0
   Compiling percent-encoding v2.3.1
   Compiling libz-sys v1.1.19
   Compiling pikchr v0.1.3
   Compiling libgit2-sys v0.16.2+1.7.2
    Checking ct-codecs v1.1.1
   Compiling lazy_static v1.5.0
    Checking amplify_num v0.5.3
    Checking unicode-bidi v0.3.15
   Compiling utf8-width v0.1.7
    Checking ascii v1.1.0
   Compiling termcolor v1.4.1
   Compiling humantime v2.1.0
   Compiling unsafe-libyaml v0.2.11
    Checking form_urlencoded v1.2.1
    Checking idna v0.5.0
   Compiling html-escape v0.2.13
    Checking ec25519 v0.1.0
   Compiling amplify_derive v4.0.1
   Compiling sqlite3-src v0.5.1
    Checking polyval v0.6.2
    Checking sha2 v0.10.8
   Compiling tracing-core v0.1.32
   Compiling pin-project-lite v0.2.14
   Compiling base64 v0.22.1
    Checking keccak v0.1.5
    Checking base64ct v1.6.0
   Compiling file_diff v1.0.0
   Compiling line-col v0.2.1
   Compiling globset v0.4.14
   Compiling regex v1.10.6
    Checking sha3 v0.10.8
    Checking pem-rfc7468 v0.7.0
    Checking url v2.5.2
    Checking ghash v0.5.1
    Checking ctr v0.9.2
    Checking aes v0.8.4
    Checking aead v0.5.2
   Compiling data-encoding v2.6.0
   Compiling ignore v0.4.22
   Compiling env_logger v0.10.2
    Checking base32 v0.4.0
    Checking hashbrown v0.14.5
   Compiling cfg_aliases v0.2.1
   Compiling nix v0.29.0
    Checking aes-gcm v0.10.3
    Checking ssh-encoding v0.2.0
    Checking blowfish v0.9.1
    Checking chacha20 v0.9.1
    Checking cbc v0.1.2
    Checking poly1305 v0.8.0
   Compiling globwalk v0.9.1
    Checking pbkdf2 v0.12.2
    Checking zeroize v1.8.1
   Compiling adler v1.0.2
    Checking radicle-std-ext v0.1.0
    Checking ssh-cipher v0.2.0
    Checking indexmap v2.4.0
   Compiling miniz_oxide v0.7.4
    Checking bcrypt-pbkdf v0.10.0
    Checking rand v0.8.5
   Compiling xattr v1.3.1
   Compiling filetime v0.2.24
   Compiling crc32fast v1.4.2
    Checking base-x v0.2.11
   Compiling data-encoding-macro-internal v0.1.13
    Checking signature v2.2.0
   Compiling slog v2.7.0
    Checking ssh-key v0.6.6
   Compiling tar v0.4.41
   Compiling flate2 v1.0.31
    Checking qcheck v1.0.0
    Checking cvt v0.1.2
   Compiling glob v0.3.1
    Checking data-encoding-macro v0.1.15
    Checking multibase v0.9.1
    Checking terminal_size v0.3.0
    Checking radicle-dag v0.9.0
    Checking normpath v1.3.0
   Compiling serde_yml v0.0.11
    Checking amplify v4.7.0
   Compiling radicle-surf v0.18.0
    Checking fs2 v0.4.3
    Checking unescape v0.1.0
    Checking base64 v0.13.1
    Checking state v0.5.3
    Checking shell-words v1.1.0
    Checking nonempty v0.5.0
    Checking fs_at v0.2.1
    Checking crossbeam-channel v0.5.13
    Checking siphasher v1.0.1
    Checking base64 v0.21.7
    Checking cyphergraphy v0.3.0
    Checking arc-swap v1.7.1
    Checking libyml v0.0.4
    Checking remove_dir_all v0.8.3
    Checking uuid v1.10.0
    Checking cypheraddr v0.4.0
    Checking slog-scope v4.4.0
   Compiling serde_derive v1.0.208
   Compiling thiserror-impl v1.0.63
   Compiling zerocopy-derive v0.7.35
   Compiling clap_derive v4.5.13
   Compiling git-testament-derive v0.2.0
   Compiling tracing-attributes v0.1.27
   Compiling culpa-macros v1.0.2
   Compiling ctor v0.2.8
    Checking html-page v0.4.0
    Checking socks5-client v0.4.1
    Checking cyphernet v0.5.2
   Compiling git-testament v0.2.5
   Compiling zerocopy v0.7.35
   Compiling pest v2.7.11
   Compiling git-ref-format-core v0.3.0
    Checking radicle-ssh v0.9.0
   Compiling culpa v1.0.2
   Compiling tracing v0.1.40
   Compiling subplotlib-derive v0.10.0
   Compiling ppv-lite86 v0.2.20
   Compiling git-ref-format-macro v0.3.0
   Compiling rand_chacha v0.3.1
   Compiling clap v4.5.16
   Compiling hashlink v0.8.4
   Compiling hashlink v0.9.1
   Compiling pest_meta v2.7.11
   Compiling yaml-rust2 v0.8.1
   Compiling tempfile-fast v0.3.4
   Compiling pest_generator v2.7.11
   Compiling pest_derive v2.7.11
    Checking nonempty v0.9.0
    Checking localtime v1.3.1
    Checking git-ref-format v0.3.0
   Compiling serde_path_to_error v0.1.16
   Compiling serde_yaml v0.9.34+deprecated
   Compiling marked-yaml v0.7.1
    Checking slog-json v2.6.1
   Compiling roadmap v0.6.1
   Compiling serde-aux v4.5.0
   Compiling tera v1.20.0
    Checking git2 v0.18.3
    Checking radicle-git-ext v0.7.0
   Compiling subplot-build v0.10.0
   Compiling subplotlib v0.10.0
   Compiling radicle-ci-broker v0.4.0 (/srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src)
    Checking sqlite3-sys v0.15.2
    Checking sqlite v0.32.0
    Checking radicle-crypto v0.10.0
    Checking radicle-cob v0.11.0
    Checking radicle v0.12.0
    Finished dev [unoptimized + debuginfo] target(s) in 20.62s
+ cargo build --all-targets --workspace
   Compiling cfg-if v1.0.0
   Compiling bitflags v2.6.0
   Compiling memchr v2.7.4
   Compiling once_cell v1.19.0
   Compiling itoa v1.0.11
   Compiling log v0.4.22
   Compiling subtle v2.6.1
   Compiling typenum v1.17.0
   Compiling libc v0.2.158
   Compiling ryu v1.0.18
   Compiling fastrand v2.1.0
   Compiling percent-encoding v2.3.1
   Compiling serde v1.0.208
   Compiling thiserror v1.0.63
   Compiling regex-syntax v0.8.4
   Compiling equivalent v1.0.1
   Compiling crossbeam-utils v0.8.20
   Compiling utf8parse v0.2.2
   Compiling cpufeatures v0.2.13
   Compiling anyhow v1.0.86
   Compiling rustix v0.38.34
   Compiling ahash v0.8.11
   Compiling anstyle-parse v0.2.5
   Compiling powerfmt v0.2.0
   Compiling anstyle v1.0.8
   Compiling anstyle-query v1.1.1
   Compiling num-conv v0.1.0
   Compiling is_terminal_polyfill v1.70.1
   Compiling colorchoice v1.0.2
   Compiling time-core v0.1.2
   Compiling getrandom v0.2.15
   Compiling deranged v0.3.11
   Compiling strsim v0.11.1
   Compiling hashbrown v0.14.5
   Compiling time-macros v0.2.18
   Compiling anstream v0.6.15
   Compiling clap_lex v0.7.2
   Compiling encoding_rs v0.8.34
   Compiling crossbeam-epoch v0.9.18
   Compiling aho-corasick v1.1.3
   Compiling pest v2.7.11
   Compiling generic-array v0.14.7
   Compiling bstr v1.10.0
   Compiling rand_core v0.6.4
   Compiling crossbeam-deque v0.8.5
   Compiling rand_chacha v0.3.1
   Compiling hashlink v0.8.4
   Compiling tinyvec_macros v0.1.1
   Compiling hashlink v0.9.1
   Compiling clap_builder v4.5.15
   Compiling tinyvec v1.8.0
   Compiling opaque-debug v0.3.1
   Compiling rand v0.8.5
   Compiling signature v1.6.4
   Compiling crypto-common v0.1.6
   Compiling block-buffer v0.10.4
   Compiling block-padding v0.3.3
   Compiling inout v0.1.3
   Compiling digest v0.10.7
   Compiling universal-hash v0.5.1
   Compiling lazy_static v1.5.0
   Compiling cipher v0.4.4
   Compiling utf8-width v0.1.7
   Compiling ed25519 v1.5.3
   Compiling html-escape v0.2.13
   Compiling yaml-rust2 v0.8.1
   Compiling indexmap v2.4.0
   Compiling subplot v0.10.0
   Compiling ct-codecs v1.1.1
   Compiling tempfile v3.12.0
   Compiling linux-raw-sys v0.4.14
   Compiling regex-automata v0.4.7
   Compiling pest_meta v2.7.11
   Compiling unicode-normalization v0.1.23
   Compiling amplify_num v0.5.3
   Compiling unicode-bidi v0.3.15
   Compiling ascii v1.1.0
   Compiling tempfile-fast v0.3.4
   Compiling ec25519 v0.1.0
   Compiling time v0.3.36
   Compiling pulldown-cmark v0.9.6
   Compiling idna v0.5.0
   Compiling sha2 v0.10.8
   Compiling polyval v0.6.2
   Compiling libz-sys v1.1.19
   Compiling pest_generator v2.7.11
   Compiling serde_json v1.0.125
   Compiling serde_path_to_error v0.1.16
   Compiling serde_yaml v0.9.34+deprecated
   Compiling tracing-core v0.1.32
   Compiling clap v4.5.16
   Compiling marked-yaml v0.7.1
   Compiling form_urlencoded v1.2.1
   Compiling git-ref-format-core v0.3.0
   Compiling base64 v0.22.1
   Compiling amplify v4.7.0
   Compiling roadmap v0.6.1
   Compiling serde-aux v4.5.0
   Compiling base64ct v1.6.0
   Compiling byteorder v1.5.0
   Compiling keccak v0.1.5
   Compiling tracing v0.1.40
   Compiling cyphergraphy v0.3.0
   Compiling globset v0.4.14
   Compiling regex v1.10.6
   Compiling git-testament-derive v0.2.0
   Compiling sha3 v0.10.8
   Compiling pem-rfc7468 v0.7.0
   Compiling git-ref-format-macro v0.3.0
   Compiling pest_derive v2.7.11
   Compiling url v2.5.2
   Compiling ignore v0.4.22
   Compiling env_logger v0.10.2
   Compiling ghash v0.5.1
   Compiling libgit2-sys v0.16.2+1.7.2
   Compiling aes v0.8.4
   Compiling ctr v0.9.2
   Compiling aead v0.5.2
   Compiling base32 v0.4.0
   Compiling git2 v0.18.3
   Compiling git-ref-format v0.3.0
   Compiling cypheraddr v0.4.0
   Compiling ssh-encoding v0.2.0
   Compiling sqlite3-src v0.5.1
   Compiling blowfish v0.9.1
   Compiling chacha20 v0.9.1
   Compiling cbc v0.1.2
   Compiling pbkdf2 v0.12.2
   Compiling poly1305 v0.8.0
   Compiling globwalk v0.9.1
   Compiling zeroize v1.8.1
   Compiling radicle-std-ext v0.1.0
   Compiling data-encoding v2.6.0
   Compiling aes-gcm v0.10.3
   Compiling bcrypt-pbkdf v0.10.0
   Compiling tera v1.20.0
   Compiling socks5-client v0.4.1
   Compiling sqlite3-sys v0.15.2
   Compiling ssh-cipher v0.2.0
   Compiling git-testament v0.2.5
   Compiling xattr v1.3.1
   Compiling crc32fast v1.4.2
   Compiling filetime v0.2.24
   Compiling data-encoding-macro v0.1.15
   Compiling signature v2.2.0
   Compiling base-x v0.2.11
   Compiling flate2 v1.0.31
   Compiling tar v0.4.41
   Compiling cyphernet v0.5.2
   Compiling ssh-key v0.6.6
   Compiling multibase v0.9.1
   Compiling nix v0.29.0
   Compiling sqlite v0.32.0
   Compiling radicle-ssh v0.9.0
   Compiling qcheck v1.0.0
   Compiling cvt v0.1.2
   Compiling culpa v1.0.2
   Compiling slog v2.7.0
   Compiling terminal_size v0.3.0
   Compiling nonempty v0.9.0
   Compiling radicle-dag v0.9.0
   Compiling normpath v1.3.0
   Compiling subplotlib-derive v0.10.0
   Compiling localtime v1.3.1
   Compiling fs2 v0.4.3
   Compiling crossbeam-channel v0.5.13
   Compiling base64 v0.13.1
   Compiling radicle-surf v0.18.0
   Compiling fs_at v0.2.1
   Compiling base64 v0.21.7
   Compiling nonempty v0.5.0
   Compiling remove_dir_all v0.8.3
   Compiling shell-words v1.1.0
   Compiling state v0.5.3
   Compiling libyml v0.0.4
   Compiling siphasher v1.0.1
   Compiling arc-swap v1.7.1
   Compiling unescape v0.1.0
   Compiling html-page v0.4.0
   Compiling uuid v1.10.0
   Compiling slog-scope v4.4.0
   Compiling radicle-git-ext v0.7.0
   Compiling slog-json v2.6.1
   Compiling serde_yml v0.0.11
   Compiling radicle-crypto v0.10.0
   Compiling radicle-cob v0.11.0
   Compiling subplot-build v0.10.0
   Compiling radicle v0.12.0
   Compiling subplotlib v0.10.0
   Compiling radicle-ci-broker v0.4.0 (/srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src)
    Finished dev [unoptimized + debuginfo] target(s) in 18.42s
+ cargo doc --workspace
    Checking unicode-ident v1.0.12
 Documenting unicode-ident v1.0.12
 Documenting cfg-if v1.0.0
 Documenting typenum v1.17.0
 Documenting libc v0.2.158
 Documenting subtle v2.6.1
 Documenting bitflags v2.6.0
 Documenting cpufeatures v0.2.13
 Documenting tinyvec_macros v0.1.1
 Documenting opaque-debug v0.3.1
 Documenting memchr v2.7.4
 Documenting ed25519 v1.5.3
 Documenting unicode-bidi v0.3.15
 Documenting ct-codecs v1.1.1
 Documenting log v0.4.22
 Documenting percent-encoding v2.3.1
    Checking proc-macro2 v1.0.86
 Documenting tinyvec v1.8.0
 Documenting amplify_num v0.5.3
 Documenting ascii v1.1.0
 Documenting linux-raw-sys v0.4.14
 Documenting proc-macro2 v1.0.86
    Checking quote v1.0.36
    Checking git-ref-format-core v0.3.0
 Documenting byteorder v1.5.0
 Documenting itoa v1.0.11
    Checking syn v2.0.75
    Checking syn v1.0.109
 Documenting form_urlencoded v1.2.1
 Documenting keccak v0.1.5
 Documenting base64ct v1.6.0
 Documenting fastrand v2.1.0
 Documenting equivalent v1.0.1
 Documenting base32 v0.4.0
 Documenting hashbrown v0.14.5
    Checking data-encoding v2.6.0
 Documenting unicode-normalization v0.1.23
 Documenting quote v1.0.36
 Documenting sqlite3-src v0.5.1
 Documenting data-encoding v2.6.0
 Documenting zeroize v1.8.1
 Documenting ryu v1.0.18
 Documenting pem-rfc7468 v0.7.0
 Documenting radicle-std-ext v0.1.0
 Documenting rustix v0.38.34
    Checking amplify_syn v2.0.1
    Checking proc-macro-error v1.0.4
 Documenting getrandom v0.2.15
 Documenting libz-sys v1.1.19
 Documenting data-encoding-macro-internal v0.1.13
 Documenting once_cell v1.19.0
 Documenting sqlite3-sys v0.15.2
 Documenting serde_derive v1.0.208
 Documenting amplify_syn v2.0.1
 Documenting thiserror-impl v1.0.63
 Documenting proc-macro-error-attr v1.0.4
 Documenting powerfmt v0.2.0
 Documenting utf8parse v0.2.2
 Documenting signature v2.2.0
 Documenting idna v0.5.0
 Documenting ec25519 v0.1.0
 Documenting generic-array v0.14.7
 Documenting rand_core v0.6.4
 Documenting indexmap v2.4.0
 Documenting libgit2-sys v0.16.2+1.7.2
 Documenting data-encoding-macro v0.1.15
 Documenting base-x v0.2.11
 Documenting deranged v0.3.11
 Documenting anstyle-parse v0.2.5
 Documenting proc-macro-error v1.0.4
 Documenting tempfile v3.12.0
 Documenting sqlite v0.32.0
 Documenting rand v0.8.5
 Documenting nix v0.29.0
 Documenting amplify_derive v4.0.1
 Documenting thiserror v1.0.63
 Documenting multibase v0.9.1
 Documenting url v2.5.2
 Documenting aho-corasick v1.1.3
 Documenting time-macros v0.2.18
 Documenting crypto-common v0.1.6
 Documenting block-buffer v0.10.4
 Documenting block-padding v0.3.3
 Documenting git-ref-format-macro v0.3.0
 Documenting cvt v0.1.2
 Documenting anstyle v1.0.8
 Documenting serde v1.0.208
 Documenting radicle-ssh v0.9.0
 Documenting regex-syntax v0.8.4
 Documenting time-core v0.1.2
 Documenting qcheck v1.0.0
 Documenting git2 v0.18.3
 Documenting universal-hash v0.5.1
 Documenting amplify v4.7.0
 Documenting digest v0.10.7
 Documenting inout v0.1.3
 Documenting aead v0.5.2
 Documenting anstyle-query v1.1.1
 Documenting colorchoice v1.0.2
 Documenting num-conv v0.1.0
 Documenting is_terminal_polyfill v1.70.1
 Documenting terminal_size v0.3.0
 Documenting fs_at v0.2.1
 Documenting culpa-macros v1.0.2
 Documenting radicle-dag v0.9.0
 Documenting cipher v0.4.4
   Compiling radicle-ci-broker v0.4.0 (/srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src)
 Documenting polyval v0.6.2
 Documenting poly1305 v0.8.0
 Documenting cyphergraphy v0.3.0
 Documenting slog v2.7.0
 Documenting time v0.3.36
 Documenting anstream v0.6.15
 Documenting regex-automata v0.4.7
 Documenting sha2 v0.10.8
 Documenting sha3 v0.10.8
 Documenting pbkdf2 v0.12.2
 Documenting crossbeam-utils v0.8.20
 Documenting lazy_static v1.5.0
 Documenting utf8-width v0.1.7
 Documenting normpath v1.3.0
 Documenting git-ref-format-core v0.3.0
 Documenting ghash v0.5.1
 Documenting serde_json v1.0.125
 Documenting nonempty v0.9.0
    Checking heck v0.5.0
 Documenting clap_lex v0.7.2
 Documenting ctr v0.9.2
 Documenting aes v0.8.4
 Documenting ssh-encoding v0.2.0
 Documenting chacha20 v0.9.1
 Documenting blowfish v0.9.1
 Documenting cypheraddr v0.4.0
 Documenting cbc v0.1.2
 Documenting strsim v0.11.1
 Documenting heck v0.5.0
 Documenting regex v1.10.6
 Documenting remove_dir_all v0.8.3
 Documenting crossbeam-channel v0.5.13
 Documenting html-escape v0.2.13
 Documenting git-ref-format v0.3.0
 Documenting localtime v1.3.1
 Documenting culpa v1.0.2
 Documenting subplotlib-derive v0.10.0
 Documenting aes-gcm v0.10.3
 Documenting bcrypt-pbkdf v0.10.0
 Documenting filetime v0.2.24
 Documenting clap_derive v4.5.13
 Documenting clap_builder v4.5.15
 Documenting fs2 v0.4.3
 Documenting arc-swap v1.7.1
 Documenting socks5-client v0.4.1
 Documenting shell-words v1.1.0
 Documenting state v0.5.3
 Documenting radicle-git-ext v0.7.0
 Documenting base64 v0.22.1
 Documenting unescape v0.1.0
 Documenting libyml v0.0.4
 Documenting siphasher v1.0.1
 Documenting ssh-cipher v0.2.0
 Documenting html-page v0.4.0
 Documenting slog-json v2.6.1
 Documenting uuid v1.10.0
 Documenting anyhow v1.0.86
 Documenting slog-scope v4.4.0
 Documenting cyphernet v0.5.2
 Documenting subplotlib v0.10.0
 Documenting radicle-surf v0.18.0
 Documenting serde_yml v0.0.11
 Documenting clap v4.5.16
 Documenting ssh-key v0.6.6
 Documenting radicle-crypto v0.10.0
 Documenting radicle-cob v0.11.0
 Documenting radicle v0.12.0
 Documenting radicle-ci-broker v0.4.0 (/srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src)
    Finished dev [unoptimized + debuginfo] target(s) in 33.15s
   Generated /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src/target/doc/radicle_ci_broker/index.html
   Generated /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src/target/doc/cib/index.html
   Generated /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src/target/doc/cibtool/index.html
   Generated /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src/target/doc/synthetic_events/index.html
+ cargo test --workspace --no-fail-fast
    Finished test [unoptimized + debuginfo] target(s) in 0.11s
     Running unittests src/lib.rs (target/debug/deps/radicle_ci_broker-a0fa34f091896d04)

running 40 tests
{"msg":"broker database: /tmp/.tmp3dIuhz/db.db","level":"INFO","ts":"2024-08-23T07:11:57.873548553Z"}
{"msg":"broker database: /tmp/.tmpPGwiL1/db.db","level":"INFO","ts":"2024-08-23T07:11:57.873656083Z"}
{"msg":"broker database: /tmp/.tmpo1E6nw/db.db","level":"INFO","ts":"2024-08-23T07:11:57.873756634Z"}
{"msg":"broker database: /tmp/.tmp9po3Z7/db.db","level":"INFO","ts":"2024-08-23T07:11:57.873845524Z"}
test broker::test::adds_adapter ... ok
{"msg":"broker database: /tmp/.tmpqlrZ3W/db.db","level":"INFO","ts":"2024-08-23T07:11:58.100919588Z"}
test broker::test::does_not_have_a_default_adapter_initially ... ok
{test broker::test::does_not_find_unknown_repo ... ok
"msg":"broker database: /tmp/.tmpySoN10/db.db","level":"INFO","ts":"2024-08-23T07:11:58.11505923Z"}
{"msg":"broker database: /tmp/.tmpkxwIDD/db.db","level":"INFO","ts":"2024-08-23T07:11:58.11523502Z"}
test adapter::test::adapter_has_bad_interpreter ... ok
{"msg":"broker database: /tmp/.tmp4CshKq/db.db","level":"INFO","ts":"2024-08-23T07:11:58.167537832Z"}
{"msg":"adapter exit code: 1","level":"DEBG","ts":"2024-08-23T07:11:58.171086848Z"}
{"msg":"adapter stderr: woe be me\n","level":"DEBG","ts":"2024-08-23T07:11:58.171130148Z"}
test adapter::test::adapter_exits_nonzero ... ok
test event::test::branch_is_not_patch_update ... ok
test event::test::get_no_push_branch ... ok
test event::test::get_push_branch ... ok
test event::test::patch_branch_is_not_patch_update ... ok
test event::test::patch_update ... ok
test event::test::test_parse_invalid_ref ... ok
test adapter::test::adapter_first_message_isnt_triggered ... ok
test event::test::test_parse_patch_ref ... ok
test event::test_broker_event::name_for_branch ... ok
test event::test_broker_event::name_for_shutdown ... ok
test event::test_broker_event::nid_for_plain_branch_name ... ok
test event::test_broker_event::nid_for_ref_with_namespace ... ok
test event::test::test_parse_push_ref ... ok
test event::test_broker_event::nid_for_ref_without_namespace ... ok
test event::test_parse_ref::namespaced_branch ... ok
test event::test_parse_ref::namespaced_branch_with_slashes ... ok
test adapter::test::adapter_does_not_exist ... ok
{"msg":"start CI run","level":"INFO","ts":"2024-08-23T07:11:58.183138976Z"}
{"msg":"trigger event: Trigger {\n    common: EventCommonFields {\n        version: 1,\n        event_type: Push,\n        repository: Repository {\n            id: RepoId(rad:z4JEEz69XqCEcp95caqJzZDM29Zb8),\n            name: \"acme\",\n            description: \"Acme's repository\",\n            private: false,\n            default_branch: \"master\",\n            delegates: [\n                Did(\"did:key:z6MkqufocYq3PWoG4SwYE1emmiDrKNCowdrrmou9gyVxqxwR\"),\n            ],\n        },\n    },\n    push: Some(\n        PushEvent {\n            pusher: Author {\n                id: Did(\"did:key:z6MkqufocYq3PWoG4SwYE1emmiDrKNCowdrrmou9gyVxqxwR\"),\n                alias: None,\n            },\n            before: Oid(\n                f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354,\n            ),\n            after: Oid(\n                532c92e4ec973d79c25652c581ae82d238e46a06,\n            ),\n            branch: \"master\",\n            commits: [\n                Oid(\n                    532c92e4ec973d79c25652c581ae82d238e46a06,\n                ),\n            ],\n        },\n    ),\n    patch: None,\n}","level":"DEBG","ts":"2024-08-23T07:11:58.183344037Z"}
test event::test_parse_ref::namespaced_patch ... ok
{"msg":"no first response message","level":"ERRO","ts":"2024-08-23T07:11:58.184062458Z"}
{"msg":"no second response message","level":"ERRO","ts":"2024-08-23T07:11:58.184106258Z"}
{"msg":"adapter did not exit voluntarily","level":"WARN","ts":"2024-08-23T07:11:58.184138808Z"}
test adapter::test::adapter_is_killed_before_any_messages ... ok
test run::test::serialize_run_state ... ok
test event::test_parse_ref::plain_branch_name_is_none ... ok
{"msg":"no second response message","level":"ERRO","ts":"2024-08-23T07:11:58.18567721Z"}
{"msg":"adapter did not exit voluntarily","level":"WARN","ts":"2024-08-23T07:11:58.18572119Z"}
test adapter::test::adapter_is_killed_after_first_message ... ok
{"msg":"too many response messages","level":"ERRO","ts":"2024-08-23T07:11:58.186047291Z"}
test adapter::test::adapter_outputs_too_many_messages ... ok
{"msg":"adapter did not exit voluntarily","level":"WARN","ts":"2024-08-23T07:11:58.188749845Z"}
test adapter::test::adapter_is_killed_after_second_message ... ok
test adapter::test::adapter_is_not_executable ... ok
test adapter::test::adapter_reports_success ... ok
test adapter::test::adapter_reports_failure ... ok
test adapter::test::adapter_produces_as_bad_message ... ok
test msg::tests::trigger_push ... ok
test msg::tests::trigger_patch ... ok
test broker::test::finds_default_adapter_for_unknown_repo ... ok
test broker::test::has_no_adapters_initially ... ok
test broker::test::sets_a_default_adapter_initially ... ok
{"msg":"adapter exit code: 1","level":"DEBG","ts":"2024-08-23T07:11:58.480675801Z"}
{"msg":"adapter stderr: woe be me\n","level":"DEBG","ts":"2024-08-23T07:11:58.480735871Z"}
{"msg":"start CI run","level":"INFO","ts":"2024-08-23T07:11:58.522429906Z"}
{"msg":"trigger event: Trigger {\n    common: EventCommonFields {\n        version: 1,\n        event_type: Push,\n        repository: Repository {\n            id: RepoId(rad:z3tybkYvJU8HqCQmnkBqwaWHajbcn),\n            name: \"acme\",\n            description: \"Acme's repository\",\n            private: false,\n            default_branch: \"master\",\n            delegates: [\n                Did(\"did:key:z6MkhUCHYkVvFpfwFf4pxJFUeZRiKWkZAuRKFECbStgvzLk2\"),\n            ],\n        },\n    },\n    push: Some(\n        PushEvent {\n            pusher: Author {\n                id: Did(\"did:key:z6MkhUCHYkVvFpfwFf4pxJFUeZRiKWkZAuRKFECbStgvzLk2\"),\n                alias: None,\n            },\n            before: Oid(\n                f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354,\n            ),\n            after: Oid(\n                532c92e4ec973d79c25652c581ae82d238e46a06,\n            ),\n            branch: \"master\",\n            commits: [\n                Oid(\n                    532c92e4ec973d79c25652c581ae82d238e46a06,\n                ),\n            ],\n        },\n    ),\n    patch: None,\n}","level":"DEBG","ts":"2024-08-23T07:11:58.522711726Z"}
{"msg":"failed to run adapter or it failed to run CI: child process failed with wait status 1","level":"ERRO","ts":"2024-08-23T07:11:58.525937271Z"}
{"msg":"Finish CI run","level":"INFO","ts":"2024-08-23T07:11:58.525972121Z"}
{"msg":"finished CI run: Run {\n    broker_run_id: RunId {\n        id: \"addfb18d-cac5-496a-b81b-975b0ca685fc\",\n    },\n    adapter_run_id: Some(\n        RunId {\n            id: \"xyzzy\",\n        },\n    ),\n    adapter_info_url: None,\n    repo_id: RepoId(rad:z4JEEz69XqCEcp95caqJzZDM29Zb8),\n    repo_name: \"acme\",\n    timestamp: \"2024-08-23 07:11:58Z\",\n    whence: Branch {\n        name: \"push-event-has-no-branch-name\",\n        commit: Oid(\n            532c92e4ec973d79c25652c581ae82d238e46a06,\n        ),\n        who: Some(\n            \"did:key:z6MkqufocYq3PWoG4SwYE1emmiDrKNCowdrrmou9gyVxqxwR\",\n        ),\n    },\n    state: Finished,\n    result: Some(\n        Success,\n    ),\n}","level":"DEBG","ts":"2024-08-23T07:11:58.526092421Z"}
test broker::test::adapter_fails ... ok
{"msg":"Finish CI run","level":"INFO","ts":"2024-08-23T07:11:58.681107663Z"}
{"msg":"finished CI run: Run {\n    broker_run_id: RunId {\n        id: \"6eac6b27-c21e-494c-8ab9-7b8e378fc3e1\",\n    },\n    adapter_run_id: Some(\n        RunId {\n            id: \"xyzzy\",\n        },\n    ),\n    adapter_info_url: None,\n    repo_id: RepoId(rad:z3tybkYvJU8HqCQmnkBqwaWHajbcn),\n    repo_name: \"acme\",\n    timestamp: \"2024-08-23 07:11:58Z\",\n    whence: Branch {\n        name: \"push-event-has-no-branch-name\",\n        commit: Oid(\n            532c92e4ec973d79c25652c581ae82d238e46a06,\n        ),\n        who: Some(\n            \"did:key:z6MkhUCHYkVvFpfwFf4pxJFUeZRiKWkZAuRKFECbStgvzLk2\",\n        ),\n    },\n    state: Finished,\n    result: Some(\n        Success,\n    ),\n}","level":"DEBG","ts":"2024-08-23T07:11:58.681212813Z"}
test broker::test::executes_adapter ... ok

test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s

     Running unittests src/bin/cib.rs (target/debug/deps/cib-d7be6a7c3ca0f24f)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/bin/cibtool.rs (target/debug/deps/cibtool-0eb13d3132e032d7)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/bin/synthetic-events.rs (target/debug/deps/synthetic_events-da3fee5d5719402f)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/subplot.rs (target/debug/deps/subplot-0db116672a3bb717)

running 28 tests
test dummy_adapter_runs_successfully ... ok
test adapter_with_url_runs_successfully ... ok
test event_synthesizer_terminates_after_first_connection ... ok
test can_add_shutdown_event_to_queue ... ok
test logs_termination_due_to_error ... FAILED
test gives_helpful_error_message_if_it_doesn_t_understand_its_configuration_file ... ok
test gives_helpful_error_message_if_node_socket_can_t_be_found ... ok
test don_t_insert_events_into_queue_when_not_allowed_by_filter ... ok
test add_information_about_run_that_s_finished_successfully_to_database ... ok
test can_trigger_a_ci_run ... ok
test add_information_about_run_that_s_running_to_database ... ok
test add_information_about_triggered_run_to_database ... ok
test add_information_about_run_that_s_finished_in_failure_to_database ... ok
test shows_config_as_json ... ok
test events_can_be_queued_and_removed_from_queue ... ok
test insert_events_into_queue ... ok
test adapter_can_provide_url_for_info_on_run ... ok
test logs_start_and_successful_end ... ok
test reports_it_version ... ok
test process_queued_events ... ok
test produces_a_report_page_upon_request ... ok
test stops_if_the_node_connection_breaks ... ok
test shuts_down_when_requested ... ok
test update_and_show_information_about_run_to_running ... ok
test smoke_test__runs_adapter ... ok
test count_in_a_single_process has been running for over 60 seconds
test count_in_concurrent_processes has been running for over 60 seconds
test insert_many_events_into_queue has been running for over 60 seconds
test count_in_a_single_process ... ok
test insert_many_events_into_queue ... ok
test count_in_concurrent_processes ... ok

failures:

---- logs_termination_due_to_error stdout ----
ci-broker.md:1077:1: scenario: Logs termination due to error
ci-broker.md:1090:1:   step: given an installed cib
ci-broker.md:1092:1:   step: given file broker.yaml
ci-broker.md:1093:1:   step: when I try to run cib --config broker.yaml queued
Running `cib` with args ["--config", "broker.yaml", "queued"]
Running in /tmp/subplotXxXRmLogs-termination-due-to-error
ENV: SHELL = /bin/sh
PATH: /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src/target/debug:/home/_rad/.cargo/bin:/bin:/usr/bin:/sbin:/usr/sbin
Exit code: 0
Stdout:

Stderr:
{"msg":"CI broker starts","level":"INFO","ts":"2024-08-23T07:11:59.097468004Z"}
{"msg":"loaded configuration Config {\n    default_adapter: \"mcadapterface\",\n    adapters: {\n        \"mcadapterface\": Adapter { \n         command: \"./adapter.sh\", \n         env: {\n            \"RADICLE_NATIVE_CI\": \"native-ci.yaml\",\n        }, \n         sensitive_env: {} },\n    },\n    filters: [\n        Branch(\n            \"main\",\n        ),\n    ],\n    report_dir: Some(\n        \"reports\",\n    ),\n    status_update_interval_seconds: None,\n    db: \"ci-broker.db\",\n}","level":"DEBG","ts":"2024-08-23T07:11:59.098034795Z"}
{"msg":"broker database: ci-broker.db","level":"INFO","ts":"2024-08-23T07:11:59.420056787Z"}
{"msg":"default adapter: Adapter { bin: \"./adapter.sh\", env: {\"RADICLE_NATIVE_CI\": \"native-ci.yaml\"} }","level":"DEBG","ts":"2024-08-23T07:11:59.420611848Z"}
{"msg":"start thread to process events until a shutdown event","level":"INFO","ts":"2024-08-23T07:11:59.427508559Z"}
{"msg":"event notification channel disconnected","level":"INFO","ts":"2024-08-23T07:11:59.427748119Z"}
{"msg":"start thread to process events ends","level":"INFO","ts":"2024-08-23T07:11:59.427804199Z"}
{"msg":"CI broker ends successfully","level":"INFO","ts":"2024-08-23T07:11:59.428051469Z"}


ci-broker.md:1095:1:   step: then stderr contains "CI broker starts"
ci-broker.md:1096:1:   step: then stderr contains "CI broker ends in unrecoverable error"
  return: Failure
thread 'logs_termination_due_to_error' panicked at /srv/http/a0fda6cb-d079-41e7-b0ad-85c6ecd58b24/src/target/debug/build/radicle-ci-broker-196714280edcb383/out/ci-broker.rs:4003:20:
called `Result::unwrap()` on an `Err` value: "stderr does not contain \"CI broker ends in unrecoverable error\""
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    logs_termination_due_to_error

test result: FAILED. 27 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 80.16s

error: test failed, to rerun pass `--test subplot`
   Doc-tests radicle-ci-broker

running 2 tests
test src/msg.rs - msg::RunId (line 44) ... ok
test src/event.rs - event (line 11) ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.60s

error: 1 target failed:
    `--test subplot`