Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

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)