Skip to content

Commit e6ee4d7

Browse files
authored
Filter out log lines ability (#15)
Signed-off-by: David Kornel <kornys@outlook.com>
1 parent 6d94de8 commit e6ee4d7

9 files changed

Lines changed: 109 additions & 26 deletions

File tree

common/src/main/java/io/streamshub/mcp/common/dto/PodLogsResult.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99
/**
1010
* Result of collecting logs from a list of pods.
1111
*
12-
* @param podNames the names of pods logs were collected from
13-
* @param logs the concatenated log content
14-
* @param errorCount the number of lines containing errors or exceptions
15-
* @param totalLines the total number of log lines collected
12+
* @param podNames the names of pods logs were collected from
13+
* @param logs the concatenated log content (filtered if a filter was applied)
14+
* @param errorCount the number of lines containing errors or exceptions
15+
* @param totalLines the total number of log lines retrieved before filtering
16+
* @param filteredLines the number of log lines after filtering (equals totalLines when no filter)
1617
*/
1718
public record PodLogsResult(
1819
List<String> podNames,
1920
String logs,
2021
int errorCount,
21-
int totalLines
22+
int totalLines,
23+
int filteredLines
2224
) {
2325

2426
/**

common/src/main/java/io/streamshub/mcp/common/service/PodsService.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.util.Locale;
3232
import java.util.Map;
3333
import java.util.Set;
34+
import java.util.regex.Pattern;
35+
import java.util.regex.PatternSyntaxException;
3436
import java.util.stream.Collectors;
3537

3638
/**
@@ -490,13 +492,37 @@ private String determineComponentFromPodInfo(String name, Map<String, String> la
490492
* @return the aggregated log result
491493
*/
492494
public PodLogsResult collectLogs(final String namespace, final List<Pod> pods) {
495+
return collectLogs(namespace, pods, null);
496+
}
497+
498+
/**
499+
* Collect logs from a list of pods with optional filtering.
500+
* Tails the last {@value #DEFAULT_LOG_TAIL_LINES} lines from each pod.
501+
*
502+
* <p>Supported filter values:</p>
503+
* <ul>
504+
* <li>{@code "errors"} - only lines containing ERROR or EXCEPTION</li>
505+
* <li>{@code "warnings"} - lines containing ERROR, EXCEPTION, or WARN</li>
506+
* <li>Any other non-blank string is treated as a regex pattern</li>
507+
* <li>{@code null} or blank - no filtering, return all lines</li>
508+
* </ul>
509+
*
510+
* @param namespace the namespace of the pods
511+
* @param pods the list of pods to collect logs from
512+
* @param filter optional filter: "errors", "warnings", or a regex pattern
513+
* @return the aggregated and optionally filtered log result
514+
*/
515+
public PodLogsResult collectLogs(final String namespace, final List<Pod> pods, final String filter) {
493516
List<String> podNames = pods.stream()
494517
.map(pod -> pod.getMetadata().getName())
495518
.toList();
496519

520+
Pattern filterPattern = compileLogFilter(filter);
521+
497522
StringBuilder allLogs = new StringBuilder();
498523
int errorCount = 0;
499524
int totalLines = 0;
525+
int filteredLines = 0;
500526

501527
for (Pod pod : pods) {
502528
String podName = pod.getMetadata().getName();
@@ -508,16 +534,24 @@ public PodLogsResult collectLogs(final String namespace, final List<Pod> pods) {
508534
.getLog();
509535

510536
if (podLog != null && !podLog.isEmpty()) {
511-
allLogs.append("=== Pod: ").append(podName).append(" ===\n");
512-
allLogs.append(podLog).append("\n");
513-
514537
String[] lines = podLog.split("\n");
515538
totalLines += lines.length;
539+
540+
StringBuilder podOutput = new StringBuilder();
516541
for (String line : lines) {
517542
String upperLine = line.toUpperCase(Locale.ENGLISH);
518543
if (upperLine.contains("ERROR") || upperLine.contains("EXCEPTION")) {
519544
errorCount++;
520545
}
546+
if (filterPattern == null || filterPattern.matcher(line).find()) {
547+
podOutput.append(line).append("\n");
548+
filteredLines++;
549+
}
550+
}
551+
552+
if (podOutput.length() > 0) {
553+
allLogs.append("=== Pod: ").append(podName).append(" ===\n");
554+
allLogs.append(podOutput);
521555
}
522556
}
523557
} catch (Exception e) {
@@ -526,6 +560,25 @@ public PodLogsResult collectLogs(final String namespace, final List<Pod> pods) {
526560
}
527561
}
528562

529-
return new PodLogsResult(podNames, allLogs.toString(), errorCount, totalLines);
563+
return new PodLogsResult(podNames, allLogs.toString(), errorCount, totalLines, filteredLines);
564+
}
565+
566+
private Pattern compileLogFilter(final String filter) {
567+
if (filter == null || filter.isBlank()) {
568+
return null;
569+
}
570+
String normalized = filter.trim().toLowerCase(Locale.ENGLISH);
571+
if ("errors".equals(normalized)) {
572+
return Pattern.compile("(?i)(ERROR|EXCEPTION)");
573+
}
574+
if ("warnings".equals(normalized)) {
575+
return Pattern.compile("(?i)(ERROR|EXCEPTION|WARN)");
576+
}
577+
try {
578+
return Pattern.compile(filter.trim());
579+
} catch (PatternSyntaxException e) {
580+
LOG.warnf("Invalid log filter regex '%s', returning unfiltered logs: %s", filter, e.getMessage());
581+
return null;
582+
}
530583
}
531584
}

strimzi-mcp/src/main/java/io/streamshub/mcp/strimzi/config/StrimziToolsPrompts.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public final class StrimziToolsPrompts {
2323
"Kafka cluster name"
2424
+ " (e.g., 'my-cluster').";
2525

26+
/**
27+
* Log filter parameter description.
28+
*/
29+
public static final String LOG_FILTER_DESC =
30+
"Filter log lines: 'errors' for ERROR/EXCEPTION only,"
31+
+ " 'warnings' for ERROR/EXCEPTION/WARN,"
32+
+ " or a regex pattern. Omit for all lines.";
33+
2634
/**
2735
* Sections parameter description.
2836
*/

strimzi-mcp/src/main/java/io/streamshub/mcp/strimzi/service/KafkaService.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,21 +198,24 @@ public KafkaBootstrapResponse getBootstrapServers(final String namespace, final
198198
}
199199

200200
/**
201-
* Get logs from Kafka cluster pods.
201+
* Get logs from Kafka cluster pods with optional filtering.
202202
*
203203
* @param namespace the namespace, or null for auto-discovery
204204
* @param clusterName the cluster name
205+
* @param filter optional log filter: "errors", "warnings", or a regex pattern
205206
* @return the cluster logs response
206207
*/
207-
public KafkaClusterLogsResponse getClusterLogs(final String namespace, final String clusterName) {
208+
public KafkaClusterLogsResponse getClusterLogs(final String namespace, final String clusterName,
209+
final String filter) {
208210
String ns = InputUtils.normalizeInput(namespace);
209211
String normalizedName = InputUtils.normalizeInput(clusterName);
210212

211213
if (normalizedName == null) {
212214
throw new ToolCallException("Cluster name is required");
213215
}
214216

215-
LOG.infof("Getting logs for cluster=%s (namespace=%s)", normalizedName, ns != null ? ns : "auto");
217+
LOG.infof("Getting logs for cluster=%s (namespace=%s, filter=%s)",
218+
normalizedName, ns != null ? ns : "auto", filter != null ? filter : "none");
216219

217220
if (ns == null) {
218221
ns = discoverClusterNamespace(normalizedName);
@@ -225,7 +228,8 @@ public KafkaClusterLogsResponse getClusterLogs(final String namespace, final Str
225228
return KafkaClusterLogsResponse.empty(normalizedName, ns);
226229
}
227230

228-
PodLogsResult result = podsService.collectLogs(ns, pods);
231+
String normalizedFilter = InputUtils.normalizeInput(filter);
232+
PodLogsResult result = podsService.collectLogs(ns, pods, normalizedFilter);
229233
return KafkaClusterLogsResponse.of(normalizedName, ns, result.podNames(),
230234
result.hasErrors(), result.errorCount(), result.totalLines(), result.logs());
231235
}

strimzi-mcp/src/main/java/io/streamshub/mcp/strimzi/service/StrimziOperatorService.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,21 @@ public StrimziOperatorResponse getOperator(final String namespace, final String
100100
}
101101

102102
/**
103-
* Get logs for Strimzi operator pods.
103+
* Get logs for Strimzi operator pods with optional filtering.
104104
* Returns a StrimziOperatorLogsResponse (including notFound) rather than throwing,
105105
* since missing operator pods is a valid business response.
106106
*
107107
* @param namespace the namespace, or null for auto-discovery
108108
* @param operatorName the operator name, or null for any operator
109+
* @param filter optional log filter: "errors", "warnings", or a regex pattern
109110
* @return the operator logs response
110111
*/
111-
public StrimziOperatorLogsResponse getOperatorLogs(final String namespace, final String operatorName) {
112+
public StrimziOperatorLogsResponse getOperatorLogs(final String namespace, final String operatorName,
113+
final String filter) {
112114
String ns = InputUtils.normalizeInput(namespace);
113115

114-
LOG.infof("Getting operator logs (namespace=%s, name=%s)", ns, operatorName);
116+
LOG.infof("Getting operator logs (namespace=%s, name=%s, filter=%s)",
117+
ns, operatorName, filter != null ? filter : "none");
115118

116119
if (ns == null) {
117120
ns = discoverOperatorNamespace(operatorName);
@@ -127,7 +130,8 @@ public StrimziOperatorLogsResponse getOperatorLogs(final String namespace, final
127130
return StrimziOperatorLogsResponse.notFound(ns);
128131
}
129132

130-
PodLogsResult result = podsService.collectLogs(ns, pods);
133+
String normalizedFilter = InputUtils.normalizeInput(filter);
134+
PodLogsResult result = podsService.collectLogs(ns, pods, normalizedFilter);
131135
return StrimziOperatorLogsResponse.of(ns, result.logs(), result.podNames(),
132136
result.hasErrors(), result.errorCount(), result.totalLines());
133137
}

strimzi-mcp/src/main/java/io/streamshub/mcp/strimzi/tool/KafkaTools.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,14 @@ public KafkaBootstrapResponse getKafkaBootstrapServers(
133133
*
134134
* @param clusterName the cluster name
135135
* @param namespace optional namespace
136+
* @param filter optional log filter
136137
* @return the cluster logs response with error analysis
137138
*/
138139
@Tool(
139140
name = "get_kafka_cluster_logs",
140141
description = "Get logs from Kafka cluster pods with error analysis."
141142
+ " Returns logs from all pods belonging to the cluster."
143+
+ " Use the filter parameter to reduce output."
142144
)
143145
public KafkaClusterLogsResponse getKafkaClusterLogs(
144146
@ToolArg(
@@ -147,8 +149,12 @@ public KafkaClusterLogsResponse getKafkaClusterLogs(
147149
@ToolArg(
148150
description = StrimziToolsPrompts.NS_DESC,
149151
required = false
150-
) final String namespace
152+
) final String namespace,
153+
@ToolArg(
154+
description = StrimziToolsPrompts.LOG_FILTER_DESC,
155+
required = false
156+
) final String filter
151157
) {
152-
return kafkaService.getClusterLogs(namespace, clusterName);
158+
return kafkaService.getClusterLogs(namespace, clusterName, filter);
153159
}
154160
}

strimzi-mcp/src/main/java/io/streamshub/mcp/strimzi/tool/StrimziOperatorTools.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,25 @@ public StrimziOperatorResponse getStrimziOperator(
8282
* Get Strimzi operator logs.
8383
*
8484
* @param namespace optional namespace
85+
* @param filter optional log filter
8586
* @return the operator logs response
8687
*/
8788
@Tool(
8889
name = "get_strimzi_operator_logs",
8990
description = "Get logs from Strimzi operator pods with error analysis."
91+
+ " Use the filter parameter to reduce output."
9092
)
9193
public StrimziOperatorLogsResponse getStrimziOperatorLogs(
9294
@ToolArg(
9395
description = StrimziToolsPrompts.NS_DESC,
9496
required = false
95-
) final String namespace
97+
) final String namespace,
98+
@ToolArg(
99+
description = StrimziToolsPrompts.LOG_FILTER_DESC,
100+
required = false
101+
) final String filter
96102
) {
97-
return operatorService.getOperatorLogs(namespace, null);
103+
return operatorService.getOperatorLogs(namespace, null, filter);
98104
}
99105

100106
/**

strimzi-mcp/src/test/java/io/streamshub/mcp/strimzi/service/StrimziServiceTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ void testOperatorLogsRetrievalWithHealthyOperator() {
8585
setupHealthyOperatorPod("kafka-system", "strimzi-cluster-operator-abc123");
8686

8787
// Test the service method
88-
StrimziOperatorLogsResponse result = operatorService.getOperatorLogs("kafka-system", null);
88+
StrimziOperatorLogsResponse result = operatorService.getOperatorLogs("kafka-system", null, null);
8989

9090
// Verify the result
9191
assertNotNull(result);
@@ -106,7 +106,7 @@ void testOperatorLogsNoOperatorFound() {
106106
setupEmptyResponses("empty-namespace");
107107

108108
// Test with namespace that has no operator
109-
StrimziOperatorLogsResponse result = operatorService.getOperatorLogs("empty-namespace", null);
109+
StrimziOperatorLogsResponse result = operatorService.getOperatorLogs("empty-namespace", null, null);
110110

111111
// Should handle gracefully - returns notFound response
112112
assertNotNull(result);
@@ -122,8 +122,8 @@ void testInputNormalizationOperatorServices() {
122122
setupHealthyOperatorPod("kafka", "operator-pod");
123123

124124
// Test with various input formats (spaces, case)
125-
StrimziOperatorLogsResponse result1 = operatorService.getOperatorLogs(" KAFKA ", null);
126-
StrimziOperatorLogsResponse result2 = operatorService.getOperatorLogs("kafka", null);
125+
StrimziOperatorLogsResponse result1 = operatorService.getOperatorLogs(" KAFKA ", null, null);
126+
StrimziOperatorLogsResponse result2 = operatorService.getOperatorLogs("kafka", null, null);
127127

128128
// Both should work and return consistent results
129129
assertNotNull(result1);

strimzi-mcp/src/test/java/io/streamshub/mcp/strimzi/tool/McpToolsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ void testGetStrimziOperator() {
306306

307307
@Test
308308
void testGetStrimziOperatorLogs() {
309-
when(operatorService.getOperatorLogs(null, null)).thenReturn(
309+
when(operatorService.getOperatorLogs(null, null, null)).thenReturn(
310310
StrimziOperatorLogsResponse.of("kafka-system",
311311
"INFO: Operator running normally", List.of("strimzi-operator-abc123"),
312312
false, 0, 1)

0 commit comments

Comments
 (0)