Skip to content

Commit d9136e0

Browse files
authored
Merge pull request #932 from sarub0b0/feat/log-buffer-limit
feat: add log buffer line limit to prevent excessive memory usage
2 parents 60f03e4 + a91f69d commit d9136e0

File tree

14 files changed

+518
-262
lines changed

14 files changed

+518
-262
lines changed

src/app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ impl App {
7373
default_pod_columns,
7474
config.theme.clone(),
7575
cmd.clipboard,
76+
config.logging.max_lines,
7677
);
7778

7879
logger!(info, "app start");

src/config.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::path::PathBuf;
44

55
use anyhow::Result;
66
use figment::{
7-
providers::{Env, Format, Serialized, YamlExtended},
7+
providers::{Env, Format, Serialized, Yaml},
88
Figment,
99
};
1010
use serde::{Deserialize, Serialize};
@@ -19,9 +19,16 @@ pub enum ConfigLoadOption {
1919
Path(PathBuf),
2020
}
2121

22+
#[derive(Default, Debug, Deserialize, Serialize)]
23+
pub struct LoggingConfig {
24+
pub max_lines: Option<usize>,
25+
}
26+
2227
#[derive(Default, Debug, Deserialize, Serialize)]
2328
pub struct Config {
2429
pub theme: ThemeConfig,
30+
#[serde(default)]
31+
pub logging: LoggingConfig,
2532
}
2633

