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
194 lines
6.7 KiB
Python
194 lines
6.7 KiB
Python
"""OpenObserve log search tool with bounded read-only retrieval."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from core.tool_framework.telemetry import report_run_error
|
|
from core.tool_framework.tool_decorator import tool
|
|
|
|
_DEFAULT_MAX_RESULTS = 100
|
|
_MAX_HARD_LIMIT = 200
|
|
|
|
|
|
def _bounded_limit(limit: int, max_results: int) -> int:
|
|
safe_max = max(1, min(max_results, _MAX_HARD_LIMIT))
|
|
return max(1, min(limit, safe_max))
|
|
|
|
|
|
def _openobserve_available(sources: dict[str, dict[str, Any]]) -> bool:
|
|
oo = sources.get("openobserve", {})
|
|
has_token = bool(str(oo.get("api_token", "")).strip())
|
|
has_user_password = bool(
|
|
str(oo.get("username", "")).strip() and str(oo.get("password", "")).strip()
|
|
)
|
|
return bool(
|
|
oo.get("connection_verified") and oo.get("base_url") and (has_token or has_user_password)
|
|
)
|
|
|
|
|
|
def _openobserve_extract_params(sources: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
oo = sources["openobserve"]
|
|
return {
|
|
"base_url": str(oo.get("base_url", "")).strip(),
|
|
"org": str(oo.get("org", "default")).strip() or "default",
|
|
"stream": str(oo.get("stream", "")).strip(),
|
|
"query": str(oo.get("query", "")).strip(),
|
|
"api_token": str(oo.get("api_token", "")).strip(),
|
|
"username": str(oo.get("username", "")).strip(),
|
|
"password": str(oo.get("password", "")).strip(),
|
|
"time_range_minutes": int(oo.get("time_range_minutes", 60) or 60),
|
|
"limit": 50,
|
|
"max_results": int(oo.get("max_results", _DEFAULT_MAX_RESULTS) or _DEFAULT_MAX_RESULTS),
|
|
"integration_id": str(oo.get("integration_id", "")).strip(),
|
|
}
|
|
|
|
|
|
def _auth_headers(api_token: str, username: str, password: str) -> dict[str, str]:
|
|
if api_token:
|
|
return {"Authorization": f"Bearer {api_token}"}
|
|
credentials = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
|
|
return {"Authorization": f"Basic {credentials}"}
|
|
|
|
|
|
def _extract_records(body: dict[str, Any]) -> list[dict[str, Any]]:
|
|
hits = body.get("hits")
|
|
if isinstance(hits, dict):
|
|
hit_docs = hits.get("hits", [])
|
|
if isinstance(hit_docs, list):
|
|
records: list[dict[str, Any]] = []
|
|
for hit in hit_docs:
|
|
if isinstance(hit, dict):
|
|
source = hit.get("_source", {})
|
|
if isinstance(source, dict):
|
|
records.append(source)
|
|
return records
|
|
if isinstance(hits, list):
|
|
return [item for item in hits if isinstance(item, dict)]
|
|
raw_records = body.get("records")
|
|
if isinstance(raw_records, list):
|
|
return [item for item in raw_records if isinstance(item, dict)]
|
|
data = body.get("data")
|
|
if isinstance(data, list):
|
|
return [item for item in data if isinstance(item, dict)]
|
|
return []
|
|
|
|
|
|
@tool(
|
|
name="query_openobserve_logs",
|
|
description="Query OpenObserve logs using bounded read-only search.",
|
|
source="openobserve",
|
|
surfaces=("investigation", "chat"),
|
|
requires=["base_url"],
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {
|
|
"base_url": {"type": "string"},
|
|
"org": {"type": "string", "default": "default"},
|
|
"stream": {"type": "string", "default": ""},
|
|
"query": {"type": "string"},
|
|
"api_token": {"type": "string"},
|
|
"username": {"type": "string"},
|
|
"password": {"type": "string"},
|
|
"time_range_minutes": {"type": "integer", "default": 60},
|
|
"limit": {"type": "integer", "default": 50},
|
|
"max_results": {"type": "integer", "default": 100},
|
|
"integration_id": {"type": "string"},
|
|
"timeout_seconds": {"type": "number", "default": 20.0},
|
|
},
|
|
"required": ["base_url"],
|
|
},
|
|
is_available=_openobserve_available,
|
|
injected_params=("base_url",),
|
|
extract_params=_openobserve_extract_params,
|
|
)
|
|
def query_openobserve_logs(
|
|
base_url: str,
|
|
org: str = "default",
|
|
stream: str = "",
|
|
query: str = "",
|
|
api_token: str = "",
|
|
username: str = "",
|
|
password: str = "",
|
|
time_range_minutes: int = 60,
|
|
limit: int = 50,
|
|
max_results: int = _DEFAULT_MAX_RESULTS,
|
|
integration_id: str = "",
|
|
timeout_seconds: float = 20.0,
|
|
**_kwargs: Any,
|
|
) -> dict[str, Any]:
|
|
"""Fetch bounded evidence from OpenObserve."""
|
|
base = base_url.strip().rstrip("/")
|
|
if not base:
|
|
return {
|
|
"source": "openobserve",
|
|
"available": False,
|
|
"error": "Missing OpenObserve URL.",
|
|
"records": [],
|
|
}
|
|
auth_token = api_token.strip()
|
|
user = username.strip()
|
|
secret = password.strip()
|
|
if not auth_token and not (user and secret):
|
|
return {
|
|
"source": "openobserve",
|
|
"available": False,
|
|
"error": "Missing OpenObserve credentials.",
|
|
"records": [],
|
|
}
|
|
|
|
effective_limit = _bounded_limit(limit, max_results)
|
|
now = datetime.now(UTC)
|
|
start = now - timedelta(minutes=max(1, time_range_minutes))
|
|
normalized_query = query.strip() or (
|
|
"SELECT * FROM \"default\" WHERE level = 'error' ORDER BY _timestamp DESC"
|
|
)
|
|
|
|
endpoint = f"{base}/api/{(org or 'default').strip()}/_search"
|
|
payload: dict[str, Any] = {
|
|
"query": {
|
|
"sql": normalized_query,
|
|
"start_time": int(start.timestamp() * 1000),
|
|
"end_time": int(now.timestamp() * 1000),
|
|
},
|
|
"size": effective_limit,
|
|
}
|
|
if stream.strip():
|
|
payload["stream_name"] = stream.strip()
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
headers.update(_auth_headers(auth_token, user, secret))
|
|
|
|
try:
|
|
response = httpx.post(
|
|
endpoint, headers=headers, json=payload, timeout=max(1.0, timeout_seconds)
|
|
)
|
|
response.raise_for_status()
|
|
body = response.json()
|
|
except Exception as err:
|
|
report_run_error(
|
|
err,
|
|
tool_name="query_openobserve_logs",
|
|
source="openobserve",
|
|
component="integrations.openobserve.tools.openobserve_logs_tool",
|
|
method="httpx.post",
|
|
extras={"endpoint": endpoint, "integration_id": integration_id},
|
|
)
|
|
return {"source": "openobserve", "available": False, "error": str(err), "records": []}
|
|
|
|
records = _extract_records(body if isinstance(body, dict) else {})[:effective_limit]
|
|
return {
|
|
"source": "openobserve",
|
|
"available": True,
|
|
"org": (org or "default").strip() or "default",
|
|
"stream": stream.strip(),
|
|
"integration_id": integration_id,
|
|
"query": normalized_query,
|
|
"total_returned": len(records),
|
|
"records": records,
|
|
}
|