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,25 +1,12 @@
1
- from enum import Enum
1
+ import json
2
2
  import logging
3
- from typing import Any, Tuple
3
+ from enum import Enum
4
+ from typing import Any, Optional, Tuple
4
5
  from urllib.parse import urljoin
5
6
 
6
7
  import requests # type: ignore
7
8
 
8
- from holmes.plugins.toolsets.coralogix.utils import (
9
- CoralogixConfig,
10
- CoralogixQueryResult,
11
- merge_log_results,
12
- parse_logs,
13
- CoralogixLogsMethodology,
14
- )
15
- from holmes.plugins.toolsets.logging_utils.logging_api import (
16
- FetchPodLogsParams,
17
- DEFAULT_TIME_SPAN_SECONDS,
18
- DEFAULT_LOG_LIMIT,
19
- )
20
- from holmes.plugins.toolsets.utils import (
21
- process_timestamps_to_rfc3339,
22
- )
9
+ from holmes.plugins.toolsets.coralogix.utils import parse_json_lines
23
10
 
24
11
 
25
12
  class CoralogixTier(str, Enum):
@@ -31,130 +18,156 @@ def get_dataprime_base_url(domain: str) -> str:
31
18
  return f"https://ng-api-http.{domain}"
32
19
 
33
20
 
