feat(readarr): fetch Readarr logs and expose them on the CLI
Adds ReadarrEvent::GetLogs backed by get_readarr_logs, which formats each record into the Readarr logs list, and surfaces it as 'readarr list logs --events N'. The --output-in-log-format branch reads the Readarr log lines, and is covered by a test that pins that source so it cannot silently drift to another servarr's data.
This commit is contained in:
6 files changed
+276
-9
No files matched your search
@@ -15,6 +15,7 @@ mod readarr_network_tests;
|
||||
pub enum ReadarrEvent {
|
||||
GetDiskSpace,
|
||||
GetHostConfig,
|
||||
GetLogs(u64),
|
||||
GetSecurityConfig,
|
||||
GetStatus,
|
||||
GetUpdates,
|
||||
@@ -27,6 +28,7 @@ impl NetworkResource for ReadarrEvent {
|
||||
ReadarrEvent::GetHostConfig | ReadarrEvent::GetSecurityConfig => "/config/host",
|
||||
ReadarrEvent::GetDiskSpace => "/diskspace",
|
||||
ReadarrEvent::HealthCheck => "/health",
|
||||
ReadarrEvent::GetLogs(_) => "/log",
|
||||
ReadarrEvent::GetStatus => "/system/status",
|
||||
ReadarrEvent::GetUpdates => "/update",
|
||||
}
|
||||
@@ -53,6 +55,10 @@ impl Network<'_, '_> {
|
||||
.get_readarr_host_config()
|
||||
.await
|
||||
.map(ReadarrSerdeable::from),
|
||||
ReadarrEvent::GetLogs(events) => self
|
||||
.get_readarr_logs(events)
|
||||
.await
|
||||
.map(ReadarrSerdeable::from),
|
||||
ReadarrEvent::GetSecurityConfig => self
|
||||
.get_readarr_security_config()
|
||||
.await
|
||||
|
||||
@@ -10,6 +10,7 @@ mod tests {
|
||||
#[case(ReadarrEvent::GetSecurityConfig, "/config/host")]
|
||||
#[case(ReadarrEvent::GetDiskSpace, "/diskspace")]
|
||||
#[case(ReadarrEvent::HealthCheck, "/health")]
|
||||
#[case(ReadarrEvent::GetLogs(500), "/log")]
|
||||
#[case(ReadarrEvent::GetStatus, "/system/status")]
|
||||
#[case(ReadarrEvent::GetUpdates, "/update")]
|
||||
fn test_resource(#[case] event: ReadarrEvent, #[case] expected_uri: &str) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::models::ScrollableText;
|
||||
use crate::models::servarr_models::{DiskSpace, HostConfig, SecurityConfig, SystemStatus, Update};
|
||||
use crate::models::servarr_models::{
|
||||
DiskSpace, HostConfig, LogResponse, SecurityConfig, SystemStatus, Update,
|
||||
};
|
||||
use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
@@ -43,6 +45,62 @@ impl Network<'_, '_> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::readarr_network) async fn get_readarr_logs(
|
||||
&mut self,
|
||||
events: u64,
|
||||
) -> Result<LogResponse> {
|
||||
info!("Fetching Readarr logs");
|
||||
let event = ReadarrEvent::GetLogs(events);
|
||||
|
||||
let params = format!("pageSize={events}&sortDirection=descending&sortKey=time");
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), LogResponse>(request_props, |log_response, mut app| {
|
||||
let mut logs = log_response.records;
|
||||
logs.reverse();
|
||||
|
||||
let log_lines = logs
|
||||
.into_iter()
|
||||
.map(|log| {
|
||||
if let Some(exception) = log.exception {
|
||||
HorizontallyScrollableText::from(format!(
|
||||
"{}|{}|{}|{}|{}",
|
||||
log.time,
|
||||
log.level.to_uppercase(),
|
||||
log
|
||||
.logger
|
||||
.as_ref()
|
||||
.expect("logger must exist when exception is present"),
|
||||
log
|
||||
.exception_type
|
||||
.as_ref()
|
||||
.expect("exception_type must exist when exception is present"),
|
||||
exception
|
||||
))
|
||||
} else {
|
||||
HorizontallyScrollableText::from(format!(
|
||||
"{}|{}|{}|{}",
|
||||
log.time,
|
||||
log.level.to_uppercase(),
|
||||
log.logger.as_ref().expect("logger must exist in log entry"),
|
||||
log
|
||||
.message
|
||||
.as_ref()
|
||||
.expect("message must exist when exception is not present")
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
app.data.readarr_data.logs.set_items(log_lines);
|
||||
app.data.readarr_data.logs.scroll_to_bottom();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::readarr_network) async fn get_readarr_security_config(
|
||||
&mut self,
|
||||
) -> Result<SecurityConfig> {
|
||||
|
||||
@@ -4,7 +4,7 @@ mod tests {
|
||||
use crate::models::readarr_models::ReadarrSerdeable;
|
||||
use crate::models::servarr_models::{
|
||||
AuthenticationMethod, AuthenticationRequired, CertificateValidation, DiskSpace, HostConfig,
|
||||
SecurityConfig, SystemStatus, Update,
|
||||
LogResponse, SecurityConfig, SystemStatus, Update,
|
||||
};
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use crate::network::readarr_network::ReadarrEvent;
|
||||
@@ -145,6 +145,92 @@ mod tests {
|
||||
assert_err!(result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_readarr_logs_event() {
|
||||
let logs_response_json = json!({
|
||||
"page": 1,
|
||||
"pageSize": 500,
|
||||
"sortKey": "time",
|
||||
"sortDirection": "descending",
|
||||
"totalRecords": 2,
|
||||
"records": [
|
||||
{
|
||||
"time": "2023-05-20T21:29:16Z",
|
||||
"level": "info",
|
||||
"logger": "TestLogger",
|
||||
"message": "test message",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"time": "2023-05-20T21:29:16Z",
|
||||
"level": "fatal",
|
||||
"logger": "ReadarrError",
|
||||
"exception": "test exception",
|
||||
"exceptionType": "Some.Big.Bad.Exception",
|
||||
"id": 2
|
||||
}
|
||||
]
|
||||
});
|
||||
let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap();
|
||||
let expected_logs = vec![
|
||||
HorizontallyScrollableText::from(
|
||||
"2023-05-20 21:29:16 UTC|FATAL|ReadarrError|Some.Big.Bad.Exception|test exception",
|
||||
),
|
||||
HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"),
|
||||
];
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(logs_response_json)
|
||||
.query("pageSize=500&sortDirection=descending&sortKey=time")
|
||||
.build_for(ReadarrEvent::GetLogs(500))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(3);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_readarr_event(ReadarrEvent::GetLogs(500))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let ReadarrSerdeable::LogResponse(logs) = result.unwrap() else {
|
||||
panic!("Expected LogResponse")
|
||||
};
|
||||
|
||||
assert_eq!(logs, response);
|
||||
assert_eq!(app.lock().await.data.readarr_data.logs.items, expected_logs);
|
||||
assert_str_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.readarr_data
|
||||
.logs
|
||||
.current_selection()
|
||||
.text,
|
||||
"2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_readarr_logs_event_failure() {
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(json!({}))
|
||||
.status(500)
|
||||
.query("pageSize=500&sortDirection=descending&sortKey=time")
|
||||
.build_for(ReadarrEvent::GetLogs(500))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(3);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_readarr_event(ReadarrEvent::GetLogs(500))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
assert_err!(result);
|
||||
assert_is_empty!(app.lock().await.data.readarr_data.logs);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_readarr_security_config_event() {
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
|
||||
Reference in new issue
Block a user