holmesgpt 0.11.5__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.
Potentially problematic release.
This version of holmesgpt might be problematic. Click here for more details.
- holmes/.git_archival.json +7 -0
- holmes/__init__.py +76 -0
- holmes/__init__.py.bak +76 -0
- holmes/clients/robusta_client.py +24 -0
- holmes/common/env_vars.py +47 -0
- holmes/config.py +526 -0
- holmes/core/__init__.py +0 -0
- holmes/core/conversations.py +578 -0
- holmes/core/investigation.py +152 -0
- holmes/core/investigation_structured_output.py +264 -0
- holmes/core/issue.py +54 -0
- holmes/core/llm.py +250 -0
- holmes/core/models.py +157 -0
- holmes/core/openai_formatting.py +51 -0
- holmes/core/performance_timing.py +72 -0
- holmes/core/prompt.py +42 -0
- holmes/core/resource_instruction.py +17 -0
- holmes/core/runbooks.py +26 -0
- holmes/core/safeguards.py +120 -0
- holmes/core/supabase_dal.py +540 -0
- holmes/core/tool_calling_llm.py +798 -0
- holmes/core/tools.py +566 -0
- holmes/core/tools_utils/__init__.py +0 -0
- holmes/core/tools_utils/tool_executor.py +65 -0
- holmes/core/tools_utils/toolset_utils.py +52 -0
- holmes/core/toolset_manager.py +418 -0
- holmes/interactive.py +229 -0
- holmes/main.py +1041 -0
- holmes/plugins/__init__.py +0 -0
- holmes/plugins/destinations/__init__.py +6 -0
- holmes/plugins/destinations/slack/__init__.py +2 -0
- holmes/plugins/destinations/slack/plugin.py +163 -0
- holmes/plugins/interfaces.py +32 -0
- holmes/plugins/prompts/__init__.py +48 -0
- holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
- holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
- holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
- holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
- holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
- holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
- holmes/plugins/prompts/generic_ask.jinja2 +36 -0
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
- holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
- holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
- holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
- holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
- holmes/plugins/runbooks/README.md +22 -0
- holmes/plugins/runbooks/__init__.py +100 -0
- holmes/plugins/runbooks/catalog.json +14 -0
- holmes/plugins/runbooks/jira.yaml +12 -0
- holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
- holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
- holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
- holmes/plugins/sources/github/__init__.py +77 -0
- holmes/plugins/sources/jira/__init__.py +123 -0
- holmes/plugins/sources/opsgenie/__init__.py +93 -0
- holmes/plugins/sources/pagerduty/__init__.py +147 -0
- holmes/plugins/sources/prometheus/__init__.py +0 -0
- holmes/plugins/sources/prometheus/models.py +104 -0
- holmes/plugins/sources/prometheus/plugin.py +154 -0
- holmes/plugins/toolsets/__init__.py +171 -0
- holmes/plugins/toolsets/aks-node-health.yaml +65 -0
- holmes/plugins/toolsets/aks.yaml +86 -0
- holmes/plugins/toolsets/argocd.yaml +70 -0
- holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
- holmes/plugins/toolsets/aws.yaml +76 -0
- holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
- holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
- holmes/plugins/toolsets/azure_sql/install.md +66 -0
- holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
- holmes/plugins/toolsets/azure_sql/utils.py +83 -0
- holmes/plugins/toolsets/bash/__init__.py +0 -0
- holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
- holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
- holmes/plugins/toolsets/bash/common/bash.py +52 -0
- holmes/plugins/toolsets/bash/common/config.py +14 -0
- holmes/plugins/toolsets/bash/common/stringify.py +25 -0
- holmes/plugins/toolsets/bash/common/validators.py +24 -0
- holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
- holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
- holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
- holmes/plugins/toolsets/bash/parse_command.py +103 -0
- holmes/plugins/toolsets/confluence.yaml +19 -0
- holmes/plugins/toolsets/consts.py +5 -0
- holmes/plugins/toolsets/coralogix/api.py +158 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
- holmes/plugins/toolsets/coralogix/utils.py +181 -0
- holmes/plugins/toolsets/datadog.py +153 -0
- holmes/plugins/toolsets/docker.yaml +46 -0
- holmes/plugins/toolsets/git.py +756 -0
- holmes/plugins/toolsets/grafana/__init__.py +0 -0
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
- holmes/plugins/toolsets/grafana/common.py +68 -0
- holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
- holmes/plugins/toolsets/grafana/loki_api.py +89 -0
- holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
- holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
- holmes/plugins/toolsets/helm.yaml +42 -0
- holmes/plugins/toolsets/internet/internet.py +275 -0
- holmes/plugins/toolsets/internet/notion.py +137 -0
- holmes/plugins/toolsets/kafka.py +638 -0
- holmes/plugins/toolsets/kubernetes.yaml +255 -0
- holmes/plugins/toolsets/kubernetes_logs.py +426 -0
- holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
- holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
- holmes/plugins/toolsets/logging_utils/types.py +0 -0
- holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
- holmes/plugins/toolsets/newrelic.py +222 -0
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
- holmes/plugins/toolsets/rabbitmq/api.py +398 -0
- holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
- holmes/plugins/toolsets/robusta/__init__.py +0 -0
- holmes/plugins/toolsets/robusta/robusta.py +235 -0
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
- holmes/plugins/toolsets/runbook/__init__.py +0 -0
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
- holmes/plugins/toolsets/service_discovery.py +92 -0
- holmes/plugins/toolsets/servicenow/install.md +37 -0
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
- holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
- holmes/plugins/toolsets/slab.yaml +20 -0
- holmes/plugins/toolsets/utils.py +137 -0
- holmes/plugins/utils.py +14 -0
- holmes/utils/__init__.py +0 -0
- holmes/utils/cache.py +84 -0
- holmes/utils/cert_utils.py +40 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
- holmes/utils/definitions.py +13 -0
- holmes/utils/env.py +53 -0
- holmes/utils/file_utils.py +56 -0
- holmes/utils/global_instructions.py +20 -0
- holmes/utils/holmes_status.py +22 -0
- holmes/utils/holmes_sync_toolsets.py +80 -0
- holmes/utils/markdown_utils.py +55 -0
- holmes/utils/pydantic_utils.py +54 -0
- holmes/utils/robusta.py +10 -0
- holmes/utils/tags.py +97 -0
- holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
- holmesgpt-0.11.5.dist-info/METADATA +400 -0
- holmesgpt-0.11.5.dist-info/RECORD +183 -0
- holmesgpt-0.11.5.dist-info/WHEEL +4 -0
- holmesgpt-0.11.5.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import shlex
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
|
|
6
|
+
from holmes.plugins.toolsets.bash.grep import create_grep_parser, stringify_grep_command
|
|
7
|
+
from holmes.plugins.toolsets.bash.kubectl import (
|
|
8
|
+
create_kubectl_parser,
|
|
9
|
+
stringify_kubectl_command,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Parser for commands", exit_on_error=False
|
|
16
|
+
)
|
|
17
|
+
commands_parser = parser.add_subparsers(
|
|
18
|
+
dest="cmd", required=True, help="The tool to command (e.g., kubectl)"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
create_kubectl_parser(commands_parser)
|
|
22
|
+
create_grep_parser(commands_parser)
|
|
23
|
+
return parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def stringify_command(
|
|
27
|
+
command: Any, original_command: str, config: Optional[BashExecutorConfig]
|
|
28
|
+
) -> str:
|
|
29
|
+
if command.cmd == "kubectl":
|
|
30
|
+
return stringify_kubectl_command(command, config)
|
|
31
|
+
elif command.cmd == "grep":
|
|
32
|
+
return stringify_grep_command(command)
|
|
33
|
+
else:
|
|
34
|
+
# This code path should not happen b/c the parsing of the command should catch an unsupported command
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Unsupported command '{command.cmd}' in {original_command}. Supported commands are: kubectl, grep"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
command_parser = create_parser()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def split_into_separate_commands(command_str: str) -> list[list[str]]:
|
|
44
|
+
"""
|
|
45
|
+
Splits a single bash command into sub commands based on the pipe '|' delimiter.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> "ls -l" -> [
|
|
49
|
+
['ls', '-l']
|
|
50
|
+
]
|
|
51
|
+
>>> "kubectl get pods | grep holmes" -> [
|
|
52
|
+
['kubectl', 'get', 'pods'],
|
|
53
|
+
['grep', 'holmes']
|
|
54
|
+
]
|
|
55
|
+
"""
|
|
56
|
+
parts = shlex.split(command_str)
|
|
57
|
+
if not parts:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
commands_list: list[list[str]] = []
|
|
61
|
+
current_command: list[str] = []
|
|
62
|
+
|
|
63
|
+
for part in parts:
|
|
64
|
+
if part == "|":
|
|
65
|
+
if current_command:
|
|
66
|
+
commands_list.append(current_command)
|
|
67
|
+
current_command = []
|
|
68
|
+
elif part == "&&":
|
|
69
|
+
raise ValueError(
|
|
70
|
+
'Double ampersand "&&" is not a supported way to chain commands. Run each command separately.'
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
current_command.append(part)
|
|
74
|
+
|
|
75
|
+
if current_command:
|
|
76
|
+
commands_list.append(current_command)
|
|
77
|
+
|
|
78
|
+
return commands_list
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def make_command_safe(command_str: str, config: Optional[BashExecutorConfig]) -> str:
|
|
82
|
+
commands = split_into_separate_commands(command_str)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
safe_commands = [
|
|
86
|
+
command_parser.parse_args(command_parts) for command_parts in commands
|
|
87
|
+
]
|
|
88
|
+
if safe_commands and safe_commands[0].cmd == "grep":
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"The command grep can only be used after another command using the pipe `|` character to connect both commands"
|
|
91
|
+
)
|
|
92
|
+
safe_commands_str = [
|
|
93
|
+
stringify_command(cmd, original_command=command_str, config=config)
|
|
94
|
+
for cmd in safe_commands
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
return " | ".join(safe_commands_str)
|
|
98
|
+
except SystemExit:
|
|
99
|
+
# argparse throws a SystemExit error when it can't parse command or arguments
|
|
100
|
+
# This ideally should be captured differently by ensuring all possible args
|
|
101
|
+
# are accounted for in the implementation for each command.
|
|
102
|
+
# When falling back, we raise a generic error
|
|
103
|
+
raise ValueError("The command failed to be parsed for safety") from None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
toolsets:
|
|
2
|
+
confluence:
|
|
3
|
+
description: "Fetch confluence pages"
|
|
4
|
+
docs_url: "https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/confluence.html"
|
|
5
|
+
icon_url: "https://platform.robusta.dev/demos/confluence.svg"
|
|
6
|
+
tags:
|
|
7
|
+
- core
|
|
8
|
+
prerequisites:
|
|
9
|
+
- command: "curl --version"
|
|
10
|
+
- env:
|
|
11
|
+
- CONFLUENCE_USER
|
|
12
|
+
- CONFLUENCE_API_KEY
|
|
13
|
+
- CONFLUENCE_BASE_URL
|
|
14
|
+
|
|
15
|
+
tools:
|
|
16
|
+
- name: "fetch_confluence_url"
|
|
17
|
+
description: "Fetch a page in confluence. Use this to fetch confluence runbooks if they are present before starting your investigation."
|
|
18
|
+
user_description: "fetch confluence page {{ confluence_page_id }}"
|
|
19
|
+
command: "curl -u ${CONFLUENCE_USER}:${CONFLUENCE_API_KEY} -X GET -H 'Content-Type: application/json' ${CONFLUENCE_BASE_URL}/wiki/rest/api/content/{{ confluence_page_id }}?expand=body.storage"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Tuple
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
import requests # type: ignore
|
|
7
|
+
|
|
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 FetchPodLogsParams
|
|
16
|
+
from holmes.plugins.toolsets.utils import (
|
|
17
|
+
process_timestamps_to_rfc3339,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_TIME_SPAN_SECONDS = 86400
|
|
22
|
+
DEFAULT_LOG_COUNT = 2000 # Coralogix's default is 2000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CoralogixTier(str, Enum):
|
|
26
|
+
FREQUENT_SEARCH = "TIER_FREQUENT_SEARCH"
|
|
27
|
+
ARCHIVE = "TIER_ARCHIVE"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_dataprime_base_url(domain: str) -> str:
|
|
31
|
+
return f"https://ng-api-http.{domain}"
|
|
32
|
+
|
|
33
|
+
|
|
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 = {
|
|
38
|
+
"Authorization": f"Bearer {api_key}",
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return requests.post(url, headers=headers, json=query)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def health_check(domain: str, api_key: str) -> Tuple[bool, str]:
|
|
46
|
+
query = {"query": "source logs | limit 1"}
|
|
47
|
+
|
|
48
|
+
response = execute_http_query(domain=domain, api_key=api_key, query=query)
|
|
49
|
+
|
|
50
|
+
if response.status_code == 200:
|
|
51
|
+
return True, ""
|
|
52
|
+
else:
|
|
53
|
+
return False, f"Failed with status_code={response.status_code}. {response.text}"
|
|
54
|
+
|
|
55
|
+
|
|
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}"')
|
|
60
|
+
|
|
61
|
+
if params.filter:
|
|
62
|
+
query_filters.append(f'{config.labels.log_message}:"{params.filter}"')
|
|
63
|
+
|
|
64
|
+
query_string = " AND ".join(query_filters)
|
|
65
|
+
query_string = f"source logs | lucene '{query_string}' | limit {params.limit or DEFAULT_LOG_COUNT}"
|
|
66
|
+
return query_string
|
|
67
|
+
|
|
68
|
+
|
|
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)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_query(
|
|
79
|
+
config: CoralogixConfig, params: FetchPodLogsParams, tier: CoralogixTier
|
|
80
|
+
):
|
|
81
|
+
(start, end) = get_start_end(params)
|
|
82
|
+
|
|
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
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
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)
|
|
101
|
+
|
|
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(raw_logs=response.text.strip())
|
|
110
|
+
return CoralogixQueryResult(logs=logs, http_status=http_status, error=None)
|
|
111
|
+
else:
|
|
112
|
+
return CoralogixQueryResult(
|
|
113
|
+
logs=[], http_status=http_status, error=response.text
|
|
114
|
+
)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logging.error("Failed to fetch coralogix logs", exc_info=True)
|
|
117
|
+
return CoralogixQueryResult(logs=[], http_status=http_status, error=str(e))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def query_logs_for_all_tiers(
|
|
121
|
+
config: CoralogixConfig, params: FetchPodLogsParams
|
|
122
|
+
) -> CoralogixQueryResult:
|
|
123
|
+
methodology = config.logs_retrieval_methodology
|
|
124
|
+
result: CoralogixQueryResult
|
|
125
|
+
|
|
126
|
+
if methodology in [
|
|
127
|
+
CoralogixLogsMethodology.FREQUENT_SEARCH_ONLY,
|
|
128
|
+
CoralogixLogsMethodology.BOTH_FREQUENT_SEARCH_AND_ARCHIVE,
|
|
129
|
+
CoralogixLogsMethodology.ARCHIVE_FALLBACK,
|
|
130
|
+
]:
|
|
131
|
+
result = query_logs_for_tier(
|
|
132
|
+
config=config, params=params, tier=CoralogixTier.FREQUENT_SEARCH
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
methodology == CoralogixLogsMethodology.ARCHIVE_FALLBACK and not result.logs
|
|
137
|
+
) or methodology == CoralogixLogsMethodology.BOTH_FREQUENT_SEARCH_AND_ARCHIVE:
|
|
138
|
+
archive_search_results = query_logs_for_tier(
|
|
139
|
+
config=config, params=params, tier=CoralogixTier.ARCHIVE
|
|
140
|
+
)
|
|
141
|
+
result = merge_log_results(result, archive_search_results)
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
# methodology in [CoralogixLogsMethodology.ARCHIVE_ONLY, CoralogixLogsMethodology.FREQUENT_SEARCH_FALLBACK]:
|
|
145
|
+
result = query_logs_for_tier(
|
|
146
|
+
config=config, params=params, tier=CoralogixTier.ARCHIVE
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
methodology == CoralogixLogsMethodology.FREQUENT_SEARCH_FALLBACK
|
|
151
|
+
and not result.logs
|
|
152
|
+
):
|
|
153
|
+
frequent_search_results = query_logs_for_tier(
|
|
154
|
+
config=config, params=params, tier=CoralogixTier.FREQUENT_SEARCH
|
|
155
|
+
)
|
|
156
|
+
result = merge_log_results(result, frequent_search_results)
|
|
157
|
+
|
|
158
|
+
return result
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Any, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from holmes.core.tools import (
|
|
4
|
+
CallablePrerequisite,
|
|
5
|
+
StructuredToolResult,
|
|
6
|
+
ToolResultStatus,
|
|
7
|
+
ToolsetTag,
|
|
8
|
+
)
|
|
9
|
+
from holmes.plugins.toolsets.consts import (
|
|
10
|
+
TOOLSET_CONFIG_MISSING_ERROR,
|
|
11
|
+
)
|
|
12
|
+
from holmes.plugins.toolsets.coralogix.api import (
|
|
13
|
+
build_query_string,
|
|
14
|
+
get_start_end,
|
|
15
|
+
health_check,
|
|
16
|
+
query_logs_for_all_tiers,
|
|
17
|
+
)
|
|
18
|
+
from holmes.plugins.toolsets.coralogix.utils import (
|
|
19
|
+
CoralogixConfig,
|
|
20
|
+
build_coralogix_link_to_logs,
|
|
21
|
+
stringify_flattened_logs,
|
|
22
|
+
)
|
|
23
|
+
from holmes.plugins.toolsets.logging_utils.logging_api import (
|
|
24
|
+
BasePodLoggingToolset,
|
|
25
|
+
FetchPodLogsParams,
|
|
26
|
+
PodLoggingTool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CoralogixLogsToolset(BasePodLoggingToolset):
|
|
31
|
+
def __init__(self):
|
|
32
|
+
super().__init__(
|
|
33
|
+
name="coralogix/logs",
|
|
34
|
+
description="Toolset for interacting with Coralogix to fetch logs",
|
|
35
|
+
docs_url="https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/coralogix_logs.html",
|
|
36
|
+
icon_url="https://avatars.githubusercontent.com/u/35295744?s=200&v=4",
|
|
37
|
+
prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
|
|
38
|
+
tools=[
|
|
39
|
+
PodLoggingTool(self),
|
|
40
|
+
],
|
|
41
|
+
tags=[ToolsetTag.CORE],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def get_example_config(self):
|
|
45
|
+
example_config = CoralogixConfig(
|
|
46
|
+
api_key="<cxuw_...>", team_hostname="my-team", domain="eu2.coralogix.com"
|
|
47
|
+
)
|
|
48
|
+
return example_config.model_dump()
|
|
49
|
+
|
|
50
|
+
def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
|
|
51
|
+
if not config:
|
|
52
|
+
return False, TOOLSET_CONFIG_MISSING_ERROR
|
|
53
|
+
|
|
54
|
+
self.config = CoralogixConfig(**config)
|
|
55
|
+
|
|
56
|
+
if not self.config.api_key:
|
|
57
|
+
return False, "Missing configuration field 'api_key'"
|
|
58
|
+
|
|
59
|
+
return health_check(domain=self.config.domain, api_key=self.config.api_key)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def coralogix_config(self) -> Optional[CoralogixConfig]:
|
|
63
|
+
return self.config
|
|
64
|
+
|
|
65
|
+
def fetch_pod_logs(self, params: FetchPodLogsParams) -> StructuredToolResult:
|
|
66
|
+
if not self.coralogix_config:
|
|
67
|
+
return StructuredToolResult(
|
|
68
|
+
status=ToolResultStatus.ERROR,
|
|
69
|
+
error=f"The {self.name} toolset is not configured",
|
|
70
|
+
params=params.model_dump(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
logs_data = query_logs_for_all_tiers(
|
|
74
|
+
config=self.coralogix_config, params=params
|
|
75
|
+
)
|
|
76
|
+
(start, end) = get_start_end(params=params)
|
|
77
|
+
query_string = build_query_string(config=self.coralogix_config, params=params)
|
|
78
|
+
|
|
79
|
+
url = build_coralogix_link_to_logs(
|
|
80
|
+
config=self.coralogix_config,
|
|
81
|
+
lucene_query=query_string,
|
|
82
|
+
start=start,
|
|
83
|
+
end=end,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
data: str
|
|
87
|
+
if logs_data.error:
|
|
88
|
+
data = logs_data.error
|
|
89
|
+
else:
|
|
90
|
+
logs = stringify_flattened_logs(logs_data.logs)
|
|
91
|
+
# Remove link and query from results once the UI and slackbot properly handle the URL from the StructuredToolResult
|
|
92
|
+
data = f"link: {url}\nquery: {query_string}\n{logs}"
|
|
93
|
+
|
|
94
|
+
return StructuredToolResult(
|
|
95
|
+
status=(
|
|
96
|
+
ToolResultStatus.ERROR if logs_data.error else ToolResultStatus.SUCCESS
|
|
97
|
+
),
|
|
98
|
+
error=logs_data.error,
|
|
99
|
+
data=data,
|
|
100
|
+
url=url,
|
|
101
|
+
invocation=query_string,
|
|
102
|
+
params=params.model_dump(),
|
|
103
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, NamedTuple, Optional, Dict, List
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlattenedLog(NamedTuple):
|
|
12
|
+
timestamp: str
|
|
13
|
+
log_message: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CoralogixQueryResult(BaseModel):
|
|
17
|
+
logs: List[FlattenedLog]
|
|
18
|
+
http_status: Optional[int]
|
|
19
|
+
error: Optional[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CoralogixLabelsConfig(BaseModel):
|
|
23
|
+
pod: str = "kubernetes.pod_name"
|
|
24
|
+
namespace: str = "kubernetes.namespace_name"
|
|
25
|
+
log_message: str = "log"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoralogixLogsMethodology(str, Enum):
|
|
29
|
+
FREQUENT_SEARCH_ONLY = "FREQUENT_SEARCH_ONLY"
|
|
30
|
+
ARCHIVE_ONLY = "ARCHIVE_ONLY"
|
|
31
|
+
ARCHIVE_FALLBACK = "ARCHIVE_FALLBACK"
|
|
32
|
+
FREQUENT_SEARCH_FALLBACK = "FREQUENT_SEARCH_FALLBACK"
|
|
33
|
+
BOTH_FREQUENT_SEARCH_AND_ARCHIVE = "BOTH_FREQUENT_SEARCH_AND_ARCHIVE"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CoralogixConfig(BaseModel):
|
|
37
|
+
team_hostname: str
|
|
38
|
+
domain: str
|
|
39
|
+
api_key: str
|
|
40
|
+
labels: CoralogixLabelsConfig = CoralogixLabelsConfig()
|
|
41
|
+
logs_retrieval_methodology: CoralogixLogsMethodology = (
|
|
42
|
+
CoralogixLogsMethodology.ARCHIVE_FALLBACK
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_json_lines(raw_text) -> List[Dict[str, Any]]:
|
|
47
|
+
"""Parses JSON objects from a raw text response."""
|
|
48
|
+
json_objects = []
|
|
49
|
+
for line in raw_text.strip().split("\n"): # Split by newlines
|
|
50
|
+
try:
|
|
51
|
+
json_objects.append(json.loads(line))
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
logging.error(f"Failed to decode JSON from line: {line}")
|
|
54
|
+
return json_objects
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def normalize_datetime(date_str: Optional[str]) -> str:
|
|
58
|
+
"""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.
|
|
59
|
+
if any error occurs during parsing or formatting, it returns the original input string.
|
|
60
|
+
The method specifically handles older Python versions by removing a trailing “Z” and truncating microseconds to 6 digits before parsing.
|
|
61
|
+
"""
|
|
62
|
+
if not date_str:
|
|
63
|
+
return "UNKNOWN_TIMESTAMP"
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# older versions of python do not support `Z` appendix nor more than 6 digits of microsecond precision
|
|
67
|
+
date_str_no_z = date_str.rstrip("Z")
|
|
68
|
+
|
|
69
|
+
parts = date_str_no_z.split(".")
|
|
70
|
+
if len(parts) > 1 and len(parts[1]) > 6:
|
|
71
|
+
date_str_no_z = f"{parts[0]}.{parts[1][:6]}"
|
|
72
|
+
|
|
73
|
+
date = datetime.fromisoformat(date_str_no_z)
|
|
74
|
+
|
|
75
|
+
normalized_date_time = date.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
76
|
+
return normalized_date_time + "Z"
|
|
77
|
+
except Exception:
|
|
78
|
+
return date_str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def flatten_structured_log_entries(
|
|
82
|
+
log_entries: List[Dict[str, Any]],
|
|
83
|
+
) -> List[FlattenedLog]:
|
|
84
|
+
flattened_logs = []
|
|
85
|
+
for log_entry in log_entries:
|
|
86
|
+
try:
|
|
87
|
+
user_data = json.loads(log_entry.get("userData", "{}"))
|
|
88
|
+
timestamp = normalize_datetime(user_data.get("time"))
|
|
89
|
+
log_message = user_data.get("log", "")
|
|
90
|
+
if log_message:
|
|
91
|
+
flattened_logs.append(
|
|
92
|
+
FlattenedLog(timestamp=timestamp, log_message=log_message)
|
|
93
|
+
) # Store as tuple for sorting
|
|
94
|
+
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
logging.error(
|
|
97
|
+
f"Failed to decode userData JSON: {log_entry.get('userData')}"
|
|
98
|
+
)
|
|
99
|
+
return flattened_logs
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def stringify_flattened_logs(log_entries: List[FlattenedLog]) -> str:
|
|
103
|
+
formatted_logs = []
|
|
104
|
+
for entry in log_entries:
|
|
105
|
+
formatted_logs.append(entry.log_message)
|
|
106
|
+
|
|
107
|
+
return "\n".join(formatted_logs) if formatted_logs else "No logs found."
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_json_objects(json_objects: List[Dict[str, Any]]) -> List[FlattenedLog]:
|
|
111
|
+
"""Extracts timestamp and log values from parsed JSON objects, sorted in ascending order (oldest first)."""
|
|
112
|
+
logs: List[FlattenedLog] = []
|
|
113
|
+
|
|
114
|
+
for data in json_objects:
|
|
115
|
+
if isinstance(data, dict) and "result" in data and "results" in data["result"]:
|
|
116
|
+
logs += flatten_structured_log_entries(
|
|
117
|
+
log_entries=data["result"]["results"]
|
|
118
|
+
)
|
|
119
|
+
elif isinstance(data, dict) and data.get("warning"):
|
|
120
|
+
logging.info(
|
|
121
|
+
f"Received the following warning when fetching coralogix logs: {data}"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
logging.debug(f"Unrecognised partial response from coralogix logs: {data}")
|
|
125
|
+
|
|
126
|
+
logs.sort(key=lambda x: x[0])
|
|
127
|
+
|
|
128
|
+
return logs
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_logs(raw_logs: str) -> List[FlattenedLog]:
|
|
132
|
+
"""Processes the HTTP response and extracts only log outputs."""
|
|
133
|
+
try:
|
|
134
|
+
json_objects = parse_json_lines(raw_logs)
|
|
135
|
+
if not json_objects:
|
|
136
|
+
raise Exception("No valid JSON objects found.")
|
|
137
|
+
return parse_json_objects(json_objects)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logging.error(
|
|
140
|
+
f"Unexpected error in format_logs for a coralogix API response: {str(e)}"
|
|
141
|
+
)
|
|
142
|
+
raise e
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_coralogix_link_to_logs(
|
|
146
|
+
config: CoralogixConfig, lucene_query: str, start: str, end: str
|
|
147
|
+
) -> str:
|
|
148
|
+
query_param = urllib.parse.quote_plus(lucene_query)
|
|
149
|
+
|
|
150
|
+
return f"https://{config.team_hostname}.app.{config.domain}/#/query-new/logs?query={query_param}&querySyntax=dataprime&time=from:{start},to:{end}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def merge_log_results(
|
|
154
|
+
a: CoralogixQueryResult, b: CoralogixQueryResult
|
|
155
|
+
) -> CoralogixQueryResult:
|
|
156
|
+
"""
|
|
157
|
+
Merges two CoralogixQueryResult objects, deduplicating logs and sorting them by timestamp.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
if a.error is None and b.error:
|
|
161
|
+
return a
|
|
162
|
+
elif b.error is None and a.error:
|
|
163
|
+
return b
|
|
164
|
+
elif a.error and b.error:
|
|
165
|
+
return a
|
|
166
|
+
|
|
167
|
+
combined_logs = a.logs + b.logs
|
|
168
|
+
|
|
169
|
+
if not combined_logs:
|
|
170
|
+
deduplicated_logs_set = set()
|
|
171
|
+
else:
|
|
172
|
+
deduplicated_logs_set = set(combined_logs)
|
|
173
|
+
|
|
174
|
+
# Assumes timestamps are in a format sortable as strings (e.g., ISO 8601)
|
|
175
|
+
sorted_logs = sorted(list(deduplicated_logs_set), key=lambda log: log.timestamp)
|
|
176
|
+
|
|
177
|
+
return CoralogixQueryResult(
|
|
178
|
+
logs=sorted_logs,
|
|
179
|
+
http_status=a.http_status if a.http_status is not None else b.http_status,
|
|
180
|
+
error=a.error,
|
|
181
|
+
)
|