Add an HTTP API endpoint + UI widgets to stream logs (#49)

* Added JSON logs to Desktop

* Move logging config into librqbit for reuse

* Log printer now available in both Desktop and Web UI

* Fix JS type error
This commit is contained in:
Igor Katson 2023-12-09 00:26:14 +00:00 committed by GitHub
parent 9385524a1a
commit 2017c5ec94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 462 additions and 333 deletions

View file

@ -4126,6 +4126,16 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
@ -4136,12 +4146,15 @@ dependencies = [
"nu-ansi-term",
"once_cell",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]

View file

@ -22,7 +22,7 @@ anyhow = "1.0.75"
base64 = "0.21.5"
http = "1.0.0"
directories = "5.0.1"
tracing-subscriber = {version = "0.3.18", features = ["env-filter"] }
tracing-subscriber = {version = "0.3.18", features = ["env-filter", "json"] }
tracing = "0.1"
serde_with = "3.4.0"
parking_lot = "0.12.1"

View file

@ -19,8 +19,7 @@ use librqbit::{
TorrentStats,
},
dht::PersistentDhtConfig,
librqbit_spawn,
log_subscriber::LineBroadcast,
tracing_subscriber_config_utils::{init_logging, InitLoggingOptions, InitLoggingResult},
AddTorrent, AddTorrentOptions, Api, ApiError, PeerConnectionOptions, Session, SessionOptions,
};
use parking_lot::RwLock;
@ -35,12 +34,10 @@ struct StateShared {
api: Option<Api>,
}
type RustLogReloadTx = tokio::sync::mpsc::UnboundedSender<String>;
struct State {
config_filename: String,
shared: Arc<RwLock<Option<StateShared>>>,
init_logging: InitLogging,
init_logging: InitLoggingResult,
}
fn read_config(path: &str) -> anyhow::Result<RqbitDesktopConfig> {
@ -65,7 +62,7 @@ fn write_config(path: &str, config: &RqbitDesktopConfig) -> anyhow::Result<()> {
}
async fn api_from_config(
init_logging: &InitLogging,
init_logging: &InitLoggingResult,
config: &RqbitDesktopConfig,
) -> anyhow::Result<Api> {
let session = Session::new_with_opts(
@ -98,7 +95,7 @@ async fn api_from_config(
let api = Api::new(
session.clone(),
Some(init_logging.reload_stdout_tx.clone()),
Some(init_logging.rust_log_reload_tx.clone()),
Some(init_logging.line_broadcast.clone()),
);
@ -118,7 +115,7 @@ async fn api_from_config(
}
impl State {
async fn new(init_logging: InitLogging) -> Self {
async fn new(init_logging: InitLoggingResult) -> Self {
let config_filename = directories::ProjectDirs::from("com", "rqbit", "desktop")
.expect("directories::ProjectDirs::from")
.config_dir()
@ -302,64 +299,16 @@ fn get_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
struct InitLogging {
reload_stdout_tx: RustLogReloadTx,
line_broadcast: LineBroadcast,
}
fn init_logging() -> InitLogging {
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
let (stderr_filter, reload_stderr_filter) = tracing_subscriber::reload::Layer::new(
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.from_env()
.unwrap(),
);
let (line_sub, line_broadcast) = librqbit::log_subscriber::Subscriber::new();
let layered = tracing_subscriber::registry()
.with(fmt::layer().with_filter(stderr_filter))
.with(
fmt::layer()
.with_ansi(false)
.fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new().delimited(","))
.event_format(fmt::format().with_ansi(false))
.with_writer(line_sub)
.with_filter(EnvFilter::builder().parse("info,librqbit=debug").unwrap()),
);
layered.init();
let (reload_tx, mut reload_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
librqbit_spawn(
"fmt_filter_reloader",
error_span!("fmt_filter_reloader"),
async move {
while let Some(rust_log) = reload_rx.recv().await {
let stderr_env_filter = match EnvFilter::builder().parse(&rust_log) {
Ok(f) => f,
Err(e) => {
eprintln!("can't parse env filter {:?}: {:#?}", rust_log, e);
continue;
}
};
eprintln!("setting RUST_LOG to {:?}", rust_log);
let _ = reload_stderr_filter.reload(stderr_env_filter);
}
Ok(())
},
);
InitLogging {
reload_stdout_tx: reload_tx,
line_broadcast,
}
}
async fn start() {
tauri::async_runtime::set(tokio::runtime::Handle::current());
let rust_log_reload_tx = init_logging();
let init_logging_result = init_logging(InitLoggingOptions {
default_rust_log_value: Some("info"),
log_file: None,
log_file_rust_log: None,
})
.unwrap();
let state = State::new(rust_log_reload_tx).await;
let state = State::new(init_logging_result).await;
tauri::Builder::default()
.manage(state)

View file

@ -18,27 +18,25 @@ export const RqbitDesktop: React.FC<{
currentState.config ?? defaultConfig
);
let [configurationOpened, setConfigurationOpened] = useState<boolean>(false);
let [logsOpened, setLogsOpened] = useState<boolean>(false);
const configButton = (
<IconButton
className="p-3 text-primary"
onClick={() => {
setConfigurationOpened(true);
}}
>
<BsSliders2 />
</IconButton>
);
return (
<APIContext.Provider value={makeAPI(config)}>
{configured && (
<RqbitWebUI title={`Rqbit Desktop v${version}`}></RqbitWebUI>
)}
{configured && (
<div className="position-absolute top-0 start-0">
<IconButton
className="p-3 text-primary"
onClick={() => {
setConfigurationOpened(true);
}}
>
<BsSliders2 />
</IconButton>
<IconButton onClick={() => setLogsOpened(true)}>
<BsBodyText />
</IconButton>
</div>
<RqbitWebUI
title={`Rqbit Desktop v${version}`}
menuButtons={[configButton]}
></RqbitWebUI>
)}
<ConfigModal
show={!configured || configurationOpened}
@ -56,7 +54,6 @@ export const RqbitDesktop: React.FC<{
initialConfig={config}
defaultConfig={defaultConfig}
/>
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
</APIContext.Provider>
);
};