holmesgpt 0.13.2__py3-none-any.whl → 0.18.4__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.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +20 -6
- holmes/common/env_vars.py +58 -3
- holmes/common/openshift.py +1 -1
- holmes/config.py +123 -148
- holmes/core/conversations.py +71 -15
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +31 -39
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +508 -88
- holmes/core/models.py +108 -4
- holmes/core/openai_formatting.py +14 -1
- holmes/core/prompt.py +48 -3
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +8 -6
- holmes/core/supabase_dal.py +295 -100
- holmes/core/tool_calling_llm.py +489 -428
- holmes/core/tools.py +325 -56
- holmes/core/tools_utils/token_counting.py +21 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
- holmes/core/tools_utils/tool_executor.py +0 -13
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +191 -5
- holmes/core/tracing.py +19 -3
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +63 -0
- holmes/core/transformers/llm_summarize.py +175 -0
- holmes/core/transformers/registry.py +123 -0
- holmes/core/transformers/transformer.py +32 -0
- holmes/core/truncation/compaction.py +94 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +219 -0
- holmes/interactive.py +228 -31
- holmes/main.py +23 -40
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
- holmes/plugins/prompts/generic_ask.jinja2 +0 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
- holmes/plugins/runbooks/__init__.py +145 -17
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +44 -27
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/utils.py +0 -32
- holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
- holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
- holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
- holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
- holmes/plugins/toolsets/bash/common/bash.py +23 -13
- holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
- holmes/plugins/toolsets/bash/common/stringify.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
- holmes/plugins/toolsets/bash/parse_command.py +12 -13
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/connectivity_check.py +124 -0
- holmes/plugins/toolsets/coralogix/api.py +132 -119
- holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
- holmes/plugins/toolsets/coralogix/utils.py +15 -79
- holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
- holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
- holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/git.py +54 -50
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +13 -29
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
- holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
- holmes/plugins/toolsets/internet/internet.py +15 -16
- holmes/plugins/toolsets/internet/notion.py +9 -11
- holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +36 -42
- holmes/plugins/toolsets/kubernetes.yaml +317 -113
- holmes/plugins/toolsets/kubernetes_logs.py +9 -9
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
- holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
- holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
- holmes/plugins/toolsets/robusta/robusta.py +239 -68
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +60 -11
- holmes/utils/holmes_status.py +6 -4
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/log.py +15 -0
- holmes/utils/markdown_utils.py +2 -3
- holmes/utils/memory_limit.py +58 -0
- holmes/utils/sentry_helper.py +64 -0
- holmes/utils/stream.py +69 -8
- holmes/utils/tags.py +4 -3
- holmes/version.py +37 -15
- holmesgpt-0.18.4.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
- holmes/plugins/toolsets/newrelic.py +0 -231
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- holmesgpt-0.13.2.dist-info/RECORD +0 -234
- /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import requests # type: ignore
|
|
3
|
-
from typing import Tuple
|
|
4
|
-
import backoff
|
|
5
|
-
|
|
6
|
-
from holmes.plugins.toolsets.grafana.common import (
|
|
7
|
-
GrafanaConfig,
|
|
8
|
-
build_headers,
|
|
9
|
-
get_base_url,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@backoff.on_exception(
|
|
14
|
-
backoff.expo, # Exponential backoff
|
|
15
|
-
requests.exceptions.RequestException, # Retry on request exceptions
|
|
16
|
-
max_tries=5, # Maximum retries
|
|
17
|
-
giveup=lambda e: isinstance(e, requests.exceptions.HTTPError)
|
|
18
|
-
and e.response.status_code < 500,
|
|
19
|
-
)
|
|
20
|
-
def grafana_health_check(config: GrafanaConfig) -> Tuple[bool, str]:
|
|
21
|
-
base_url = get_base_url(config)
|
|
22
|
-
url = f"{base_url}/{config.healthcheck}"
|
|
23
|
-
try:
|
|
24
|
-
headers_ = build_headers(api_key=config.api_key, additional_headers=None)
|
|
25
|
-
|
|
26
|
-
response = requests.get(url, headers=headers_, timeout=10) # Added timeout
|
|
27
|
-
response.raise_for_status()
|
|
28
|
-
return True, ""
|
|
29
|
-
except Exception as e:
|
|
30
|
-
logging.error(f"Failed to fetch grafana health status at {url}", exc_info=True)
|
|
31
|
-
error_msg = f"Failed to fetch grafana health status at {url}. {str(e)}"
|
|
32
|
-
|
|
33
|
-
# Add helpful hint if this looks like a common misconfiguration
|
|
34
|
-
if config.grafana_datasource_uid and ":3100" in config.url:
|
|
35
|
-
error_msg += (
|
|
36
|
-
"\n\nPossible configuration issue: grafana_datasource_uid is set but URL contains port 3100 "
|
|
37
|
-
"(typically used for direct Loki connections). Please verify:\n"
|
|
38
|
-
"- If connecting directly to Loki: remove grafana_datasource_uid from config\n"
|
|
39
|
-
"- If connecting via Grafana proxy: ensure URL points to Grafana (usually port 3000)"
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
return False, error_msg
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import requests # type: ignore
|
|
2
|
-
from typing import Dict, List, Optional
|
|
3
|
-
import backoff
|
|
4
|
-
|
|
5
|
-
from holmes.plugins.toolsets.grafana.common import build_headers
|
|
6
|
-
from holmes.plugins.toolsets.grafana.trace_parser import process_trace
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def execute_tempo_query_with_retry(
|
|
10
|
-
base_url: str,
|
|
11
|
-
api_key: Optional[str],
|
|
12
|
-
headers: Optional[Dict[str, str]],
|
|
13
|
-
query_params: dict,
|
|
14
|
-
retries: int = 3,
|
|
15
|
-
timeout: int = 5,
|
|
16
|
-
):
|
|
17
|
-
"""
|
|
18
|
-
Execute a Tempo API query through Grafana with retries and timeout.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
tempo_datasource_uid: The UID of the Tempo datasource.
|
|
22
|
-
query_params: Query parameters for the API.
|
|
23
|
-
retries: Number of retries for the request.
|
|
24
|
-
timeout: Timeout for each request in seconds.
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
List of trace results.
|
|
28
|
-
"""
|
|
29
|
-
url = f"{base_url}/api/search"
|
|
30
|
-
|
|
31
|
-
@backoff.on_exception(
|
|
32
|
-
backoff.expo, # Exponential backoff
|
|
33
|
-
requests.exceptions.RequestException, # Retry on request exceptions
|
|
34
|
-
max_tries=retries, # Maximum retries
|
|
35
|
-
giveup=lambda e: isinstance(e, requests.exceptions.HTTPError)
|
|
36
|
-
and e.response.status_code < 500,
|
|
37
|
-
)
|
|
38
|
-
def make_request():
|
|
39
|
-
response = requests.post(
|
|
40
|
-
url,
|
|
41
|
-
headers=build_headers(api_key=api_key, additional_headers=headers),
|
|
42
|
-
json=query_params,
|
|
43
|
-
timeout=timeout, # Set timeout for the request
|
|
44
|
-
)
|
|
45
|
-
response.raise_for_status() # Raise an error for non-2xx responses
|
|
46
|
-
return response.json()
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
return make_request()
|
|
50
|
-
except requests.exceptions.RequestException as e:
|
|
51
|
-
raise Exception(f"Request to Tempo API failed after retries: {e}")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def query_tempo_traces(
|
|
55
|
-
base_url: str,
|
|
56
|
-
api_key: Optional[str],
|
|
57
|
-
headers: Optional[Dict[str, str]],
|
|
58
|
-
query: Optional[str],
|
|
59
|
-
start: int,
|
|
60
|
-
end: int,
|
|
61
|
-
limit: int,
|
|
62
|
-
) -> Dict:
|
|
63
|
-
query_params = {
|
|
64
|
-
"start": str(start),
|
|
65
|
-
"end": str(end),
|
|
66
|
-
"limit": str(limit),
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if query:
|
|
70
|
-
query_params["q"] = query
|
|
71
|
-
data = execute_tempo_query_with_retry(
|
|
72
|
-
base_url=base_url,
|
|
73
|
-
api_key=api_key,
|
|
74
|
-
headers=headers,
|
|
75
|
-
query_params=query_params,
|
|
76
|
-
)
|
|
77
|
-
return data
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def query_tempo_trace_by_id(
|
|
81
|
-
base_url: str,
|
|
82
|
-
api_key: Optional[str],
|
|
83
|
-
headers: Optional[Dict[str, str]],
|
|
84
|
-
trace_id: str,
|
|
85
|
-
key_labels: List[str],
|
|
86
|
-
retries: int = 3,
|
|
87
|
-
timeout: int = 5,
|
|
88
|
-
) -> str:
|
|
89
|
-
"""
|
|
90
|
-
Query Tempo for a specific trace by its ID with retries and backoff.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
tempo_datasource_id: The ID of the Tempo datasource.
|
|
94
|
-
trace_id: The trace ID to retrieve.
|
|
95
|
-
retries: Number of retries for the request.
|
|
96
|
-
timeout: Timeout for each request in seconds.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
A formatted trace details string
|
|
100
|
-
"""
|
|
101
|
-
url = f"{base_url}/api/traces/{trace_id}"
|
|
102
|
-
|
|
103
|
-
@backoff.on_exception(
|
|
104
|
-
backoff.expo,
|
|
105
|
-
requests.exceptions.RequestException,
|
|
106
|
-
max_tries=retries,
|
|
107
|
-
giveup=lambda e: isinstance(e, requests.exceptions.HTTPError)
|
|
108
|
-
and e.response.status_code < 500,
|
|
109
|
-
)
|
|
110
|
-
def make_request():
|
|
111
|
-
response = requests.get(
|
|
112
|
-
url,
|
|
113
|
-
headers=build_headers(api_key=api_key, additional_headers=headers),
|
|
114
|
-
timeout=timeout,
|
|
115
|
-
)
|
|
116
|
-
response.raise_for_status()
|
|
117
|
-
return process_trace(response.json(), key_labels)
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
return make_request()
|
|
121
|
-
except requests.exceptions.RequestException as e:
|
|
122
|
-
raise Exception(
|
|
123
|
-
f"Failed to retrieve trace by ID after retries: {e} \n for URL: {url}"
|
|
124
|
-
)
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
from typing import Any, cast, Set
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
|
|
4
|
-
from holmes.core.tools import CallablePrerequisite
|
|
5
|
-
from holmes.plugins.toolsets.grafana.common import (
|
|
6
|
-
GrafanaConfig,
|
|
7
|
-
format_log,
|
|
8
|
-
get_base_url,
|
|
9
|
-
)
|
|
10
|
-
from holmes.plugins.toolsets.grafana.grafana_api import grafana_health_check
|
|
11
|
-
from holmes.plugins.toolsets.logging_utils.logging_api import (
|
|
12
|
-
BasePodLoggingToolset,
|
|
13
|
-
FetchPodLogsParams,
|
|
14
|
-
LoggingCapability,
|
|
15
|
-
PodLoggingTool,
|
|
16
|
-
DEFAULT_TIME_SPAN_SECONDS,
|
|
17
|
-
)
|
|
18
|
-
from holmes.plugins.toolsets.utils import (
|
|
19
|
-
process_timestamps_to_rfc3339,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
from holmes.plugins.toolsets.grafana.loki_api import (
|
|
23
|
-
query_loki_logs_by_label,
|
|
24
|
-
)
|
|
25
|
-
from holmes.core.tools import StructuredToolResult, ToolResultStatus
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class GrafanaLokiLabelsConfig(BaseModel):
|
|
29
|
-
pod: str = "pod"
|
|
30
|
-
namespace: str = "namespace"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class GrafanaLokiConfig(GrafanaConfig):
|
|
34
|
-
labels: GrafanaLokiLabelsConfig = GrafanaLokiLabelsConfig()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class GrafanaLokiToolset(BasePodLoggingToolset):
|
|
38
|
-
@property
|
|
39
|
-
def supported_capabilities(self) -> Set[LoggingCapability]:
|
|
40
|
-
"""Loki only supports substring matching, not regex or exclude filters"""
|
|
41
|
-
return set() # No regex support, no exclude filter
|
|
42
|
-
|
|
43
|
-
def __init__(self):
|
|
44
|
-
super().__init__(
|
|
45
|
-
name="grafana/loki",
|
|
46
|
-
description="Fetches kubernetes pods logs from Loki",
|
|
47
|
-
icon_url="https://grafana.com/media/docs/loki/logo-grafana-loki.png",
|
|
48
|
-
docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanaloki/",
|
|
49
|
-
prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
|
|
50
|
-
tools=[], # Initialize with empty tools first
|
|
51
|
-
)
|
|
52
|
-
# Now that parent is initialized and self.name exists, create the tool
|
|
53
|
-
self.tools = [PodLoggingTool(self)]
|
|
54
|
-
|
|
55
|
-
def prerequisites_callable(self, config: dict[str, Any]) -> tuple[bool, str]:
|
|
56
|
-
if not config:
|
|
57
|
-
return False, "Missing Loki configuration. Check your config."
|
|
58
|
-
|
|
59
|
-
self.config = GrafanaLokiConfig(**config)
|
|
60
|
-
|
|
61
|
-
return grafana_health_check(self.config)
|
|
62
|
-
|
|
63
|
-
def get_example_config(self):
|
|
64
|
-
example_config = GrafanaLokiConfig(
|
|
65
|
-
api_key="YOUR API KEY",
|
|
66
|
-
url="YOUR GRAFANA URL",
|
|
67
|
-
grafana_datasource_uid="<UID of the loki datasource to use>",
|
|
68
|
-
)
|
|
69
|
-
return example_config.model_dump()
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def grafana_config(self) -> GrafanaLokiConfig:
|
|
73
|
-
return cast(GrafanaLokiConfig, self.config)
|
|
74
|
-
|
|
75
|
-
def logger_name(self) -> str:
|
|
76
|
-
return "Loki"
|
|
77
|
-
|
|
78
|
-
def fetch_pod_logs(self, params: FetchPodLogsParams) -> StructuredToolResult:
|
|
79
|
-
(start, end) = process_timestamps_to_rfc3339(
|
|
80
|
-
start_timestamp=params.start_time,
|
|
81
|
-
end_timestamp=params.end_time,
|
|
82
|
-
default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
base_url = get_base_url(self.grafana_config)
|
|
86
|
-
logs = query_loki_logs_by_label(
|
|
87
|
-
base_url=base_url,
|
|
88
|
-
api_key=self.grafana_config.api_key,
|
|
89
|
-
headers=self.grafana_config.headers,
|
|
90
|
-
filter=params.filter,
|
|
91
|
-
namespace=params.namespace,
|
|
92
|
-
namespace_search_key=self.grafana_config.labels.namespace,
|
|
93
|
-
label=self.grafana_config.labels.pod,
|
|
94
|
-
label_value=params.pod_name,
|
|
95
|
-
start=start,
|
|
96
|
-
end=end,
|
|
97
|
-
limit=params.limit or 2000,
|
|
98
|
-
)
|
|
99
|
-
if logs:
|
|
100
|
-
logs.sort(key=lambda x: x["timestamp"])
|
|
101
|
-
return StructuredToolResult(
|
|
102
|
-
status=ToolResultStatus.SUCCESS,
|
|
103
|
-
data="\n".join([format_log(log) for log in logs]),
|
|
104
|
-
params=params.model_dump(),
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
return StructuredToolResult(
|
|
108
|
-
status=ToolResultStatus.NO_DATA,
|
|
109
|
-
params=params.model_dump(),
|
|
110
|
-
)
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import requests # type: ignore
|
|
2
|
-
import logging
|
|
3
|
-
from typing import Any, Optional, Dict
|
|
4
|
-
from holmes.core.tools import (
|
|
5
|
-
CallablePrerequisite,
|
|
6
|
-
Tool,
|
|
7
|
-
ToolParameter,
|
|
8
|
-
Toolset,
|
|
9
|
-
ToolsetTag,
|
|
10
|
-
)
|
|
11
|
-
from pydantic import BaseModel
|
|
12
|
-
from holmes.core.tools import StructuredToolResult, ToolResultStatus
|
|
13
|
-
from holmes.plugins.toolsets.utils import get_param_or_raise, toolset_name_for_one_liner
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class BaseNewRelicTool(Tool):
|
|
17
|
-
toolset: "NewRelicToolset"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class GetLogs(BaseNewRelicTool):
|
|
21
|
-
def __init__(self, toolset: "NewRelicToolset"):
|
|
22
|
-
super().__init__(
|
|
23
|
-
name="newrelic_get_logs",
|
|
24
|
-
description="Retrieve logs from New Relic",
|
|
25
|
-
parameters={
|
|
26
|
-
"app": ToolParameter(
|
|
27
|
-
description="The application name to filter logs",
|
|
28
|
-
type="string",
|
|
29
|
-
required=True,
|
|
30
|
-
),
|
|
31
|
-
"since": ToolParameter(
|
|
32
|
-
description="Time range to fetch logs (e.g., '1 hour ago')",
|
|
33
|
-
type="string",
|
|
34
|
-
required=True,
|
|
35
|
-
),
|
|
36
|
-
},
|
|
37
|
-
toolset=toolset,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
def _invoke(
|
|
41
|
-
self, params: dict, user_approved: bool = False
|
|
42
|
-
) -> StructuredToolResult:
|
|
43
|
-
def success(msg: Any) -> StructuredToolResult:
|
|
44
|
-
return StructuredToolResult(
|
|
45
|
-
status=ToolResultStatus.SUCCESS,
|
|
46
|
-
data=msg,
|
|
47
|
-
params=params,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
def error(msg: str) -> StructuredToolResult:
|
|
51
|
-
return StructuredToolResult(
|
|
52
|
-
status=ToolResultStatus.ERROR,
|
|
53
|
-
data=msg,
|
|
54
|
-
params=params,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
app = params.get("app")
|
|
58
|
-
since = params.get("since")
|
|
59
|
-
|
|
60
|
-
query = {
|
|
61
|
-
"query": f"""
|
|
62
|
-
{{
|
|
63
|
-
actor {{
|
|
64
|
-
account(id: {self.toolset.nr_account_id}) {{
|
|
65
|
-
nrql(query: \"SELECT * FROM Log WHERE app = '{app}' SINCE {since}\") {{
|
|
66
|
-
results
|
|
67
|
-
}}
|
|
68
|
-
}}
|
|
69
|
-
}}
|
|
70
|
-
}}
|
|
71
|
-
"""
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
url = "https://api.newrelic.com/graphql"
|
|
75
|
-
headers = {
|
|
76
|
-
"Content-Type": "application/json",
|
|
77
|
-
"Api-Key": self.toolset.nr_api_key,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
logging.info(f"Getting New Relic logs for app {app} since {since}")
|
|
82
|
-
response = requests.post(url, headers=headers, json=query)
|
|
83
|
-
|
|
84
|
-
if response.status_code == 200:
|
|
85
|
-
return success(response.json())
|
|
86
|
-
else:
|
|
87
|
-
return error(
|
|
88
|
-
f"Failed to fetch logs. Status code: {response.status_code}\n{response.text}"
|
|
89
|
-
)
|
|
90
|
-
except Exception as e:
|
|
91
|
-
logging.exception("Exception while fetching logs")
|
|
92
|
-
return error(f"Error while fetching logs: {str(e)}")
|
|
93
|
-
|
|
94
|
-
def get_parameterized_one_liner(self, params) -> str:
|
|
95
|
-
app = params.get("app", "")
|
|
96
|
-
since = params.get("since", "")
|
|
97
|
-
return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Logs ({app} - {since})"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class GetTraces(BaseNewRelicTool):
|
|
101
|
-
def __init__(self, toolset: "NewRelicToolset"):
|
|
102
|
-
super().__init__(
|
|
103
|
-
name="newrelic_get_traces",
|
|
104
|
-
description="Retrieve traces from New Relic",
|
|
105
|
-
parameters={
|
|
106
|
-
"duration": ToolParameter(
|
|
107
|
-
description="Minimum trace duration in seconds",
|
|
108
|
-
type="number",
|
|
109
|
-
required=True,
|
|
110
|
-
),
|
|
111
|
-
"trace_id": ToolParameter(
|
|
112
|
-
description="Specific trace ID to fetch details (optional)",
|
|
113
|
-
type="string",
|
|
114
|
-
required=False,
|
|
115
|
-
),
|
|
116
|
-
},
|
|
117
|
-
toolset=toolset,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
def _invoke(
|
|
121
|
-
self, params: dict, user_approved: bool = False
|
|
122
|
-
) -> StructuredToolResult:
|
|
123
|
-
def success(msg: Any) -> StructuredToolResult:
|
|
124
|
-
return StructuredToolResult(
|
|
125
|
-
status=ToolResultStatus.SUCCESS,
|
|
126
|
-
data=msg,
|
|
127
|
-
params=params,
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
def error(msg: str) -> StructuredToolResult:
|
|
131
|
-
return StructuredToolResult(
|
|
132
|
-
status=ToolResultStatus.ERROR,
|
|
133
|
-
data=msg,
|
|
134
|
-
params=params,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
duration = get_param_or_raise(params, "duration")
|
|
138
|
-
trace_id = params.get("trace_id")
|
|
139
|
-
|
|
140
|
-
if trace_id:
|
|
141
|
-
query_string = f"SELECT * FROM Span WHERE trace.id = '{trace_id}' and duration.ms > {duration * 1000} and span.kind != 'internal'"
|
|
142
|
-
else:
|
|
143
|
-
query_string = f"SELECT * FROM Span WHERE duration.ms > {duration * 1000} and span.kind != 'internal'"
|
|
144
|
-
|
|
145
|
-
query = {
|
|
146
|
-
"query": f"""
|
|
147
|
-
{{
|
|
148
|
-
actor {{
|
|
149
|
-
account(id: {self.toolset.nr_account_id}) {{
|
|
150
|
-
nrql(query: \"{query_string}\") {{
|
|
151
|
-
results
|
|
152
|
-
}}
|
|
153
|
-
}}
|
|
154
|
-
}}
|
|
155
|
-
}}
|
|
156
|
-
"""
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
url = "https://api.newrelic.com/graphql"
|
|
160
|
-
headers = {
|
|
161
|
-
"Content-Type": "application/json",
|
|
162
|
-
"Api-Key": self.toolset.nr_api_key,
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
try:
|
|
166
|
-
logging.info(f"Getting New Relic traces with duration > {duration}s")
|
|
167
|
-
response = requests.post(url, headers=headers, json=query)
|
|
168
|
-
|
|
169
|
-
if response.status_code == 200:
|
|
170
|
-
return success(response.json())
|
|
171
|
-
else:
|
|
172
|
-
return error(
|
|
173
|
-
f"Failed to fetch traces. Status code: {response.status_code}\n{response.text}"
|
|
174
|
-
)
|
|
175
|
-
except Exception as e:
|
|
176
|
-
logging.exception("Exception while fetching traces")
|
|
177
|
-
return error(f"Error while fetching traces: {str(e)}")
|
|
178
|
-
|
|
179
|
-
def get_parameterized_one_liner(self, params) -> str:
|
|
180
|
-
if "trace_id" in params and params["trace_id"]:
|
|
181
|
-
trace_id = params.get("trace_id", "")
|
|
182
|
-
return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Trace Details ({trace_id})"
|
|
183
|
-
duration = params.get("duration", "")
|
|
184
|
-
return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Traces (>{duration}s)"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
class NewrelicConfig(BaseModel):
|
|
188
|
-
nr_api_key: Optional[str] = None
|
|
189
|
-
nr_account_id: Optional[str] = None
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class NewRelicToolset(Toolset):
|
|
193
|
-
nr_api_key: Optional[str] = None
|
|
194
|
-
nr_account_id: Optional[str] = None
|
|
195
|
-
|
|
196
|
-
def __init__(self):
|
|
197
|
-
super().__init__(
|
|
198
|
-
name="newrelic",
|
|
199
|
-
description="Toolset for interacting with New Relic to fetch logs and traces",
|
|
200
|
-
docs_url="https://docs.newrelic.com/docs/apis/nerdgraph-api/",
|
|
201
|
-
icon_url="https://companieslogo.com/img/orig/NEWR-de5fcb2e.png?t=1720244493",
|
|
202
|
-
prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
|
|
203
|
-
tools=[
|
|
204
|
-
GetLogs(self),
|
|
205
|
-
GetTraces(self),
|
|
206
|
-
],
|
|
207
|
-
experimental=True,
|
|
208
|
-
tags=[ToolsetTag.CORE],
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
def prerequisites_callable(
|
|
212
|
-
self, config: dict[str, Any]
|
|
213
|
-
) -> tuple[bool, Optional[str]]:
|
|
214
|
-
if not config:
|
|
215
|
-
return False, "No configuration provided"
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
nr_config = NewrelicConfig(**config)
|
|
219
|
-
self.nr_account_id = nr_config.nr_account_id
|
|
220
|
-
self.nr_api_key = nr_config.nr_api_key
|
|
221
|
-
|
|
222
|
-
if not self.nr_account_id or not self.nr_api_key:
|
|
223
|
-
return False, "New Relic account ID or API key is missing"
|
|
224
|
-
|
|
225
|
-
return True, None
|
|
226
|
-
except Exception as e:
|
|
227
|
-
logging.exception("Failed to set up New Relic toolset")
|
|
228
|
-
return False, str(e)
|
|
229
|
-
|
|
230
|
-
def get_example_config(self) -> Dict[str, Any]:
|
|
231
|
-
return {}
|