holmesgpt 0.12.4__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of holmesgpt might be problematic. Click here for more details.
- holmes/__init__.py +1 -1
- holmes/clients/robusta_client.py +19 -1
- holmes/common/env_vars.py +13 -0
- holmes/config.py +69 -9
- holmes/core/conversations.py +11 -0
- holmes/core/investigation.py +16 -3
- holmes/core/investigation_structured_output.py +12 -0
- holmes/core/llm.py +10 -0
- holmes/core/models.py +9 -1
- holmes/core/openai_formatting.py +72 -12
- holmes/core/prompt.py +13 -0
- holmes/core/supabase_dal.py +3 -0
- holmes/core/todo_manager.py +88 -0
- holmes/core/tool_calling_llm.py +121 -149
- holmes/core/tools.py +10 -1
- holmes/core/tools_utils/tool_executor.py +7 -2
- holmes/core/tools_utils/toolset_utils.py +7 -2
- holmes/core/tracing.py +8 -7
- holmes/interactive.py +1 -0
- holmes/main.py +2 -1
- holmes/plugins/prompts/__init__.py +7 -1
- holmes/plugins/prompts/_ai_safety.jinja2 +43 -0
- holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
- holmes/plugins/prompts/_default_log_prompt.jinja2 +4 -2
- holmes/plugins/prompts/_fetch_logs.jinja2 +6 -1
- holmes/plugins/prompts/_general_instructions.jinja2 +16 -0
- holmes/plugins/prompts/_permission_errors.jinja2 +1 -1
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +4 -4
- holmes/plugins/prompts/generic_ask.jinja2 +4 -3
- holmes/plugins/prompts/investigation_procedure.jinja2 +210 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +4 -0
- holmes/plugins/toolsets/__init__.py +19 -6
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +27 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -2
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +3 -1
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -1
- holmes/plugins/toolsets/coralogix/api.py +6 -6
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +7 -1
- holmes/plugins/toolsets/datadog/datadog_api.py +20 -8
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +8 -1
- holmes/plugins/toolsets/datadog/datadog_rds_instructions.jinja2 +82 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +12 -5
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +20 -11
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +735 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +18 -11
- holmes/plugins/toolsets/git.py +15 -15
- holmes/plugins/toolsets/grafana/grafana_api.py +12 -1
- holmes/plugins/toolsets/grafana/toolset_grafana.py +5 -1
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +9 -4
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +12 -5
- holmes/plugins/toolsets/internet/internet.py +2 -1
- holmes/plugins/toolsets/internet/notion.py +2 -1
- holmes/plugins/toolsets/investigator/__init__.py +0 -0
- holmes/plugins/toolsets/investigator/core_investigation.py +157 -0
- holmes/plugins/toolsets/investigator/investigator_instructions.jinja2 +253 -0
- holmes/plugins/toolsets/investigator/model.py +15 -0
- holmes/plugins/toolsets/kafka.py +14 -7
- holmes/plugins/toolsets/kubernetes.yaml +7 -7
- holmes/plugins/toolsets/kubernetes_logs.py +454 -25
- holmes/plugins/toolsets/logging_utils/logging_api.py +115 -55
- holmes/plugins/toolsets/mcp/toolset_mcp.py +1 -1
- holmes/plugins/toolsets/newrelic.py +8 -3
- holmes/plugins/toolsets/opensearch/opensearch.py +8 -4
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +9 -2
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +6 -2
- holmes/plugins/toolsets/prometheus/prometheus.py +149 -44
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +8 -2
- holmes/plugins/toolsets/robusta/robusta.py +4 -4
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +6 -5
- holmes/plugins/toolsets/servicenow/servicenow.py +18 -3
- holmes/plugins/toolsets/utils.py +8 -1
- holmes/utils/llms.py +20 -0
- holmes/utils/stream.py +90 -0
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/METADATA +48 -35
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/RECORD +85 -75
- holmes/utils/robusta.py +0 -9
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
import subprocess
|
|
4
|
-
from
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Optional, List, Tuple, Set
|
|
5
7
|
from pydantic import BaseModel
|
|
6
8
|
|
|
7
9
|
from holmes.common.env_vars import KUBERNETES_LOGS_TIMEOUT_SECONDS
|
|
@@ -14,8 +16,10 @@ from holmes.core.tools import (
|
|
|
14
16
|
from holmes.plugins.toolsets.logging_utils.logging_api import (
|
|
15
17
|
BasePodLoggingToolset,
|
|
16
18
|
FetchPodLogsParams,
|
|
19
|
+
LoggingCapability,
|
|
17
20
|
LoggingConfig,
|
|
18
21
|
PodLoggingTool,
|
|
22
|
+
DEFAULT_TIME_SPAN_SECONDS,
|
|
19
23
|
)
|
|
20
24
|
from holmes.plugins.toolsets.utils import process_timestamps_to_int, to_unix_ms
|
|
21
25
|
|
|
@@ -46,6 +50,14 @@ class LogResult(BaseModel):
|
|
|
46
50
|
class KubernetesLogsToolset(BasePodLoggingToolset):
|
|
47
51
|
"""Implementation of the unified logging API for Kubernetes logs using kubectl commands"""
|
|
48
52
|
|
|
53
|
+
@property
|
|
54
|
+
def supported_capabilities(self) -> Set[LoggingCapability]:
|
|
55
|
+
"""Kubernetes native logging supports regex and exclude filters"""
|
|
56
|
+
return {
|
|
57
|
+
LoggingCapability.REGEX_FILTER,
|
|
58
|
+
LoggingCapability.EXCLUDE_FILTER,
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
def __init__(self):
|
|
50
62
|
prerequisite = StaticPrerequisite(enabled=False, disabled_reason="Initializing")
|
|
51
63
|
super().__init__(
|
|
@@ -91,17 +103,47 @@ class KubernetesLogsToolset(BasePodLoggingToolset):
|
|
|
91
103
|
try:
|
|
92
104
|
all_logs: list[StructuredLog] = []
|
|
93
105
|
|
|
94
|
-
# Fetch previous logs
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
# Fetch previous and current logs in parallel
|
|
107
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
108
|
+
future_previous = executor.submit(
|
|
109
|
+
self._fetch_kubectl_logs, params, previous=True
|
|
110
|
+
)
|
|
111
|
+
future_current = executor.submit(
|
|
112
|
+
self._fetch_kubectl_logs, params, previous=False
|
|
113
|
+
)
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
futures = {future_previous: "previous", future_current: "current"}
|
|
116
|
+
previous_logs_result = None
|
|
117
|
+
current_logs_result = None
|
|
118
|
+
|
|
119
|
+
for future in as_completed(futures):
|
|
120
|
+
log_type = futures[future]
|
|
121
|
+
try:
|
|
122
|
+
result = future.result()
|
|
123
|
+
if log_type == "previous":
|
|
124
|
+
previous_logs_result = result
|
|
125
|
+
else:
|
|
126
|
+
current_logs_result = result
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logging.error(f"Error fetching {log_type} logs: {str(e)}")
|
|
129
|
+
error_result = LogResult(
|
|
130
|
+
logs=[],
|
|
131
|
+
error=f"Error fetching {log_type} logs: {str(e)}",
|
|
132
|
+
return_code=None,
|
|
133
|
+
has_multiple_containers=False,
|
|
134
|
+
)
|
|
135
|
+
if log_type == "previous":
|
|
136
|
+
previous_logs_result = error_result
|
|
137
|
+
else:
|
|
138
|
+
current_logs_result = error_result
|
|
139
|
+
|
|
140
|
+
# Ensure both results are not None (they should always be set by the loop)
|
|
141
|
+
if current_logs_result is None or previous_logs_result is None:
|
|
142
|
+
return StructuredToolResult(
|
|
143
|
+
status=ToolResultStatus.ERROR,
|
|
144
|
+
error="Internal error: Failed to fetch logs",
|
|
145
|
+
params=params.model_dump(),
|
|
146
|
+
)
|
|
105
147
|
|
|
106
148
|
return_code: Optional[int] = current_logs_result.return_code
|
|
107
149
|
|
|
@@ -126,24 +168,58 @@ class KubernetesLogsToolset(BasePodLoggingToolset):
|
|
|
126
168
|
return_code=return_code,
|
|
127
169
|
)
|
|
128
170
|
|
|
129
|
-
|
|
171
|
+
# Track counts for metadata
|
|
172
|
+
total_count = len(all_logs)
|
|
173
|
+
(
|
|
174
|
+
filtered_logs,
|
|
175
|
+
filtered_count_before_limit,
|
|
176
|
+
used_substring_fallback,
|
|
177
|
+
exclude_used_substring_fallback,
|
|
178
|
+
removed_by_include_filter,
|
|
179
|
+
removed_by_exclude_filter,
|
|
180
|
+
) = filter_logs(all_logs, params)
|
|
181
|
+
|
|
182
|
+
has_multiple_containers = (
|
|
183
|
+
previous_logs_result.has_multiple_containers
|
|
184
|
+
or current_logs_result.has_multiple_containers
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
formatted_logs = format_logs(
|
|
188
|
+
logs=filtered_logs,
|
|
189
|
+
display_container_name=has_multiple_containers,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Generate metadata
|
|
193
|
+
metadata_lines = add_metadata(
|
|
194
|
+
params=params,
|
|
195
|
+
total_count=total_count,
|
|
196
|
+
filtered_logs=filtered_logs,
|
|
197
|
+
filtered_count_before_limit=filtered_count_before_limit,
|
|
198
|
+
used_substring_fallback=used_substring_fallback,
|
|
199
|
+
exclude_used_substring_fallback=exclude_used_substring_fallback,
|
|
200
|
+
removed_by_include_filter=removed_by_include_filter,
|
|
201
|
+
removed_by_exclude_filter=removed_by_exclude_filter,
|
|
202
|
+
has_multiple_containers=has_multiple_containers,
|
|
203
|
+
)
|
|
130
204
|
|
|
131
|
-
if
|
|
205
|
+
# Check if we have any logs to return
|
|
206
|
+
if len(filtered_logs) == 0:
|
|
207
|
+
# Return NO_DATA status when there are no logs
|
|
132
208
|
return StructuredToolResult(
|
|
133
209
|
status=ToolResultStatus.NO_DATA,
|
|
210
|
+
data="\n".join(
|
|
211
|
+
metadata_lines
|
|
212
|
+
), # Still include metadata for context
|
|
134
213
|
params=params.model_dump(),
|
|
135
214
|
return_code=return_code,
|
|
136
215
|
)
|
|
137
216
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
display_container_name=previous_logs_result.has_multiple_containers
|
|
141
|
-
or current_logs_result.has_multiple_containers,
|
|
142
|
-
)
|
|
217
|
+
# Put metadata at the end
|
|
218
|
+
response_data = formatted_logs + "\n" + "\n".join(metadata_lines)
|
|
143
219
|
|
|
144
220
|
return StructuredToolResult(
|
|
145
221
|
status=ToolResultStatus.SUCCESS,
|
|
146
|
-
data=
|
|
222
|
+
data=response_data,
|
|
147
223
|
params=params.model_dump(),
|
|
148
224
|
return_code=return_code,
|
|
149
225
|
)
|
|
@@ -318,6 +394,287 @@ class KubernetesLogsToolset(BasePodLoggingToolset):
|
|
|
318
394
|
)
|
|
319
395
|
|
|
320
396
|
|
|
397
|
+
# TODO: review this
|
|
398
|
+
def format_relative_time(timestamp_str: str, current_time: datetime) -> str:
|
|
399
|
+
"""Format a timestamp as relative to current time (e.g., '2 hours 15 minutes ago')"""
|
|
400
|
+
try:
|
|
401
|
+
# Handle relative timestamps (negative numbers)
|
|
402
|
+
if timestamp_str and timestamp_str.startswith("-"):
|
|
403
|
+
seconds = abs(int(timestamp_str))
|
|
404
|
+
if seconds < 60:
|
|
405
|
+
return f"{seconds} second{'s' if seconds != 1 else ''} before end time"
|
|
406
|
+
minutes = seconds // 60
|
|
407
|
+
if minutes < 60:
|
|
408
|
+
return f"{minutes} minute{'s' if minutes != 1 else ''} before end time"
|
|
409
|
+
hours = minutes // 60
|
|
410
|
+
if hours < 24:
|
|
411
|
+
return f"{hours} hour{'s' if hours != 1 else ''} before end time"
|
|
412
|
+
days = hours // 24
|
|
413
|
+
return f"{days} day{'s' if days != 1 else ''} before end time"
|
|
414
|
+
|
|
415
|
+
# Parse the timestamp
|
|
416
|
+
timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
417
|
+
|
|
418
|
+
# Calculate the difference
|
|
419
|
+
diff = current_time - timestamp
|
|
420
|
+
|
|
421
|
+
# If in the future
|
|
422
|
+
if diff.total_seconds() < 0:
|
|
423
|
+
diff = timestamp - current_time
|
|
424
|
+
suffix = "from now"
|
|
425
|
+
else:
|
|
426
|
+
suffix = "ago"
|
|
427
|
+
|
|
428
|
+
# Format the difference
|
|
429
|
+
days = diff.days
|
|
430
|
+
hours = diff.seconds // 3600
|
|
431
|
+
minutes = (diff.seconds % 3600) // 60
|
|
432
|
+
|
|
433
|
+
parts = []
|
|
434
|
+
if days > 0:
|
|
435
|
+
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
|
436
|
+
if hours > 0:
|
|
437
|
+
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
|
438
|
+
if minutes > 0 and days == 0: # Only show minutes if less than a day
|
|
439
|
+
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
|
440
|
+
|
|
441
|
+
if not parts:
|
|
442
|
+
if diff.seconds < 60:
|
|
443
|
+
return "just now" if suffix == "ago" else "right now"
|
|
444
|
+
|
|
445
|
+
return f"{' '.join(parts)} {suffix}"
|
|
446
|
+
except Exception:
|
|
447
|
+
# If we can't parse it, just return the original
|
|
448
|
+
return timestamp_str
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# TODO: review this
|
|
452
|
+
def add_metadata(
|
|
453
|
+
params: FetchPodLogsParams,
|
|
454
|
+
total_count: int,
|
|
455
|
+
filtered_logs: List[StructuredLog],
|
|
456
|
+
filtered_count_before_limit: int,
|
|
457
|
+
used_substring_fallback: bool,
|
|
458
|
+
exclude_used_substring_fallback: bool,
|
|
459
|
+
removed_by_include_filter: int,
|
|
460
|
+
removed_by_exclude_filter: int,
|
|
461
|
+
has_multiple_containers: bool,
|
|
462
|
+
) -> List[str]:
|
|
463
|
+
"""Generate all metadata for the log query"""
|
|
464
|
+
metadata_lines = [
|
|
465
|
+
"\n" + "=" * 80,
|
|
466
|
+
"LOG QUERY METADATA",
|
|
467
|
+
"=" * 80,
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
# Time Context section
|
|
471
|
+
current_time = datetime.now(timezone.utc)
|
|
472
|
+
current_time_str = current_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
473
|
+
metadata_lines.extend(
|
|
474
|
+
[
|
|
475
|
+
"Time Context:",
|
|
476
|
+
f"- Query executed at: {current_time_str} (UTC)",
|
|
477
|
+
"",
|
|
478
|
+
"Query Parameters:",
|
|
479
|
+
f"- Pod: {params.pod_name}",
|
|
480
|
+
f"- Namespace: {params.namespace}",
|
|
481
|
+
"- Log source: Current and previous container logs",
|
|
482
|
+
]
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Always show time range info
|
|
486
|
+
if params.start_time or params.end_time:
|
|
487
|
+
start_str = params.start_time or "beginning"
|
|
488
|
+
end_str = params.end_time or "now"
|
|
489
|
+
|
|
490
|
+
# Calculate relative times and duration
|
|
491
|
+
relative_parts = []
|
|
492
|
+
|
|
493
|
+
# Parse timestamps for duration calculation
|
|
494
|
+
start_dt = None
|
|
495
|
+
end_dt = None
|
|
496
|
+
|
|
497
|
+
if params.start_time and params.start_time != "beginning":
|
|
498
|
+
start_relative = format_relative_time(params.start_time, current_time)
|
|
499
|
+
relative_parts.append(f"Started: {start_relative}")
|
|
500
|
+
try:
|
|
501
|
+
if not params.start_time.startswith("-"):
|
|
502
|
+
start_dt = datetime.fromisoformat(
|
|
503
|
+
params.start_time.replace("Z", "+00:00")
|
|
504
|
+
)
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
|
|
508
|
+
if params.end_time and params.end_time != "now":
|
|
509
|
+
end_relative = format_relative_time(params.end_time, current_time)
|
|
510
|
+
relative_parts.append(f"Ended: {end_relative}")
|
|
511
|
+
try:
|
|
512
|
+
end_dt = datetime.fromisoformat(params.end_time.replace("Z", "+00:00"))
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
else:
|
|
516
|
+
# If end_time is "now" or not specified, use current time
|
|
517
|
+
end_dt = current_time
|
|
518
|
+
|
|
519
|
+
# Calculate duration if we have both timestamps
|
|
520
|
+
if start_dt and end_dt:
|
|
521
|
+
duration = end_dt - start_dt
|
|
522
|
+
if duration.total_seconds() > 0:
|
|
523
|
+
days = duration.days
|
|
524
|
+
hours = duration.seconds // 3600
|
|
525
|
+
minutes = (duration.seconds % 3600) // 60
|
|
526
|
+
|
|
527
|
+
duration_parts = []
|
|
528
|
+
if days > 0:
|
|
529
|
+
duration_parts.append(f"{days} day{'s' if days != 1 else ''}")
|
|
530
|
+
if hours > 0:
|
|
531
|
+
duration_parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
|
532
|
+
if minutes > 0:
|
|
533
|
+
duration_parts.append(
|
|
534
|
+
f"{minutes} minute{'s' if minutes != 1 else ''}"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
if duration_parts:
|
|
538
|
+
duration_str = " ".join(duration_parts)
|
|
539
|
+
else:
|
|
540
|
+
duration_str = "less than 1 minute"
|
|
541
|
+
|
|
542
|
+
metadata_lines.append(
|
|
543
|
+
f"- Log time range: {start_str} (UTC) to {end_str} (UTC) ({duration_str})"
|
|
544
|
+
)
|
|
545
|
+
else:
|
|
546
|
+
metadata_lines.append(
|
|
547
|
+
f"- Log time range: {start_str} (UTC) to {end_str} (UTC)"
|
|
548
|
+
)
|
|
549
|
+
else:
|
|
550
|
+
metadata_lines.append(
|
|
551
|
+
f"- Log time range: {start_str} (UTC) to {end_str} (UTC)"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if relative_parts:
|
|
555
|
+
metadata_lines.append(f" {' | '.join(relative_parts)}")
|
|
556
|
+
else:
|
|
557
|
+
metadata_lines.append(
|
|
558
|
+
"- Log time range: None (fetching logs available via `kubectl logs`)"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Add container info if multiple containers
|
|
562
|
+
if has_multiple_containers:
|
|
563
|
+
metadata_lines.append("- Container(s): Multiple containers")
|
|
564
|
+
|
|
565
|
+
metadata_lines.extend(
|
|
566
|
+
[
|
|
567
|
+
"",
|
|
568
|
+
f"Total logs found before filtering: {total_count:,}",
|
|
569
|
+
]
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Only show filtering details if filters were applied
|
|
573
|
+
if params.filter or params.exclude_filter:
|
|
574
|
+
metadata_lines.append("")
|
|
575
|
+
metadata_lines.append("Filtering Applied:")
|
|
576
|
+
|
|
577
|
+
if params.filter:
|
|
578
|
+
if used_substring_fallback:
|
|
579
|
+
metadata_lines.append(
|
|
580
|
+
f" ⚠️ Filter '{params.filter}' is not valid regex, using substring match"
|
|
581
|
+
)
|
|
582
|
+
matched_by_filter = total_count - removed_by_include_filter
|
|
583
|
+
percentage = (matched_by_filter / total_count * 100) if total_count > 0 else 0
|
|
584
|
+
metadata_lines.append(f" 1. Include filter: '{params.filter}'")
|
|
585
|
+
metadata_lines.append(
|
|
586
|
+
f" → Matched: {matched_by_filter:,} logs ({percentage:.1f}% of total)"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
if params.exclude_filter:
|
|
590
|
+
if exclude_used_substring_fallback:
|
|
591
|
+
metadata_lines.append(
|
|
592
|
+
f" ⚠️ Exclude filter '{params.exclude_filter}' is not valid regex, using substring match"
|
|
593
|
+
)
|
|
594
|
+
metadata_lines.append("")
|
|
595
|
+
metadata_lines.append(f" 2. Exclude filter: '{params.exclude_filter}'")
|
|
596
|
+
metadata_lines.append(f" → Excluded: {removed_by_exclude_filter:,} logs")
|
|
597
|
+
metadata_lines.append(f" → Remaining: {filtered_count_before_limit:,} logs")
|
|
598
|
+
|
|
599
|
+
# Display section
|
|
600
|
+
metadata_lines.append("")
|
|
601
|
+
hit_limit = params.limit is not None and params.limit < filtered_count_before_limit
|
|
602
|
+
if hit_limit and params.limit is not None:
|
|
603
|
+
logs_omitted = filtered_count_before_limit - params.limit
|
|
604
|
+
metadata_lines.append(
|
|
605
|
+
f"Display: Showing latest {params.limit:,} of {filtered_count_before_limit:,} filtered logs ({logs_omitted:,} omitted)"
|
|
606
|
+
)
|
|
607
|
+
else:
|
|
608
|
+
if filtered_count_before_limit == total_count:
|
|
609
|
+
metadata_lines.append(f"Display: Showing all {len(filtered_logs):,} logs")
|
|
610
|
+
else:
|
|
611
|
+
metadata_lines.append(
|
|
612
|
+
f"Display: Showing all {len(filtered_logs):,} filtered logs"
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Add contextual hints based on results
|
|
616
|
+
if len(filtered_logs) == 0:
|
|
617
|
+
metadata_lines.append("")
|
|
618
|
+
if params.filter and total_count > 0:
|
|
619
|
+
# Logs exist but none matched the filter
|
|
620
|
+
metadata_lines.append("Result: No logs matched your filters")
|
|
621
|
+
metadata_lines.append("")
|
|
622
|
+
metadata_lines.append("⚠️ Suggestions:")
|
|
623
|
+
metadata_lines.append(" - Try a broader filter pattern")
|
|
624
|
+
metadata_lines.append(
|
|
625
|
+
f" - Remove the filter to see all {total_count:,} available logs"
|
|
626
|
+
)
|
|
627
|
+
metadata_lines.append(
|
|
628
|
+
" - Your filter may be too specific for the log format used"
|
|
629
|
+
)
|
|
630
|
+
else:
|
|
631
|
+
# No logs exist at all
|
|
632
|
+
metadata_lines.append("Result: No logs found for this pod")
|
|
633
|
+
metadata_lines.append("")
|
|
634
|
+
metadata_lines.append("⚠️ Possible reasons:")
|
|
635
|
+
if params.start_time or params.end_time:
|
|
636
|
+
metadata_lines.append(" - Pod was not running during this time period")
|
|
637
|
+
else:
|
|
638
|
+
metadata_lines.append(
|
|
639
|
+
" - Pod may not exist or may have been recently created"
|
|
640
|
+
)
|
|
641
|
+
metadata_lines.append(" - Container might not be logging to stdout/stderr")
|
|
642
|
+
metadata_lines.append(
|
|
643
|
+
" - Logs might be going to a file instead of stdout/stderr"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Only show time range suggestions if a time range was specified
|
|
647
|
+
if params.start_time or params.end_time:
|
|
648
|
+
metadata_lines.append("")
|
|
649
|
+
metadata_lines.append("⚠️ Try:")
|
|
650
|
+
metadata_lines.append(
|
|
651
|
+
" - Remove time range to see ALL available logs (recommended unless you need this specific timeframe)"
|
|
652
|
+
)
|
|
653
|
+
metadata_lines.append(" - Or expand time range (e.g., last 24 hours)")
|
|
654
|
+
else:
|
|
655
|
+
metadata_lines.append("")
|
|
656
|
+
metadata_lines.append("⚠️ Try:")
|
|
657
|
+
metadata_lines.append(
|
|
658
|
+
f" - Check if pod exists: kubectl get pods -n {params.namespace}"
|
|
659
|
+
)
|
|
660
|
+
metadata_lines.append(
|
|
661
|
+
f" - Check pod events: kubectl describe pod {params.pod_name} -n {params.namespace}"
|
|
662
|
+
)
|
|
663
|
+
elif hit_limit:
|
|
664
|
+
metadata_lines.append("")
|
|
665
|
+
metadata_lines.append("⚠️ Hit display limit! Suggestions:")
|
|
666
|
+
metadata_lines.append(
|
|
667
|
+
" - Add exclude_filter to remove noise: exclude_filter='<pattern1>|<pattern2>|<pattern3>'"
|
|
668
|
+
)
|
|
669
|
+
metadata_lines.append(" - Narrow time range to see fewer logs")
|
|
670
|
+
metadata_lines.append(
|
|
671
|
+
" - Use more specific filter: filter='<term1>.*<term2>|<exact-phrase>'"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
metadata_lines.append("=" * 80)
|
|
675
|
+
return metadata_lines
|
|
676
|
+
|
|
677
|
+
|
|
321
678
|
def format_logs(logs: List[StructuredLog], display_container_name: bool) -> str:
|
|
322
679
|
if display_container_name:
|
|
323
680
|
return "\n".join([f"{log.container or 'N/A'}: {log.content}" for log in logs])
|
|
@@ -332,23 +689,84 @@ class TimeFilter(BaseModel):
|
|
|
332
689
|
|
|
333
690
|
def filter_logs(
|
|
334
691
|
logs: List[StructuredLog], params: FetchPodLogsParams
|
|
335
|
-
) -> List[StructuredLog]:
|
|
692
|
+
) -> Tuple[List[StructuredLog], int, bool, bool, int, int]:
|
|
336
693
|
time_filter: Optional[TimeFilter] = None
|
|
337
694
|
if params.start_time or params.end_time:
|
|
338
695
|
start, end = process_timestamps_to_int(
|
|
339
696
|
start=params.start_time,
|
|
340
697
|
end=params.end_time,
|
|
341
|
-
default_time_span_seconds=
|
|
698
|
+
default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
|
|
342
699
|
)
|
|
343
700
|
time_filter = TimeFilter(start_ms=start * 1000, end_ms=end * 1000)
|
|
344
701
|
|
|
345
702
|
filtered_logs = []
|
|
703
|
+
# is this really needed? doesn't kubectl already sort logs for us
|
|
346
704
|
logs.sort(key=lambda x: x.timestamp_ms or 0)
|
|
347
705
|
|
|
706
|
+
# Pre-compile regex patterns if provided
|
|
707
|
+
regex_pattern = None
|
|
708
|
+
exclude_regex_pattern = None
|
|
709
|
+
used_substring_fallback = False
|
|
710
|
+
exclude_used_substring_fallback = False
|
|
711
|
+
|
|
712
|
+
# Track filtering statistics
|
|
713
|
+
removed_by_include_filter = 0
|
|
714
|
+
removed_by_exclude_filter = 0
|
|
715
|
+
|
|
716
|
+
if params.filter:
|
|
717
|
+
try:
|
|
718
|
+
# Try to compile as regex first
|
|
719
|
+
regex_pattern = re.compile(params.filter, re.IGNORECASE)
|
|
720
|
+
except re.error:
|
|
721
|
+
# If not a valid regex, fall back to simple substring matching
|
|
722
|
+
logging.debug(
|
|
723
|
+
f"Filter '{params.filter}' is not a valid regex, using substring matching"
|
|
724
|
+
)
|
|
725
|
+
regex_pattern = None
|
|
726
|
+
used_substring_fallback = True
|
|
727
|
+
|
|
728
|
+
if params.exclude_filter:
|
|
729
|
+
try:
|
|
730
|
+
# Try to compile as regex first
|
|
731
|
+
exclude_regex_pattern = re.compile(params.exclude_filter, re.IGNORECASE)
|
|
732
|
+
except re.error:
|
|
733
|
+
# If not a valid regex, fall back to simple substring matching
|
|
734
|
+
logging.debug(
|
|
735
|
+
f"Exclude filter '{params.exclude_filter}' is not a valid regex, using substring matching"
|
|
736
|
+
)
|
|
737
|
+
exclude_regex_pattern = None
|
|
738
|
+
exclude_used_substring_fallback = True
|
|
739
|
+
|
|
348
740
|
for log in logs:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
741
|
+
# Apply inclusion filter
|
|
742
|
+
if params.filter:
|
|
743
|
+
if regex_pattern:
|
|
744
|
+
# Use regex matching
|
|
745
|
+
if not regex_pattern.search(log.content):
|
|
746
|
+
# exclude this log
|
|
747
|
+
removed_by_include_filter += 1
|
|
748
|
+
continue
|
|
749
|
+
else:
|
|
750
|
+
# Fall back to simple substring matching (case-insensitive)
|
|
751
|
+
if params.filter.lower() not in log.content.lower():
|
|
752
|
+
# exclude this log
|
|
753
|
+
removed_by_include_filter += 1
|
|
754
|
+
continue
|
|
755
|
+
|
|
756
|
+
# Apply exclusion filter
|
|
757
|
+
if params.exclude_filter:
|
|
758
|
+
if exclude_regex_pattern:
|
|
759
|
+
# Use regex matching
|
|
760
|
+
if exclude_regex_pattern.search(log.content):
|
|
761
|
+
# exclude this log
|
|
762
|
+
removed_by_exclude_filter += 1
|
|
763
|
+
continue
|
|
764
|
+
else:
|
|
765
|
+
# Fall back to simple substring matching (case-insensitive)
|
|
766
|
+
if params.exclude_filter.lower() in log.content.lower():
|
|
767
|
+
# exclude this log
|
|
768
|
+
removed_by_exclude_filter += 1
|
|
769
|
+
continue
|
|
352
770
|
|
|
353
771
|
if (
|
|
354
772
|
time_filter
|
|
@@ -365,9 +783,20 @@ def filter_logs(
|
|
|
365
783
|
else:
|
|
366
784
|
filtered_logs.append(log)
|
|
367
785
|
|
|
786
|
+
# Track count before limiting
|
|
787
|
+
filtered_count_before_limit = len(filtered_logs)
|
|
788
|
+
|
|
368
789
|
if params.limit and params.limit < len(filtered_logs):
|
|
369
790
|
filtered_logs = filtered_logs[-params.limit :]
|
|
370
|
-
|
|
791
|
+
|
|
792
|
+
return (
|
|
793
|
+
filtered_logs,
|
|
794
|
+
filtered_count_before_limit,
|
|
795
|
+
used_substring_fallback,
|
|
796
|
+
exclude_used_substring_fallback,
|
|
797
|
+
removed_by_include_filter,
|
|
798
|
+
removed_by_exclude_filter,
|
|
799
|
+
)
|
|
371
800
|
|
|
372
801
|
|
|
373
802
|
def parse_logs(
|