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.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +4 -3
- holmes/common/env_vars.py +18 -2
- holmes/common/openshift.py +1 -1
- holmes/config.py +11 -6
- holmes/core/conversations.py +30 -13
- holmes/core/investigation.py +21 -25
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +50 -31
- holmes/core/models.py +19 -17
- holmes/core/openai_formatting.py +1 -1
- holmes/core/prompt.py +47 -2
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +4 -2
- holmes/core/supabase_dal.py +4 -2
- holmes/core/tool_calling_llm.py +102 -141
- holmes/core/tools.py +19 -28
- holmes/core/tools_utils/token_counting.py +9 -2
- holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
- holmes/core/tools_utils/tool_executor.py +0 -18
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +37 -2
- holmes/core/tracing.py +13 -2
- holmes/core/transformers/__init__.py +1 -1
- holmes/core/transformers/base.py +1 -0
- holmes/core/transformers/llm_summarize.py +3 -2
- holmes/core/transformers/registry.py +2 -1
- holmes/core/transformers/transformer.py +1 -0
- holmes/core/truncation/compaction.py +37 -2
- holmes/core/truncation/input_context_window_limiter.py +3 -2
- holmes/interactive.py +52 -8
- holmes/main.py +17 -37
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
- holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
- holmes/plugins/prompts/generic_ask.jinja2 +0 -2
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
- holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
- holmes/plugins/runbooks/__init__.py +32 -3
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +30 -26
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
- 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 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
- 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 +2 -3
- holmes/plugins/toolsets/bash/common/bash.py +19 -9
- 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/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 +36 -3
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
- 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 +71 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/git.py +7 -8
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +2 -30
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
- holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
- holmes/plugins/toolsets/internet/internet.py +10 -10
- holmes/plugins/toolsets/internet/notion.py +5 -6
- holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +12 -7
- holmes/plugins/toolsets/kubernetes.yaml +260 -30
- holmes/plugins/toolsets/kubernetes_logs.py +3 -3
- holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
- holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
- holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
- holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
- holmes/plugins/toolsets/robusta/robusta.py +5 -5
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
- holmes/plugins/toolsets/utils.py +1 -1
- holmes/utils/config_utils.py +1 -1
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +10 -26
- holmes/utils/holmes_status.py +4 -3
- 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 +23 -0
- holmes/utils/stream.py +12 -5
- holmes/utils/tags.py +4 -3
- holmes/version.py +3 -1
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
- holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
- holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
- /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
2
|
import logging
|
|
3
|
-
from
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
62
|
-
|
|
57
|
+
if not results:
|
|
58
|
+
return None
|
|
63
59
|
|
|
64
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
)
|