2734
impl Config {
@@ -30,7 +37,9 @@ impl Config {
3037

3138
let config = match option {
3239
ConfigLoadOption::Default => figment.merge(Serialized::defaults(Self::default())),
33-
ConfigLoadOption::Path(path) => figment.merge(YamlExtended::file(path)),
40+
ConfigLoadOption::Path(path) => figment
41+
.merge(Serialized::defaults(Self::default()))
42+
.merge(Yaml::file(path)),
3443
}
3544
.merge(Env::prefixed("KUBETUI_").split("__"))
3645
.extract_lossy()?;

src/features/pod/kube/filter.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ pub struct Filter {
9090
pub exclude_log: Option<Vec<Regex>>,
9191
/// JSONログに適用するフィルター(jq、JMESPathなど)
9292
pub json_filter: Option<JsonFilter>,
93+
/// ログバッファの最大行数
94+
pub limit: Option<usize>,
9395
}
9496

9597
impl Filter {
@@ -238,6 +240,10 @@ impl Filter {
238240

239241
filter.json_filter = Some(json_filter);
240242
}
243+
244+
FilterAttribute::Limit(n) => {
245+
filter.limit = Some(n);
246+
}
241247
}
242248
}
243249

@@ -251,6 +257,7 @@ impl Filter {
251257
.fold((false, false), |(ls, rl), filter| match filter {
252258
FilterAttribute::Resource(_) => (ls, true),
253259
FilterAttribute::LabelSelector(_) => (true, rl),
260+
FilterAttribute::Limit(_) => (ls, rl),
254261
_ => (ls, rl),
255262
});
256263

@@ -398,6 +405,10 @@ impl std::fmt::Display for Filter {
398405
}
399406
}
400407

408+
if let Some(limit) = self.limit {
409+
buf.push(format!("limit={}", limit));
410+
}
411+
401412
write!(f, "{}", buf.join(" "))
402413
}
403414
}
@@ -476,6 +487,7 @@ pub enum FilterAttribute<'a> {
476487
ExcludeLog(Cow<'a, str>),
477488
Jq(Cow<'a, str>),
478489
JMESPath(Cow<'a, str>),
490+
Limit(usize),
479491
}
480492

481493
struct FilterAttributes;
@@ -659,4 +671,30 @@ mod tests {
659671
assert!(display2.contains("jq=.message"));
660672
assert!(!display2.contains("jmespath="));
661673
}
674+
675+
#[test]
676+
fn test_parse_with_limit() {
677+
let filter = Filter::parse("limit:5000").unwrap();
678+
assert_eq!(filter.limit, Some(5000));
679+
}
680+
681+
#[test]
682+
fn test_parse_with_limit_and_other_filters() {
683+
let filter = Filter::parse("pod:api limit:5000 log:error").unwrap();
684+
assert_eq!(filter.limit, Some(5000));
685+
assert!(filter.pod.is_some());
686+
assert!(filter.include_log.is_some());
687+
}
688+
689+
#[test]
690+
fn test_parse_without_limit() {
691+
let filter = Filter::parse("pod:api").unwrap();
692+
assert_eq!(filter.limit, None);
693+
}
694+
695+
#[test]
696+
fn test_parse_with_lim_alias() {
697+
let filter = Filter::parse("lim:10000").unwrap();
698+
assert_eq!(filter.limit, Some(10000));
699+
}
662700
}

src/features/pod/kube/filter/parser.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::borrow::Cow;
33
use nom::{
44
branch::alt,
55
bytes::complete::{is_not, tag},
6-
character::complete::{alphanumeric1, anychar, char, multispace0, multispace1},
6+
character::complete::{alphanumeric1, anychar, char, digit1, multispace0, multispace1},
77
combinator::{all_consuming, map, recognize, value, verify},
88
error::{ContextError, ParseError},
99
multi::{fold_many0, many1_count, separated_list1},
@@ -235,6 +235,28 @@ fn jmespath<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
235235
Ok((remaining, FilterAttribute::JMESPath(value)))
236236
}
237237

238+
fn positive_integer<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
239+
s: &'a str,
240+
) -> IResult<&'a str, usize, E> {
241+
let (remaining, digits) = digit1(s)?;
242+
let n = digits
243+
.parse::<usize>()
244+
.map_err(|_| nom::Err::Error(E::from_error_kind(s, nom::error::ErrorKind::Digit)))?;
245+
Ok((remaining, n))
246+
}
247+
248+
fn limit<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
249+
s: &'a str,
250+
) -> IResult<&'a str, FilterAttribute<'a>, E> {
251+
let (remaining, (_, value)) = separated_pair(
252+
alt((tag("limit"), tag("lim"))),
253+
char(':'),
254+
positive_integer,
255+
)
256+
.parse(s)?;
257+
Ok((remaining, FilterAttribute::Limit(value)))
258+
}
259+
238260
fn specified_daemonset<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
239261
s: &'a str,
240262
) -> IResult<&'a str, FilterAttribute<'a>, E> {
@@ -349,6 +371,7 @@ fn attribute<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
349371
specified_statefulset,
350372
field_selector,
351373
label_selector,
374+
limit,
352375
pod,
353376
exclude_pod,
354377
container,
@@ -714,6 +737,18 @@ mod tests {
714737
assert_eq!(remaining, "");
715738
}
716739

740+
#[rstest]
741+
#[case("limit:5000", 5000)]
742+
#[case("lim:5000", 5000)]
743+
#[case("limit:1", 1)]
744+
#[case("limit:100000", 100000)]
745+
fn limit(#[case] query: &str, #[case] expected: usize) {
746+
let (remaining, actual) = super::limit::<Error<_>>(query).unwrap();
747+
748+
assert_eq!(actual, FilterAttribute::Limit(expected));
749+
assert_eq!(remaining, "");
750+
}
751+
717752
#[rustfmt::skip]
718753
#[rstest]
719754
#[case("pod:hoge", FilterAttribute::Pod("hoge".into()))]
@@ -735,6 +770,8 @@ mod tests {
735770
#[case("jmespath:message", FilterAttribute::JMESPath("message".into()))]
736771
#[case("jmes:level", FilterAttribute::JMESPath("level".into()))]
737772
#[case("jm:data.id", FilterAttribute::JMESPath("data.id".into()))]
773+
#[case("limit:5000", FilterAttribute::Limit(5000))]
774+
#[case("lim:1000", FilterAttribute::Limit(1000))]
738775
fn attribute(#[case] query: &str, #[case] expected: FilterAttribute) {
739776
let (remaining, actual) = super::attribute::<Error<_>>(query).unwrap();
740777

@@ -763,6 +800,7 @@ mod tests {
763800
"statefulset/app",
764801
"jq:.message",
765802
"jmespath:data.id",
803+
"limit:5000",
766804
" ",
767805
]
768806
.join(" ");
@@ -787,6 +825,7 @@ mod tests {
787825
FilterAttribute::Resource(SpecifiedResource::StatefulSet("app")),
788826
FilterAttribute::Jq(".message".into()),
789827
FilterAttribute::JMESPath("data.id".into()),
828+
FilterAttribute::Limit(5000),
790829
];
791830

792831
assert_eq!(actual, expected);

src/features/pod/kube/log.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use kube::Api;
1818
use tokio::task::{JoinError, JoinHandle};
1919

2020
use crate::{
21+
features::pod::message::LogMessage,
2122
kube::{context::Namespace, KubeClient},
2223
logger,
2324
message::Message,
@@ -148,6 +149,13 @@ impl AbortWorker for LogWorker {
148149
async fn run(&self) {
149150
match Filter::parse(&self.config.query) {
150151
Ok(filter) => {
152+
// Send SetMaxLines message if limit is specified in the query
153+
if filter.limit.is_some() {
154+
self.tx
155+
.send(LogMessage::SetMaxLines(filter.limit).into())
156+
.expect("Failed to send LogMessage::SetMaxLines");
157+
}
158+
151159
match self.spawn_tasks(filter).await {
152160
Ok(mut handles) => {
153161
handles.join().await;

src/features/pod/message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub enum LogMessage {
99
Request(LogConfig),
1010
Response(Result<Vec<String>>),
1111
ToggleJsonPrettyPrint,
12+
SetMaxLines(Option<usize>),
1213
}
1314

1415
impl From<LogMessage> for Message {

src/features/pod/view/tab.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ impl PodTab {
3838
namespaces: Rc<RefCell<Namespace>>,
3939
default_columns: Option<PodColumns>,
4040
theme: WidgetThemeConfig,
41+
log_max_lines: Option<usize>,
4142
) -> Self {
4243
let pod_widget = pod_widget(tx, theme.clone());
4344
let log_query_widget = log_query_widget(tx, namespaces, theme.clone());
4445
let pod_columns_dialog = pod_columns_dialog(tx, default_columns, theme.clone());
45-
let log_widget = log_widget(tx, clipboard, theme);
46+
let log_widget = log_widget(tx, clipboard, theme, log_max_lines);
4647
let log_query_help_widget = log_query_help_widget();
4748

4849
let layout = TabLayout::new(layout, split_direction);

src/features/pod/view/widgets/log.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub fn log_widget(
2222
tx: &Sender<Message>,
2323
clipboard: &Option<Rc<RefCell<Clipboard>>>,
2424
theme: WidgetThemeConfig,
25+
max_lines: Option<usize>,
2526
) -> Widget<'static> {
2627
let widget_theme = WidgetTheme::from(theme.clone());
2728

@@ -36,7 +37,7 @@ pub fn log_widget(
3637

3738
let text_theme = TextTheme::from(theme);
3839

39-
let builder = Text::builder()
40+
let mut builder = Text::builder()
4041
.id(POD_LOG_WIDGET_ID)
4142
.widget_base(widget_base)
4243
.search_form(search_form)
@@ -54,6 +55,10 @@ pub fn log_widget(
5455
toggle_json_pretty_print(tx.clone()),
5556
);
5657

58+
if let Some(max) = max_lines {
59+
builder = builder.max_lines(Some(max));
60+
}
61+
5762
if let Some(cb) = clipboard {
5863
builder.clipboard(cb.clone())
5964
} else {

src/features/pod/view/widgets/log_query_help.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ fn content() -> Vec<String> {
3636
field:<selector> (alias: fields)
3737
jq:<expr>
3838
jmespath:<expr> (alias: jmes, jm)
39+
limit:<number> (alias: lim)
3940
<resource>/<name>
4041
4142
Resources:

0 commit comments

Comments
 (0)