diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index 3e6aa732..5655c01a 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -1,6 +1,7 @@ use crate::api::Context; use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock}; use crate::cli::{list_sellers, EventLoop, SellerStatus}; +use crate::common::print_or_write_logs; use crate::libp2p_ext::MultiAddrExt; use crate::network::quote::{BidQuote, ZeroQuoteReceived}; use crate::network::swarm; @@ -16,6 +17,7 @@ use std::cmp::min; use std::convert::TryInto; use std::future::Future; use std::net::SocketAddr; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tracing::{debug_span, field, Instrument, Span}; @@ -45,6 +47,12 @@ pub enum Method { swap_id: Uuid, }, History, + Logs { + logs_dir: Option, + output_path: Option, + redact: bool, + swap_id: Option + }, Config, WithdrawBtc { amount: Option, @@ -122,6 +130,13 @@ impl Method { log_reference_id = field::Empty ) } + Method::Logs { .. } => { + debug_span!( + "method", + method_name = "Logs", + log_reference_id = field::Empty + ) + } Method::ListSellers { .. } => { debug_span!( "method", @@ -647,6 +662,11 @@ impl Request { Ok(json!({ "swaps": vec })) } + Method::Logs { logs_dir, output_path, redact, swap_id } => { + print_or_write_logs(logs_dir, output_path, swap_id, redact).await?; + + Ok(json!({ "success": true })) + } Method::GetRawStates => { let raw_history = context.db.raw_all().await?; diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index f50bf4da..3de587d5 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -20,10 +20,9 @@ use libp2p::swarm::AddressScore; use libp2p::Swarm; use swap::common::tracing_util::Format; use std::convert::TryInto; -use std::fs::read_dir; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; -use std::{env, io}; +use std::env; use structopt::clap; use structopt::clap::ErrorKind; use swap::asb::command::{parse_args, Arguments, Command}; @@ -31,7 +30,7 @@ use swap::asb::config::{ initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, }; use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate}; -use swap::common::{self, check_latest_version}; +use swap::common::{self, check_latest_version, print_or_write_logs}; use swap::database::{open_db, AccessMode}; use swap::fs::system_data_dir; use swap::network::rendezvous::XmrBtcNamespace; @@ -40,8 +39,6 @@ use swap::protocol::alice::{run, AliceState}; use swap::seed::Seed; use swap::tor::AuthenticatedClient; use swap::{bitcoin, kraken, monero, tor}; -use tokio::fs::{create_dir_all, try_exists, File}; -use tokio::io::{stdout, AsyncBufReadExt, AsyncWriteExt, BufReader, Stdout}; use tracing_subscriber::filter::LevelFilter; const DEFAULT_WALLET_NAME: &str = "asb-wallet"; @@ -261,94 +258,7 @@ async fn main() -> Result<()> { swap_id, redact, } => { - // use provided directory of default one - let default_dir = system_data_dir()?.join("logs"); - let logs_dir = logs_dir.unwrap_or(default_dir); - - tracing::info!("Reading `*.log` files from `{}`", logs_dir.display()); - - // get all files in the directory - let log_files = read_dir(&logs_dir)?; - - /// Enum for abstracting over output channels - enum OutputChannel { - File(File), - Stdout(Stdout), - } - - /// Conveniance method for writing to either file or stdout - async fn write_to_channel( - mut channel: &mut OutputChannel, - output: &str, - ) -> Result<(), io::Error> { - match &mut channel { - OutputChannel::File(file) => file.write_all(output.as_bytes()).await, - OutputChannel::Stdout(stdout) => stdout.write_all(output.as_bytes()).await, - } - } - - // check where we should write to - let mut output_channel = match output_path { - Some(path) => { - // make sure the directory exists - if !try_exists(&path).await? { - let mut dir_part = path.clone(); - dir_part.pop(); - create_dir_all(&dir_part).await?; - } - - tracing::info!("Writing logs to `{}`", path.display()); - - // create/open and truncate file. - // this means we aren't appending which is probably intuitive behaviour - // since we reprint the complete logs anyway - OutputChannel::File(File::create(&path).await?) - } - None => OutputChannel::Stdout(stdout()), - }; - - // conveniance method for checking whether we should filter a specific line - let filter_by_swap_id: Box bool> = match swap_id { - // if we should filter by swap id, check if the line contains the string - Some(swap_id) => { - let swap_id = swap_id.to_string(); - Box::new(move |line: &str| line.contains(&swap_id)) - } - // otherwise we let every line pass - None => Box::new(|_| true), - }; - - // print all lines from every log file. TODO: sort files by date? - for entry in log_files { - // get the file path - let file_path = entry?.path(); - - // filter for .log files - let file_name = file_path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(""); - - if !file_name.ends_with(".log") { - continue; - } - - let buf_reader = BufReader::new(File::open(&file_path).await?); - let mut lines = buf_reader.lines(); - - // print each line, redacted if the flag is set - while let Some(line) = lines.next_line().await? { - // check if we should filter this line - if !filter_by_swap_id(&line) { - continue; - } - - let line = if redact { common::redact(&line) } else { line }; - write_to_channel(&mut output_channel, &line).await?; - // don't forget newlines - write_to_channel(&mut output_channel, "\n").await?; - } - } + print_or_write_logs(logs_dir, output_path, swap_id, redact).await?; } Command::WithdrawBtc { amount, address } => { let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?; diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 4881d94e..810f88ef 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -103,6 +103,17 @@ where Context::build(None, None, None, data, is_testnet, debug, json, None).await?; (context, request) } + CliCommand::Logs { + logs_dir, + output_path, + redact, + swap_id + } => { + let request = Request::new(Method::Logs { logs_dir, output_path, redact, swap_id }); + let context = Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + + (context, request) + } CliCommand::Config => { let request = Request::new(Method::Config); @@ -321,6 +332,30 @@ enum CliCommand { }, /// Show a list of past, ongoing and completed swaps History, + /// Output all logging messages that have been issued. + Logs { + #[structopt( + short = "d", + help = "Print the logs from this directory instead of the default one." + )] + logs_dir: Option, + #[structopt( + short = "o", + help = "Print the logs into this file instead of the terminal." + )] + output_path: Option, + #[structopt( + help = "Redact swap-ids, Bitcoin and Monero addresses.", + long = "redact" + )] + redact: bool, + #[structopt( + long = "swap-id", + help = "Filter for logs concerning this swap.", + long_help = "This checks whether each logging message contains the swap id. Some messages might be skipped when they don't contain the swap id even though they're relevant.", + )] + swap_id: Option + }, #[structopt(about = "Prints the current config")] Config, #[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")] diff --git a/swap/src/common/mod.rs b/swap/src/common/mod.rs index 4916bb21..c80e7f38 100644 --- a/swap/src/common/mod.rs +++ b/swap/src/common/mod.rs @@ -1,8 +1,12 @@ pub mod tracing_util; -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; use anyhow::anyhow; +use tokio::{fs::{create_dir_all, read_dir, try_exists, File}, io::{self, stdout, AsyncBufReadExt, AsyncWriteExt, BufReader, Stdout}}; +use uuid::Uuid; + +use crate::fs::system_data_dir; const LATEST_RELEASE_URL: &str = "https://github.com/comit-network/xmr-btc-swap/releases/latest"; @@ -64,6 +68,103 @@ macro_rules! regex_find_placeholders { }}; } +/// Print the logs from the specified logs or from the default location +/// to the specified path or the terminal. +/// +/// If specified, filter by swap id or redact addresses. +pub async fn print_or_write_logs(logs_dir: Option, output_path: Option, swap_id: Option, redact_addresses: bool) -> anyhow::Result<()> { + // use provided directory of default one + let default_dir = system_data_dir()?.join("logs"); + let logs_dir = logs_dir.unwrap_or(default_dir); + + tracing::info!("Reading `*.log` files from `{}`", logs_dir.display()); + + // get all files in the directory + let mut log_files = read_dir(&logs_dir).await?; + + /// Enum for abstracting over output channels + enum OutputChannel { + File(File), + Stdout(Stdout), + } + + /// Conveniance method for writing to either file or stdout + async fn write_to_channel( + mut channel: &mut OutputChannel, + output: &str, + ) -> Result<(), io::Error> { + match &mut channel { + OutputChannel::File(file) => file.write_all(output.as_bytes()).await, + OutputChannel::Stdout(stdout) => stdout.write_all(output.as_bytes()).await, + } + } + + // check where we should write to + let mut output_channel = match output_path { + Some(path) => { + // make sure the directory exists + if !try_exists(&path).await? { + let mut dir_part = path.clone(); + dir_part.pop(); + create_dir_all(&dir_part).await?; + } + + tracing::info!("Writing logs to `{}`", path.display()); + + // create/open and truncate file. + // this means we aren't appending which is probably intuitive behaviour + // since we reprint the complete logs anyway + OutputChannel::File(File::create(&path).await?) + } + None => OutputChannel::Stdout(stdout()), + }; + + // conveniance method for checking whether we should filter a specific line + let filter_by_swap_id: Box bool + Send + Sync> = match swap_id { + // if we should filter by swap id, check if the line contains the string + Some(swap_id) => { + let swap_id = swap_id.to_string(); + Box::new(move |line: &str| line.contains(&swap_id)) + } + // otherwise we let every line pass + None => Box::new(|_| true), + }; + + // print all lines from every log file. TODO: sort files by date? + while let Some(entry) = log_files.next_entry().await? { + // get the file path + let file_path = entry.path(); + + // filter for .log files + let file_name = file_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""); + + if !file_name.ends_with(".log") { + continue; + } + + let buf_reader = BufReader::new(File::open(&file_path).await?); + let mut lines = buf_reader.lines(); + + // print each line, redacted if the flag is set + while let Some(line) = lines.next_line().await? { + // check if we should filter this line + if !filter_by_swap_id(&line) { + continue; + } + + let line = if redact_addresses { redact(&line) } else { line }; + write_to_channel(&mut output_channel, &line).await?; + // don't forget newlines + write_to_channel(&mut output_channel, "\n").await?; + } + } + + Ok(()) +} + /// Redact logs, etc. by replacing Bitcoin and Monero addresses /// with generic placeholders. ///