use std::path::Path;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
use crate::client::logs::{LogDownload, LogEntry, LogQuery, LoggerInfo};
use crate::client::GatewayApi;
pub use crate::client::logs::LogPage;
use crate::client::query::ListEnvelope;
use crate::error::CoreError;
use crate::poll::{self, PollConfig, PollState};
#[derive(Debug, Serialize)]
pub struct DownloadResult {
pub file: String,
pub bytes: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SetLevelResult {
pub logger: String,
pub level: String,
}
#[derive(Debug, Serialize)]
pub struct ResetResult {
pub reset: bool,
}
pub type LoggersEnvelope = ListEnvelope<LoggerInfo>;
pub async fn list_logs(
api: &dyn GatewayApi,
logger: Option<&str>,
min_level: Option<&str>,
since_ms: Option<i64>,
limit: i64,
) -> Result<LogPage, CoreError> {
let query = LogQuery {
start_time: since_ms,
logger: logger.map(str::to_string),
min_level: min_level.map(str::to_string),
limit,
sort_by: Some("desc(timestamp)".to_string()),
..LogQuery::default()
};
api.logs(&query).await
}
pub async fn loggers(
api: &dyn GatewayApi,
search: Option<&str>,
) -> Result<ListEnvelope<LoggerInfo>, CoreError> {
let query = crate::client::query::ListQuery {
limit: crate::client::logs::DEFAULT_LOG_LIMIT,
search: search.map(str::to_string),
..Default::default()
};
api.loggers(&query).await
}
pub async fn set_logger_level(
api: &dyn GatewayApi,
logger: &str,
level: &str,
) -> Result<SetLevelResult, CoreError> {
api.set_logger_level(logger, level).await?;
Ok(SetLevelResult {
logger: logger.to_string(),
level: level.to_string(),
})
}
pub async fn reset_logger_levels(api: &dyn GatewayApi) -> Result<ResetResult, CoreError> {
api.reset_logger_levels().await?;
Ok(ResetResult { reset: true })
}
fn download_filename(
output: Option<&Path>,
download: &LogDownload,
stem: &str,
now_secs: i64,
) -> String {
if let Some(output) = output {
return output.display().to_string();
}
if let Some(filename) = download.filename.as_deref().filter(|name| !name.is_empty()) {
return filename.to_string();
}
format!("{stem}-logs-{now_secs}.idb")
}
pub async fn download(
api: &dyn GatewayApi,
output: Option<&Path>,
fallback_stem: &str,
) -> Result<DownloadResult, CoreError> {
let fetched = api.logs_download().await?;
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|since| since.as_secs() as i64)
.unwrap_or_default();
let file = download_filename(output, &fetched, fallback_stem, now_secs);
std::fs::write(&file, &fetched.bytes)
.map_err(|err| CoreError::Internal(format!("cannot write log archive {file}: {err}")))?;
Ok(DownloadResult {
bytes: fetched.bytes.len(),
content_type: fetched.content_type,
file,
})
}
pub fn parse_since(spec: &str, now_ms: i64) -> Result<i64, String> {
let spec = spec.trim();
for (suffix, unit_ms) in [
("ms", 1_i64),
("min", 60_000),
("h", 3_600_000),
("s", 1_000),
] {
if let Some(digits) = spec.strip_suffix(suffix)
&& let Ok(count) = digits.parse::<i64>()
&& count >= 0
{
return Ok(now_ms - count * unit_ms);
}
}
match spec.parse::<i64>() {
Ok(epoch_ms) if epoch_ms >= 0 => Ok(epoch_ms),
_ => Err(format!(
"invalid --since {spec:?}: expected EPOCH-MS or a relative span like 500ms, 30s, 5min, 2h"
)),
}
}
#[derive(Debug, Default, Serialize)]
pub struct TailResult {
pub streamed: usize,
}
struct TailState<'a> {
cursor: i64,
sink: &'a mut (dyn FnMut(&LogEntry) + Send),
}
pub async fn tail(
api: &dyn GatewayApi,
logger: Option<&str>,
min_level: Option<&str>,
since_ms: Option<i64>,
interval: Duration,
deadline: Option<Duration>,
sink: &mut (dyn FnMut(&LogEntry) + Send),
) -> Result<TailResult, CoreError> {
let state = TailState {
cursor: since_ms.unwrap_or(0) - 1,
sink,
};
let streamed = Mutex::new(0usize);
let cfg = PollConfig {
subject: "log tail (GET /data/api/v1/logs)".to_string(),
interval,
deadline: deadline.unwrap_or(Duration::MAX),
..PollConfig::default()
};
let outcome = poll::poll(cfg, state, |state| {
Box::pin(async {
let query = LogQuery {
start_time: Some(state.cursor + 1),
logger: logger.map(str::to_string),
min_level: min_level.map(str::to_string),
..LogQuery::default()
};
let page = api.logs(&query).await?;
let mut entries = page.items;
entries.sort_by_key(|entry| entry.timestamp);
let observation = entries
.last()
.map(|last| format!("{} entries, latest at {}", entries.len(), last.timestamp));
for entry in &entries {
(state.sink)(entry);
}
if let Some(last) = entries.last() {
state.cursor = last.timestamp;
}
*streamed.lock().expect("streamed count") += entries.len();
Ok(PollState::<()>::Pending(observation))
})
})
.await;
match outcome {
Ok(()) => Ok(TailResult {
streamed: *streamed.lock().expect("streamed count"),
}),
Err(CoreError::Network { source: None, .. }) => Ok(TailResult {
streamed: *streamed.lock().expect("streamed count"),
}),
Err(err) => Err(err),
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::time::Duration;
use super::{download_filename, parse_since, tail};
use crate::client::GatewayApi;
use crate::client::logs::{LogDownload, LogEntry, LogQuery};
use crate::client::query::{ListEnvelope, ListMetadata};
use crate::error::CoreError;
#[test]
fn parse_since_accepts_epoch_and_relative_spans() {
const NOW: i64 = 1_787_346_747_022;
assert_eq!(parse_since("1787346747022", NOW), Ok(1787346747022));
assert_eq!(parse_since("0", NOW), Ok(0));
assert_eq!(parse_since("500ms", NOW), Ok(NOW - 500));
assert_eq!(parse_since("30s", NOW), Ok(NOW - 30_000));
assert_eq!(parse_since("5min", NOW), Ok(NOW - 300_000));
assert_eq!(parse_since("2h", NOW), Ok(NOW - 7_200_000));
assert_eq!(parse_since("1s", NOW), Ok(NOW - 1_000));
assert!(parse_since("banana", NOW).is_err());
assert!(
parse_since("-5s", NOW).is_err(),
"negative spans are invalid"
);
assert!(parse_since("", NOW).is_err());
}
#[test]
fn download_filename_precedence() {
let fetched = LogDownload {
bytes: Vec::new(),
filename: Some("GW_Ignition_logs_20260822-0307.idb".into()),
content_type: Some("application/x-sqlite3".into()),
};
assert_eq!(
download_filename(
Some(std::path::Path::new("/tmp/out.idb")),
&fetched,
"dev",
1000
),
"/tmp/out.idb",
"-o wins"
);
assert_eq!(
download_filename(None, &fetched, "dev", 1000),
"GW_Ignition_logs_20260822-0307.idb",
"Content-Disposition name second"
);
let anonymous = LogDownload {
bytes: Vec::new(),
filename: None,
content_type: None,
};
assert_eq!(
download_filename(None, &anonymous, "dev", 1_787_346_747),
"dev-logs-1787346747.idb",
"fallback = <stem>-logs-<unix_ts>.idb — never .zip"
);
}
#[derive(Default)]
struct TailRig {
pages: Mutex<std::collections::VecDeque<Vec<LogEntry>>>,
queries: Mutex<Vec<LogQuery>>,
}
fn entry(timestamp: i64, message: &str) -> LogEntry {
LogEntry {
timestamp,
logger_name: "GatewayManager".into(),
level: "INFO".into(),
message: message.into(),
stack: Vec::new(),
mdc: Default::default(),
extra: Default::default(),
}
}
#[async_trait::async_trait]
impl GatewayApi for TailRig {
async fn bundle_generate(
&self,
) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn bundle_status(
&self,
) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn bundle_download(
&self,
_out: &std::path::Path,
) -> Result<crate::client::projects::ExportMeta, CoreError> {
unreachable!("not part of this action")
}
async fn tag_provider_list(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<
crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
CoreError,
> {
unreachable!("not part of this action")
}
async fn tag_provider_find(
&self,
_name: &str,
) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
unreachable!("not part of this action")
}
async fn tag_provider_create(
&self,
_body: &[crate::client::tags::TagProviderCreate],
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn tag_provider_delete(
&self,
_name: &str,
_signature: &str,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
unreachable!("not part of this action")
}
async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
unreachable!("not part of this action")
}
async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
unreachable!("not part of this action")
}
async fn backup_download(
&self,
_out: &std::path::Path,
_backup_type: crate::client::backup::BackupType,
) -> Result<crate::client::projects::ExportMeta, CoreError> {
unreachable!("not part of this action")
}
async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_history(
&self,
_limit: Option<u32>,
_search: Option<&str>,
) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
{
unreachable!("not part of this action")
}
async fn eam_task_definitions(
&self,
) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
{
unreachable!("not part of this action")
}
async fn eam_task_find(
&self,
_name: &str,
) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_tasks_scheduled(
&self,
_running: bool,
) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_modify(
&self,
_definition: &serde_json::Value,
) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_delete(
&self,
_name: &str,
_signature: &str,
_confirm: bool,
) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
unreachable!("not part of this action")
}
async fn api_call(
&self,
_call: &crate::client::apicall::ApiCallRequest,
) -> Result<crate::client::apicall::ApiCallData, CoreError> {
unreachable!("not part of this action")
}
async fn license_status(
&self,
) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn redundancy_status(
&self,
) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn logs(&self, filter: &LogQuery) -> Result<ListEnvelope<LogEntry>, CoreError> {
self.queries.lock().unwrap().push(filter.clone());
let items = self.pages.lock().unwrap().pop_front().unwrap_or_default();
Ok(ListEnvelope {
metadata: ListMetadata {
total: items.len() as i64,
matching: items.len() as i64,
limit: 200,
offset: 0,
},
items,
})
}
async fn gateway_info(&self) -> Result<crate::client::version::GatewayInfo, CoreError> {
unreachable!("not part of this action")
}
async fn overview(&self) -> Result<crate::client::status::Overview, CoreError> {
unreachable!("not part of this action")
}
async fn status_ping(&self) -> Result<crate::client::status::StatusPing, CoreError> {
unreachable!("not part of this action")
}
async fn modules(
&self,
_quarantined: bool,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::status::ModuleInfo>, CoreError> {
unreachable!("not part of this action")
}
async fn metrics_current(
&self,
) -> Result<crate::client::metrics::CurrentGauges, CoreError> {
unreachable!("not part of this action")
}
async fn metrics_historic(
&self,
) -> Result<crate::client::metrics::PerformanceCharts, CoreError> {
unreachable!("not part of this action")
}
async fn metrics_threads(&self) -> Result<crate::client::metrics::ThreadCounts, CoreError> {
unreachable!("not part of this action")
}
async fn designers(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
unreachable!("not part of this action")
}
async fn perspective_sessions(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
unreachable!("not part of this action")
}
async fn vision_clients(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
unreachable!("not part of this action")
}
async fn terminate_perspective_session(
&self,
_id: &str,
_message: Option<&str>,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn database_connections(
&self,
) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
{
unreachable!("not part of this action")
}
async fn opc_connections(
&self,
) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
{
unreachable!("not part of this action")
}
async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
unreachable!("not part of this action")
}
async fn loggers(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
unreachable!("not part of this action")
}
async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn reset_logger_levels(&self) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn restart(&self) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn scan_projects(&self) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn security_properties(
&self,
) -> Result<crate::client::restart::SecurityProperties, CoreError> {
unreachable!("not part of this action")
}
async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
unreachable!("not part of this action")
}
async fn webdev_route_call(
&self,
_project: &str,
_route: &str,
_body: &serde_json::Value,
_extra_headers: &[(&str, &str)],
) -> Result<serde_json::Value, CoreError> {
unreachable!("not part of this action")
}
async fn webdev_route_probe(
&self,
_project: &str,
_route: &str,
_extra_headers: &[(&str, &str)],
) -> Result<crate::client::webdev::RouteProbe, CoreError> {
unreachable!("not part of this action")
}
async fn projects(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<
crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
CoreError,
> {
unreachable!("not part of this action")
}
async fn project_find(
&self,
_name: &str,
) -> Result<crate::client::projects::ProjectRecord, CoreError> {
unreachable!("not part of this action")
}
async fn project_create(
&self,
_body: &crate::client::projects::ProjectCreate,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_modify(
&self,
_name: &str,
_body: &crate::client::projects::ProjectModify,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_export_to_file(
&self,
_name: &str,
_out: &std::path::Path,
) -> Result<crate::client::projects::ExportMeta, CoreError> {
unreachable!("not part of this action")
}
async fn project_import(
&self,
_name: &str,
_zip: Vec<u8>,
_overwrite: bool,
) -> Result<crate::client::projects::ImportOutcome, CoreError> {
unreachable!("not part of this action")
}
}
#[tokio::test]
async fn tail_streams_pages_in_order_and_ends_cleanly_on_deadline() {
let rig = TailRig {
pages: Mutex::new(
vec![
vec![entry(1010, "second"), entry(1005, "first")],
vec![entry(1022, "third"), entry(1018, "wait, also")],
]
.into(),
),
queries: Mutex::new(Vec::new()),
};
let mut received: Vec<(i64, String)> = Vec::new();
let sink: &mut (dyn FnMut(&LogEntry) + Send) = &mut |entry: &LogEntry| {
received.push((entry.timestamp, entry.message.clone()));
};
let result = tail(
&rig,
None,
None,
Some(1000), Duration::from_millis(5),
Some(Duration::from_millis(400)),
sink,
)
.await
.expect("deadline expiry ends the tail cleanly");
assert_eq!(
received,
vec![
(1005, "first".into()),
(1010, "second".into()),
(1018, "wait, also".into()),
(1022, "third".into()),
],
"stream order is timestamp order (client-side sort)"
);
assert_eq!(result.streamed, 4);
let queries = rig.queries.lock().unwrap();
assert_eq!(queries[0].start_time, Some(1000), "first = since");
assert!(
queries.len() >= 3,
"polled again after each page: {}",
queries.len()
);
assert_eq!(queries[1].start_time, Some(1011), "cursor = max + 1");
assert!(
queries[2].start_time == Some(1023),
"cursor advanced past page 2: {:?}",
queries[2].start_time
);
assert!(queries.iter().all(|query| query.limit == 200));
}
#[tokio::test]
async fn tail_fails_fast_on_auth() {
struct AuthRig;
#[async_trait::async_trait]
impl GatewayApi for AuthRig {
async fn bundle_generate(
&self,
) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn bundle_status(
&self,
) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn bundle_download(
&self,
_out: &std::path::Path,
) -> Result<crate::client::projects::ExportMeta, CoreError> {
unreachable!("not part of this action")
}
async fn tag_provider_list(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<
crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
CoreError,
> {
unreachable!("not part of this action")
}
async fn tag_provider_find(
&self,
_name: &str,
) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
unreachable!("not part of this action")
}
async fn tag_provider_create(
&self,
_body: &[crate::client::tags::TagProviderCreate],
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn tag_provider_delete(
&self,
_name: &str,
_signature: &str,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn trial_status_wire(
&self,
) -> Result<crate::client::trial::TrialWire, CoreError> {
unreachable!("not part of this action")
}
async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
unreachable!("not part of this action")
}
async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
unreachable!("not part of this action")
}
async fn backup_download(
&self,
_out: &std::path::Path,
_backup_type: crate::client::backup::BackupType,
) -> Result<crate::client::projects::ExportMeta, CoreError> {
unreachable!("not part of this action")
}
async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_history(
&self,
_limit: Option<u32>,
_search: Option<&str>,
) -> Result<
crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>,
CoreError,
> {
unreachable!("not part of this action")
}
async fn eam_task_definitions(
&self,
) -> Result<
crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>,
CoreError,
> {
unreachable!("not part of this action")
}
async fn eam_task_find(
&self,
_name: &str,
) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_create(
&self,
_definition: &serde_json::Value,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn eam_tasks_scheduled(
&self,
_running: bool,
) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_modify(
&self,
_definition: &serde_json::Value,
) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
unreachable!("not part of this action")
}
async fn eam_task_delete(
&self,
_name: &str,
_signature: &str,
_confirm: bool,
) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
unreachable!("not part of this action")
}
async fn api_call(
&self,
_call: &crate::client::apicall::ApiCallRequest,
) -> Result<crate::client::apicall::ApiCallData, CoreError> {
unreachable!("not part of this action")
}
async fn license_status(
&self,
) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn redundancy_status(
&self,
) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
unreachable!("not part of this action")
}
async fn logs(&self, _filter: &LogQuery) -> Result<ListEnvelope<LogEntry>, CoreError> {
Err(CoreError::Auth {
status: 401,
endpoint: Some("http://gw/data/api/v1/logs".into()),
})
}
async fn gateway_info(&self) -> Result<crate::client::version::GatewayInfo, CoreError> {
unreachable!("not part of this action")
}
async fn overview(&self) -> Result<crate::client::status::Overview, CoreError> {
unreachable!("not part of this action")
}
async fn status_ping(&self) -> Result<crate::client::status::StatusPing, CoreError> {
unreachable!("not part of this action")
}
async fn modules(
&self,
_quarantined: bool,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::status::ModuleInfo>, CoreError> {
unreachable!("not part of this action")
}
async fn metrics_current(
&self,
) -> Result<crate::client::metrics::CurrentGauges, CoreError> {
unreachable!("not part of this action")
}
async fn metrics_historic(
&self,
) -> Result<crate::client::metrics::PerformanceCharts, CoreError> {
unreachable!("not part of this action")
}
async fn metrics_threads(
&self,
) -> Result<crate::client::metrics::ThreadCounts, CoreError> {
unreachable!("not part of this action")
}
async fn designers(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError>
{
unreachable!("not part of this action")
}
async fn perspective_sessions(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError>
{
unreachable!("not part of this action")
}
async fn vision_clients(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError>
{
unreachable!("not part of this action")
}
async fn terminate_perspective_session(
&self,
_id: &str,
_message: Option<&str>,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn database_connections(
&self,
) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
{
unreachable!("not part of this action")
}
async fn opc_connections(
&self,
) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
{
unreachable!("not part of this action")
}
async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
unreachable!("not part of this action")
}
async fn loggers(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
unreachable!("not part of this action")
}
async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn reset_logger_levels(&self) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn restart(&self) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn scan_projects(&self) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn security_properties(
&self,
) -> Result<crate::client::restart::SecurityProperties, CoreError> {
unreachable!("not part of this action")
}
async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
unreachable!("not part of this action")
}
async fn webdev_route_call(
&self,
_project: &str,
_route: &str,
_body: &serde_json::Value,
_extra_headers: &[(&str, &str)],
) -> Result<serde_json::Value, CoreError> {
unreachable!("not part of this action")
}
async fn webdev_route_probe(
&self,
_project: &str,
_route: &str,
_extra_headers: &[(&str, &str)],
) -> Result<crate::client::webdev::RouteProbe, CoreError> {
unreachable!("not part of this action")
}
async fn projects(
&self,
_query: &crate::client::query::ListQuery,
) -> Result<
crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
CoreError,
> {
unreachable!("not part of this action")
}
async fn project_find(
&self,
_name: &str,
) -> Result<crate::client::projects::ProjectRecord, CoreError> {
unreachable!("not part of this action")
}
async fn project_create(
&self,
_body: &crate::client::projects::ProjectCreate,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_modify(
&self,
_name: &str,
_body: &crate::client::projects::ProjectModify,
) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
unreachable!("not part of this action")
}
async fn project_export_to_file(
&self,
_name: &str,
_out: &std::path::Path,
) -> Result<crate::client::projects::ExportMeta, CoreError> {
unreachable!("not part of this action")
}
async fn project_import(
&self,
_name: &str,
_zip: Vec<u8>,
_overwrite: bool,
) -> Result<crate::client::projects::ImportOutcome, CoreError> {
unreachable!("not part of this action")
}
}
let sink: &mut (dyn FnMut(&LogEntry) + Send) = &mut |_| {};
let err = tail(
&AuthRig,
None,
None,
None,
Duration::from_millis(5),
Some(Duration::from_secs(5)),
sink,
)
.await
.expect_err("auth must fail fast");
assert!(matches!(err, CoreError::Auth { status: 401, .. }));
assert_eq!(err.exit_code(), 5);
}
}