mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-11-06 08:44:05 +08:00
feat(tui): add log copy and visual selection mode
Add clipboard support for the TUI log viewer using OSC 52 escape sequences. Users can copy individual log lines (y), the visible viewport (Y), or enter visual selection mode (v) to select a range of lines with j/k and yank with y. - Add clipboard module with OSC 52 base64 encoding - Add format_log_line_plain() for plain-text log rendering - Add visual selection mode with highlight styling and status bar - Context-switch nav bar hints between normal and visual modes - Add 7 unit tests for plain-text log formatting Closes #274
This commit is contained in:
Generated
+1
@@ -2890,6 +2890,7 @@ dependencies = [
|
||||
name = "navigator-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"crossterm 0.28.1",
|
||||
"miette",
|
||||
"navigator-bootstrap",
|
||||
|
||||
@@ -72,6 +72,9 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus
|
||||
# WebSocket
|
||||
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] }
|
||||
|
||||
# Clipboard (OSC 52)
|
||||
base64 = "0.22"
|
||||
|
||||
# Utilities
|
||||
futures = "0.3"
|
||||
bytes = "1"
|
||||
|
||||
@@ -16,6 +16,7 @@ navigator-bootstrap = { path = "../navigator-bootstrap" }
|
||||
navigator-policy = { path = "../navigator-policy" }
|
||||
navigator-providers = { path = "../navigator-providers" }
|
||||
|
||||
base64 = { workspace = true }
|
||||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
terminal-colorsaurus = { workspace = true }
|
||||
|
||||
@@ -355,6 +355,9 @@ pub struct App {
|
||||
pub log_viewport_height: usize,
|
||||
/// When `Some(idx)`, a detail popup is shown for the filtered log line at this index.
|
||||
pub log_detail_index: Option<usize>,
|
||||
/// Anchor index (absolute in filtered list) for visual selection mode.
|
||||
/// When `Some`, the user is in visual-select mode (`v`).
|
||||
pub log_selection_anchor: Option<usize>,
|
||||
/// Handle for the streaming log task. Dropped to cancel.
|
||||
pub log_stream_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
|
||||
@@ -448,6 +451,7 @@ impl App {
|
||||
log_autoscroll: true,
|
||||
log_viewport_height: 0,
|
||||
log_detail_index: None,
|
||||
log_selection_anchor: None,
|
||||
log_stream_handle: None,
|
||||
draft_chunks: Vec::new(),
|
||||
draft_version: 0,
|
||||
@@ -900,12 +904,69 @@ impl App {
|
||||
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.cancel_log_stream();
|
||||
self.focus = Focus::SandboxPolicy;
|
||||
if self.log_selection_anchor.is_some() {
|
||||
// Cancel visual selection, stay in log viewer.
|
||||
self.log_selection_anchor = None;
|
||||
} else {
|
||||
self.cancel_log_stream();
|
||||
self.log_selection_anchor = None;
|
||||
self.focus = Focus::SandboxPolicy;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => self.running = false,
|
||||
KeyCode::Char('y') => {
|
||||
if filtered_len == 0 {
|
||||
return;
|
||||
}
|
||||
let filtered = self.filtered_log_lines();
|
||||
if let Some(anchor) = self.log_selection_anchor {
|
||||
// Visual mode: yank selected range.
|
||||
let cursor_abs = self.sandbox_log_scroll + self.log_cursor;
|
||||
let start = anchor.min(cursor_abs);
|
||||
let end = anchor.max(cursor_abs);
|
||||
let text: String = filtered[start..=end.min(filtered.len() - 1)]
|
||||
.iter()
|
||||
.map(|l| crate::ui::sandbox_logs::format_log_line_plain(l))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
crate::clipboard::copy_to_clipboard(&text);
|
||||
self.log_selection_anchor = None;
|
||||
} else {
|
||||
// Normal mode: yank current line.
|
||||
let abs = self.sandbox_log_scroll + self.log_cursor;
|
||||
if let Some(log) = filtered.get(abs) {
|
||||
let text = crate::ui::sandbox_logs::format_log_line_plain(log);
|
||||
crate::clipboard::copy_to_clipboard(&text);
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('Y') => {
|
||||
// Yank all visible lines in the viewport.
|
||||
if filtered_len == 0 {
|
||||
return;
|
||||
}
|
||||
let filtered = self.filtered_log_lines();
|
||||
let start = self.sandbox_log_scroll;
|
||||
let end = (start + vh).min(filtered.len());
|
||||
let text: String = filtered[start..end]
|
||||
.iter()
|
||||
.map(|l| crate::ui::sandbox_logs::format_log_line_plain(l))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
crate::clipboard::copy_to_clipboard(&text);
|
||||
}
|
||||
KeyCode::Char('v') => {
|
||||
// Toggle visual selection mode.
|
||||
if self.log_selection_anchor.is_some() {
|
||||
self.log_selection_anchor = None;
|
||||
} else {
|
||||
let abs = self.sandbox_log_scroll + self.log_cursor;
|
||||
self.log_selection_anchor = Some(abs);
|
||||
self.log_autoscroll = false;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if filtered_len > 0 {
|
||||
if filtered_len > 0 && self.log_selection_anchor.is_none() {
|
||||
let abs = self.sandbox_log_scroll + self.log_cursor;
|
||||
if abs < filtered_len {
|
||||
self.log_detail_index = Some(abs);
|
||||
@@ -936,6 +997,7 @@ impl App {
|
||||
self.log_autoscroll = false;
|
||||
}
|
||||
KeyCode::Char('G' | 'f') => {
|
||||
self.log_selection_anchor = None;
|
||||
self.sandbox_log_scroll = self.log_autoscroll_offset();
|
||||
self.log_autoscroll = true;
|
||||
let visible = filtered_len.saturating_sub(self.sandbox_log_scroll);
|
||||
@@ -948,13 +1010,16 @@ impl App {
|
||||
}
|
||||
KeyCode::Char('s') => {
|
||||
self.log_source_filter = self.log_source_filter.next();
|
||||
self.log_selection_anchor = None;
|
||||
self.sandbox_log_scroll = 0;
|
||||
self.log_cursor = 0;
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
self.log_selection_anchor = None;
|
||||
self.focus = Focus::SandboxDraft;
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
self.log_selection_anchor = None;
|
||||
self.focus = Focus::SandboxPolicy;
|
||||
}
|
||||
_ => {}
|
||||
@@ -1533,6 +1598,7 @@ impl App {
|
||||
self.log_cursor = 0;
|
||||
self.log_autoscroll = true;
|
||||
self.log_detail_index = None;
|
||||
self.log_selection_anchor = None;
|
||||
self.confirm_delete = false;
|
||||
self.sandbox_policy = None;
|
||||
self.sandbox_providers_list.clear();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! OSC 52 clipboard support.
|
||||
//!
|
||||
//! Writes the OSC 52 escape sequence to stdout, which instructs the terminal
|
||||
//! emulator to copy text to the system clipboard. Works over SSH, tmux, and
|
||||
//! mosh — the sequence is forwarded to the local terminal.
|
||||
|
||||
use base64::Engine;
|
||||
use std::io::Write;
|
||||
|
||||
/// Copy `text` to the system clipboard via the OSC 52 escape sequence.
|
||||
///
|
||||
/// This is fire-and-forget: if the terminal does not support OSC 52 the
|
||||
/// sequence is silently ignored.
|
||||
pub fn copy_to_clipboard(text: &str) {
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(text);
|
||||
// OSC 52 ; c ; <base64> ST — "c" selects the system clipboard.
|
||||
let seq = format!("\x1b]52;c;{encoded}\x07");
|
||||
let _ = std::io::stdout().write_all(seq.as_bytes());
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mod app;
|
||||
mod clipboard;
|
||||
mod event;
|
||||
pub mod theme;
|
||||
mod ui;
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct Theme {
|
||||
|
||||
// Log viewer
|
||||
pub log_cursor: Style,
|
||||
pub log_selection: Style,
|
||||
|
||||
// Animation
|
||||
pub claw: Style,
|
||||
@@ -121,6 +122,7 @@ impl Theme {
|
||||
key_hint: Style::new().fg(brand::NVIDIA_GREEN),
|
||||
|
||||
log_cursor: Style::new().bg(brand::EVERGLADE),
|
||||
log_selection: Style::new().bg(Color::Rgb(30, 60, 45)),
|
||||
|
||||
claw: Style::new().fg(brand::MAROON).add_modifier(Modifier::BOLD),
|
||||
|
||||
@@ -143,6 +145,7 @@ impl Theme {
|
||||
let border_color = Color::Rgb(180, 200, 170);
|
||||
let title_bg = Color::Rgb(220, 235, 210);
|
||||
let cursor_bg = Color::Rgb(230, 245, 220);
|
||||
let selection_bg = Color::Rgb(215, 235, 200);
|
||||
|
||||
Self {
|
||||
text: Style::new().fg(fg),
|
||||
@@ -166,6 +169,7 @@ impl Theme {
|
||||
key_hint: Style::new().fg(brand::NVIDIA_GREEN_DARK),
|
||||
|
||||
log_cursor: Style::new().bg(cursor_bg),
|
||||
log_selection: Style::new().bg(selection_bg),
|
||||
|
||||
claw: Style::new().fg(brand::MAROON).add_modifier(Modifier::BOLD),
|
||||
|
||||
@@ -244,6 +248,10 @@ mod tests {
|
||||
dark.log_cursor, light.log_cursor,
|
||||
"log_cursor style should differ"
|
||||
);
|
||||
assert_ne!(
|
||||
dark.log_selection, light.log_selection,
|
||||
"log_selection style should differ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -277,6 +285,7 @@ mod tests {
|
||||
assert_eq!(d.status_err, Style::new().fg(Color::Red));
|
||||
assert_eq!(d.key_hint, Style::new().fg(brand::NVIDIA_GREEN));
|
||||
assert_eq!(d.log_cursor, Style::new().bg(brand::EVERGLADE));
|
||||
assert_eq!(d.log_selection, Style::new().bg(Color::Rgb(30, 60, 45)));
|
||||
assert_eq!(
|
||||
d.claw,
|
||||
Style::new().fg(brand::MAROON).add_modifier(Modifier::BOLD)
|
||||
|
||||
@@ -224,43 +224,74 @@ fn draw_nav_bar(frame: &mut Frame<'_>, app: &App, area: Rect) {
|
||||
},
|
||||
Screen::Sandbox => match app.focus {
|
||||
Focus::SandboxLogs => {
|
||||
let filter_label = app.log_source_filter.label();
|
||||
let autoscroll_label = if app.log_autoscroll {
|
||||
" Autoscroll"
|
||||
if app.log_selection_anchor.is_some() {
|
||||
// Visual selection mode — reduced hint set.
|
||||
vec![
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[j/k]", t.key_hint),
|
||||
Span::styled(" Extend", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[y]", t.key_hint),
|
||||
Span::styled(" Yank", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[g/G]", t.key_hint),
|
||||
Span::styled(" Top/Bottom", t.text),
|
||||
Span::styled(" | ", t.border),
|
||||
Span::styled("[Esc]", t.muted),
|
||||
Span::styled(" Cancel", t.muted),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[q]", t.muted),
|
||||
Span::styled(" Quit", t.muted),
|
||||
]
|
||||
} else {
|
||||
" Follow"
|
||||
};
|
||||
let autoscroll_style = if app.log_autoscroll {
|
||||
t.status_ok
|
||||
} else {
|
||||
t.text
|
||||
};
|
||||
vec![
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[j/k]", t.key_hint),
|
||||
Span::styled(" Navigate", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[Enter]", t.key_hint),
|
||||
Span::styled(" Detail", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[g/G]", t.key_hint),
|
||||
Span::styled(" Top/Bottom", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[f]", t.key_hint),
|
||||
Span::styled(autoscroll_label, autoscroll_style),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[s]", t.key_hint),
|
||||
Span::styled(format!(" Source: {filter_label}"), t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[r]", t.key_hint),
|
||||
Span::styled(" Rules", t.text),
|
||||
Span::styled(" | ", t.border),
|
||||
Span::styled("[Esc]", t.muted),
|
||||
Span::styled(" Policy", t.muted),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[q]", t.muted),
|
||||
Span::styled(" Quit", t.muted),
|
||||
]
|
||||
// Normal log viewer mode.
|
||||
let filter_label = app.log_source_filter.label();
|
||||
let autoscroll_label = if app.log_autoscroll {
|
||||
" Autoscroll"
|
||||
} else {
|
||||
" Follow"
|
||||
};
|
||||
let autoscroll_style = if app.log_autoscroll {
|
||||
t.status_ok
|
||||
} else {
|
||||
t.text
|
||||
};
|
||||
vec![
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[j/k]", t.key_hint),
|
||||
Span::styled(" Navigate", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[Enter]", t.key_hint),
|
||||
Span::styled(" Detail", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[g/G]", t.key_hint),
|
||||
Span::styled(" Top/Bottom", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[f]", t.key_hint),
|
||||
Span::styled(autoscroll_label, autoscroll_style),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[s]", t.key_hint),
|
||||
Span::styled(format!(" Source: {filter_label}"), t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[y]", t.key_hint),
|
||||
Span::styled(" Copy", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[Y]", t.key_hint),
|
||||
Span::styled(" Copy All", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[v]", t.key_hint),
|
||||
Span::styled(" Select", t.text),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[r]", t.key_hint),
|
||||
Span::styled(" Rules", t.text),
|
||||
Span::styled(" | ", t.border),
|
||||
Span::styled("[Esc]", t.muted),
|
||||
Span::styled(" Policy", t.muted),
|
||||
Span::styled(" ", t.text),
|
||||
Span::styled("[q]", t.muted),
|
||||
Span::styled(" Quit", t.muted),
|
||||
]
|
||||
}
|
||||
}
|
||||
Focus::SandboxDraft => {
|
||||
// Build state-aware action hints based on selected chunk.
|
||||
|
||||
@@ -57,17 +57,29 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute visual selection range (if active).
|
||||
let selection_range = app.log_selection_anchor.map(|anchor| {
|
||||
let cursor_abs = app.sandbox_log_scroll + cursor_pos;
|
||||
(anchor.min(cursor_abs), anchor.max(cursor_abs))
|
||||
});
|
||||
|
||||
let lines: Vec<Line<'_>> = filtered
|
||||
.iter()
|
||||
.skip(app.sandbox_log_scroll)
|
||||
.take(inner_height)
|
||||
.enumerate()
|
||||
.map(|(i, log)| {
|
||||
let abs_idx = app.sandbox_log_scroll + i;
|
||||
let in_selection =
|
||||
selection_range.is_some_and(|(start, end)| abs_idx >= start && abs_idx <= end);
|
||||
let mut line = render_log_line(log, inner_width.saturating_sub(2), t);
|
||||
if i == cursor_pos {
|
||||
// Prepend green cursor marker and apply highlight background.
|
||||
line.spans.insert(0, Span::styled("▌ ", t.accent));
|
||||
line = line.style(t.log_cursor);
|
||||
} else if in_selection {
|
||||
line.spans.insert(0, Span::styled("▌ ", t.accent));
|
||||
line = line.style(t.log_selection);
|
||||
} else {
|
||||
line.spans.insert(0, Span::raw(" "));
|
||||
}
|
||||
@@ -84,14 +96,17 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let autoscroll_span = if app.log_autoscroll {
|
||||
let status_span = if let Some((start, end)) = selection_range {
|
||||
let count = end - start + 1;
|
||||
Span::styled(format!(" VISUAL ({count} lines) "), t.status_warn)
|
||||
} else if app.log_autoscroll {
|
||||
Span::styled(" ● FOLLOWING ", t.status_ok)
|
||||
} else {
|
||||
Span::styled(" ○ PAUSED ", t.status_warn)
|
||||
};
|
||||
|
||||
let block = block.title_bottom(Line::from(vec![
|
||||
autoscroll_span,
|
||||
status_span,
|
||||
Span::styled(scroll_info, t.muted),
|
||||
Span::styled(format!(" filter: {filter_label} "), t.muted),
|
||||
]));
|
||||
@@ -333,7 +348,7 @@ const L7_FIELD_ORDER: &[&str] = &[
|
||||
];
|
||||
|
||||
/// Return fields in a smart order based on the log message type.
|
||||
fn ordered_fields<'a>(log: &'a LogLine) -> Vec<(&'a str, &'a str)> {
|
||||
pub(crate) fn ordered_fields<'a>(log: &'a LogLine) -> Vec<(&'a str, &'a str)> {
|
||||
let order: Option<&[&str]> = if log.message.starts_with("CONNECT") {
|
||||
Some(CONNECT_FIELD_ORDER)
|
||||
} else if log.message.starts_with("L7_REQUEST") {
|
||||
@@ -388,7 +403,7 @@ fn level_style(level: &str, t: &crate::theme::Theme) -> ratatui::style::Style {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_short_time(epoch_ms: i64) -> String {
|
||||
pub(crate) fn format_short_time(epoch_ms: i64) -> String {
|
||||
if epoch_ms <= 0 {
|
||||
return String::from("--:--:--");
|
||||
}
|
||||
@@ -399,3 +414,133 @@ fn format_short_time(epoch_ms: i64) -> String {
|
||||
let seconds = time_of_day % 60;
|
||||
format!("{hours:02}:{minutes:02}:{seconds:02}")
|
||||
}
|
||||
|
||||
/// Format a log line as plain text for clipboard copy.
|
||||
///
|
||||
/// Produces the same layout as `render_log_line()` but without styles or
|
||||
/// truncation: `HH:MM:SS {source:<7} {level:<5} {message} [key=value ...]`
|
||||
pub(crate) fn format_log_line_plain(log: &LogLine) -> String {
|
||||
let ts = format_short_time(log.timestamp_ms);
|
||||
let mut s = format!("{ts} {:<7} {:<5} {}", log.source, log.level, log.message);
|
||||
|
||||
if !log.fields.is_empty() {
|
||||
let ordered = ordered_fields(log);
|
||||
for (k, v) in &ordered {
|
||||
if !v.is_empty() {
|
||||
s.push(' ');
|
||||
s.push_str(k);
|
||||
s.push('=');
|
||||
s.push_str(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn make_log(message: &str, fields: Vec<(&str, &str)>) -> LogLine {
|
||||
LogLine {
|
||||
timestamp_ms: 1_700_000_000_000, // 2023-11-14 22:13:20 UTC
|
||||
level: "INFO".to_string(),
|
||||
source: "sandbox".to_string(),
|
||||
target: "test".to_string(),
|
||||
message: message.to_string(),
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_basic_line() {
|
||||
let log = make_log("hello world", vec![]);
|
||||
let result = format_log_line_plain(&log);
|
||||
assert!(result.starts_with("22:13:20 sandbox INFO hello world"));
|
||||
assert!(!result.contains('='));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_with_fields() {
|
||||
let log = make_log(
|
||||
"CONNECT",
|
||||
vec![("dst_host", "example.com"), ("dst_port", "443")],
|
||||
);
|
||||
let result = format_log_line_plain(&log);
|
||||
assert!(result.contains("dst_host=example.com"));
|
||||
assert!(result.contains("dst_port=443"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_skips_empty_fields() {
|
||||
let log = make_log("test", vec![("key1", "val"), ("key2", "")]);
|
||||
let result = format_log_line_plain(&log);
|
||||
assert!(result.contains("key1=val"));
|
||||
assert!(!result.contains("key2="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_zero_timestamp() {
|
||||
let log = LogLine {
|
||||
timestamp_ms: 0,
|
||||
level: "ERROR".to_string(),
|
||||
source: "gateway".to_string(),
|
||||
target: String::new(),
|
||||
message: "fail".to_string(),
|
||||
fields: HashMap::new(),
|
||||
};
|
||||
let result = format_log_line_plain(&log);
|
||||
assert!(result.starts_with("--:--:-- gateway ERROR fail"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_connect_field_order() {
|
||||
let log = make_log(
|
||||
"CONNECT",
|
||||
vec![
|
||||
("binary", "/usr/bin/curl"),
|
||||
("action", "allow"),
|
||||
("dst_host", "example.com"),
|
||||
],
|
||||
);
|
||||
let result = format_log_line_plain(&log);
|
||||
// "action" should appear before "dst_host" which should appear before "binary"
|
||||
let action_pos = result.find("action=").unwrap();
|
||||
let dst_pos = result.find("dst_host=").unwrap();
|
||||
let binary_pos = result.find("binary=").unwrap();
|
||||
assert!(action_pos < dst_pos);
|
||||
assert!(dst_pos < binary_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_l7_field_order() {
|
||||
let log = make_log(
|
||||
"L7_REQUEST",
|
||||
vec![
|
||||
("policy", "default"),
|
||||
("l7_action", "allow"),
|
||||
("l7_target", "/api"),
|
||||
],
|
||||
);
|
||||
let result = format_log_line_plain(&log);
|
||||
let action_pos = result.find("l7_action=").unwrap();
|
||||
let target_pos = result.find("l7_target=").unwrap();
|
||||
let policy_pos = result.find("policy=").unwrap();
|
||||
assert!(action_pos < target_pos);
|
||||
assert!(target_pos < policy_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_format_alphabetical_fields_for_unknown_message() {
|
||||
let log = make_log("SOMETHING", vec![("zebra", "z"), ("alpha", "a")]);
|
||||
let result = format_log_line_plain(&log);
|
||||
let alpha_pos = result.find("alpha=").unwrap();
|
||||
let zebra_pos = result.find("zebra=").unwrap();
|
||||
assert!(alpha_pos < zebra_pos);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user