4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
286 lines
11 KiB
Python
286 lines
11 KiB
Python
"""Honeycomb API client for RCA query helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from integrations.config_models import HoneycombIntegrationConfig
|
|
from integrations.probes import ProbeResult
|
|
from platform.observability.errors.service import capture_service_error
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
_DEFAULT_POLL_ATTEMPTS = 10
|
|
_DEFAULT_POLL_INTERVAL_SECONDS = 0.5
|
|
|
|
|
|
class HoneycombClient:
|
|
"""Synchronous Honeycomb client for validation and query execution."""
|
|
|
|
def __init__(self, config: HoneycombIntegrationConfig) -> None:
|
|
self.config = config
|
|
self._client: httpx.Client | None = None
|
|
|
|
def _get_client(self) -> httpx.Client:
|
|
if self._client is None:
|
|
self._client = httpx.Client(
|
|
base_url=self.config.base_url,
|
|
headers={
|
|
"X-Honeycomb-Team": self.config.api_key,
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
},
|
|
timeout=_DEFAULT_TIMEOUT_SECONDS,
|
|
)
|
|
return self._client
|
|
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
return bool(self.config.api_key and self.config.dataset)
|
|
|
|
def probe_access(self) -> ProbeResult:
|
|
"""Validate Honeycomb credentials and run a minimal query."""
|
|
if not self.is_configured:
|
|
return ProbeResult.missing("Missing Honeycomb API key or dataset.")
|
|
|
|
auth_result = self.validate_access()
|
|
if not auth_result.get("success"):
|
|
return ProbeResult.failed(
|
|
f"Auth check failed: {auth_result.get('error', 'unknown error')}"
|
|
)
|
|
|
|
query_result = self.run_query(
|
|
{
|
|
"calculations": [{"op": "COUNT"}],
|
|
"time_range": 900,
|
|
},
|
|
limit=1,
|
|
)
|
|
if not query_result.get("success"):
|
|
return ProbeResult.failed(
|
|
f"Query check failed: {query_result.get('error', 'unknown error')}"
|
|
)
|
|
|
|
environment = auth_result.get("environment", {})
|
|
environment_slug = (
|
|
str(environment.get("slug", "")).strip() if isinstance(environment, dict) else ""
|
|
)
|
|
environment_label = environment_slug or "classic"
|
|
return ProbeResult.passed(
|
|
(
|
|
f"Connected to {self.config.base_url} "
|
|
f"(environment {environment_label}) and queried dataset {self.config.dataset}."
|
|
),
|
|
dataset=self.config.dataset,
|
|
environment=environment_label,
|
|
)
|
|
|
|
def validate_access(self) -> dict[str, Any]:
|
|
"""Validate the Honeycomb API key against the auth endpoint."""
|
|
try:
|
|
response = self._get_client().get("/1/auth")
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="validate_access"
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
|
|
}
|
|
except Exception as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="validate_access"
|
|
)
|
|
return {"success": False, "error": str(exc)}
|
|
|
|
environment = payload.get("environment", {}) if isinstance(payload, dict) else {}
|
|
team = payload.get("team", {}) if isinstance(payload, dict) else {}
|
|
return {
|
|
"success": True,
|
|
"environment": environment,
|
|
"team": team,
|
|
"key_type": str(payload.get("type", "")).strip() if isinstance(payload, dict) else "",
|
|
}
|
|
|
|
def create_query(self, query: dict[str, Any]) -> dict[str, Any]:
|
|
"""Create a Honeycomb query specification."""
|
|
try:
|
|
response = self._get_client().post(f"/1/queries/{self.config.dataset}", json=query)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="create_query"
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
|
|
}
|
|
except Exception as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="create_query"
|
|
)
|
|
return {"success": False, "error": str(exc)}
|
|
|
|
query_id = str(payload.get("id", "")).strip() if isinstance(payload, dict) else ""
|
|
if not query_id:
|
|
return {"success": False, "error": "Honeycomb query creation returned no query ID."}
|
|
return {"success": True, "query_id": query_id, "query": payload}
|
|
|
|
def create_query_result(self, query_id: str, *, limit: int) -> dict[str, Any]:
|
|
"""Run a previously created query and return the query-result envelope."""
|
|
payload = {
|
|
"query_id": query_id,
|
|
"disable_series": True,
|
|
"disable_total_by_aggregate": True,
|
|
"disable_other_by_aggregate": True,
|
|
"limit": limit,
|
|
}
|
|
try:
|
|
response = self._get_client().post(
|
|
f"/1/query_results/{self.config.dataset}",
|
|
json=payload,
|
|
)
|
|
response.raise_for_status()
|
|
result = response.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="create_query_result"
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
|
|
}
|
|
except Exception as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="create_query_result"
|
|
)
|
|
return {"success": False, "error": str(exc)}
|
|
|
|
return {"success": True, "result": result}
|
|
|
|
def get_query_result(self, query_result_id: str) -> dict[str, Any]:
|
|
"""Fetch an existing query result."""
|
|
try:
|
|
response = self._get_client().get(
|
|
f"/1/query_results/{self.config.dataset}/{query_result_id}"
|
|
)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="get_query_result"
|
|
)
|
|
return {
|
|
"success": False,
|
|
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
|
|
}
|
|
except Exception as exc:
|
|
capture_service_error(
|
|
exc, logger=logger, integration="honeycomb", method="get_query_result"
|
|
)
|
|
return {"success": False, "error": str(exc)}
|
|
return {"success": True, "result": payload}
|
|
|
|
def run_query(
|
|
self,
|
|
query: dict[str, Any],
|
|
*,
|
|
limit: int = 20,
|
|
poll_attempts: int = _DEFAULT_POLL_ATTEMPTS,
|
|
poll_interval_seconds: float = _DEFAULT_POLL_INTERVAL_SECONDS,
|
|
) -> dict[str, Any]:
|
|
"""Create, execute, and poll a Honeycomb query result until completion."""
|
|
created_query = self.create_query(query)
|
|
if not created_query.get("success"):
|
|
return created_query
|
|
|
|
query_id = str(created_query.get("query_id", "")).strip()
|
|
created_result = self.create_query_result(query_id, limit=limit)
|
|
if not created_result.get("success"):
|
|
return created_result
|
|
|
|
result = created_result.get("result", {})
|
|
query_result_id = str(result.get("id", "")).strip() if isinstance(result, dict) else ""
|
|
if not query_result_id:
|
|
return {"success": False, "error": "Honeycomb query result returned no result ID."}
|
|
|
|
current_result = result
|
|
for _ in range(max(poll_attempts, 1)):
|
|
if current_result.get("complete") is True:
|
|
break
|
|
time.sleep(max(poll_interval_seconds, 0.0))
|
|
fetched = self.get_query_result(query_result_id)
|
|
if not fetched.get("success"):
|
|
return fetched
|
|
fetched_result = fetched.get("result", {})
|
|
if isinstance(fetched_result, dict):
|
|
current_result = fetched_result
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"error": "Honeycomb query result did not complete before the timeout.",
|
|
}
|
|
|
|
data = current_result.get("data", {}) if isinstance(current_result, dict) else {}
|
|
raw_results = data.get("results", []) if isinstance(data, dict) else []
|
|
results = [
|
|
item.get("data", {})
|
|
for item in raw_results
|
|
if isinstance(item, dict) and isinstance(item.get("data"), dict)
|
|
]
|
|
links = current_result.get("links", {}) if isinstance(current_result, dict) else {}
|
|
|
|
return {
|
|
"success": True,
|
|
"query_id": query_id,
|
|
"query_result_id": query_result_id,
|
|
"query": query,
|
|
"results": results,
|
|
"raw_result": current_result,
|
|
"query_url": str(links.get("query_url", "")).strip(),
|
|
"graph_image_url": str(links.get("graph_image_url", "")).strip(),
|
|
}
|
|
|
|
def query_traces(
|
|
self,
|
|
*,
|
|
service_name: str = "",
|
|
trace_id: str = "",
|
|
time_range_seconds: int = 3600,
|
|
limit: int = 20,
|
|
) -> dict[str, Any]:
|
|
"""Query Honeycomb trace/span groups for a service or trace ID."""
|
|
filters: list[dict[str, Any]] = []
|
|
if service_name:
|
|
filters.append({"column": "service.name", "op": "=", "value": service_name})
|
|
if trace_id:
|
|
filters.append({"column": "trace.trace_id", "op": "=", "value": trace_id})
|
|
|
|
if not filters:
|
|
return {
|
|
"success": False,
|
|
"error": "Honeycomb trace queries require a service_name or trace_id.",
|
|
}
|
|
|
|
query = {
|
|
"calculations": [
|
|
{"op": "COUNT"},
|
|
{"op": "MAX", "column": "duration_ms"},
|
|
{"op": "AVG", "column": "duration_ms"},
|
|
],
|
|
"breakdowns": ["trace.trace_id", "service.name", "name"],
|
|
"filters": filters,
|
|
"filter_combination": "AND",
|
|
"orders": [{"op": "MAX", "column": "duration_ms", "order": "descending"}],
|
|
"time_range": max(int(time_range_seconds), 60),
|
|
}
|
|
return self.run_query(query, limit=limit)
|