1use std::path::Path;
23use std::sync::Mutex;
24use std::time::{Duration, SystemTime, UNIX_EPOCH};
25
26use serde::Serialize;
27
28use crate::client::logs::{LogDownload, LogEntry, LogQuery, LoggerInfo};
29use crate::client::GatewayApi;
32pub use crate::client::logs::LogPage;
33use crate::client::query::ListEnvelope;
34use crate::error::CoreError;
35use crate::poll::{self, PollConfig, PollState};
36
37#[derive(Debug, Serialize)]
39pub struct DownloadResult {
40 pub file: String,
42 pub bytes: usize,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub content_type: Option<String>,
47}
48
49#[derive(Debug, Serialize)]
51pub struct SetLevelResult {
52 pub logger: String,
54 pub level: String,
56}
57
58#[derive(Debug, Serialize)]
60pub struct ResetResult {
61 pub reset: bool,
63}
64
65pub type LoggersEnvelope = ListEnvelope<LoggerInfo>;
67
68pub async fn list_logs(
74 api: &dyn GatewayApi,
75 logger: Option<&str>,
76 min_level: Option<&str>,
77 since_ms: Option<i64>,
78 limit: i64,
79) -> Result<LogPage, CoreError> {
80 let query = LogQuery {
81 start_time: since_ms,
82 logger: logger.map(str::to_string),
83 min_level: min_level.map(str::to_string),
84 limit,
85 sort_by: Some("desc(timestamp)".to_string()),
86 ..LogQuery::default()
87 };
88 api.logs(&query).await
89}
90
91pub async fn loggers(
95 api: &dyn GatewayApi,
96 search: Option<&str>,
97) -> Result<ListEnvelope<LoggerInfo>, CoreError> {
98 let query = crate::client::query::ListQuery {
99 limit: crate::client::logs::DEFAULT_LOG_LIMIT,
100 search: search.map(str::to_string),
101 ..Default::default()
102 };
103 api.loggers(&query).await
104}
105
106pub async fn set_logger_level(
110 api: &dyn GatewayApi,
111 logger: &str,
112 level: &str,
113) -> Result<SetLevelResult, CoreError> {
114 api.set_logger_level(logger, level).await?;
115 Ok(SetLevelResult {
116 logger: logger.to_string(),
117 level: level.to_string(),
118 })
119}
120
121pub async fn reset_logger_levels(api: &dyn GatewayApi) -> Result<ResetResult, CoreError> {
123 api.reset_logger_levels().await?;
124 Ok(ResetResult { reset: true })
125}
126
127fn download_filename(
131 output: Option<&Path>,
132 download: &LogDownload,
133 stem: &str,
134 now_secs: i64,
135) -> String {
136 if let Some(output) = output {
137 return output.display().to_string();
138 }
139 if let Some(filename) = download.filename.as_deref().filter(|name| !name.is_empty()) {
140 return filename.to_string();
141 }
142 format!("{stem}-logs-{now_secs}.idb")
143}
144
145pub async fn download(
149 api: &dyn GatewayApi,
150 output: Option<&Path>,
151 fallback_stem: &str,
152) -> Result<DownloadResult, CoreError> {
153 let fetched = api.logs_download().await?;
154 let now_secs = SystemTime::now()
155 .duration_since(UNIX_EPOCH)
156 .map(|since| since.as_secs() as i64)
157 .unwrap_or_default();
158 let file = download_filename(output, &fetched, fallback_stem, now_secs);
159 std::fs::write(&file, &fetched.bytes)
160 .map_err(|err| CoreError::Internal(format!("cannot write log archive {file}: {err}")))?;
161 Ok(DownloadResult {
162 bytes: fetched.bytes.len(),
163 content_type: fetched.content_type,
164 file,
165 })
166}
167
168pub fn parse_since(spec: &str, now_ms: i64) -> Result<i64, String> {
174 let spec = spec.trim();
175 for (suffix, unit_ms) in [
177 ("ms", 1_i64),
178 ("min", 60_000),
179 ("h", 3_600_000),
180 ("s", 1_000),
181 ] {
182 if let Some(digits) = spec.strip_suffix(suffix)
183 && let Ok(count) = digits.parse::<i64>()
184 && count >= 0
185 {
186 return Ok(now_ms - count * unit_ms);
187 }
188 }
189 match spec.parse::<i64>() {
191 Ok(epoch_ms) if epoch_ms >= 0 => Ok(epoch_ms),
192 _ => Err(format!(
193 "invalid --since {spec:?}: expected EPOCH-MS or a relative span like 500ms, 30s, 5min, 2h"
194 )),
195 }
196}
197
198#[derive(Debug, Default, Serialize)]
202pub struct TailResult {
203 pub streamed: usize,
205}
206
207struct TailState<'a> {
210 cursor: i64,
212 sink: &'a mut (dyn FnMut(&LogEntry) + Send),
218}
219
220pub async fn tail(
228 api: &dyn GatewayApi,
229 logger: Option<&str>,
230 min_level: Option<&str>,
231 since_ms: Option<i64>,
232 interval: Duration,
233 deadline: Option<Duration>,
234 sink: &mut (dyn FnMut(&LogEntry) + Send),
235) -> Result<TailResult, CoreError> {
236 let state = TailState {
239 cursor: since_ms.unwrap_or(0) - 1,
240 sink,
241 };
242 let streamed = Mutex::new(0usize);
246
247 let cfg = PollConfig {
248 subject: "log tail (GET /data/api/v1/logs)".to_string(),
249 interval,
250 deadline: deadline.unwrap_or(Duration::MAX),
251 ..PollConfig::default()
252 };
253
254 let outcome = poll::poll(cfg, state, |state| {
255 Box::pin(async {
256 let query = LogQuery {
257 start_time: Some(state.cursor + 1),
258 logger: logger.map(str::to_string),
259 min_level: min_level.map(str::to_string),
260 ..LogQuery::default()
261 };
262 let page = api.logs(&query).await?;
263 let mut entries = page.items;
264 entries.sort_by_key(|entry| entry.timestamp);
266 let observation = entries
267 .last()
268 .map(|last| format!("{} entries, latest at {}", entries.len(), last.timestamp));
269 for entry in &entries {
270 (state.sink)(entry);
271 }
272 if let Some(last) = entries.last() {
273 state.cursor = last.timestamp;
274 }
275 *streamed.lock().expect("streamed count") += entries.len();
276 Ok(PollState::<()>::Pending(observation))
277 })
278 })
279 .await;
280
281 match outcome {
282 Ok(()) => Ok(TailResult {
285 streamed: *streamed.lock().expect("streamed count"),
286 }),
287 Err(CoreError::Network { source: None, .. }) => Ok(TailResult {
291 streamed: *streamed.lock().expect("streamed count"),
292 }),
293 Err(err) => Err(err),
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use std::sync::Mutex;
300 use std::time::Duration;
301
302 use super::{download_filename, parse_since, tail};
303 use crate::client::GatewayApi;
304 use crate::client::logs::{LogDownload, LogEntry, LogQuery};
305 use crate::client::query::{ListEnvelope, ListMetadata};
306 use crate::error::CoreError;
307
308 #[test]
311 fn parse_since_accepts_epoch_and_relative_spans() {
312 const NOW: i64 = 1_787_346_747_022;
313 assert_eq!(parse_since("1787346747022", NOW), Ok(1787346747022));
314 assert_eq!(parse_since("0", NOW), Ok(0));
315 assert_eq!(parse_since("500ms", NOW), Ok(NOW - 500));
316 assert_eq!(parse_since("30s", NOW), Ok(NOW - 30_000));
317 assert_eq!(parse_since("5min", NOW), Ok(NOW - 300_000));
318 assert_eq!(parse_since("2h", NOW), Ok(NOW - 7_200_000));
319 assert_eq!(parse_since("1s", NOW), Ok(NOW - 1_000));
321 assert!(parse_since("banana", NOW).is_err());
322 assert!(
323 parse_since("-5s", NOW).is_err(),
324 "negative spans are invalid"
325 );
326 assert!(parse_since("", NOW).is_err());
327 }
328
329 #[test]
332 fn download_filename_precedence() {
333 let fetched = LogDownload {
334 bytes: Vec::new(),
335 filename: Some("GW_Ignition_logs_20260822-0307.idb".into()),
336 content_type: Some("application/x-sqlite3".into()),
337 };
338 assert_eq!(
339 download_filename(
340 Some(std::path::Path::new("/tmp/out.idb")),
341 &fetched,
342 "dev",
343 1000
344 ),
345 "/tmp/out.idb",
346 "-o wins"
347 );
348 assert_eq!(
349 download_filename(None, &fetched, "dev", 1000),
350 "GW_Ignition_logs_20260822-0307.idb",
351 "Content-Disposition name second"
352 );
353 let anonymous = LogDownload {
354 bytes: Vec::new(),
355 filename: None,
356 content_type: None,
357 };
358 assert_eq!(
359 download_filename(None, &anonymous, "dev", 1_787_346_747),
360 "dev-logs-1787346747.idb",
361 "fallback = <stem>-logs-<unix_ts>.idb — never .zip"
362 );
363 }
364
365 #[derive(Default)]
368 struct TailRig {
369 pages: Mutex<std::collections::VecDeque<Vec<LogEntry>>>,
370 queries: Mutex<Vec<LogQuery>>,
371 }
372
373 fn entry(timestamp: i64, message: &str) -> LogEntry {
374 LogEntry {
375 timestamp,
376 logger_name: "GatewayManager".into(),
377 level: "INFO".into(),
378 message: message.into(),
379 stack: Vec::new(),
380 mdc: Default::default(),
381 extra: Default::default(),
382 }
383 }
384
385 #[async_trait::async_trait]
386 impl GatewayApi for TailRig {
387 async fn thread_dump(
388 &self,
389 ) -> Result<crate::client::threads::FormattedThreadDump, CoreError> {
390 unreachable!("not part of this action")
391 }
392 async fn thread_deadlocks(
393 &self,
394 ) -> Result<crate::client::threads::DeadlocksWire, CoreError> {
395 unreachable!("not part of this action")
396 }
397 async fn uninstall_module(
398 &self,
399 _gateway_module_id: &str,
400 _module_id: &str,
401 ) -> Result<(), CoreError> {
402 unreachable!("not part of this action")
403 }
404 async fn bundle_generate(
405 &self,
406 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
407 unreachable!("not part of this action")
408 }
409 async fn bundle_status(
410 &self,
411 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
412 unreachable!("not part of this action")
413 }
414 async fn bundle_download(
415 &self,
416 _out: &std::path::Path,
417 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
418 unreachable!("not part of this action")
419 }
420 async fn tag_provider_list(
421 &self,
422 _query: &crate::client::query::ListQuery,
423 ) -> Result<
424 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
425 CoreError,
426 > {
427 unreachable!("not part of this action")
428 }
429 async fn tag_provider_find(
430 &self,
431 _name: &str,
432 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
433 unreachable!("not part of this action")
434 }
435 async fn tag_provider_create(
436 &self,
437 _body: &[crate::client::tags::TagProviderCreate],
438 ) -> Result<(), CoreError> {
439 unreachable!("not part of this action")
440 }
441 async fn tag_provider_delete(
442 &self,
443 _name: &str,
444 _signature: &str,
445 ) -> Result<(), CoreError> {
446 unreachable!("not part of this action")
447 }
448 async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
449 unreachable!("not part of this action")
450 }
451 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
452 unreachable!("not part of this action")
453 }
454 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
455 unreachable!("not part of this action")
456 }
457 async fn backup_download(
458 &self,
459 _out: &std::path::Path,
460 _backup_type: crate::client::backup::BackupType,
461 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
462 unreachable!("not part of this action")
463 }
464 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
465 unreachable!("not part of this action")
466 }
467 async fn eam_task_history(
468 &self,
469 _limit: Option<u32>,
470 _search: Option<&str>,
471 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
472 {
473 unreachable!("not part of this action")
474 }
475 async fn eam_task_definitions(
476 &self,
477 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
478 {
479 unreachable!("not part of this action")
480 }
481 async fn eam_task_find(
482 &self,
483 _name: &str,
484 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
485 unreachable!("not part of this action")
486 }
487 async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
488 unreachable!("not part of this action")
489 }
490 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
491 unreachable!("not part of this action")
492 }
493 async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
494 unreachable!("not part of this action")
495 }
496 async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
497 unreachable!("not part of this action")
498 }
499 async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
500 unreachable!("not part of this action")
501 }
502 async fn eam_tasks_scheduled(
503 &self,
504 _running: bool,
505 ) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
506 unreachable!("not part of this action")
507 }
508 async fn eam_task_modify(
509 &self,
510 _definition: &serde_json::Value,
511 ) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
512 unreachable!("not part of this action")
513 }
514 async fn eam_task_delete(
515 &self,
516 _name: &str,
517 _signature: &str,
518 _confirm: bool,
519 ) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
520 unreachable!("not part of this action")
521 }
522 async fn api_call(
523 &self,
524 _call: &crate::client::apicall::ApiCallRequest,
525 ) -> Result<crate::client::apicall::ApiCallData, CoreError> {
526 unreachable!("not part of this action")
527 }
528 async fn license_status(
529 &self,
530 ) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
531 unreachable!("not part of this action")
532 }
533 async fn redundancy_status(
534 &self,
535 ) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
536 unreachable!("not part of this action")
537 }
538 async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
539 unreachable!("not part of this action")
540 }
541 async fn logs(&self, filter: &LogQuery) -> Result<ListEnvelope<LogEntry>, CoreError> {
542 self.queries.lock().unwrap().push(filter.clone());
543 let items = self.pages.lock().unwrap().pop_front().unwrap_or_default();
544 Ok(ListEnvelope {
545 metadata: ListMetadata {
546 total: items.len() as i64,
547 matching: items.len() as i64,
548 limit: 200,
549 offset: 0,
550 },
551 items,
552 })
553 }
554 async fn gateway_info(&self) -> Result<crate::client::version::GatewayInfo, CoreError> {
555 unreachable!("not part of this action")
556 }
557 async fn overview(&self) -> Result<crate::client::status::Overview, CoreError> {
558 unreachable!("not part of this action")
559 }
560 async fn status_ping(&self) -> Result<crate::client::status::StatusPing, CoreError> {
561 unreachable!("not part of this action")
562 }
563 async fn modules(
564 &self,
565 _quarantined: bool,
566 _query: &crate::client::query::ListQuery,
567 ) -> Result<ListEnvelope<crate::client::status::ModuleInfo>, CoreError> {
568 unreachable!("not part of this action")
569 }
570 async fn metrics_current(
571 &self,
572 ) -> Result<crate::client::metrics::CurrentGauges, CoreError> {
573 unreachable!("not part of this action")
574 }
575 async fn metrics_historic(
576 &self,
577 ) -> Result<crate::client::metrics::PerformanceCharts, CoreError> {
578 unreachable!("not part of this action")
579 }
580 async fn metrics_threads(&self) -> Result<crate::client::metrics::ThreadCounts, CoreError> {
581 unreachable!("not part of this action")
582 }
583 async fn designers(
584 &self,
585 _query: &crate::client::query::ListQuery,
586 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
587 unreachable!("not part of this action")
588 }
589 async fn perspective_sessions(
590 &self,
591 _query: &crate::client::query::ListQuery,
592 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
593 unreachable!("not part of this action")
594 }
595 async fn vision_clients(
596 &self,
597 _query: &crate::client::query::ListQuery,
598 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
599 unreachable!("not part of this action")
600 }
601 async fn terminate_perspective_session(
602 &self,
603 _id: &str,
604 _message: Option<&str>,
605 ) -> Result<(), CoreError> {
606 unreachable!("not part of this action")
607 }
608 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
609 unreachable!("not part of this action")
610 }
611 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
612 unreachable!("not part of this action")
613 }
614 async fn database_connections(
615 &self,
616 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
617 {
618 unreachable!("not part of this action")
619 }
620 async fn opc_connections(
621 &self,
622 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
623 {
624 unreachable!("not part of this action")
625 }
626 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
627 unreachable!("not part of this action")
628 }
629 async fn loggers(
630 &self,
631 _query: &crate::client::query::ListQuery,
632 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
633 unreachable!("not part of this action")
634 }
635 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
636 unreachable!("not part of this action")
637 }
638 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
639 unreachable!("not part of this action")
640 }
641 async fn restart(&self) -> Result<(), CoreError> {
642 unreachable!("not part of this action")
643 }
644 async fn scan_projects(&self) -> Result<(), CoreError> {
645 unreachable!("not part of this action")
646 }
647 async fn security_properties(
648 &self,
649 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
650 unreachable!("not part of this action")
651 }
652 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
653 unreachable!("not part of this action")
654 }
655 async fn webdev_route_call(
656 &self,
657 _project: &str,
658 _route: &str,
659 _body: &serde_json::Value,
660 _extra_headers: &[(&str, &str)],
661 ) -> Result<serde_json::Value, CoreError> {
662 unreachable!("not part of this action")
663 }
664 async fn webdev_route_probe(
665 &self,
666 _project: &str,
667 _route: &str,
668 _extra_headers: &[(&str, &str)],
669 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
670 unreachable!("not part of this action")
671 }
672 async fn projects(
673 &self,
674 _query: &crate::client::query::ListQuery,
675 ) -> Result<
676 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
677 CoreError,
678 > {
679 unreachable!("not part of this action")
680 }
681 async fn project_find(
682 &self,
683 _name: &str,
684 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
685 unreachable!("not part of this action")
686 }
687 async fn project_create(
688 &self,
689 _body: &crate::client::projects::ProjectCreate,
690 ) -> Result<(), CoreError> {
691 unreachable!("not part of this action")
692 }
693 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
694 unreachable!("not part of this action")
695 }
696 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
697 unreachable!("not part of this action")
698 }
699 async fn project_modify(
700 &self,
701 _name: &str,
702 _body: &crate::client::projects::ProjectModify,
703 ) -> Result<(), CoreError> {
704 unreachable!("not part of this action")
705 }
706 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
707 unreachable!("not part of this action")
708 }
709 async fn project_export_to_file(
710 &self,
711 _name: &str,
712 _out: &std::path::Path,
713 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
714 unreachable!("not part of this action")
715 }
716 async fn project_import(
717 &self,
718 _name: &str,
719 _zip: Vec<u8>,
720 _overwrite: bool,
721 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
722 unreachable!("not part of this action")
723 }
724 }
725
726 #[tokio::test]
731 async fn tail_streams_pages_in_order_and_ends_cleanly_on_deadline() {
732 let rig = TailRig {
733 pages: Mutex::new(
734 vec![
735 vec![entry(1010, "second"), entry(1005, "first")],
738 vec![entry(1022, "third"), entry(1018, "wait, also")],
739 ]
740 .into(),
741 ),
742 queries: Mutex::new(Vec::new()),
743 };
744
745 let mut received: Vec<(i64, String)> = Vec::new();
746 let sink: &mut (dyn FnMut(&LogEntry) + Send) = &mut |entry: &LogEntry| {
747 received.push((entry.timestamp, entry.message.clone()));
748 };
749
750 let result = tail(
751 &rig,
752 None,
753 None,
754 Some(1000), Duration::from_millis(5),
756 Some(Duration::from_millis(400)),
762 sink,
763 )
764 .await
765 .expect("deadline expiry ends the tail cleanly");
766
767 assert_eq!(
769 received,
770 vec![
771 (1005, "first".into()),
772 (1010, "second".into()),
773 (1018, "wait, also".into()),
774 (1022, "third".into()),
775 ],
776 "stream order is timestamp order (client-side sort)"
777 );
778 assert_eq!(result.streamed, 4);
779
780 let queries = rig.queries.lock().unwrap();
784 assert_eq!(queries[0].start_time, Some(1000), "first = since");
785 assert!(
786 queries.len() >= 3,
787 "polled again after each page: {}",
788 queries.len()
789 );
790 assert_eq!(queries[1].start_time, Some(1011), "cursor = max + 1");
791 assert!(
792 queries[2].start_time == Some(1023),
793 "cursor advanced past page 2: {:?}",
794 queries[2].start_time
795 );
796 assert!(queries.iter().all(|query| query.limit == 200));
798 }
799
800 #[tokio::test]
804 async fn tail_fails_fast_on_auth() {
805 struct AuthRig;
806 #[async_trait::async_trait]
807 impl GatewayApi for AuthRig {
808 async fn thread_dump(
809 &self,
810 ) -> Result<crate::client::threads::FormattedThreadDump, CoreError> {
811 unreachable!("not part of this action")
812 }
813 async fn thread_deadlocks(
814 &self,
815 ) -> Result<crate::client::threads::DeadlocksWire, CoreError> {
816 unreachable!("not part of this action")
817 }
818 async fn uninstall_module(
819 &self,
820 _gateway_module_id: &str,
821 _module_id: &str,
822 ) -> Result<(), CoreError> {
823 unreachable!("not part of this action")
824 }
825 async fn bundle_generate(
826 &self,
827 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
828 unreachable!("not part of this action")
829 }
830 async fn bundle_status(
831 &self,
832 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
833 unreachable!("not part of this action")
834 }
835 async fn bundle_download(
836 &self,
837 _out: &std::path::Path,
838 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
839 unreachable!("not part of this action")
840 }
841 async fn tag_provider_list(
842 &self,
843 _query: &crate::client::query::ListQuery,
844 ) -> Result<
845 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
846 CoreError,
847 > {
848 unreachable!("not part of this action")
849 }
850 async fn tag_provider_find(
851 &self,
852 _name: &str,
853 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
854 unreachable!("not part of this action")
855 }
856 async fn tag_provider_create(
857 &self,
858 _body: &[crate::client::tags::TagProviderCreate],
859 ) -> Result<(), CoreError> {
860 unreachable!("not part of this action")
861 }
862 async fn tag_provider_delete(
863 &self,
864 _name: &str,
865 _signature: &str,
866 ) -> Result<(), CoreError> {
867 unreachable!("not part of this action")
868 }
869 async fn trial_status_wire(
870 &self,
871 ) -> Result<crate::client::trial::TrialWire, CoreError> {
872 unreachable!("not part of this action")
873 }
874 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
875 unreachable!("not part of this action")
876 }
877 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
878 unreachable!("not part of this action")
879 }
880 async fn backup_download(
881 &self,
882 _out: &std::path::Path,
883 _backup_type: crate::client::backup::BackupType,
884 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
885 unreachable!("not part of this action")
886 }
887 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
888 unreachable!("not part of this action")
889 }
890 async fn eam_task_history(
891 &self,
892 _limit: Option<u32>,
893 _search: Option<&str>,
894 ) -> Result<
895 crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>,
896 CoreError,
897 > {
898 unreachable!("not part of this action")
899 }
900 async fn eam_task_definitions(
901 &self,
902 ) -> Result<
903 crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>,
904 CoreError,
905 > {
906 unreachable!("not part of this action")
907 }
908 async fn eam_task_find(
909 &self,
910 _name: &str,
911 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
912 unreachable!("not part of this action")
913 }
914 async fn eam_task_create(
915 &self,
916 _definition: &serde_json::Value,
917 ) -> Result<(), CoreError> {
918 unreachable!("not part of this action")
919 }
920 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
921 unreachable!("not part of this action")
922 }
923 async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
924 unreachable!("not part of this action")
925 }
926 async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
927 unreachable!("not part of this action")
928 }
929 async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
930 unreachable!("not part of this action")
931 }
932 async fn eam_tasks_scheduled(
933 &self,
934 _running: bool,
935 ) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
936 unreachable!("not part of this action")
937 }
938 async fn eam_task_modify(
939 &self,
940 _definition: &serde_json::Value,
941 ) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
942 unreachable!("not part of this action")
943 }
944 async fn eam_task_delete(
945 &self,
946 _name: &str,
947 _signature: &str,
948 _confirm: bool,
949 ) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
950 unreachable!("not part of this action")
951 }
952 async fn api_call(
953 &self,
954 _call: &crate::client::apicall::ApiCallRequest,
955 ) -> Result<crate::client::apicall::ApiCallData, CoreError> {
956 unreachable!("not part of this action")
957 }
958 async fn license_status(
959 &self,
960 ) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
961 unreachable!("not part of this action")
962 }
963 async fn redundancy_status(
964 &self,
965 ) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
966 unreachable!("not part of this action")
967 }
968 async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
969 unreachable!("not part of this action")
970 }
971 async fn logs(&self, _filter: &LogQuery) -> Result<ListEnvelope<LogEntry>, CoreError> {
972 Err(CoreError::Auth {
973 status: 401,
974 endpoint: Some("http://gw/data/api/v1/logs".into()),
975 })
976 }
977 async fn gateway_info(&self) -> Result<crate::client::version::GatewayInfo, CoreError> {
978 unreachable!("not part of this action")
979 }
980 async fn overview(&self) -> Result<crate::client::status::Overview, CoreError> {
981 unreachable!("not part of this action")
982 }
983 async fn status_ping(&self) -> Result<crate::client::status::StatusPing, CoreError> {
984 unreachable!("not part of this action")
985 }
986 async fn modules(
987 &self,
988 _quarantined: bool,
989 _query: &crate::client::query::ListQuery,
990 ) -> Result<ListEnvelope<crate::client::status::ModuleInfo>, CoreError> {
991 unreachable!("not part of this action")
992 }
993 async fn metrics_current(
994 &self,
995 ) -> Result<crate::client::metrics::CurrentGauges, CoreError> {
996 unreachable!("not part of this action")
997 }
998 async fn metrics_historic(
999 &self,
1000 ) -> Result<crate::client::metrics::PerformanceCharts, CoreError> {
1001 unreachable!("not part of this action")
1002 }
1003 async fn metrics_threads(
1004 &self,
1005 ) -> Result<crate::client::metrics::ThreadCounts, CoreError> {
1006 unreachable!("not part of this action")
1007 }
1008 async fn designers(
1009 &self,
1010 _query: &crate::client::query::ListQuery,
1011 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError>
1012 {
1013 unreachable!("not part of this action")
1014 }
1015 async fn perspective_sessions(
1016 &self,
1017 _query: &crate::client::query::ListQuery,
1018 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError>
1019 {
1020 unreachable!("not part of this action")
1021 }
1022 async fn vision_clients(
1023 &self,
1024 _query: &crate::client::query::ListQuery,
1025 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError>
1026 {
1027 unreachable!("not part of this action")
1028 }
1029 async fn terminate_perspective_session(
1030 &self,
1031 _id: &str,
1032 _message: Option<&str>,
1033 ) -> Result<(), CoreError> {
1034 unreachable!("not part of this action")
1035 }
1036 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
1037 unreachable!("not part of this action")
1038 }
1039 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
1040 unreachable!("not part of this action")
1041 }
1042 async fn database_connections(
1043 &self,
1044 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
1045 {
1046 unreachable!("not part of this action")
1047 }
1048 async fn opc_connections(
1049 &self,
1050 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
1051 {
1052 unreachable!("not part of this action")
1053 }
1054 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
1055 unreachable!("not part of this action")
1056 }
1057 async fn loggers(
1058 &self,
1059 _query: &crate::client::query::ListQuery,
1060 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
1061 unreachable!("not part of this action")
1062 }
1063 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
1064 unreachable!("not part of this action")
1065 }
1066 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
1067 unreachable!("not part of this action")
1068 }
1069 async fn restart(&self) -> Result<(), CoreError> {
1070 unreachable!("not part of this action")
1071 }
1072 async fn scan_projects(&self) -> Result<(), CoreError> {
1073 unreachable!("not part of this action")
1074 }
1075 async fn security_properties(
1076 &self,
1077 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
1078 unreachable!("not part of this action")
1079 }
1080 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
1081 unreachable!("not part of this action")
1082 }
1083 async fn webdev_route_call(
1084 &self,
1085 _project: &str,
1086 _route: &str,
1087 _body: &serde_json::Value,
1088 _extra_headers: &[(&str, &str)],
1089 ) -> Result<serde_json::Value, CoreError> {
1090 unreachable!("not part of this action")
1091 }
1092 async fn webdev_route_probe(
1093 &self,
1094 _project: &str,
1095 _route: &str,
1096 _extra_headers: &[(&str, &str)],
1097 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
1098 unreachable!("not part of this action")
1099 }
1100 async fn projects(
1101 &self,
1102 _query: &crate::client::query::ListQuery,
1103 ) -> Result<
1104 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
1105 CoreError,
1106 > {
1107 unreachable!("not part of this action")
1108 }
1109 async fn project_find(
1110 &self,
1111 _name: &str,
1112 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
1113 unreachable!("not part of this action")
1114 }
1115 async fn project_create(
1116 &self,
1117 _body: &crate::client::projects::ProjectCreate,
1118 ) -> Result<(), CoreError> {
1119 unreachable!("not part of this action")
1120 }
1121 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
1122 unreachable!("not part of this action")
1123 }
1124 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
1125 unreachable!("not part of this action")
1126 }
1127 async fn project_modify(
1128 &self,
1129 _name: &str,
1130 _body: &crate::client::projects::ProjectModify,
1131 ) -> Result<(), CoreError> {
1132 unreachable!("not part of this action")
1133 }
1134 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
1135 unreachable!("not part of this action")
1136 }
1137 async fn project_export_to_file(
1138 &self,
1139 _name: &str,
1140 _out: &std::path::Path,
1141 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
1142 unreachable!("not part of this action")
1143 }
1144 async fn project_import(
1145 &self,
1146 _name: &str,
1147 _zip: Vec<u8>,
1148 _overwrite: bool,
1149 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
1150 unreachable!("not part of this action")
1151 }
1152 }
1153
1154 let sink: &mut (dyn FnMut(&LogEntry) + Send) = &mut |_| {};
1155 let err = tail(
1156 &AuthRig,
1157 None,
1158 None,
1159 None,
1160 Duration::from_millis(5),
1161 Some(Duration::from_secs(5)),
1162 sink,
1163 )
1164 .await
1165 .expect_err("auth must fail fast");
1166 assert!(matches!(err, CoreError::Auth { status: 401, .. }));
1167 assert_eq!(err.exit_code(), 5);
1168 }
1169}