holmesgpt 0.16.2a0__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.
Files changed (162) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +4 -3
  3. holmes/common/env_vars.py +18 -2
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +11 -6
  6. holmes/core/conversations.py +30 -13
  7. holmes/core/investigation.py +21 -25
  8. holmes/core/investigation_structured_output.py +3 -3
  9. holmes/core/issue.py +1 -1
  10. holmes/core/llm.py +50 -31
  11. holmes/core/models.py +19 -17
  12. holmes/core/openai_formatting.py +1 -1
  13. holmes/core/prompt.py +47 -2
  14. holmes/core/runbooks.py +1 -0
  15. holmes/core/safeguards.py +4 -2
  16. holmes/core/supabase_dal.py +4 -2
  17. holmes/core/tool_calling_llm.py +102 -141
  18. holmes/core/tools.py +19 -28
  19. holmes/core/tools_utils/token_counting.py +9 -2
  20. holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
  21. holmes/core/tools_utils/tool_executor.py +0 -18
  22. holmes/core/tools_utils/toolset_utils.py +1 -0
  23. holmes/core/toolset_manager.py +37 -2
  24. holmes/core/tracing.py +13 -2
  25. holmes/core/transformers/__init__.py +1 -1
  26. holmes/core/transformers/base.py +1 -0
  27. holmes/core/transformers/llm_summarize.py +3 -2
  28. holmes/core/transformers/registry.py +2 -1
  29. holmes/core/transformers/transformer.py +1 -0
  30. holmes/core/truncation/compaction.py +37 -2
  31. holmes/core/truncation/input_context_window_limiter.py +3 -2
  32. holmes/interactive.py +52 -8
  33. holmes/main.py +17 -37
  34. holmes/plugins/interfaces.py +2 -1
  35. holmes/plugins/prompts/__init__.py +2 -1
  36. holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
  37. holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
  38. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  39. holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
  40. holmes/plugins/prompts/generic_ask.jinja2 +0 -2
  41. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
  42. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
  43. holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
  44. holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
  45. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
  46. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
  47. holmes/plugins/runbooks/__init__.py +32 -3
  48. holmes/plugins/sources/github/__init__.py +4 -2
  49. holmes/plugins/sources/prometheus/models.py +1 -0
  50. holmes/plugins/toolsets/__init__.py +30 -26
  51. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
  52. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  53. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  54. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  55. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  56. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  57. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
  58. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
  59. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
  60. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
  61. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  62. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
  63. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
  64. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
  65. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
  66. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
  67. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
  68. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  69. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  70. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  71. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  72. holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
  73. holmes/plugins/toolsets/bash/common/bash.py +19 -9
  74. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  75. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  76. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  77. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  78. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  79. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  80. holmes/plugins/toolsets/connectivity_check.py +124 -0
  81. holmes/plugins/toolsets/coralogix/api.py +132 -119
  82. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  84. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  85. holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
  86. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
  87. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  88. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  89. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  90. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  91. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
  92. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
  93. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
  94. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
  95. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  96. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  97. holmes/plugins/toolsets/git.py +7 -8
  98. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  99. holmes/plugins/toolsets/grafana/common.py +2 -30
  100. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
  101. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
  102. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
  103. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  104. holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
  105. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
  106. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
  107. holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
  108. holmes/plugins/toolsets/internet/internet.py +10 -10
  109. holmes/plugins/toolsets/internet/notion.py +5 -6
  110. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  111. holmes/plugins/toolsets/investigator/model.py +3 -1
  112. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  113. holmes/plugins/toolsets/kafka.py +12 -7
  114. holmes/plugins/toolsets/kubernetes.yaml +260 -30
  115. holmes/plugins/toolsets/kubernetes_logs.py +3 -3
  116. holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
  117. holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
  118. holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
  119. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
  120. holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
  121. holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
  122. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
  123. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  124. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
  125. holmes/plugins/toolsets/robusta/robusta.py +5 -5
  126. holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
  127. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
  128. holmes/plugins/toolsets/utils.py +1 -1
  129. holmes/utils/config_utils.py +1 -1
  130. holmes/utils/connection_utils.py +31 -0
  131. holmes/utils/console/result.py +10 -0
  132. holmes/utils/file_utils.py +2 -1
  133. holmes/utils/global_instructions.py +10 -26
  134. holmes/utils/holmes_status.py +4 -3
  135. holmes/utils/log.py +15 -0
  136. holmes/utils/markdown_utils.py +2 -3
  137. holmes/utils/memory_limit.py +58 -0
  138. holmes/utils/sentry_helper.py +23 -0
  139. holmes/utils/stream.py +12 -5
  140. holmes/utils/tags.py +4 -3
  141. holmes/version.py +3 -1
  142. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
  143. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  144. holmes/plugins/toolsets/aws.yaml +0 -80
  145. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
  146. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  147. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
  148. holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
  149. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  150. holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
  151. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  152. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
  153. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  154. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  155. holmes/utils/keygen_utils.py +0 -6
  156. holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
  157. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
  158. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
  159. /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
  160. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
  161. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  162. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -1,64 +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
