Skip to content

ROB-1303 unified logging api loki - #430

Merged
nherment merged 1 commit into
rob-1267_unified_tool_logs_apifrom
rob-1303_unified_logging_api_loki
May 20, 2025
Merged

nherment merged 1 commit into
rob-1267_unified_tool_logs_apifrom
rob-1303_unified_logging_api_loki

Conversation

@nherment

@nherment nherment commented May 20, 2025 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Introduced a unified logging API for Kubernetes pod logs with flexible time range and filtering options.
    • Added a new toolset for fetching Kubernetes logs using the official Kubernetes Python client.
    • Enhanced prompt templates and instructions for log retrieval and investigation scenarios.
  • Bug Fixes

    • Improved error handling for pod-not-found scenarios and log timestamp parsing.
    • Corrected logic for default time span assignment and log formatting.
  • Refactor

    • Simplified and unified log fetching interfaces across toolsets.
    • Centralized and improved span management for evaluation and mock toolsets.
    • Consolidated and clarified evaluation logic for test classifiers.
  • Tests

    • Added comprehensive tests for Kubernetes and Grafana Loki log fetching, timestamp filtering, and prompt rendering.
    • Updated and expanded test fixtures for various Kubernetes troubleshooting scenarios.
  • Chores

    • Updated and cleaned up test fixtures, removing deprecated files and improving output formats.
    • Improved documentation and configuration in test cases and evaluation files.

@nherment
nherment requested a review from moshemorad May 20, 2025 05:57
@coderabbitai

coderabbitai Bot commented May 20, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

This update introduces a unified, strongly-typed logging API for Kubernetes and Grafana Loki log retrieval, replacing legacy YAML-based and loosely-typed tools. It implements new toolset classes, prompt templates, and test suites for log fetching, with improved time filtering and error handling. Numerous test fixtures and cases are updated or removed to align with the new logging interface and tool naming conventions.

Changes

