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 uninstall_module(
388 &self,
389 _gateway_module_id: &str,
390 _module_id: &str,
391 ) -> Result<(), CoreError> {
392 unreachable!("not part of this action")
393 }
394 async fn bundle_generate(
395 &self,
396 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
397 unreachable!("not part of this action")
398 }
399 async fn bundle_status(
400 &self,
401 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
402 unreachable!("not part of this action")
403 }
404 async fn bundle_download(
405 &self,
406 _out: &std::path::Path,
407 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
408 unreachable!("not part of this action")
409 }
410 async fn tag_provider_list(
411 &self,
412 _query: &crate::client::query::ListQuery,
413 ) -> Result<
414 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
415 CoreError,
416 > {
417 unreachable!("not part of this action")
418 }
419 async fn tag_provider_find(
420 &self,
421 _name: &str,
422 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
423 unreachable!("not part of this action")
424 }
425 async fn tag_provider_create(
426 &self,
427 _body: &[crate::client::tags::TagProviderCreate],
428 ) -> Result<(), CoreError> {
429 unreachable!("not part of this action")
430 }
431 async fn tag_provider_delete(
432 &self,
433 _name: &str,
434 _signature: &str,
435 ) -> Result<(), CoreError> {
436 unreachable!("not part of this action")
437 }
438 async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
439 unreachable!("not part of this action")
440 }
441 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
442 unreachable!("not part of this action")
443 }
444 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
445 unreachable!("not part of this action")
446 }
447 async fn backup_download(
448 &self,
449 _out: &std::path::Path,
450 _backup_type: crate::client::backup::BackupType,
451 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
452 unreachable!("not part of this action")
453 }
454 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
455 unreachable!("not part of this action")
456 }
457 async fn eam_task_history(
458 &self,
459 _limit: Option<u32>,
460 _search: Option<&str>,
461 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
462 {
463 unreachable!("not part of this action")
464 }
465 async fn eam_task_definitions(
466 &self,
467 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
468 {
469 unreachable!("not part of this action")
470 }
471 async fn eam_task_find(
472 &self,
473 _name: &str,
474 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
475 unreachable!("not part of this action")
476 }
477 async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
478 unreachable!("not part of this action")
479 }
480 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
481 unreachable!("not part of this action")
482 }
483 async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
484 unreachable!("not part of this action")
485 }
486 async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
487 unreachable!("not part of this action")
488 }
489 async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
490 unreachable!("not part of this action")
491 }
492 async fn eam_tasks_scheduled(
493 &self,
494 _running: bool,
495 ) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
496 unreachable!("not part of this action")
497 }
498 async fn eam_task_modify(
499 &self,
500 _definition: &serde_json::Value,
501 ) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
502 unreachable!("not part of this action")
503 }
504 async fn eam_task_delete(
505 &self,
506 _name: &str,
507 _signature: &str,
508 _confirm: bool,
509 ) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
510 unreachable!("not part of this action")
511 }
512 async fn api_call(
513 &self,
514 _call: &crate::client::apicall::ApiCallRequest,
515 ) -> Result<crate::client::apicall::ApiCallData, CoreError> {
516 unreachable!("not part of this action")
517 }
518 async fn license_status(
519 &self,
520 ) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
521 unreachable!("not part of this action")
522 }
523 async fn redundancy_status(
524 &self,
525 ) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
526 unreachable!("not part of this action")
527 }
528 async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
529 unreachable!("not part of this action")
530 }
531 async fn logs(&self, filter: &LogQuery) -> Result<ListEnvelope<LogEntry>, CoreError> {
532 self.queries.lock().unwrap().push(filter.clone());
533 let items = self.pages.lock().unwrap().pop_front().unwrap_or_default();
534 Ok(ListEnvelope {
535 metadata: ListMetadata {
536 total: items.len() as i64,
537 matching: items.len() as i64,
538 limit: 200,
539 offset: 0,
540 },
541 items,
542 })
543 }
544 async fn gateway_info(&self) -> Result<crate::client::version::GatewayInfo, CoreError> {
545 unreachable!("not part of this action")
546 }
547 async fn overview(&self) -> Result<crate::client::status::Overview, CoreError> {
548 unreachable!("not part of this action")
549 }
550 async fn status_ping(&self) -> Result<crate::client::status::StatusPing, CoreError> {
551 unreachable!("not part of this action")
552 }
553 async fn modules(
554 &self,
555 _quarantined: bool,
556 _query: &crate::client::query::ListQuery,
557 ) -> Result<ListEnvelope<crate::client::status::ModuleInfo>, CoreError> {
558 unreachable!("not part of this action")
559 }
560 async fn metrics_current(
561 &self,
562 ) -> Result<crate::client::metrics::CurrentGauges, CoreError> {
563 unreachable!("not part of this action")
564 }
565 async fn metrics_historic(
566 &self,
567 ) -> Result<crate::client::metrics::PerformanceCharts, CoreError> {
568 unreachable!("not part of this action")
569 }
570 async fn metrics_threads(&self) -> Result<crate::client::metrics::ThreadCounts, CoreError> {
571 unreachable!("not part of this action")
572 }
573 async fn designers(
574 &self,
575 _query: &crate::client::query::ListQuery,
576 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
577 unreachable!("not part of this action")
578 }
579 async fn perspective_sessions(
580 &self,
581 _query: &crate::client::query::ListQuery,
582 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
583 unreachable!("not part of this action")
584 }
585 async fn vision_clients(
586 &self,
587 _query: &crate::client::query::ListQuery,
588 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
589 unreachable!("not part of this action")
590 }
591 async fn terminate_perspective_session(
592 &self,
593 _id: &str,
594 _message: Option<&str>,
595 ) -> Result<(), CoreError> {
596 unreachable!("not part of this action")
597 }
598 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
599 unreachable!("not part of this action")
600 }
601 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
602 unreachable!("not part of this action")
603 }
604 async fn database_connections(
605 &self,
606 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
607 {
608 unreachable!("not part of this action")
609 }
610 async fn opc_connections(
611 &self,
612 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
613 {
614 unreachable!("not part of this action")
615 }
616 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
617 unreachable!("not part of this action")
618 }
619 async fn loggers(
620 &self,
621 _query: &crate::client::query::ListQuery,
622 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
623 unreachable!("not part of this action")
624 }
625 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
626 unreachable!("not part of this action")
627 }
628 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
629 unreachable!("not part of this action")
630 }
631 async fn restart(&self) -> Result<(), CoreError> {
632 unreachable!("not part of this action")
633 }
634 async fn scan_projects(&self) -> Result<(), CoreError> {
635 unreachable!("not part of this action")
636 }
637 async fn security_properties(
638 &self,
639 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
640 unreachable!("not part of this action")
641 }
642 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
643 unreachable!("not part of this action")
644 }
645 async fn webdev_route_call(
646 &self,
647 _project: &str,
648 _route: &str,
649 _body: &serde_json::Value,
650 _extra_headers: &[(&str, &str)],
651 ) -> Result<serde_json::Value, CoreError> {
652 unreachable!("not part of this action")
653 }
654 async fn webdev_route_probe(
655 &self,
656 _project: &str,
657 _route: &str,
658 _extra_headers: &[(&str, &str)],
659 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
660 unreachable!("not part of this action")
661 }
662 async fn projects(
663 &self,
664 _query: &crate::client::query::ListQuery,
665 ) -> Result<
666 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
667 CoreError,
668 > {
669 unreachable!("not part of this action")
670 }
671 async fn project_find(
672 &self,
673 _name: &str,
674 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
675 unreachable!("not part of this action")
676 }
677 async fn project_create(
678 &self,
679 _body: &crate::client::projects::ProjectCreate,
680 ) -> Result<(), CoreError> {
681 unreachable!("not part of this action")
682 }
683 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
684 unreachable!("not part of this action")
685 }
686 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
687 unreachable!("not part of this action")
688 }
689 async fn project_modify(
690 &self,
691 _name: &str,
692 _body: &crate::client::projects::ProjectModify,
693 ) -> Result<(), CoreError> {
694 unreachable!("not part of this action")
695 }
696 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
697 unreachable!("not part of this action")
698 }
699 async fn project_export_to_file(
700 &self,
701 _name: &str,
702 _out: &std::path::Path,
703 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
704 unreachable!("not part of this action")
705 }
706 async fn project_import(
707 &self,
708 _name: &str,
709 _zip: Vec<u8>,
710 _overwrite: bool,
711 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
712 unreachable!("not part of this action")
713 }
714 }
715
716 #[tokio::test]
721 async fn tail_streams_pages_in_order_and_ends_cleanly_on_deadline() {
722 let rig = TailRig {
723 pages: Mutex::new(
724 vec![
725 vec![entry(1010, "second"), entry(1005, "first")],
728 vec![entry(1022, "third"), entry(1018, "wait, also")],
729 ]
730 .into(),
731 ),
732 queries: Mutex::new(Vec::new()),
733 };
734
735 let mut received: Vec<(i64, String)> = Vec::new();
736 let sink: &mut (dyn FnMut(&LogEntry) + Send) = &mut |entry: &LogEntry| {
737 received.push((entry.timestamp, entry.message.clone()));
738 };
739
740 let result = tail(
741 &rig,
742 None,
743 None,
744 Some(1000), Duration::from_millis(5),
746 Some(Duration::from_millis(400)),
752 sink,
753 )
754 .await
755 .expect("deadline expiry ends the tail cleanly");
756
757 assert_eq!(
759 received,
760 vec![
761 (1005, "first".into()),
762 (1010, "second".into()),
763 (1018, "wait, also".into()),
764 (1022, "third".into()),
765 ],
766 "stream order is timestamp order (client-side sort)"
767 );
768 assert_eq!(result.streamed, 4);
769
770 let queries = rig.queries.lock().unwrap();
774 assert_eq!(queries[0].start_time, Some(1000), "first = since");
775 assert!(
776 queries.len() >= 3,
777 "polled again after each page: {}",
778 queries.len()
779 );
780 assert_eq!(queries[1].start_time, Some(1011), "cursor = max + 1");
781 assert!(
782 queries[2].start_time == Some(1023),
783 "cursor advanced past page 2: {:?}",
784 queries[2].start_time
785 );
786 assert!(queries.iter().all(|query| query.limit == 200));
788 }
789
790 #[tokio::test]
794 async fn tail_fails_fast_on_auth() {
795 struct AuthRig;
796 #[async_trait::async_trait]
797 impl GatewayApi for AuthRig {
798 async fn uninstall_module(
799 &self,
800 _gateway_module_id: &str,
801 _module_id: &str,
802 ) -> Result<(), CoreError> {
803 unreachable!("not part of this action")
804 }
805 async fn bundle_generate(
806 &self,
807 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
808 unreachable!("not part of this action")
809 }
810 async fn bundle_status(
811 &self,
812 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
813 unreachable!("not part of this action")
814 }
815 async fn bundle_download(
816 &self,
817 _out: &std::path::Path,
818 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
819 unreachable!("not part of this action")
820 }
821 async fn tag_provider_list(
822 &self,
823 _query: &crate::client::query::ListQuery,
824 ) -> Result<
825 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
826 CoreError,
827 > {
828 unreachable!("not part of this action")
829 }
830 async fn tag_provider_find(
831 &self,
832 _name: &str,
833 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
834 unreachable!("not part of this action")
835 }
836 async fn tag_provider_create(
837 &self,
838 _body: &[crate::client::tags::TagProviderCreate],
839 ) -> Result<(), CoreError> {
840 unreachable!("not part of this action")
841 }
842 async fn tag_provider_delete(
843 &self,
844 _name: &str,
845 _signature: &str,
846 ) -> Result<(), CoreError> {
847 unreachable!("not part of this action")
848 }
849 async fn trial_status_wire(
850 &self,
851 ) -> Result<crate::client::trial::TrialWire, CoreError> {
852 unreachable!("not part of this action")
853 }
854 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
855 unreachable!("not part of this action")
856 }
857 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
858 unreachable!("not part of this action")
859 }
860 async fn backup_download(
861 &self,
862 _out: &std::path::Path,
863 _backup_type: crate::client::backup::BackupType,
864 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
865 unreachable!("not part of this action")
866 }
867 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
868 unreachable!("not part of this action")
869 }
870 async fn eam_task_history(
871 &self,
872 _limit: Option<u32>,
873 _search: Option<&str>,
874 ) -> Result<
875 crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>,
876 CoreError,
877 > {
878 unreachable!("not part of this action")
879 }
880 async fn eam_task_definitions(
881 &self,
882 ) -> Result<
883 crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>,
884 CoreError,
885 > {
886 unreachable!("not part of this action")
887 }
888 async fn eam_task_find(
889 &self,
890 _name: &str,
891 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
892 unreachable!("not part of this action")
893 }
894 async fn eam_task_create(
895 &self,
896 _definition: &serde_json::Value,
897 ) -> Result<(), CoreError> {
898 unreachable!("not part of this action")
899 }
900 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
901 unreachable!("not part of this action")
902 }
903 async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
904 unreachable!("not part of this action")
905 }
906 async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
907 unreachable!("not part of this action")
908 }
909 async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
910 unreachable!("not part of this action")
911 }
912 async fn eam_tasks_scheduled(
913 &self,
914 _running: bool,
915 ) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
916 unreachable!("not part of this action")
917 }
918 async fn eam_task_modify(
919 &self,
920 _definition: &serde_json::Value,
921 ) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
922 unreachable!("not part of this action")
923 }
924 async fn eam_task_delete(
925 &self,
926 _name: &str,
927 _signature: &str,
928 _confirm: bool,
929 ) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
930 unreachable!("not part of this action")
931 }
932 async fn api_call(
933 &self,
934 _call: &crate::client::apicall::ApiCallRequest,
935 ) -> Result<crate::client::apicall::ApiCallData, CoreError> {
936 unreachable!("not part of this action")
937 }
938 async fn license_status(
939 &self,
940 ) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
941 unreachable!("not part of this action")
942 }
943 async fn redundancy_status(
944 &self,
945 ) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
946 unreachable!("not part of this action")
947 }
948 async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
949 unreachable!("not part of this action")
950 }
951 async fn logs(&self, _filter: &LogQuery) -> Result<ListEnvelope<LogEntry>, CoreError> {
952 Err(CoreError::Auth {
953 status: 401,
954 endpoint: Some("http://gw/data/api/v1/logs".into()),
955 })
956 }
957 async fn gateway_info(&self) -> Result<crate::client::version::GatewayInfo, CoreError> {
958 unreachable!("not part of this action")
959 }
960 async fn overview(&self) -> Result<crate::client::status::Overview, CoreError> {
961 unreachable!("not part of this action")
962 }
963 async fn status_ping(&self) -> Result<crate::client::status::StatusPing, CoreError> {
964 unreachable!("not part of this action")
965 }
966 async fn modules(
967 &self,
968 _quarantined: bool,
969 _query: &crate::client::query::ListQuery,
970 ) -> Result<ListEnvelope<crate::client::status::ModuleInfo>, CoreError> {
971 unreachable!("not part of this action")
972 }
973 async fn metrics_current(
974 &self,
975 ) -> Result<crate::client::metrics::CurrentGauges, CoreError> {
976 unreachable!("not part of this action")
977 }
978 async fn metrics_historic(
979 &self,
980 ) -> Result<crate::client::metrics::PerformanceCharts, CoreError> {
981 unreachable!("not part of this action")
982 }
983 async fn metrics_threads(
984 &self,
985 ) -> Result<crate::client::metrics::ThreadCounts, CoreError> {
986 unreachable!("not part of this action")
987 }
988 async fn designers(
989 &self,
990 _query: &crate::client::query::ListQuery,
991 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError>
992 {
993 unreachable!("not part of this action")
994 }
995 async fn perspective_sessions(
996 &self,
997 _query: &crate::client::query::ListQuery,
998 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError>
999 {
1000 unreachable!("not part of this action")
1001 }
1002 async fn vision_clients(
1003 &self,
1004 _query: &crate::client::query::ListQuery,
1005 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError>
1006 {
1007 unreachable!("not part of this action")
1008 }
1009 async fn terminate_perspective_session(
1010 &self,
1011 _id: &str,
1012 _message: Option<&str>,
1013 ) -> Result<(), CoreError> {
1014 unreachable!("not part of this action")
1015 }
1016 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
1017 unreachable!("not part of this action")
1018 }
1019 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
1020 unreachable!("not part of this action")
1021 }
1022 async fn database_connections(
1023 &self,
1024 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
1025 {
1026 unreachable!("not part of this action")
1027 }
1028 async fn opc_connections(
1029 &self,
1030 ) -> Result<ListEnvelope<crate::client::connections::GatewayConnection>, CoreError>
1031 {
1032 unreachable!("not part of this action")
1033 }
1034 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
1035 unreachable!("not part of this action")
1036 }
1037 async fn loggers(
1038 &self,
1039 _query: &crate::client::query::ListQuery,
1040 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
1041 unreachable!("not part of this action")
1042 }
1043 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
1044 unreachable!("not part of this action")
1045 }
1046 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
1047 unreachable!("not part of this action")
1048 }
1049 async fn restart(&self) -> Result<(), CoreError> {
1050 unreachable!("not part of this action")
1051 }
1052 async fn scan_projects(&self) -> Result<(), CoreError> {
1053 unreachable!("not part of this action")
1054 }
1055 async fn security_properties(
1056 &self,
1057 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
1058 unreachable!("not part of this action")
1059 }
1060 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
1061 unreachable!("not part of this action")
1062 }
1063 async fn webdev_route_call(
1064 &self,
1065 _project: &str,
1066 _route: &str,
1067 _body: &serde_json::Value,
1068 _extra_headers: &[(&str, &str)],
1069 ) -> Result<serde_json::Value, CoreError> {
1070 unreachable!("not part of this action")
1071 }
1072 async fn webdev_route_probe(
1073 &self,
1074 _project: &str,
1075 _route: &str,
1076 _extra_headers: &[(&str, &str)],
1077 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
1078 unreachable!("not part of this action")
1079 }
1080 async fn projects(
1081 &self,
1082 _query: &crate::client::query::ListQuery,
1083 ) -> Result<
1084 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
1085 CoreError,
1086 > {
1087 unreachable!("not part of this action")
1088 }
1089 async fn project_find(
1090 &self,
1091 _name: &str,
1092 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
1093 unreachable!("not part of this action")
1094 }
1095 async fn project_create(
1096 &self,
1097 _body: &crate::client::projects::ProjectCreate,
1098 ) -> Result<(), CoreError> {
1099 unreachable!("not part of this action")
1100 }
1101 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
1102 unreachable!("not part of this action")
1103 }
1104 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
1105 unreachable!("not part of this action")
1106 }
1107 async fn project_modify(
1108 &self,
1109 _name: &str,
1110 _body: &crate::client::projects::ProjectModify,
1111 ) -> Result<(), CoreError> {
1112 unreachable!("not part of this action")
1113 }
1114 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
1115 unreachable!("not part of this action")
1116 }
1117 async fn project_export_to_file(
1118 &self,
1119 _name: &str,
1120 _out: &std::path::Path,
1121 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
1122 unreachable!("not part of this action")
1123 }
1124 async fn project_import(
1125 &self,
1126 _name: &str,
1127 _zip: Vec<u8>,
1128 _overwrite: bool,
1129 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
1130 unreachable!("not part of this action")
1131 }
1132 }
1133
1134 let sink: &mut (dyn FnMut(&LogEntry) + Send) = &mut |_| {};
1135 let err = tail(
1136 &AuthRig,
1137 None,
1138 None,
1139 None,
1140 Duration::from_millis(5),
1141 Some(Duration::from_secs(5)),
1142 sink,
1143 )
1144 .await
1145 .expect_err("auth must fail fast");
1146 assert!(matches!(err, CoreError::Auth { status: 401, .. }));
1147 assert_eq!(err.exit_code(), 5);
1148 }
1149}