- )
10
-
11
-
12
- @backoff.on_exception(
13
- backoff.expo,
14
- requests.exceptions.RequestException,
15
- max_tries=2,
16
- giveup=lambda e: isinstance(e, requests.exceptions.HTTPError)
17
- and e.response.status_code < 500,
18
- )
19
- def _try_health_url(url: str, headers: dict) -> None:
20
- response = requests.get(url, headers=headers, timeout=5)
21
- response.raise_for_status()
22
-
23
-
24
- def grafana_health_check(config: GrafanaConfig) -> Tuple[bool, str]:
25
- """
26
- Tests a healthcheck url for grafna loki.
27
- 1. When using grafana as proxy, grafana_datasource_uid is provided, use the data source health url (docs are added).
28
- 2. When using loki directly there are two cases.
29
- a. Using loki cloud, health check is provided on the base url.
30
- b. Using local loki, uses url/healthcheck default is url/ready
31
- c. This function tries both direct loki cases for the user.
32
- """
33
- health_urls = []
34
- if config.grafana_datasource_uid:
35
- # https://grafana.com/docs/grafana/latest/developers/http_api/data_source/#check-data-source-health
36
- health_urls.append(
37
- f"{config.url}/api/datasources/uid/{config.grafana_datasource_uid}/health"
38
- )
39
- else:
40
- health_urls.append(f"{config.url}/{config.healthcheck}")
41
- health_urls.append(config.url) # loki cloud uses no suffix.
42
- g_headers = build_headers(api_key=config.api_key, additional_headers=config.headers)
43
-
44
- error_msg = ""
45
- for url in health_urls:
46
- try:
47
- _try_health_url(url, g_headers)
48
- return True, ""
49
- except Exception as e:
50
- logging.debug(
51
- f"Failed to fetch grafana health status at {url}", exc_info=True
52
- )
53
- error_msg += f"Failed to fetch grafana health status at {url}. {str(e)}\n"
54
-
55
- # Add helpful hint if this looks like a common misconfiguration
56
- if config.grafana_datasource_uid and ":3100" in config.url:
57
- error_msg += (
58
- "\n\nPossible configuration issue: grafana_datasource_uid is set but URL contains port 3100 "
59
- "(typically used for direct Loki connections). Please verify:\n"
60
- "- If connecting directly to Loki: remove grafana_datasource_uid from config\n"
61
- "- If connecting via Grafana proxy: ensure URL points to Grafana (usually port 3000)"
62
- )
63
-
64
- return False, error_msg
File without changes
@@ -1,250 +0,0 @@
1
- import logging
2
- from typing import Any, Dict, List, Optional, Tuple
3
-
4
- from opensearchpy import OpenSearch
5
- from pydantic import BaseModel, ConfigDict
6
-
7
- from holmes.core.tools import (
8
- CallablePrerequisite,
9
- StructuredToolResult,
10
- Tool,
11
- ToolInvokeContext,
12
- ToolParameter,
13
- StructuredToolResultStatus,
14
- Toolset,
15
- ToolsetTag,
16
- )
17
- from holmes.plugins.toolsets.consts import TOOLSET_CONFIG_MISSING_ERROR
18
- from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
19
-
20
-
21
- class OpenSearchHttpAuth(BaseModel):
22
- username: str
23
- password: str
24
-
25
-
26
- class OpenSearchHost(BaseModel):
27
- host: str
28
- port: int = 9200
29
-
30
-
31
- class OpenSearchCluster(BaseModel):
32
- hosts: list[OpenSearchHost]
33
- headers: Optional[dict[str, Any]] = None
34
- use_ssl: bool = True
35
- ssl_assert_hostname: bool = False
36
- verify_certs: bool = False
37
- ssl_show_warn: bool = False
38
- http_auth: Optional[OpenSearchHttpAuth] = None
39
-
40
-
41
- class OpenSearchConfig(BaseModel):
42
- opensearch_clusters: list[OpenSearchCluster]
43
-
44
-
45
- class OpenSearchClient:
46
- def __init__(self, **kwargs):
47
- # Handle http_auth explicitly
48
- if "http_auth" in kwargs:
49
- http_auth = kwargs.pop("http_auth")
50
- if isinstance(http_auth, dict):
51
- kwargs["http_auth"] = (
52
- http_auth.get("username"),
53
- http_auth.get("password"),
54
- )
55
- # Initialize OpenSearch client
56
- self.hosts = [host.get("host") for host in kwargs.get("hosts", [])]
57
- self.client = OpenSearch(**kwargs)
58
-
59
-
60
- def get_client(clients: List[OpenSearchClient], host: Optional[str]):
61
- if len(clients) == 1:
62
- return clients[0]
63
-
64
- if not host:
65
- raise Exception("Missing host to resolve opensearch client")
66
-
67
- for client in clients:
68
- found = any(host in client.hosts for client in clients)
69
- if found:
70
- return client
71
-
72
- raise Exception(
73
- f"Failed to resolve opensearch client. Could not find a matching host: {host}"
74
- )
75
-
76
-
77
- class BaseOpenSearchTool(Tool):
78
- model_config = ConfigDict(arbitrary_types_allowed=True)
79
- toolset: "OpenSearchToolset"
80
-
81
-
82
- class ListShards(BaseOpenSearchTool):
83
- def __init__(self, toolset: "OpenSearchToolset"):
84
- super().__init__(
85
- name="opensearch_list_shards",
86
- description="List the shards within an opensearch cluster",
87
- parameters={
88
- "host": ToolParameter(
89
- description="The cluster host",
90
- type="string",
91
- required=True,
92
- )
93
- },
94
- toolset=toolset,
95
- )
96
-
97
- def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
98
- client = get_client(self.toolset.clients, host=params.get("host", ""))
99
- shards = client.client.cat.shards()
100
- return StructuredToolResult(
101
- status=StructuredToolResultStatus.SUCCESS,
102
- data=str(shards),
103
- params=params,
104
- )
105
-
106
- def get_parameterized_one_liner(self, params: Dict) -> str:
107
- host = params.get("host", "")
108
- return f"{toolset_name_for_one_liner(self.toolset.name)}: List Shards ({host})"
109
-
110
-
111
- class GetClusterSettings(BaseOpenSearchTool):
112
- def __init__(self, toolset: "OpenSearchToolset"):
113
- super().__init__(
114
- name="opensearch_get_cluster_settings",
115
- description="Retrieve the cluster's settings",
116
- parameters={
117
- "host": ToolParameter(
118
- description="The cluster host",
119
- type="string",
120
- required=True,
121
- )
122
- },
123
- toolset=toolset,
124
- )
125
-
126
- def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
127
- client = get_client(self.toolset.clients, host=params.get("host"))
128
- response = client.client.cluster.get_settings(
129
- include_defaults=True, flat_settings=True
130
- )
131
- return StructuredToolResult(
132
- status=StructuredToolResultStatus.SUCCESS,
133
- data=str(response),
134
- params=params,
135
- )
136
-
137
- def get_parameterized_one_liner(self, params) -> str:
138
- host = params.get("host", "")
139
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Cluster Settings ({host})"
140
-
141
-
142
- class GetClusterHealth(BaseOpenSearchTool):
143
- def __init__(self, toolset: "OpenSearchToolset"):
144
- super().__init__(
145
- name="opensearch_get_cluster_health",
146
- description="Retrieve the cluster's health",
147
- parameters={
148
- "host": ToolParameter(
149
- description="The cluster host",
150
- type="string",
151
- required=True,
152
- )
153
- },
154
- toolset=toolset,
155
- )
156
-
157
- def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
158
- client = get_client(self.toolset.clients, host=params.get("host", ""))
159
- health = client.client.cluster.health()
160
- return StructuredToolResult(
161
- status=StructuredToolResultStatus.SUCCESS,
162
- data=str(health),
163
- params=params,
164
- )
165
-
166
- def get_parameterized_one_liner(self, params) -> str:
167
- host = params.get("host", "")
168
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Check Cluster Health ({host})"
169
-
170
-
171
- class ListOpenSearchHosts(BaseOpenSearchTool):
172
- def __init__(self, toolset: "OpenSearchToolset"):
173
- super().__init__(
174
- name="opensearch_list_hosts",
175
- description="List all OpenSearch hosts in the cluster",
176
- parameters={},
177
- toolset=toolset,
178
- )
179
-
180
- def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
181
- hosts = [host for client in self.toolset.clients for host in client.hosts]
182
- return StructuredToolResult(
183
- status=StructuredToolResultStatus.SUCCESS,
184
- data=str(hosts),
185
- params=params,
186
- )
187
-
188
- def get_parameterized_one_liner(self, params: Dict) -> str:
189
- return f"{toolset_name_for_one_liner(self.toolset.name)}: List OpenSearch Hosts"
190
-
191
-
192
- class OpenSearchToolset(Toolset):
193
- model_config = ConfigDict(arbitrary_types_allowed=True)
194
- clients: List[OpenSearchClient] = []
195
-
196
- def __init__(self):
197
- super().__init__(
198
- name="opensearch/status",
199
- enabled=False,
200
- description="Provide cluster metadata information like health, shards, settings.",
201
- docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/opensearch-status/",
202
- icon_url="https://opensearch.org/assets/brand/PNG/Mark/opensearch_mark_default.png",
203
- prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
204
- tools=[
205
- ListShards(self),
206
- GetClusterSettings(self),
207
- GetClusterHealth(self),
208
- ListOpenSearchHosts(self),
209
- ],
210
- tags=[
211
- ToolsetTag.CORE,
212
- ],
213
- )
214
-
215
- def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
216
- if not config:
217
- return False, TOOLSET_CONFIG_MISSING_ERROR
218
-
219
- try:
220
- os_config = OpenSearchConfig(**config)
221
- errors = []
222
- for cluster in os_config.opensearch_clusters:
223
- try:
224
- logging.info("Setting up OpenSearch client")
225
- cluster_kwargs = cluster.model_dump()
226
- client = OpenSearchClient(**cluster_kwargs)
227
- if client.client.cluster.health(params={"timeout": 5}):
228
- self.clients.append(client)
229
- except Exception as e:
230
- message = f"Failed to set up opensearch client {str(cluster.hosts)}. {str(e)}"
231
- logging.exception(message)
232
- errors.append(message)
233
-
234
- return len(self.clients) > 0, "\n".join(errors)
235
- except Exception as e:
236
- logging.exception("Failed to set up OpenSearch toolset")
237
- return False, str(e)
238
-
239
- def get_example_config(self) -> Dict[str, Any]:
240
- example_config = OpenSearchConfig(
241
- opensearch_clusters=[
242
- OpenSearchCluster(
243
- hosts=[OpenSearchHost(host="YOUR OPENSEACH HOST")],
244
- headers={"Authorization": "{{ env.OPENSEARCH_BEARER_TOKEN }}"},
245
- use_ssl=True,
246
- ssl_assert_hostname=False,
247
- )
248
- ]
249
- )
250
- return example_config.model_dump()
@@ -1,161 +0,0 @@
1
- import logging
2
- from typing import Any, Dict, Optional, Tuple, Set
3
-
4
- import requests # type: ignore
5
- from requests import RequestException # type: ignore
6
- from urllib.parse import urljoin
7
-
8
- from holmes.core.tools import (
9
- CallablePrerequisite,
10
- StructuredToolResult,
11
- StructuredToolResultStatus,
12
- ToolsetTag,
13
- )
14
- from holmes.plugins.toolsets.logging_utils.logging_api import (
15
- BasePodLoggingToolset,
16
- FetchPodLogsParams,
17
- LoggingCapability,
18
- PodLoggingTool,
19
- process_time_parameters,
20
- DEFAULT_TIME_SPAN_SECONDS,
21
- )
22
- from holmes.plugins.toolsets.opensearch.opensearch_utils import (
23
- OpenSearchLoggingConfig,
24
- add_auth_header,
25
- build_query,
26
- format_logs,
27
- opensearch_health_check,
28
- )
29
-
30
- LOGS_FIELDS_CACHE_KEY = "cached_logs_fields"
31
-
32
-
33
- class OpenSearchLogsToolset(BasePodLoggingToolset):
34
- """Implementation of the unified logging API for OpenSearch logs"""
35
-
36
- @property
37
- def supported_capabilities(self) -> Set[LoggingCapability]:
38
- """OpenSearch only supports phrase matching, not regex or exclude filters"""
39
- return set() # No regex support, no exclude filter
40
-
41
- def __init__(self):
42
- super().__init__(
43
- name="opensearch/logs",
44
- description="OpenSearch integration to fetch logs",
45
- docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/opensearch-logs/",
46
- icon_url="https://opensearch.org/wp-content/uploads/2025/01/opensearch_mark_default.png",
47
- prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
48
- tools=[], # Initialize with empty tools first
49
- tags=[
50
- ToolsetTag.CORE,
51
- ],
52
- )
53
- # Now that parent is initialized and self.name exists, create the tool
54
- self.tools = [PodLoggingTool(self)]
55
-
56
- def get_example_config(self) -> Dict[str, Any]:
57
- example_config = OpenSearchLoggingConfig(
58
- opensearch_url="YOUR OPENSEARCH LOGS URL",
59
- index_pattern="YOUR OPENSEARCH LOGS INDEX NAME",
60
- opensearch_auth_header="YOUR OPENSEARCH LOGS AUTH HEADER (Optional)",
61
- )
62
- return example_config.model_dump()
63
-
64
- def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
65
- if not config:
66
- return False, "Missing OpenSearch configuration. Check your config."
67
-
68
- self.config = OpenSearchLoggingConfig(**config)
69
-
70
- return opensearch_health_check(self.config)
71
-
72
- @property
73
- def opensearch_config(self) -> Optional[OpenSearchLoggingConfig]:
74
- return self.config
75
-
76
- def logger_name(self) -> str:
77
- return "OpenSearch"
78
-
79
- def fetch_pod_logs(self, params: FetchPodLogsParams) -> StructuredToolResult:
80
- if not self.opensearch_config:
81
- return StructuredToolResult(
82
- status=StructuredToolResultStatus.ERROR,
83
- error="Missing OpenSearch configuration",
84
- params=params.model_dump(),
85
- )
86
-
87
- try:
88
- start_time = None
89
- end_time = None
90
- if params.start_time or params.end_time:
91
- start_time, end_time = process_time_parameters(
92
- params.start_time, params.end_time, DEFAULT_TIME_SPAN_SECONDS
93
- )
94
-
95
- query = build_query(
96
- config=self.opensearch_config,
97
- namespace=params.namespace,
98
- pod_name=params.pod_name,
99
- start_time=start_time,
100
- end_time=end_time,
101
- match=params.filter,
102
- limit=params.limit,
103
- )
104
-
105
- headers = {"Content-Type": "application/json"}
106
- headers.update(
107
- add_auth_header(self.opensearch_config.opensearch_auth_header)
108
- )
109
-
110
- url = urljoin(
111
- self.opensearch_config.opensearch_url,
112
- f"/{self.opensearch_config.index_pattern}/_search",
113
- )
114
- logs_response = requests.post(
115
- url=url,
116
- timeout=180,
117
- verify=True,
118
- json=query,
119
- headers=headers,
120
- )
121
-
122
- if logs_response.status_code == 200:
123
- response = logs_response.json()
124
- logs = format_logs(
125
- logs=response.get("hits", {}).get("hits", []),
126
- config=self.opensearch_config,
127
- )
128
- return StructuredToolResult(
129
- status=StructuredToolResultStatus.SUCCESS,
130
- data=logs,
131
- params=params.model_dump(),
132
- )
133
- else:
134
- return StructuredToolResult(
135
- status=StructuredToolResultStatus.ERROR,
136
- return_code=logs_response.status_code,
137
- error=logs_response.text,
138
- params=params.model_dump(),
139
- )
140
-
141
- except requests.Timeout:
142
- logging.warning("Timeout while fetching OpenSearch logs", exc_info=True)
143
- return StructuredToolResult(
144
- status=StructuredToolResultStatus.ERROR,
145
- error="Request timed out while fetching OpenSearch logs",
146
- params=params.model_dump(),
147
- )
148
- except RequestException as e:
149
- logging.warning("Failed to fetch OpenSearch logs", exc_info=True)
150
- return StructuredToolResult(
151
- status=StructuredToolResultStatus.ERROR,
152
- error=f"Network error while fetching OpenSearch logs: {str(e)}",
153
- params=params.model_dump(),
154
- )
155
- except Exception as e:
156
- logging.warning("Failed to process OpenSearch logs", exc_info=True)
157
- return StructuredToolResult(
158
- status=StructuredToolResultStatus.ERROR,
159
- error=f"Unexpected error: {str(e)}",
160
- params=params.model_dump(),
161
- )
@@ -1,215 +0,0 @@
1
- import os
2
- import logging
3
-
4
-
5
- import requests # type: ignore
6
- from cachetools import TTLCache # type: ignore
7
- from holmes.core.tools import (
8
- CallablePrerequisite,
9
- Tool,
10
- ToolInvokeContext,
11
- ToolParameter,
12
- ToolsetTag,
13
- )
14
- import json
15
- from requests import RequestException
16
-
17
- from holmes.plugins.toolsets.opensearch.opensearch_utils import (
18
- BaseOpenSearchToolset,
19
- add_auth_header,
20
- get_search_url,
21
- )
22
- from holmes.core.tools import StructuredToolResult, StructuredToolResultStatus
23
- from holmes.plugins.toolsets.utils import get_param_or_raise, toolset_name_for_one_liner
24
-
25
- TRACES_FIELDS_CACHE_KEY = "cached_traces_fields"
26
-
27
-
28
- class GetTracesFields(Tool):
29
- def __init__(self, toolset: BaseOpenSearchToolset):
30
- super().__init__(
31
- name="get_traces_fields",
32
- description="Get all the fields in the traces documents",
33
- parameters={},
34
- )
35
- self._toolset = toolset
36
- self._cache = None
37
-
38
- def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
39
- try:
40
- if not self._cache and self._toolset.opensearch_config.fields_ttl_seconds:
41
- self._cache = TTLCache(
42
- maxsize=5, ttl=self._toolset.opensearch_config.fields_ttl_seconds
43
- )
44
-
45
- if self._cache:
46
- cached_response = self._cache.get(TRACES_FIELDS_CACHE_KEY, None)
47
- if cached_response:
48
- logging.debug("traces fields returned from cache")
49
- return StructuredToolResult(
50
- status=StructuredToolResultStatus.SUCCESS,
51
- data=cached_response,
52
- params=params,
53
- )
54
-
55
- body = {
56
- "size": 1,
57
- "_source": False,
58
- "script_fields": {
59
- "all_fields": {
60
- "script": {
61
- "lang": "painless",
62
- "source": "Map fields = new HashMap(); List stack = new ArrayList(); stack.add(['prefix': '', 'obj': params['_source']]); while (!stack.isEmpty()) { Map item = stack.remove(stack.size() - 1); String prefix = item.get('prefix'); Map obj = item.get('obj'); for (entry in obj.entrySet()) { String fullPath = prefix.isEmpty() ? entry.getKey() : prefix + '.' + entry.getKey(); fields.put(fullPath, true); if (entry.getValue() instanceof Map) { stack.add(['prefix': fullPath, 'obj': entry.getValue()]); } } } return fields.keySet();",
63
- }
64
- }
65
- },
66
- }
67
- headers = {"Content-Type": "application/json"}
68
- headers.update(
69
- add_auth_header(self._toolset.opensearch_config.opensearch_auth_header)
70
- )
71
- logs_response = requests.get(
72
- url=get_search_url(self._toolset.opensearch_config),
73
- timeout=180,
74
- verify=True,
75
- data=json.dumps(body),
76
- headers=headers,
77
- )
78
- logs_response.raise_for_status()
79
- response = json.dumps(logs_response.json())
80
- if self._cache:
81
- self._cache[TRACES_FIELDS_CACHE_KEY] = response
82
- return StructuredToolResult(
83
- status=StructuredToolResultStatus.SUCCESS,
84
- data=response,
85
- params=params,
86
- )
87
- except requests.Timeout:
88
- logging.warning(
89
- "Timeout while fetching opensearch traces fields", exc_info=True
90
- )
91
- return StructuredToolResult(
92
- status=StructuredToolResultStatus.ERROR,
93
- error="Request timed out while fetching opensearch traces fields",
94
- params=params,
95
- )
96
- except RequestException as e:
97
- logging.warning("Failed to fetch opensearch traces fields", exc_info=True)
98
- return StructuredToolResult(
99
- status=StructuredToolResultStatus.ERROR,
100
- error=f"Network error while opensearch traces fields: {str(e)}",
101
- params=params,
102
- )
103
- except Exception as e:
104
- logging.warning("Failed to process opensearch traces fields", exc_info=True)
105
- return StructuredToolResult(
106
- status=StructuredToolResultStatus.ERROR,
107
- error=f"Unexpected error: {str(e)}",
108
- params=params,
109
- )
110
-
111
- def get_parameterized_one_liner(self, params) -> str:
112
- return f"{toolset_name_for_one_liner(self._toolset.name)}: List Trace Fields"
113
-
114
-
115
- class TracesSearchQuery(Tool):
116
- def __init__(self, toolset: "OpenSearchTracesToolset"):
117
- super().__init__(
118
- name="traces_in_range_search",
119
- description="Get traces in a specified time range for an opensearch query",
120
- parameters={
121
- "query": ToolParameter(
122
- description="An OpenSearch search query. It should be a stringified json, matching opensearch search syntax. "
123
- "The query must contain a 'range' on the startTimeMillis field. From a given startTimeMillis, until a given startTimeMillis. This is time in milliseconds",
124
- type="string",
125
- ),
126
- },
127
- )
128
- self._toolset = toolset
129
- self._cache = None
130
-
131
- def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
132
- err_msg = ""
133
- try:
134
- body = json.loads(get_param_or_raise(params, "query"))
135
- full_query = body
136
- full_query["size"] = int(
137
- os.environ.get("OPENSEARCH_TRACES_SEARCH_SIZE", "5000")
138
- )
139
- logging.debug(f"opensearch traces search query: {full_query}")
140
- headers = {"Content-Type": "application/json"}
141
- headers.update(
142
- add_auth_header(self._toolset.opensearch_config.opensearch_auth_header)
143
- )
144
-
145
- logs_response = requests.get(
146
- url=get_search_url(self._toolset.opensearch_config),
147
- timeout=180,
148
- verify=True,
149
- data=json.dumps(full_query),
150
- headers=headers,
151
- )
152
- if logs_response.status_code > 300:
153
- err_msg = logs_response.text
154
-
155
- logs_response.raise_for_status()
156
- return StructuredToolResult(
157
- status=StructuredToolResultStatus.SUCCESS,
158
- data=json.dumps(logs_response.json()),
159
- params=params,
160
- )
161
- except requests.Timeout:
162
- logging.warning(
163
- "Timeout while fetching opensearch traces search", exc_info=True
164
- )
165
- return StructuredToolResult(
166
- status=StructuredToolResultStatus.ERROR,
167
- error=f"Request timed out while fetching opensearch traces search {err_msg}",
168
- params=params,
169
- )
170
- except RequestException as e:
171
- logging.warning("Failed to fetch opensearch traces search", exc_info=True)
172
- return StructuredToolResult(
173
- status=StructuredToolResultStatus.ERROR,
174
- error=f"Network error while opensearch traces search {err_msg} : {str(e)}",
175
- params=params,
176
- )
177
- except Exception as e:
178
- logging.warning(
179
- "Failed to process opensearch traces search ", exc_info=True
180
- )
181
- return StructuredToolResult(
182
- status=StructuredToolResultStatus.ERROR,
183
- error=f"Unexpected error {err_msg}: {str(e)}",
184
- params=params,
185
- )
186
-
187
- def get_parameterized_one_liner(self, params) -> str:
188
- query = params.get("query", "")
189
- return (
190
- f"{toolset_name_for_one_liner(self._toolset.name)}: Search Traces ({query})"
191
- )
192
-
193
-
194
- class OpenSearchTracesToolset(BaseOpenSearchToolset):
195
- def __init__(self):
196
- super().__init__(
197
- name="opensearch/traces",
198
- description="OpenSearch integration to fetch traces",
199
- docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/opensearch-status/",
200
- icon_url="https://opensearch.org/assets/brand/PNG/Mark/opensearch_mark_default.png",
201
- prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
202
- tools=[
203
- GetTracesFields(toolset=self),
204
- TracesSearchQuery(toolset=self),
205
- ],
206
- tags=[
207
- ToolsetTag.CORE,
208
- ],
209
- )
210
- template_file_path = os.path.abspath(
211
- os.path.join(
212
- os.path.dirname(__file__), "opensearch_traces_instructions.jinja2"
213
- )
214
- )
215
- self._load_llm_instructions(jinja_template=f"file://{template_file_path}")