34
- def execute_http_query(domain: str, api_key: str, query: dict[str, Any]):
35
- base_url = get_dataprime_base_url(domain)
36
- url = urljoin(base_url, "api/v1/dataprime/query")
37
- headers = {
21
+ def _get_auth_headers(api_key: str) -> dict[str, str]:
22
+ return {
38
23
  "Authorization": f"Bearer {api_key}",
39
24
  "Content-Type": "application/json",
40
25
  }
41
26
 
42
- return requests.post(url, headers=headers, json=query)
43
27
 
28
+ def execute_coralogix_query(
29
+ domain: str, api_key: str, query: dict[str, Any]
30
+ ) -> Tuple[requests.Response, str]:
31
+ base_url = get_dataprime_base_url(domain).rstrip("/") + "/"
32
+ url = urljoin(base_url, "api/v1/dataprime/query")
33
+ response = requests.post(
34
+ url,
35
+ headers=_get_auth_headers(api_key),
36
+ json=query,
37
+ timeout=(10, 120),
38
+ )
39
+ return response, url
44
40
 
45
- def health_check(domain: str, api_key: str) -> Tuple[bool, str]:
46
- query = {"query": "source logs | limit 1"}
47
41
 
48
- response = execute_http_query(domain=domain, api_key=api_key, query=query)
42
+ def _parse_ndjson_response(response_text: str) -> Optional[Any]:
43
+ """Parse NDJSON response from Coralogix API."""
44
+ json_objects = parse_json_lines(response_text)
45
+ if not json_objects:
46
+ return None
49
47
 
50
- if response.status_code == 200:
51
- return True, ""
52
- else:
53
- return False, f"Failed with status_code={response.status_code}. {response.text}"
48
+ results: list[Any] = []
54
49
 
50
+ for obj in json_objects:
51
+ if not isinstance(obj, dict):
52
+ continue
55
53
 
56
- def build_query_string(config: CoralogixConfig, params: FetchPodLogsParams) -> str:
57
- query_filters = []
58
- query_filters.append(f'{config.labels.namespace}:"{params.namespace}"')
59
- query_filters.append(f'{config.labels.pod}:"{params.pod_name}"')
54
+ if any(k in obj for k in ("result", "results", "batches", "records")):
55
+ results.append(obj)
60
56
 
61
- if params.filter:
62
- query_filters.append(f'{config.labels.log_message}:"{params.filter}"')
57
+ if not results:
58
+ return None
63
59
 
64
- query_string = " AND ".join(query_filters)
65
- query_string = f"source logs | lucene '{query_string}' | limit {params.limit or DEFAULT_LOG_LIMIT}"
66
- return query_string
60
+ return results
67
61
 
68
62
 
69
- def get_start_end(params: FetchPodLogsParams):
70
- (start, end) = process_timestamps_to_rfc3339(
71
- start_timestamp=params.start_time,
72
- end_timestamp=params.end_time,
73
- default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
74
- )
75
- return (start, end)
63
+ def _build_query_dict(
64
+ dataprime_query: str,
65
+ start_date: Optional[str] = None,
66
+ end_date: Optional[str] = None,
67
+ tier: Optional[CoralogixTier] = None,
68
+ ) -> dict[str, Any]:
69
+ metadata: dict[str, Any] = {"syntax": "QUERY_SYNTAX_DATAPRIME"}
70
+ if start_date:
71
+ metadata["startDate"] = start_date
72
+ if end_date:
73
+ metadata["endDate"] = end_date
74
+ if tier:
75
+ metadata["tier"] = tier.value
76
76
 
77
+ return {"query": dataprime_query, "metadata": metadata}
77
78
 
78
- def build_query(
79
- config: CoralogixConfig, params: FetchPodLogsParams, tier: CoralogixTier
80
- ):
81
- (start, end) = get_start_end(params)
82
79
 
83
- query_string = build_query_string(config, params)
84
- return {
85
- "query": query_string,
86
- "metadata": {
87
- "tier": tier.value,
88
- "syntax": "QUERY_SYNTAX_DATAPRIME",
89
- "startDate": start,
90
- "endDate": end,
91
- },
92
- }
80
+ def _get_error_body(response: requests.Response) -> str:
81
+ """Extract error body from response."""
82
+ try:
83
+ return (response.text or "").strip()
84
+ except Exception:
85
+ return ""
86
+
87
+
88
+ def _cleanup_coralogix_results(parsed: list[Any]) -> Any:
89
+ """Clean up and normalize parsed Coralogix results structure."""
90
+ # Extract nested results if present
91
+ if len(parsed) == 1 and isinstance(parsed[0], dict) and "result" in parsed[0]:
92
+ nested_result = parsed[0]["result"]
93
+ if isinstance(nested_result, dict) and "results" in nested_result:
94
+ parsed = nested_result["results"]
95
+
96
+ # Replace items with userData JSON if present
97
+ # userData has additional data that is missing in the main result object along with the actual result
98
+ for i, item in enumerate(parsed):
99
+ if isinstance(item, dict) and "userData" in item:
100
+ try:
101
+ parsed[i] = json.loads(item["userData"])
102
+ except (json.JSONDecodeError, TypeError):
103
+ # If parsing fails, keep the original item
104
+ pass
105
+
106
+ return parsed
107
+
108
+
109
+ def execute_dataprime_query(
110
+ domain: str,
111
+ api_key: str,
112
+ dataprime_query: str,
113
+ start_date: Optional[str] = None,
114
+ end_date: Optional[str] = None,
115
+ tier: Optional[CoralogixTier] = None,
116
+ max_poll_attempts: int = 60,
117
+ poll_interval_seconds: float = 1.0,
118
+ ) -> Tuple[Optional[Any], Optional[str]]:
119
+ try:
120
+ query_dict = _build_query_dict(dataprime_query, start_date, end_date, tier)
121
+ response, submit_url = execute_coralogix_query(domain, api_key, query_dict)
122
+
123
+ if response.status_code != 200:
124
+ body = _get_error_body(response)
125
+ if "Compiler error" in body or "Compilation errors" in body:
126
+ return (
127
+ None,
128
+ f"Compilation errors: {body}\nUse lucene instead of filter and verify that all labels are present before using them.",
129
+ )
130
+ return (
131
+ None,
132
+ f"Failed to submit query: status_code={response.status_code}, {body}\nURL: {submit_url}",
133
+ )
93
134
 
135
+ raw = response.text.strip()
136
+ if not raw:
137
+ return None, f"Empty 200 response from query submission\nURL: {submit_url}"
94
138
 
95
- def query_logs_for_tier(
96
- config: CoralogixConfig, params: FetchPodLogsParams, tier: CoralogixTier
97
- ) -> CoralogixQueryResult:
98
- http_status = None
99
- try:
100
- query = build_query(config, params, tier)
139
+ parsed = _parse_ndjson_response(raw)
101
140
 
102
- response = execute_http_query(
103
- domain=config.domain,
104
- api_key=config.api_key,
105
- query=query,
106
- )
107
- http_status = response.status_code
108
- if http_status == 200:
109
- logs = parse_logs(
110
- raw_logs=response.text.strip(), labels_config=config.labels
111
- )
112
- return CoralogixQueryResult(logs=logs, http_status=http_status, error=None)
113
- else:
114
- return CoralogixQueryResult(
115
- logs=[], http_status=http_status, error=response.text
116
- )
117
- except Exception as e:
118
- logging.error("Failed to fetch coralogix logs", exc_info=True)
119
- return CoralogixQueryResult(logs=[], http_status=http_status, error=str(e))
120
-
121
-
122
- def query_logs_for_all_tiers(
123
- config: CoralogixConfig, params: FetchPodLogsParams
124
- ) -> CoralogixQueryResult:
125
- methodology = config.logs_retrieval_methodology
126
- result: CoralogixQueryResult
127
-
128
- if methodology in [
129
- CoralogixLogsMethodology.FREQUENT_SEARCH_ONLY,
130
- CoralogixLogsMethodology.BOTH_FREQUENT_SEARCH_AND_ARCHIVE,
131
- CoralogixLogsMethodology.ARCHIVE_FALLBACK,
132
- ]:
133
- result = query_logs_for_tier(
134
- config=config, params=params, tier=CoralogixTier.FREQUENT_SEARCH
135
- )
141
+ # Usually if someone ran query that returns no results
142
+ if not parsed and response.status_code in [200, 204]:
143
+ return [], None
136
144
 
137
- if (
138
- methodology == CoralogixLogsMethodology.ARCHIVE_FALLBACK and not result.logs
139
- ) or methodology == CoralogixLogsMethodology.BOTH_FREQUENT_SEARCH_AND_ARCHIVE:
140
- archive_search_results = query_logs_for_tier(
141
- config=config, params=params, tier=CoralogixTier.ARCHIVE
145
+ if not parsed:
146
+ return None, (
147
+ f"Query submission:\n"
148
+ f"URL: {submit_url}\n"
149
+ f"Response status: {response.status_code}\n"
150
+ f"Response body (first 2000 chars): {raw[:2000]}\n\n"
142
151
  )
143
- result = merge_log_results(result, archive_search_results)
144
152
 
145
- else:
146
- # methodology in [CoralogixLogsMethodology.ARCHIVE_ONLY, CoralogixLogsMethodology.FREQUENT_SEARCH_FALLBACK]:
147
- result = query_logs_for_tier(
148
- config=config, params=params, tier=CoralogixTier.ARCHIVE
149
- )
153
+ cleaned_results = _cleanup_coralogix_results(parsed)
154
+ return cleaned_results, None
150
155
 
151
- if (
152
- methodology == CoralogixLogsMethodology.FREQUENT_SEARCH_FALLBACK
153
- and not result.logs
154
- ):
155
- frequent_search_results = query_logs_for_tier(
156
- config=config, params=params, tier=CoralogixTier.FREQUENT_SEARCH
157
- )
158
- result = merge_log_results(result, frequent_search_results)
156
+ except Exception as e:
157
+ logging.error("Failed to execute DataPrime query", exc_info=True)
158
+ return None, str(e)
159
+
160
+
161
+ def health_check(domain: str, api_key: str) -> Tuple[bool, str]:
162
+ query_dict = _build_query_dict("source logs | limit 1")
163
+ response, submit_url = execute_coralogix_query(
164
+ domain=domain, api_key=api_key, query=query_dict
165
+ )
159
166
 
160
- return result
167
+ if response.status_code != 200:
168
+ body = _get_error_body(response)
169
+ return (
170
+ False,
171
+ f"Failed with status_code={response.status_code}. {body}\nURL: {submit_url}",
172
+ )
173
+ return True, ""
@@ -0,0 +1,14 @@
1
+ Coralogix DataPrime (tool: coralogix_execute_dataprime_query) queries logs/traces (sources: logs, spans).
2
+
3
+ Rules:
4
+ - Always set explicit time ranges: start_date/end_date
5
+ - Include `limit` on every query (start with limit 100 and adjust accordingly)
6
+ - Use `source logs | lucene 'text'` for log searches; prefer lucene over filter
7
+ - Start broad, then narrow down
8
+ - Never assume labels: start with plain text (e.g., `source logs | lucene 'app-173'`); only use label equality after seeing the exact field path in results
9
+ - Traces: use `source spans`
10
+ - Never query archive tier; only query frequent search data
11
+
12
+ Example of a bad query: `source logs | filter $l.k8s.namespace_name == 'my_namespace'` (uses filter, unverified labels, no limit)
13
+
14
+ To discover custom labels: run `source logs | limit 1` or `source spans | limit 1`
@@ -0,0 +1,219 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Optional, Tuple
4
+ from urllib.parse import quote
5
+
6
+ from holmes.core.tools import (
7
+ CallablePrerequisite,
8
+ StructuredToolResult,
9
+ StructuredToolResultStatus,
10
+ Tool,
11
+ ToolInvokeContext,
12
+ ToolParameter,
13
+ Toolset,
14
+ ToolsetTag,
15
+ )
16
+ from holmes.plugins.toolsets.consts import TOOLSET_CONFIG_MISSING_ERROR
17
+ from holmes.plugins.toolsets.coralogix.api import (
18
+ CoralogixTier,
19
+ execute_dataprime_query,
20
+ health_check,
21
+ )
22
+ from holmes.plugins.toolsets.coralogix.utils import CoralogixConfig, normalize_datetime
23
+ from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
24
+
25
+
26
+ def _build_coralogix_query_url(
27
+ config: CoralogixConfig,
28
+ query: str,
29
+ start_date: str,
30
+ end_date: str,
31
+ tier: Optional[CoralogixTier] = None,
32
+ ) -> Optional[str]:
33
+ try:
34
+ if tier == CoralogixTier.ARCHIVE:
35
+ data_pipeline = "archive-logs"
36
+ else:
37
+ # due to a bug in Coralogix, we always use the logs pipeline
38
+ # since the tracing url does not support the query parameter
39
+ # https://coralogix.com/docs/user-guides/monitoring-and-insights/logs-screen/query-urls/
40
+ data_pipeline = "logs"
41
+
42
+ time_range = f"from:{start_date},to:{end_date}"
43
+
44
+ encoded_query = quote(query)
45
+ encoded_time = quote(time_range)
46
+ base_url = f"https://{config.team_hostname}.{config.domain}"
47
+
48
+ url = (
49
+ f"{base_url}/#/query-new/{data_pipeline}"
50
+ f"?querySyntax=dataprime"
51
+ f"&time={encoded_time}"
52
+ f"&query={encoded_query}"
53
+ f"&permalink=true"
54
+ )
55
+ return url
56
+
57
+ except Exception:
58
+ return None
59
+
60
+
61
+ class ExecuteDataPrimeQuery(Tool):
62
+ def __init__(self, toolset: "CoralogixToolset"):
63
+ super().__init__(
64
+ name="coralogix_execute_dataprime_query",
65
+ description="Execute a DataPrime query against Coralogix to fetch logs, traces, metrics, and other telemetry data. "
66
+ "Returns the raw query results from Coralogix.",
67
+ parameters={
68
+ "query": ToolParameter(
69
+ description="DataPrime query string. Examples: `source logs | lucene 'error' | limit 100`, `source spans | lucene 'my-service' | limit 100`. Always include a `limit` clause.",
70
+ type="string",
71
+ required=True,
72
+ ),
73
+ "description": ToolParameter(
74
+ description="Brief 6-word description of the query.",
75
+ type="string",
76
+ required=True,
77
+ ),
78
+ "query_type": ToolParameter(
79
+ description="'Logs', 'Traces', 'Metrics', 'Discover Data' or 'Other'.",
80
+ type="string",
81
+ required=True,
82
+ ),
83
+ "start_date": ToolParameter(
84
+ description="Optional start date in RFC3339 format (e.g., '2024-01-01T00:00:00Z').",
85
+ type="string",
86
+ required=True,
87
+ ),
88
+ "end_date": ToolParameter(
89
+ description="Optional end date in RFC3339 format (e.g., '2024-01-01T23:59:59Z').",
90
+ type="string",
91
+ required=True,
92
+ ),
93
+ "tier": ToolParameter(
94
+ description="Optional tier: 'FREQUENT_SEARCH' or 'ARCHIVE'.",
95
+ type="string",
96
+ required=False,
97
+ ),
98
+ },
99
+ )
100
+ self._toolset = toolset
101
+
102
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
103
+ if not self._toolset.coralogix_config:
104
+ return StructuredToolResult(
105
+ status=StructuredToolResultStatus.ERROR,
106
+ error="Coralogix toolset is not configured",
107
+ params=params,
108
+ )
109
+
110
+ tier = None
111
+ if tier_str := params.get("tier"):
112
+ try:
113
+ tier = CoralogixTier[tier_str]
114
+ except KeyError:
115
+ return StructuredToolResult(
116
+ status=StructuredToolResultStatus.ERROR,
117
+ error=f"Invalid tier '{tier_str}'. Must be 'FREQUENT_SEARCH' or 'ARCHIVE'",
118
+ params=params,
119
+ )
120
+
121
+ start_time = normalize_datetime(params.get("start_date"))
122
+ end_time = normalize_datetime(params.get("end_date"))
123
+ if start_time == "UNKNOWN_TIMESTAMP" or end_time == "UNKNOWN_TIMESTAMP":
124
+ return StructuredToolResult(
125
+ status=StructuredToolResultStatus.ERROR,
126
+ error=f"Invalid start or end date: {params.get('start_date')} or {params.get('end_date')}. Please provide valid dates in RFC3339 format (e.g., '2024-01-01T00:00:00Z').",
127
+ params=params,
128
+ )
129
+
130
+ if start_time > end_time:
131
+ start_time, end_time = end_time, start_time
132
+
133
+ result, error = execute_dataprime_query(
134
+ domain=self._toolset.coralogix_config.domain,
135
+ api_key=self._toolset.coralogix_config.api_key,
136
+ dataprime_query=params["query"],
137
+ start_date=start_time,
138
+ end_date=end_time,
139
+ tier=tier,
140
+ )
141
+
142
+ if error:
143
+ return StructuredToolResult(
144
+ status=StructuredToolResultStatus.ERROR,
145
+ error=error,
146
+ params=params,
147
+ )
148
+
149
+ result_dict = {
150
+ "tool_name": self.name,
151
+ "data": result,
152
+ }
153
+ status = StructuredToolResultStatus.SUCCESS
154
+
155
+ if not result:
156
+ results_msg = "No results found, it is possible that the query is not correct, using incorrect labels or filters."
157
+ result_dict["results_msg"] = results_msg
158
+ status = StructuredToolResultStatus.NO_DATA
159
+
160
+ # Build Coralogix query URL
161
+ explore_url = _build_coralogix_query_url(
162
+ config=self._toolset.coralogix_config,
163
+ query=params["query"],
164
+ start_date=start_time,
165
+ end_date=end_time,
166
+ tier=tier,
167
+ )
168
+
169
+ # Return a pretty-printed JSON string for readability by the model/user.
170
+ final_result = json.dumps(result_dict, indent=2, sort_keys=False)
171
+ return StructuredToolResult(
172
+ status=status,
173
+ data=final_result,
174
+ params=params,
175
+ url=explore_url,
176
+ )
177
+
178
+ def get_parameterized_one_liner(self, params) -> str:
179
+ description = params.get("description", "")
180
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Execute DataPrime ({description})"
181
+
182
+
183
+ class CoralogixToolset(Toolset):
184
+ def __init__(self):
185
+ super().__init__(
186
+ name="coralogix",
187
+ description="Toolset for interacting with Coralogix to fetch logs, traces, metrics, and execute DataPrime queries",
188
+ docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/coralogix-logs/",
189
+ icon_url="https://avatars.githubusercontent.com/u/35295744?s=200&v=4",
190
+ prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
191
+ tools=[ExecuteDataPrimeQuery(self)],
192
+ tags=[ToolsetTag.CORE],
193
+ )
194
+ template_path = os.path.join(os.path.dirname(__file__), "coralogix.jinja2")
195
+ if os.path.exists(template_path):
196
+ self._load_llm_instructions(
197
+ jinja_template=f"file://{os.path.abspath(template_path)}"
198
+ )
199
+
200
+ def get_example_config(self):
201
+ example_config = CoralogixConfig(
202
+ api_key="<cxuw_...>", team_hostname="my-team", domain="eu2.coralogix.com"
203
+ )
204
+ return example_config.model_dump()
205
+
206
+ def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
207
+ if not config:
208
+ return False, TOOLSET_CONFIG_MISSING_ERROR
209
+
210
+ self.config = CoralogixConfig(**config)
211
+
212
+ if not self.config.api_key:
213
+ return False, "Missing configuration field 'api_key'"
214
+
215
+ return health_check(domain=self.config.domain, api_key=self.config.api_key)
216
+
217
+ @property
218
+ def coralogix_config(self) -> Optional[CoralogixConfig]:
219
+ return self.config
@@ -1,9 +1,7 @@
1
- from enum import Enum
2
1
  import json
3
2
  import logging
4
- import urllib.parse
5
3
  from datetime import datetime
6
- from typing import Any, NamedTuple, Optional, Dict, List
4
+ from typing import Any, Dict, List, NamedTuple, Optional
7
5
 
8
6
  from pydantic import BaseModel
9
7
 
@@ -26,45 +24,41 @@ class CoralogixLabelsConfig(BaseModel):
26
24
  timestamp: str = "logRecord.attributes.time"
27
25
 
28
26
 
29
- class CoralogixLogsMethodology(str, Enum):
30
- FREQUENT_SEARCH_ONLY = "FREQUENT_SEARCH_ONLY"
31
- ARCHIVE_ONLY = "ARCHIVE_ONLY"
32
- ARCHIVE_FALLBACK = "ARCHIVE_FALLBACK"
33
- FREQUENT_SEARCH_FALLBACK = "FREQUENT_SEARCH_FALLBACK"
34
- BOTH_FREQUENT_SEARCH_AND_ARCHIVE = "BOTH_FREQUENT_SEARCH_AND_ARCHIVE"
35
-
36
-
37
27
  class CoralogixConfig(BaseModel):
38
28
  team_hostname: str
39
29
  domain: str
40
30
  api_key: str
41
31
  labels: CoralogixLabelsConfig = CoralogixLabelsConfig()
42
- logs_retrieval_methodology: CoralogixLogsMethodology = (
43
- CoralogixLogsMethodology.ARCHIVE_FALLBACK
44
- )
45
32
 
46
33
 
47
34
  def parse_json_lines(raw_text) -> List[Dict[str, Any]]:
48
- """Parses JSON objects from a raw text response."""
35
+ """Parses JSON objects from a raw text response and removes duplicate userData fields from child objects."""
49
36
  json_objects = []
50
37
  for line in raw_text.strip().split("\n"): # Split by newlines
51
38
  try:
52
- json_objects.append(json.loads(line))
39
+ obj = json.loads(line)
40
+ if isinstance(obj, dict):
41
+ # Remove userData from top level
42
+ obj.pop("userData", None)
43
+ # Remove userData from direct child dicts (one level deep, no recursion)
44
+ for key, value in list(obj.items()):
45
+ if isinstance(value, dict):
46
+ value.pop("userData", None)
47
+ elif isinstance(value, list):
48
+ for item in value:
49
+ if isinstance(item, dict):
50
+ item.pop("userData", None)
51
+ json_objects.append(obj)
53
52
  except json.JSONDecodeError:
54
53
  logging.error(f"Failed to decode JSON from line: {line}")
55
54
  return json_objects
56
55
 
57
56
 
58
57
  def normalize_datetime(date_str: Optional[str]) -> str:
59
- """takes a date string as input and attempts to convert it into a standardized ISO 8601 format with UTC timezone (“Z” suffix) and microsecond precision.
60
- if any error occurs during parsing or formatting, it returns the original input string.
61
- The method specifically handles older Python versions by removing a trailing “Z” and truncating microseconds to 6 digits before parsing.
62
- """
63
58
  if not date_str:
64
59
  return "UNKNOWN_TIMESTAMP"
65
60
 
66
61
  try:
67
- # older versions of python do not support `Z` appendix nor more than 6 digits of microsecond precision
68
62
  date_str_no_z = date_str.rstrip("Z")
69
63
 
70
64
  parts = date_str_no_z.split(".")
@@ -148,61 +142,3 @@ def parse_json_objects(
148
142
  logs.sort(key=lambda x: x[0])
149
143
 
150
144
  return logs
151
-
152
-
153
- def parse_logs(
154
- raw_logs: str,
155
- labels_config: CoralogixLabelsConfig,
156
- ) -> List[FlattenedLog]:
157
- """Processes the HTTP response and extracts only log outputs."""
158
- try:
159
- json_objects = parse_json_lines(raw_logs)
160
- if not json_objects:
161
- raise Exception("No valid JSON objects found.")
162
- return parse_json_objects(
163
- json_objects=json_objects, labels_config=labels_config
164
- )
165
- except Exception as e:
166
- logging.error(
167
- f"Unexpected error in format_logs for a coralogix API response: {str(e)}"
168
- )
169
- raise e
170
-
171
-
172
- def build_coralogix_link_to_logs(
173
- config: CoralogixConfig, lucene_query: str, start: str, end: str
174
- ) -> str:
175
- query_param = urllib.parse.quote_plus(lucene_query)
176
-
177
- return f"https://{config.team_hostname}.app.{config.domain}/#/query-new/logs?query={query_param}&querySyntax=dataprime&time=from:{start},to:{end}"
178
-
179
-
180
- def merge_log_results(
181
- a: CoralogixQueryResult, b: CoralogixQueryResult
182
- ) -> CoralogixQueryResult:
183
- """
184
- Merges two CoralogixQueryResult objects, deduplicating logs and sorting them by timestamp.
185
-
186
- """
187
- if a.error is None and b.error:
188
- return a
189
- elif b.error is None and a.error:
190
- return b
191
- elif a.error and b.error:
192
- return a
193
-
194
- combined_logs = a.logs + b.logs
195
-
196
- if not combined_logs:
197
- deduplicated_logs_set = set()
198
- else:
199
- deduplicated_logs_set = set(combined_logs)
200
-
201
- # Assumes timestamps are in a format sortable as strings (e.g., ISO 8601)
202
- sorted_logs = sorted(list(deduplicated_logs_set), key=lambda log: log.timestamp)
203
-
204
- return CoralogixQueryResult(
205
- logs=sorted_logs,
206
- http_status=a.http_status if a.http_status is not None else b.http_status,
207
- error=a.error,
208
- )