holmesgpt 0.12.6__py3-none-any.whl → 0.13.1__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.

Files changed (125) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +19 -1
  3. holmes/common/env_vars.py +17 -0
  4. holmes/config.py +69 -9
  5. holmes/core/conversations.py +11 -0
  6. holmes/core/investigation.py +16 -3
  7. holmes/core/investigation_structured_output.py +12 -0
  8. holmes/core/llm.py +13 -1
  9. holmes/core/models.py +9 -1
  10. holmes/core/openai_formatting.py +72 -12
  11. holmes/core/prompt.py +13 -0
  12. holmes/core/supabase_dal.py +3 -0
  13. holmes/core/todo_manager.py +88 -0
  14. holmes/core/tool_calling_llm.py +230 -157
  15. holmes/core/tools.py +10 -1
  16. holmes/core/tools_utils/tool_executor.py +7 -2
  17. holmes/core/tools_utils/toolset_utils.py +7 -2
  18. holmes/core/toolset_manager.py +1 -5
  19. holmes/core/tracing.py +4 -3
  20. holmes/interactive.py +1 -0
  21. holmes/main.py +9 -2
  22. holmes/plugins/prompts/__init__.py +7 -1
  23. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  24. holmes/plugins/prompts/_default_log_prompt.jinja2 +4 -2
  25. holmes/plugins/prompts/_fetch_logs.jinja2 +10 -1
  26. holmes/plugins/prompts/_general_instructions.jinja2 +14 -0
  27. holmes/plugins/prompts/_permission_errors.jinja2 +1 -1
  28. holmes/plugins/prompts/_toolsets_instructions.jinja2 +4 -4
  29. holmes/plugins/prompts/generic_ask.jinja2 +4 -3
  30. holmes/plugins/prompts/investigation_procedure.jinja2 +210 -0
  31. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -0
  32. holmes/plugins/runbooks/CLAUDE.md +85 -0
  33. holmes/plugins/runbooks/README.md +24 -0
  34. holmes/plugins/toolsets/__init__.py +19 -6
  35. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +27 -0
  36. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -2
  37. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -1
  38. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -1
  39. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +2 -1
  40. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -1
  41. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +3 -1
  42. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -1
  43. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -1
  44. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -1
  45. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -1
  46. holmes/plugins/toolsets/bash/argocd/__init__.py +65 -0
  47. holmes/plugins/toolsets/bash/argocd/constants.py +120 -0
  48. holmes/plugins/toolsets/bash/aws/__init__.py +66 -0
  49. holmes/plugins/toolsets/bash/aws/constants.py +529 -0
  50. holmes/plugins/toolsets/bash/azure/__init__.py +56 -0
  51. holmes/plugins/toolsets/bash/azure/constants.py +339 -0
  52. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +6 -7
  53. holmes/plugins/toolsets/bash/bash_toolset.py +47 -13
  54. holmes/plugins/toolsets/bash/common/bash_command.py +131 -0
  55. holmes/plugins/toolsets/bash/common/stringify.py +14 -1
  56. holmes/plugins/toolsets/bash/common/validators.py +91 -0
  57. holmes/plugins/toolsets/bash/docker/__init__.py +59 -0
  58. holmes/plugins/toolsets/bash/docker/constants.py +255 -0
  59. holmes/plugins/toolsets/bash/helm/__init__.py +61 -0
  60. holmes/plugins/toolsets/bash/helm/constants.py +92 -0
  61. holmes/plugins/toolsets/bash/kubectl/__init__.py +80 -79
  62. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -14
  63. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +38 -56
  64. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +28 -76
  65. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +39 -99
  66. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +34 -15
  67. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +1 -1
  68. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +38 -77
  69. holmes/plugins/toolsets/bash/parse_command.py +106 -32
  70. holmes/plugins/toolsets/bash/utilities/__init__.py +0 -0
  71. holmes/plugins/toolsets/bash/utilities/base64_util.py +12 -0
  72. holmes/plugins/toolsets/bash/utilities/cut.py +12 -0
  73. holmes/plugins/toolsets/bash/utilities/grep/__init__.py +10 -0
  74. holmes/plugins/toolsets/bash/utilities/head.py +12 -0
  75. holmes/plugins/toolsets/bash/utilities/jq.py +79 -0
  76. holmes/plugins/toolsets/bash/utilities/sed.py +164 -0
  77. holmes/plugins/toolsets/bash/utilities/sort.py +15 -0
  78. holmes/plugins/toolsets/bash/utilities/tail.py +12 -0
  79. holmes/plugins/toolsets/bash/utilities/tr.py +57 -0
  80. holmes/plugins/toolsets/bash/utilities/uniq.py +12 -0
  81. holmes/plugins/toolsets/bash/utilities/wc.py +12 -0
  82. holmes/plugins/toolsets/coralogix/api.py +6 -6
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +7 -1
  84. holmes/plugins/toolsets/datadog/datadog_api.py +20 -8
  85. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +8 -1
  86. holmes/plugins/toolsets/datadog/datadog_rds_instructions.jinja2 +82 -0
  87. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +12 -5
  88. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +20 -11
  89. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +735 -0
  90. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +18 -11
  91. holmes/plugins/toolsets/git.py +15 -15
  92. holmes/plugins/toolsets/grafana/grafana_api.py +12 -1
  93. holmes/plugins/toolsets/grafana/toolset_grafana.py +5 -1
  94. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +9 -4
  95. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +12 -5
  96. holmes/plugins/toolsets/internet/internet.py +2 -1
  97. holmes/plugins/toolsets/internet/notion.py +2 -1
  98. holmes/plugins/toolsets/investigator/__init__.py +0 -0
  99. holmes/plugins/toolsets/investigator/core_investigation.py +157 -0
  100. holmes/plugins/toolsets/investigator/investigator_instructions.jinja2 +253 -0
  101. holmes/plugins/toolsets/investigator/model.py +15 -0
  102. holmes/plugins/toolsets/kafka.py +14 -7
  103. holmes/plugins/toolsets/kubernetes_logs.py +454 -25
  104. holmes/plugins/toolsets/logging_utils/logging_api.py +115 -55
  105. holmes/plugins/toolsets/mcp/toolset_mcp.py +1 -1
  106. holmes/plugins/toolsets/newrelic.py +8 -3
  107. holmes/plugins/toolsets/opensearch/opensearch.py +8 -4
  108. holmes/plugins/toolsets/opensearch/opensearch_logs.py +9 -2
  109. holmes/plugins/toolsets/opensearch/opensearch_traces.py +6 -2
  110. holmes/plugins/toolsets/prometheus/prometheus.py +179 -44
  111. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +8 -2
  112. holmes/plugins/toolsets/robusta/robusta.py +4 -4
  113. holmes/plugins/toolsets/runbook/runbook_fetcher.py +6 -5
  114. holmes/plugins/toolsets/servicenow/servicenow.py +18 -3
  115. holmes/plugins/toolsets/utils.py +8 -1
  116. holmes/utils/console/logging.py +6 -1
  117. holmes/utils/llms.py +20 -0
  118. holmes/utils/stream.py +90 -0
  119. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/METADATA +47 -34
  120. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/RECORD +123 -91
  121. holmes/plugins/toolsets/bash/grep/__init__.py +0 -52
  122. holmes/utils/robusta.py +0 -9
  123. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/LICENSE.txt +0 -0
  124. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/WHEEL +0 -0
  125. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.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 typing import Optional, List, Tuple
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
- previous_logs_result = self._fetch_kubectl_logs(
96
- params=params,
97
- previous=True,
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
- # Fetch current logs
101
- current_logs_result = self._fetch_kubectl_logs(
102
- params=params,
103
- previous=False,
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
- all_logs = filter_logs(all_logs, params)
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 not all_logs:
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
- formatted_logs = format_logs(
139
- logs=all_logs,
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=formatted_logs,
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=3600,
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
- if params.filter and params.filter.lower() not in log.content.lower():
350
- # exclude this log
351
- continue
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
- return filtered_logs
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(