File(s) / Path(s) Change Summary
.github/workflows/llm-evaluation.yaml Simplified workflow by removing matrix strategy; renamed job and hardcoded Python version.
holmes/common/env_vars.py Added USE_LEGACY_KUBERNETES_LOGS environment variable.
holmes/plugins/prompts/_default_log_prompt.jinja2 Added a new prompt template for guiding log fetching via the unified tool.
holmes/plugins/prompts/_fetch_logs.jinja2, holmes/plugins/prompts/_general_instructions.jinja2 Updated prompt logic to support new log toolsets and generalized instructions.
holmes/plugins/toolsets/__init__.py Conditional loading of KubernetesLogsToolset based on legacy flag; improved toolset loading logic.
holmes/plugins/toolsets/grafana/base_grafana_toolset.py, holmes/plugins/toolsets/grafana/grafana_api.py, holmes/plugins/toolsets/grafana/common.py, holmes/plugins/toolsets/grafana/loki_api.py Refactored function names, improved log formatting, and updated query construction for Loki logs.
holmes/plugins/toolsets/grafana/toolset_grafana_loki.py Replaced legacy Loki toolset with a new, unified, strongly-typed toolset for pod log fetching.
holmes/plugins/toolsets/kubernetes_logs.py New implementation of a Kubernetes logs toolset using the Python client, with advanced filtering and error handling.
holmes/plugins/toolsets/logging_api.py New module defining the unified logging API, parameter models, and time normalization utilities.
holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 Added section on using fetch_finding_by_id for investigations.
holmes/plugins/toolsets/utils.py Ensured negative default for start timestamp assignment.
tests/llm/README.md Increased minimum correctness threshold in test documentation.
tests/llm/fixtures/test_ask_holmes/*, tests/llm/fixtures/test_investigate/* Massive update: Renamed tool invocations to fetch_pod_logs, updated prompts, removed legacy log fixtures, added new test data for unified logging, and restructured output formats for clarity and consistency.
tests/llm/test_ask_holmes.py, tests/llm/test_investigate.py Refactored span management for Braintrust evaluation, passing parent spans throughout.
tests/llm/utils/classifiers.py Unified and refactored evaluation functions with tracing and improved prompts; removed redundant functions.
tests/llm/utils/mock_toolset.py Enhanced mock tool and toolset classes with span-based tracing and improved encapsulation.
tests/plugins/prompt/test_fetch_logs.py New test module verifying prompt rendering for various log toolset scenarios.
tests/plugins/toolsets/grafana/test_grafana_loki.py Refactored and extended tests for the new Grafana Loki toolset, including new fixtures and parameterized queries.
tests/plugins/toolsets/grafana/test_grafana_tempo.py Updated to use the renamed Grafana health check function.
tests/plugins/toolsets/kubernetes/test_kubernetes_logs.py New unit tests for the KubernetesLogsToolset, covering log retrieval, error handling, and filtering.
tests/plugins/toolsets/kubernetes/test_kubernetes_logs_filter_by_timestamp.py New tests for timestamp-based log filtering and error handling in the log filter utility.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PromptEngine
    participant UnifiedLogToolset
    participant KubernetesAPI
    participant LokiAPI

    User->>PromptEngine: Requests logs for a pod
    PromptEngine->>UnifiedLogToolset: fetch_pod_logs(params)
    alt Kubernetes logs
        UnifiedLogToolset->>KubernetesAPI: Fetch pod logs (with filtering, time window)
        KubernetesAPI-->>UnifiedLogToolset: Log lines or error
    else Loki logs
        UnifiedLogToolset->>LokiAPI: Query logs by label (with match, time window)
        LokiAPI-->>UnifiedLogToolset: Log lines or error
    end
    UnifiedLogToolset-->>PromptEngine: StructuredToolResult (logs, status)
    PromptEngine-->>User: Rendered logs or error message
Loading
sequenceDiagram
    participant TestRunner
    participant MockToolsets
    participant UnifiedLogToolset
    participant Braintrust

    TestRunner->>Braintrust: Start evaluation span
    TestRunner->>MockToolsets: Initialize with parent_span
    TestRunner->>UnifiedLogToolset: fetch_pod_logs(params, parent_span)
    UnifiedLogToolset-->>MockToolsets: Save/return logs (mocked or real)
    TestRunner->>Braintrust: Evaluate correctness/context/sections (with parent_span)
    Braintrust-->>TestRunner: Evaluation results
    TestRunner->>Braintrust: End evaluation span
Loading

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@nherment
nherment changed the base branch from master to rob-1267_unified_tool_logs_api May 20, 2025 05:59
@github-actions

Copy link
Copy Markdown
Contributor

Results of HolmesGPT evals

Test suite Test case Status
ask_holmes 01_how_many_pods ⚠️
ask_holmes 02_what_is_wrong_with_pod ✅
ask_holmes 03_what_is_the_command_to_port_forward ✅
ask_holmes 04_related_k8s_events ✅
ask_holmes 05_image_version ✅
ask_holmes 06_explain_issue ✅
ask_holmes 07_high_latency ✅
ask_holmes 08_sock_shop_frontend ✅
ask_holmes 09_crashpod ✅
ask_holmes 10_image_pull_backoff ✅
ask_holmes 11_init_containers ✅
ask_holmes 12_job_crashing ✅
ask_holmes 13_pending_node_selector ✅
ask_holmes 14_pending_resources ✅
ask_holmes 15_failed_readiness_probe ✅
ask_holmes 16_failed_no_toolset_found ✅
ask_holmes 17_oom_kill ✅
ask_holmes 18_crash_looping_v2 ✅
ask_holmes 19_detect_missing_app_details ✅
ask_holmes 20_long_log_file_search ✅
ask_holmes 21_job_fail_curl_no_svc_account ⚠️
ask_holmes 22_high_latency_dbi_down ⚠️
ask_holmes 23_app_error_in_current_logs ✅
ask_holmes 24_misconfigured_pvc ✅
ask_holmes 25_misconfigured_ingress_class ⚠️
ask_holmes 26_multi_container_logs ✅
ask_holmes 27_permissions_error_no_helm_tools ✅
ask_holmes 28_permissions_error_helm_tools_enabled ✅
ask_holmes 29_events_from_alert_manager ✅
ask_holmes 30_basic_promql_graph_cluster_memory ✅
ask_holmes 31_basic_promql_graph_pod_memory ✅
ask_holmes 32_basic_promql_graph_pod_cpu ✅
ask_holmes 33_http_latency_graph ✅
ask_holmes 34_memory_graph ✅
ask_holmes 35_tempo ✅
ask_holmes 36_argocd_find_resource ✅
ask_holmes 37_argocd_wrong_namespace ⚠️
ask_holmes 38_rabbitmq_split_head ✅
ask_holmes 39_failed_toolset ✅
ask_holmes 40_disabled_toolset ✅
ask_holmes 41_setup_argo ✅
investigate 01_oom_kill ✅
investigate 02_crashloop_backoff ✅
investigate 03_cpu_throttling ✅
investigate 04_image_pull_backoff ✅
investigate 05_crashpod ✅
investigate 06_job_failure ✅
investigate 07_job_syntax_error ✅
investigate 08_memory_pressure ✅
investigate 09_high_latency ✅
investigate 10_kube_controller_manager_down ⚠️
investigate 11_KubeDeploymentReplicasMismatch ✅
investigate 12_KubePodCrashLooping ✅
investigate 13_KubePodNotReady ✅
investigate 14_Watchdog ✅
investigate 15_tempo ✅

Legend

  • ✅ the test was successful
  • ⚠️ the test failed but is known to be flakky or known to fail
  • ❌ the test failed and should be fixed before merging the PR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 17

🧹 Nitpick comments (23)
tests/llm/fixtures/test_ask_holmes/35_tempo/kubectl_logs_backend.txt (1)

1-1: Ensure consistency of the new tool name across fixtures and prompts.

The tool_name has been updated to fetch_pod_logs to match the new unified logging API. Please verify that:

  • All other test fixtures use fetch_pod_logs instead of legacy names (kubectl_logs, kubectl_logs_all_containers, etc.).
  • Prompt templates and examples in documentation/reference files also reference fetch_pod_logs.
  • There are no remaining references to the old tool names in CI configurations or code comments.
tests/llm/fixtures/test_ask_holmes/25_misconfigured_ingress_class/test_case.yaml (1)

3-4: Ensure consistent CLI flag formatting
Consider adding a space between -f and the file path for clarity and to match common kubectl usage (-f ./ingress_with_class.yaml).

tests/llm/fixtures/test_ask_holmes/26_multi_container_logs/kubectl_logs_filter.txt (1)

2-5: Define expected stdout fixture content
Currently, stdout: and stderr: are empty, which may not fully exercise the log filtering logic. Consider adding representative log entries containing the "render time" keyword (and non-matching entries) to validate that filtering works as intended.

holmes/plugins/prompts/_fetch_logs.jinja2 (1)

1-43: Add file documentation for better maintainability.

While the template is well-structured, consider adding a comment at the top of the file explaining its purpose and how it works with different logging toolsets for future maintainers.

+{# 
+ This template provides logging instructions based on available toolsets.
+ It checks for various logging providers (Loki, Coralogix, Kubernetes, OpenSearch)
+ and includes appropriate instructions for the first available/enabled one.
+ 
+ For the unified logging API (k8s_base_ts), it includes the _default_log_prompt.jinja2
+ template which provides standardized instructions across logging providers.
+#}
 {%- set loki_ts = toolsets | selectattr("name", "equalto", "grafana/loki") | first -%}
holmes/plugins/prompts/_default_log_prompt.jinja2 (1)

1-12: Improve bullet formatting and clarify time parameters.

  • Use consistent single‐asterisk (*) bullets for top‐level and two‐space indents for nested items.
  • Clarify that start_time should be computed as the issue’s starts_at minus 300 seconds, and end_time set to the issue’s starts_at.

Proposed diff:

* Use the tool `fetch_pod_logs` to access an application's logs
* Prior to fetching logs, ensure the pod exists using kubectl tools
* If you find no logs, double check that the namespace and pod names are exact. Use kubectl tools to find the right resource names and pod name.
* If you are not given the pod's namespace, look for existing pods using kubectl tools and infer the namespace that way
* If you are not given the pod's exact name, or only have an application or deployment name, look for related pods using kubectl commands. Ask the user if you can't infer the pod logs.
* Do fetch application logs yourself and do not ask users to do so
* If you have an issue id or finding id, use `fetch_finding_by_id` as it contains time metadata about the issue (`starts_at`, `updated_at`, and `ends_at`).
  * Then, set `start_time` to `issue.starts_at - 300` (five minutes before the issue’s start time) and `end_time` to the issue’s `starts_at` when calling `fetch_pod_logs`.
  * If there are too many logs or not enough, narrow or widen the timestamps.
  * If looking for a specific keyword, use the `filter` argument.
* If you are not provided with time information, ignore the `start_time` and `end_time`; `fetch_pod_logs` will default to the latest logs.

</blockquote></details>
<details>
<summary>tests/llm/test_investigate.py (3)</summary><blockquote>

`32-37`: **Unused stored attribute – possible dead code**  
`MockConfig.__init__` stores `self._parent_span` but the attribute is never referenced inside the class. If the only consumer is `MockToolsets`, pass the span directly and drop the field to prevent stale state.

```diff
-    def __init__(self, test_case: InvestigateTestCase, parent_span: Span):
+    def __init__(self, test_case: InvestigateTestCase, parent_span: Span):
         super().__init__()
         self._test_case = test_case
-        self._parent_span = parent_span
+        # Pass through; no need to persist

38-43: Forwarding parent_span but not documenting the new constructor
MockToolsets now relies on a new parent_span kw-arg, yet its constructor’s docstring or type signature (in the implementation file) was not updated in this PR. Future maintainers will be confused by the silent dependency.
Please update tests/llm/utils/mock_toolset.py with an explicit parent_span: Span | None = None parameter and docstring.


125-149: Evaluation helpers swallow classifier exceptions
All three evaluation helpers (evaluate_correctness, evaluate_sections, evaluate_context_usage) are called without error handling. If any of them throws, you lose diagnostic information from later assertions and the Braintrust span ends without scores. Consider catching Exception around each helper, logging inside the span, and recording a score of 0.0 to avoid hard failures unrelated to the LLM output.

holmes/plugins/toolsets/__init__.py (2)

80-82: Lazy instantiation suggestion
Even after deferring the import, the toolset object is still constructed at startup. If the cluster has no kube-config, KubernetesLogsToolset() may attempt to connect and raise. Consider wrapping the append in try/except Exception as exc: and logging a warning so the rest of Holmes continues to load.


90-97: YAML/py duplication guard looks correct – minor readability nit
Good catch skipping kubernetes_logs.yaml when the new Python toolset is active. A tiny readability tweak:

-        if filename == "kubernetes_logs.yaml" and not USE_LEGACY_KUBERNETES_LOGS:
+        if not USE_LEGACY_KUBERNETES_LOGS and filename == "kubernetes_logs.yaml":

Placing the inexpensive boolean first avoids the string comparison in the common case.

tests/plugins/prompt/test_fetch_logs.py (2)

8-12: Avoid brittle relative path construction

Using os.path.join(THIS_DIR, "../../../...") assumes the test file will always live exactly three directories below the YAML file.
A small package-layout change will break the tests.

-THIS_DIR = os.path.abspath(os.path.dirname(__file__))
-KUBERNETES_YAML_TOOLSET_PATH = os.path.join(
-    THIS_DIR, "../../../holmes/plugins/toolsets/kubernetes_logs.yaml"
-)
+from importlib.resources import files
+# Locate the YAML file relative to the installed package instead of the test file.
+KUBERNETES_YAML_TOOLSET_PATH = (
+    files("holmes.plugins.toolsets")
+    / "kubernetes_logs.yaml"
+)

20-28: Remove noisy print() calls in tests

The three print(f"** PROMPT: ...") statements clutter the test output and slow down CI runners.
If the output is really needed for debugging, use the -s pytest flag or capsys fixture instead.

-    print(f"** PROMPT:\n{prompt}")

Also applies to: 30-38, 41-49

tests/plugins/toolsets/kubernetes/test_kubernetes_logs.py (1)

14-18: Patch the class method instead of the instance

patch.object(self.toolset, "_initialize_client") stubs only the instance copy created in setUp.
If the test ever reinstantiates the toolset, or other tests import the class directly, the real client
initialisation could leak through.

-patcher = patch.object(self.toolset, "_initialize_client")
+patcher = patch.object(KubernetesLogsToolset, "_initialize_client")
tests/plugins/toolsets/kubernetes/test_kubernetes_logs_filter_by_timestamp.py (1)

127-129: Suppress gratuitous debug prints

The print() calls inside parameterised tests clutter CI logs and slow down execution.
Rely on pytest’s assertion introspection or use capsys when the diff is really needed.

-    print(f"EXPECTED:\n{expected_output}")
-    print(f"ACTUAL:\n{result}")

Also applies to: 154-156

tests/llm/test_ask_holmes.py (2)

73-84: Avoid shadowing the built-in input

Assigning to a variable named input hides Python’s built-in function within the remainder of the scope and can lead to confusing errors during debugging.

-    input = test_case.user_prompt
+    user_input = test_case.user_prompt

82-93: Missing score for mandatory correctness evaluation

If evaluate_correctness raises or returns None, the subsequent access to correctness_eval.score will raise.
Wrap the call with a try/except or validate the return type to ensure the test always records a numeric score.

tests/plugins/toolsets/grafana/test_grafana_loki.py (2)

108-112: Fragile header-skipping logic

The test discards the first two lines assuming they are always “link” and “query”.
Any change in Loki’s output format (extra blank line, order swap, etc.) will break the test.

Prefer an explicit filter:

content_lines = [l for l in result.data.splitlines() if l and not l.startswith(("link:", "query:"))]

This keeps the intent clear and resilient.


125-129: Brittle assertion on exact line count

The test enforces len(result.data.split("\n")) == 1, which can fail if Loki adds a trailing newline or extra metadata.
A safer check is to assert that at least one log line is returned and that every line contains the expected term.

lines = [l for l in result.data.splitlines() if l]
assert lines, "No log lines returned"
for line in lines:
    assert TEST_SEARCH_TERM in line
holmes/plugins/toolsets/kubernetes_logs.py (2)

153-165: Single-container pods skip explicit container querying

When containers has length 1, the branch guarded by len(containers) > 1 is skipped, and the fallback fetches logs without the
container parameter.
If the pod previously had a different container name, the returned logs may be incomplete.

You can simplify and unify the path:

for container_name in containers or [None]:
    ...

and always prefix only when container_name is not None and there are multiple containers.


222-229: Ruff hint – collapsible error branches

The two early-return branches on 400/404 status codes can be merged for brevity:

-        except ApiException as e:
-            if e.status == 400 and "previous terminated container" in str(e).lower():
-                return []
-            elif e.status == 404:
-                return []
+        except ApiException as e:
+            if e.status == 404 or (
+                e.status == 400 and "previous terminated container" in str(e).lower()
+            ):
+                return []

Not critical, but trims five lines.

🧰 Tools
🪛 Ruff (0.11.9)

222-225: Combine if branches using logical or operator

Combine if branches

(SIM114)

holmes/plugins/toolsets/logging_api.py (1)

146-153: Handling of negative start_time values can mis-detect strings with whitespace or “s” suffix

start_time.startswith("-") and start_time[1:].isdigit() returns False for valid inputs like "-3600 " or "-3600s".
A more robust check would attempt int(start_time) inside a try/except.

-            if isinstance(start_time, int) or (
-                isinstance(start_time, str)
-                and start_time.startswith("-")
-                and start_time[1:].isdigit()
-            ):
+            if isinstance(start_time, (int, str)):
+                try:
+                    seconds_before = abs(int(start_time))
+                    is_relative = True
+                except ValueError:
+                    is_relative = False
+
+            if is_relative:

This keeps relative time parsing flexible without sacrificing validation.

holmes/plugins/toolsets/grafana/toolset_grafana_loki.py (2)

50-53: Clarify error message – toolset is Loki, not OpenSearch

The prerequisite failure message still refers to “OpenSearch”, which will mislead users configuring the Grafana / Loki backend and make troubleshooting harder.

-        if not config:
-            return False, "Missing OpenSearch configuration. Check your config."
+        if not config:
+            return False, "Missing Grafana Loki configuration. Check your config."

91-98: Return path is fine but logs.sort mutates caller-visible list

Because query_loki_logs_by_label already returns a list that might be reused by the caller/tests, sorting it in-place couples your implementation to the function’s return value. A defensive copy keeps side-effects local and avoids surprises.

-            logs.sort(key=lambda x: x["timestamp"])
+            logs = sorted(logs, key=lambda x: x["timestamp"])
🛑 Comments failed to post (17)
tests/llm/fixtures/test_investigate/03_cpu_throttling/kubectl_previous_logs.txt (1)

1-1: ⚠️ Potential issue

Missing previous flag in fetch_pod_logs invocation

The fixture is for previous logs, but the fetch_pod_logs invocation lacks a previous parameter. Without it, only current logs will be fetched, breaking this scenario.

Suggested diff:

-{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"frontend-service","namespace":"default"}}
+{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"frontend-service","namespace":"default","previous": true}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"frontend-service","namespace":"default","previous": true}}
🤖 Prompt for AI Agents
In
tests/llm/fixtures/test_investigate/03_cpu_throttling/kubectl_previous_logs.txt
at line 1, the fetch_pod_logs invocation is missing the 'previous' flag, which
is necessary to fetch previous logs as intended by this fixture. Add "previous":
true to the match_params object to ensure the logs fetched are from the previous
container instance.
tests/llm/fixtures/test_investigate/07_job_syntax_error/kubectl_logs_all_containers.txt (1)

1-1: ⚠️ Potential issue

Missing all_containers flag for multi-container logs

This fixture targets logs from all containers, but the fetch_pod_logs invocation lacks an all_containers parameter (or equivalent). The new toolset likely requires explicit instruction to fetch logs across every container.

Suggested diff:

-{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"*","namespace":"default"}}
+{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"*","namespace":"default","all_containers": true}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"*","namespace":"default","all_containers": true}}
🤖 Prompt for AI Agents
In
tests/llm/fixtures/test_investigate/07_job_syntax_error/kubectl_logs_all_containers.txt
at line 1, the JSON object for fetch_pod_logs is missing the all_containers flag
needed to fetch logs from all containers in a pod. Add an "all_containers": true
key-value pair inside the match_params object to explicitly instruct the toolset
to retrieve logs from every container.
tests/llm/fixtures/test_investigate/04_image_pull_backoff/kubectl_previous_logs.txt (1)

1-1: ⚠️ Potential issue

Missing previous flag in fetch_pod_logs invocation

This scenario fetches previous logs, but the invocation is missing the previous parameter. It must be added to retrieve terminated container logs correctly.

Suggested diff:

-{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"customer-relations-webapp-5d98ffcfd-tz4nc","namespace":"default"}}
+{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"customer-relations-webapp-5d98ffcfd-tz4nc","namespace":"default","previous": true}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

{"toolset_name":"kubernetes/logs","tool_name":"fetch_pod_logs","match_params":{"pod_name":"customer-relations-webapp-5d98ffcfd-tz4nc","namespace":"default","previous": true}}
🤖 Prompt for AI Agents
In
tests/llm/fixtures/test_investigate/04_image_pull_backoff/kubectl_previous_logs.txt
at line 1, the fetch_pod_logs invocation is missing the 'previous' flag needed
to retrieve logs from terminated containers. Add the 'previous' parameter set to
true within the match_params object to ensure the scenario fetches the previous
logs correctly.
tests/llm/fixtures/test_ask_holmes/08_sock_shop_frontend/test_case.yaml (1)

20-20: ⚠️ Potential issue

Fix YAML indentation for generate_mocks
generate_mocks: False is not indented under the evaluation block, so it will be treated as a top-level key. It should be nested alongside correctness:

 evaluation:
   correctness: 1
- generate_mocks: False
+   generate_mocks: False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

evaluation:
  correctness: 1
  generate_mocks: False
🤖 Prompt for AI Agents
In tests/llm/fixtures/test_ask_holmes/08_sock_shop_frontend/test_case.yaml at
line 20, the key `generate_mocks: False` is incorrectly placed at the top level.
Move it to be indented under the `evaluation` block, aligned with the
`correctness` key, to ensure it is properly nested within the evaluation
configuration.
tests/llm/fixtures/test_ask_holmes/20_long_log_file_search/test_case.yaml (1)

8-8: ⚠️ Potential issue

Correct indentation for generate_mocks
generate_mocks: True is currently at the root level; it must be nested under evaluation to be recognized:

 evaluation:
   correctness: 1
- generate_mocks: True
+   generate_mocks: True
🤖 Prompt for AI Agents
In tests/llm/fixtures/test_ask_holmes/20_long_log_file_search/test_case.yaml at
line 8, the key `generate_mocks: True` is incorrectly placed at the root level.
Move this line so that it is indented under the `evaluation` section to ensure
it is properly recognized as part of the evaluation configuration.
.github/workflows/llm-evaluation.yaml (1)

20-24: 💡 Verification agent

🧩 Analysis chain

Pin Python version consistently
Set up Python to use version 3.12—ensure this version is supported across all workflows and aligns with your .github/workflows/build-binaries-and-brew.yaml.

Run this script to verify Python versions in other workflows:


🏁 Script executed:

#!/bin/bash
# Verify Python versions across workflows
rg --no-heading -n 'python-version:' .github/workflows

Length of output: 400


Align Python versions across workflows
The llm-evaluation.yaml workflow pins Python 3.12, but build-binaries-and-brew.yaml is still on 3.11. To avoid surprises, choose one version and use it everywhere. Since your matrix in build-and-test.yaml already includes 3.12, update the brew workflow to match:

• In .github/workflows/build-binaries-and-brew.yaml (around line 24):

-        python-version: '3.11'
+        python-version: 3.12

• Verify all other workflows (e.g. CI matrix) support the chosen version.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .github/workflows/llm-evaluation.yaml at lines 20 to 24, Python version is
pinned to 3.12, but other workflows like build-binaries-and-brew.yaml use 3.11.
To maintain consistency and avoid version conflicts, update the Python version
in .github/workflows/build-binaries-and-brew.yaml around line 24 to 3.12, and
verify all other workflows also support Python 3.12 to ensure uniformity across
the project.
tests/llm/test_investigate.py (1)

83-90: ⚠️ Potential issue

Missed finally – evaluation span may leak on early failure
If investigate_issues, an assertion, or any evaluation raises, bt_helper.end_evaluation() is never executed, leaving the span open in Braintrust. Wrap the body in try/finally:

-    eval_span = bt_helper.start_evaluation(experiment_name, name=test_case.id)
-    ...
-    if bt_helper and eval_span:
-        bt_helper.end_evaluation(...)
+    eval_span = bt_helper.start_evaluation(experiment_name, name=test_case.id)
+    try:
+        ...
+    finally:
+        if bt_helper and eval_span:
+            bt_helper.end_evaluation(...)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tests/llm/test_investigate.py around lines 83 to 90, the evaluation span
started by bt_helper.start_evaluation is not properly closed if an exception
occurs, potentially leaking the span. To fix this, wrap the code following the
start_evaluation call in a try block and ensure bt_helper.end_evaluation() is
called in a finally block to guarantee the evaluation span is always ended
regardless of errors.
holmes/plugins/toolsets/__init__.py (1)

6-13: ⚠️ Potential issue

Eager import defeats optional-dependency flag
KubernetesLogsToolset (and its heavy kubernetes PyPI dependency) is imported at module import time even when USE_LEGACY_KUBERNETES_LOGS is true. This means:

  1. Environments that wish to stay on the legacy path must still install the Kubernetes client.
  2. Any import error inside kubernetes_logs.py crashes Holmes startup before the flag is checked.

Move the import inside the if not USE_LEGACY_KUBERNETES_LOGS: block:

if not USE_LEGACY_KUBERNETES_LOGS:
    from holmes.plugins.toolsets.kubernetes_logs import KubernetesLogsToolset

and adjust the subsequent references.

🤖 Prompt for AI Agents
In holmes/plugins/toolsets/__init__.py around lines 6 to 13, the
KubernetesLogsToolset is imported eagerly regardless of the
USE_LEGACY_KUBERNETES_LOGS flag, causing unnecessary dependency loading and
potential import errors. Move the import of KubernetesLogsToolset inside an if
not USE_LEGACY_KUBERNETES_LOGS: block to defer the import until the flag is
checked. Then update any references to KubernetesLogsToolset to ensure they only
occur when the import is valid.
tests/plugins/prompt/test_fetch_logs.py (1)

20-23: 🛠️ Refactor suggestion

Do not rely on private attributes for state manipulation

The tests mutate the “private” _status attribute of each toolset:

toolset._status = ToolsetStatusEnum.ENABLED

Provide an official helper (e.g. toolset.set_status(ToolsetStatusEnum.ENABLED)) or expose a public property to avoid tight coupling to the implementation.

Also applies to: 31-34, 42-45

🤖 Prompt for AI Agents
In tests/plugins/prompt/test_fetch_logs.py around lines 20 to 23 (and similarly
at lines 31-34 and 42-45), the test code directly modifies the private attribute
_status of toolset instances, which creates tight coupling to the
implementation. To fix this, replace direct assignments to _status with calls to
a public method like set_status or use a public property setter designed for
changing the toolset's status. If such a method or property does not exist, add
one to the Toolset class to safely update the status without accessing private
attributes.
tests/plugins/toolsets/kubernetes/test_kubernetes_logs.py (1)

32-40: ⚠️ Potential issue

Decorator-parameter order mismatch breaks all patched tests

unittest.mock.patch injects mocks in the same order the decorators are applied (top → bottom).
Here @patch("kubernetes.client.CoreV1Api") is applied first, so the first function argument should
be mock_core_v1_api, followed by mock_config. Currently they are reversed, causing the “config” mock to
be treated as an API instance and vice-versa, which will explode on .return_value.

-@patch("kubernetes.client.CoreV1Api")
-@patch("kubernetes.config")
-def test_default_log_formatting(self, mock_config, mock_api):
+@patch("kubernetes.client.CoreV1Api")      # top decorator -> first arg
+@patch("kubernetes.config")                # bottom decorator -> second arg
+def test_default_log_formatting(self, mock_api, mock_config):

Apply the same fix to all test functions patched in this module.

Also applies to: 66-74, 112-120, 162-170, 194-202

🤖 Prompt for AI Agents
In tests/plugins/toolsets/kubernetes/test_kubernetes_logs.py at lines 32 to 40
and similarly at lines 66-74, 112-120, 162-170, and 194-202, the order of
parameters in the test functions does not match the order of the @patch
decorators, causing mocks to be assigned incorrectly. Fix this by reversing the
order of the function parameters so that the first parameter corresponds to the
first (top) @patch decorator and the second parameter corresponds to the second
decorator, ensuring mocks are injected in the correct order.
tests/plugins/toolsets/grafana/test_grafana_loki.py (2)

88-95: 🛠️ Refactor suggestion

The “basic” query asserts on a hard-coded search term that is never requested

TEST_SEARCH_TERM is not supplied in FetchPodLogsParams, yet the assertion requires it to appear in the response.
If the pod stops emitting the word “WARNING”, this test will fail for reasons unrelated to correctness.

Options:
• Pass match=TEST_SEARCH_TERM so the API guarantees the presence.
• Or drop the assertion and only check result.status/error.

🤖 Prompt for AI Agents
In tests/plugins/toolsets/grafana/test_grafana_loki.py around lines 88 to 95,
the test asserts that TEST_SEARCH_TERM appears in the logs without requesting it
in FetchPodLogsParams, which can cause flaky failures. Fix this by adding
match=TEST_SEARCH_TERM to the FetchPodLogsParams call to ensure the logs contain
the search term, or alternatively remove the assertion checking for
TEST_SEARCH_TERM and only verify result.status and result.error.

60-65: 🛠️ Refactor suggestion

Avoid bypassing the toolset’s public configuration API and fix misleading doc-string

  1. The doc-string still mentions OpenSearchLogsToolset, which can confuse future maintainers.
  2. Assigning toolset.config = loki_config.model_dump() mutates a (likely) public attribute directly.
    If GrafanaLokiToolset later adds validation or side-effects inside a setter / configure() helper, this test will silently skip them.
-"""Create an OpenSearchLogsToolset with the test configuration"""
+"""Create a GrafanaLokiToolset instance with the test configuration"""
-
-toolset = GrafanaLokiToolset()
-toolset.config = loki_config.model_dump()
+# Prefer dedicated ctor/initialiser if available
+# e.g. toolset = GrafanaLokiToolset(config=loki_config)
+# or, if not, expose a helper:
+# toolset.configure(loki_config)
+toolset = GrafanaLokiToolset()
+toolset.config = loki_config.model_dump()   # TODO: replace direct mutation

Consider changing the production class to expose an explicit configure() or constructor parameter and update the test accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

@pytest.fixture
def loki_toolset(loki_config) -> GrafanaLokiToolset:
    """Create a GrafanaLokiToolset instance with the test configuration"""
    # Prefer a dedicated ctor/initializer if available:
    # e.g. toolset = GrafanaLokiToolset(config=loki_config)
    # or, if not, expose a helper:
    # toolset.configure(loki_config)
    toolset = GrafanaLokiToolset()
    toolset.config = loki_config.model_dump()   # TODO: replace direct mutation
    toolset.check_prerequisites()
🤖 Prompt for AI Agents
In tests/plugins/toolsets/grafana/test_grafana_loki.py around lines 60 to 65,
update the doc-string to correctly reference GrafanaLokiToolset instead of
OpenSearchLogsToolset to avoid confusion. Replace the direct assignment to
toolset.config with a call to a proper configuration method like configure() or
pass the configuration via the constructor if supported, ensuring any validation
or side-effects in the class are executed. If such a method does not exist, add
one to the production class and use it in the test to set the configuration
properly.
holmes/plugins/toolsets/kubernetes_logs.py (2)

86-98: 🛠️ Refactor suggestion

Potential duplication of log lines when combining previous and current logs

fetch_pod_logs() concatenates previous=True and previous=False results without de-duplication.
When a container hasn’t rolled over, the same lines may appear in both calls, doubling the output and breaking limit logic.

Consider deduplicating while preserving order:

all_logs = list(dict.fromkeys(all_logs))  # preserves first-seen order

or compare timestamps if available.

🤖 Prompt for AI Agents
In holmes/plugins/toolsets/kubernetes_logs.py around lines 86 to 98, the code
concatenates logs fetched with previous=True and previous=False without removing
duplicates, which can cause duplicated log lines and affect the limit logic. To
fix this, after combining the two log lists, deduplicate the entries while
preserving their order by converting the list to a dict and back to a list, or
implement a timestamp-based comparison if timestamps are available, ensuring no
duplicate log lines appear in the final all_logs list.

70-78: ⚠️ Potential issue

config.ConfigException may not exist – import the correct exception class

kubernetes.config does not expose ConfigException at the module root in some client versions.
Accessing config.ConfigException can raise AttributeError, causing _initialize_client() to mask the real configuration issue.

-from kubernetes import client, config
-from kubernetes.client.exceptions import ApiException
+from kubernetes import client, config
+from kubernetes.client.exceptions import ApiException
+from kubernetes.config.config_exception import ConfigException
 ...
-            except config.ConfigException:
+            except ConfigException:

This keeps the code compatible with both in-cluster and out-of-cluster setups.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

from kubernetes import client, config
from kubernetes.client.exceptions import ApiException
+from kubernetes.config.config_exception import ConfigException

    ...

        try:
            try:
                config.load_incluster_config()
-            except config.ConfigException:
+            except ConfigException:
                logging.debug(
                    f"_initialize_client for {self.name} toolset falling back to loading kube_config"
                )
                config.load_kube_config()
🤖 Prompt for AI Agents
In holmes/plugins/toolsets/kubernetes_logs.py around lines 70 to 78, the code
catches config.ConfigException which may not exist in some kubernetes client
versions, causing an AttributeError. To fix this, import the correct exception
class explicitly from kubernetes.config or its submodules and catch that instead
of config.ConfigException, ensuring compatibility with different client versions
and avoiding masking real configuration errors.
tests/llm/utils/classifiers.py (1)

101-103: ⚠️ Potential issue

Bug: passing Python built-in input instead of the real input string

evaluate_correctness() has no input parameter, yet you pass input=input to the classifier.
At runtime this resolves to the built-in input() function, leading to meaningless prompts and unpredictable behaviour.

-def evaluate_correctness(
-    expected_elements: list[str], output: Optional[str], parent_span: Span
-):
+def evaluate_correctness(
+    expected_elements: list[str],
+    output: Optional[str],
+    *,
+    input: Optional[str] = None,       # new, mirrors other helpers
+    parent_span: Span,
+):
...
-        correctness_eval = classifier(
-            input=input, output=output, expected=expected_elements_str
-        )
+        correctness_eval = classifier(
+            input=input,
+            output=output,
+            expected=expected_elements_str,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

def evaluate_correctness(
    expected_elements: list[str],
    output: Optional[str],
    *,
    input: Optional[str] = None,       # new, mirrors other helpers
    parent_span: Span,
):
    # … existing implementation …

    correctness_eval = classifier(
        input=input,
        output=output,
        expected=expected_elements_str,
    )

    # … rest of implementation …
🤖 Prompt for AI Agents
In tests/llm/utils/classifiers.py around lines 101 to 103, the code incorrectly
passes the built-in Python function `input` as a parameter to the classifier
because `evaluate_correctness()` does not have an `input` argument. To fix this,
identify the correct variable holding the input string intended for the
classifier and pass that variable instead of `input=input`. Remove or replace
the `input=input` argument with the actual input string variable to avoid
passing the built-in function.
tests/llm/utils/mock_toolset.py (2)

142-176: 🛠️ Refactor suggestion

Same exception-swallowing issue as above in MockToolWrapper._invoke

Apply the same else/finally restructuring here to ensure errors propagate correctly.

🧰 Tools
🪛 Ruff (0.11.9)

152-155: Use ternary operator result = mock.return_value if mock else self._unmocked_tool.invoke(params) instead of if-else-block

Replace if-else-block with result = mock.return_value if mock else self._unmocked_tool.invoke(params)

(SIM108)

🤖 Prompt for AI Agents
In tests/llm/utils/mock_toolset.py between lines 142 and 176, the _invoke method
currently catches exceptions and logs them but then re-raises, which can obscure
error propagation. Refactor the try-except-finally block by moving the
span.end() call into a finally block and restructuring the else block so that
the span.log call for successful results happens only if no exception occurs.
This ensures exceptions propagate correctly without being swallowed while
maintaining proper span lifecycle management.

118-119: ⚠️ Potential issue

Shared mutable default list leaks state between tests

mocks: List[ToolMock] = [] is evaluated once at import time, so instances of MockToolWrapper share the same list, causing cross-test pollution.

-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional
+from pydantic import Field
...
-class MockToolWrapper(Tool, BaseModel):
-    mocks: List[ToolMock] = []
+class MockToolWrapper(Tool, BaseModel):
+    mocks: List[ToolMock] = Field(default_factory=list)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

from typing import Any, Dict, List, Optional
from pydantic import Field

class MockToolWrapper(Tool, BaseModel):
    mocks: List[ToolMock] = Field(default_factory=list)
🤖 Prompt for AI Agents
In tests/llm/utils/mock_toolset.py at line 118, the declaration of mocks as a
mutable default list causes shared state across test instances. To fix this,
change mocks to be initialized inside the constructor or a method so that each
instance gets its own new list, avoiding cross-test pollution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants