blob: 28b54c583d8288f38f185bcae128dbd273241dd4 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Strict, evidence-bearing runtime for the read-only Cluster domain."""
from __future__ import annotations
import re
import uuid
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any
from .db import DorisConnectionManager
from .monitoring_tools import DorisMonitoringTools
from .redaction import redact_sensitive_data
from .security import get_current_auth_context
_MAX_ROWS = 1_000
_MAX_METRIC_NAMES = 100
_MAX_METRICS_PER_NODE = 256
_MEMORY_MARKERS = ("memory", "mem_tracker", "memtable", "jemalloc")
_CACHE_MARKERS = ("cache", "file_cache")
_COMPACTION_MARKERS = ("compaction",)
_SNAKE_BOUNDARY = re.compile(r"(?<!^)(?=[A-Z])")
_SAFE_KEY = re.compile(r"[^a-z0-9]+")
_GROWTH_SQL = {
("query_volume", "hour"): (
"SELECT DATE_FORMAT(`time`, '%%Y-%%m-%%dT%%H:00:00') AS bucket, "
"COUNT(*) AS value "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("query_volume", "day"): (
"SELECT DATE(`time`) AS bucket, COUNT(*) AS value "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("query_volume", "week"): (
"SELECT DATE_FORMAT("
"DATE_SUB(DATE(`time`), INTERVAL WEEKDAY(`time`) DAY), "
"'%%Y-%%m-%%d') AS bucket, COUNT(*) AS value "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("user_activity", "hour"): (
"SELECT DATE_FORMAT(`time`, '%%Y-%%m-%%dT%%H:00:00') AS bucket, "
"COUNT(DISTINCT `user`) AS value "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("user_activity", "day"): (
"SELECT DATE(`time`) AS bucket, COUNT(DISTINCT `user`) AS value "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("user_activity", "week"): (
"SELECT DATE_FORMAT("
"DATE_SUB(DATE(`time`), INTERVAL WEEKDAY(`time`) DAY), "
"'%%Y-%%m-%%d') AS bucket, COUNT(DISTINCT `user`) AS value "
"FROM internal.__internal_schema.audit_log "
"WHERE `time` >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("storage", "hour"): (
"SELECT DATE_FORMAT(CREATE_TIME, '%%Y-%%m-%%dT%%H:00:00') AS bucket, "
"SUM(COALESCE(DATA_LENGTH, 0) + COALESCE(INDEX_LENGTH, 0)) AS value "
"FROM information_schema.partitions "
"WHERE CREATE_TIME >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("storage", "day"): (
"SELECT DATE(CREATE_TIME) AS bucket, "
"SUM(COALESCE(DATA_LENGTH, 0) + COALESCE(INDEX_LENGTH, 0)) AS value "
"FROM information_schema.partitions "
"WHERE CREATE_TIME >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
("storage", "week"): (
"SELECT DATE_FORMAT("
"DATE_SUB(DATE(CREATE_TIME), INTERVAL WEEKDAY(CREATE_TIME) DAY), "
"'%%Y-%%m-%%d') AS bucket, "
"SUM(COALESCE(DATA_LENGTH, 0) + COALESCE(INDEX_LENGTH, 0)) AS value "
"FROM information_schema.partitions "
"WHERE CREATE_TIME >= DATE_SUB(NOW(), INTERVAL %s DAY) "
"GROUP BY bucket ORDER BY bucket"
),
}
class ClusterRuntimeFailure(RuntimeError):
"""Sanitized Cluster-domain failure with a stable reason code."""
def __init__(
self,
message: str,
*,
reason_code: str,
status_code: int,
retryable: bool = False,
) -> None:
super().__init__(message)
self.reason_code = reason_code
self.status_code = status_code
self.retryable = retryable
class DorisClusterRuntime:
"""Read bounded cluster metadata and monitoring evidence without invention."""
def __init__(
self,
connection_manager: DorisConnectionManager,
monitoring_tools: DorisMonitoringTools,
) -> None:
self._connection_manager = connection_manager
self._monitoring_tools = monitoring_tools
self._session_prefix = f"formal_cluster_{uuid.uuid4().hex[:8]}"
async def get_cluster_overview(
self,
*,
include: Sequence[str] | None,
) -> dict[str, Any]:
sections = tuple(
dict.fromkeys(
include
or (
"nodes",
"tasks",
"metrics",
"memory",
"cache",
"compaction",
)
)
)
data: dict[str, Any] = {}
evidence: list[dict[str, Any]] = []
warnings: list[str] = []
operations = {
"nodes": lambda: self.list_cluster_nodes(
node_types=None,
include_metrics=False,
),
"tasks": lambda: self.list_active_tasks(
task_types=None,
states=None,
limit=100,
),
"metrics": lambda: self.get_monitoring_metrics(
metric_names=None,
node_ids=None,
window=None,
),
"memory": lambda: self.get_memory_stats(
node_ids=None,
detail="summary",
),
"cache": lambda: self.get_cache_status(
scope=None,
node_ids=None,
include_queues=False,
),
"compaction": lambda: self.get_compaction_status(
database=None,
table=None,
state=None,
limit=100,
),
}
for section in sections:
operation = operations[section]
try:
result = await operation()
except ClusterRuntimeFailure as exc:
data[section] = {
"status": "unavailable",
"reason_code": exc.reason_code,
}
warnings.append(
f"{section} evidence is unavailable ({exc.reason_code})."
)
evidence.append(
{
"section": section,
"success": False,
"reason_code": exc.reason_code,
}
)
continue
data[section] = {
"status": result["status"],
"data": result["data"],
}
warnings.extend(result["warnings"])
evidence.append(
{
"section": section,
"success": True,
"source": result["metadata"].get("source"),
}
)
if not any(item["success"] for item in evidence):
raise ClusterRuntimeFailure(
"Doris cluster evidence is unavailable.",
reason_code="CLUSTER_EVIDENCE_UNAVAILABLE",
status_code=503,
retryable=True,
)
return _result(
data,
source="cluster_composite",
warnings=warnings,
evidence=evidence,
metadata={"sections": list(sections)},
)
async def list_cluster_nodes(
self,
*,
node_types: Sequence[str] | None,
include_metrics: bool,
) -> dict[str, Any]:
requested = set(node_types or ("fe", "be", "broker"))
statements = {
"fe": "SHOW FRONTENDS",
"be": "SHOW BACKENDS",
"broker": "SHOW BROKER",
}
items: list[dict[str, Any]] = []
warnings: list[str] = []
successful_sources: list[str] = []
for node_type in ("fe", "be", "broker"):
if node_type not in requested:
continue
try:
rows = await self._execute(statements[node_type], max_rows=512)
except ClusterRuntimeFailure as exc:
if node_type in {"fe", "be"}:
warnings.append(
f"{node_type.upper()} metadata is unavailable "
f"({exc.reason_code})."
)
continue
successful_sources.append(statements[node_type])
items.extend(_node_item(node_type, row) for row in rows)
if not successful_sources:
raise ClusterRuntimeFailure(
"Doris node metadata is unavailable.",
reason_code="CLUSTER_NODE_METADATA_UNAVAILABLE",
status_code=503,
retryable=True,
)
metric_nodes: dict[str, dict[str, Any]] = {}
if include_metrics:
try:
monitoring_result = await self.get_monitoring_metrics(
metric_names=None,
node_ids=None,
window=None,
)
except ClusterRuntimeFailure as exc:
warnings.append(f"Node metrics are unavailable ({exc.reason_code}).")
else:
for node in monitoring_result["data"].get("nodes", []):
for identity in (
node.get("node_id"),
node.get("host"),
):
if identity:
metric_nodes[str(identity)] = node.get("metrics", {})
for item in items:
node_metrics: dict[str, Any] | None = None
for identity in (item.get("node_id"), item.get("host")):
if identity is not None and str(identity) in metric_nodes:
node_metrics = metric_nodes[str(identity)]
break
if node_metrics is not None:
item["metrics"] = node_metrics
items.sort(
key=lambda item: (
str(item.get("node_type", "")),
str(item.get("node_id", "")),
str(item.get("host", "")),
)
)
return _collection(
items,
source=",".join(successful_sources),
warnings=warnings,
)
async def list_active_tasks(
self,
*,
task_types: Sequence[str] | None,
states: Sequence[str] | None,
limit: int | None,
) -> dict[str, Any]:
requested_types = {value.casefold() for value in (task_types or ("query",))}
max_rows = _bounded_limit(limit, default=100)
items: list[dict[str, Any]] = []
warnings: list[str] = []
sources: list[str] = []
if "query" in requested_types:
query_rows: list[Mapping[str, Any]] | None = None
for statement in (
'SHOW PROC "/current_queries"',
"SELECT * FROM information_schema.active_queries",
"SHOW FULL PROCESSLIST",
):
try:
query_rows = await self._execute(
statement,
max_rows=max_rows + 1,
)
except ClusterRuntimeFailure:
continue
sources.append(statement)
break
if query_rows is None:
warnings.append("Active query tasks are unavailable.")
else:
for row in query_rows:
item = _normalized_row(row)
command = str(item.get("command", "")).casefold()
if command == "sleep":
continue
item["task_type"] = "query"
items.append(item)
if "compaction" in requested_types:
try:
rows = await self._execute(
"SELECT * FROM information_schema.doris_be_compaction_tasks",
max_rows=max_rows + 1,
)
except ClusterRuntimeFailure:
warnings.append(
"Compaction task detail is unavailable; use "
"get_compaction_status for a summary fallback."
)
else:
sources.append("information_schema.doris_be_compaction_tasks")
for row in rows:
item = _normalized_row(row)
item["task_type"] = "compaction"
items.append(item)
unsupported = requested_types - {"query", "compaction"}
if unsupported:
warnings.append(
"No cluster-wide read-only source is available for requested "
f"task types: {', '.join(sorted(unsupported))}."
)
state_filter = {value.casefold() for value in (states or ())}
if state_filter:
items = [
item for item in items if _task_state(item).casefold() in state_filter
]
items.sort(
key=lambda item: (
str(item.get("task_type", "")),
str(
item.get("query_id") or item.get("task_id") or item.get("id") or ""
),
)
)
truncated = len(items) > max_rows
items = items[:max_rows]
if not sources and not items:
raise ClusterRuntimeFailure(
"Doris active task metadata is unavailable.",
reason_code="CLUSTER_TASK_METADATA_UNAVAILABLE",
status_code=503,
retryable=True,
)
return _collection(
items,
source=",".join(sources) or "cluster_task_views",
warnings=warnings,
truncated=truncated,
)
async def get_monitoring_metrics(
self,
*,
metric_names: Sequence[str] | None,
node_ids: Sequence[str] | None,
window: str | None,
) -> dict[str, Any]:
requested_names = tuple(dict.fromkeys(metric_names or ()))
if len(requested_names) > _MAX_METRIC_NAMES:
raise _argument_failure("At most 100 metric names may be requested.")
raw = await self._monitoring_tools.get_monitoring_metrics(
role="all",
monitor_type="all",
priority="all" if requested_names else "p0",
info_only=False,
include_raw_metrics=bool(requested_names),
format_type="prometheus",
)
if raw.get("success") is False:
raise ClusterRuntimeFailure(
"Doris monitoring metrics are unavailable.",
reason_code="CLUSTER_METRICS_UNAVAILABLE",
status_code=503,
retryable=True,
)
nodes, warnings = _monitoring_nodes(
raw,
metric_names=requested_names,
node_ids=node_ids,
)
if not nodes:
raise ClusterRuntimeFailure(
"No readable Doris monitoring endpoint returned metrics.",
reason_code="CLUSTER_METRICS_UNAVAILABLE",
status_code=503,
retryable=True,
)
if window:
warnings.append(
"The Doris metrics endpoint returns a point-in-time snapshot; "
"the requested window was not synthesized."
)
return _result(
{"nodes": nodes},
source="doris_http_metrics",
warnings=warnings,
metadata={
"snapshot_at": raw.get("timestamp"),
"requested_metric_names": list(requested_names),
},
)
async def get_memory_stats(
self,
*,
node_ids: Sequence[str] | None,
detail: str | None,
) -> dict[str, Any]:
nodes, warnings = await self._metric_family(
markers=_MEMORY_MARKERS,
node_ids=node_ids,
)
if not nodes:
raise ClusterRuntimeFailure(
"No real Doris memory metrics were observed.",
reason_code="MEMORY_METRICS_UNAVAILABLE",
status_code=503,
retryable=True,
)
effective_detail = detail or "summary"
if effective_detail == "top_consumers":
for node in nodes:
metrics = node["metrics"]
node["metrics"] = dict(
sorted(
metrics.items(),
key=lambda item: _metric_total(item[1]),
reverse=True,
)[:20]
)
return _result(
{"detail": effective_detail, "nodes": nodes},
source="doris_be_metrics",
warnings=warnings,
metadata={"invented_values": False},
)
async def get_cache_status(
self,
*,
scope: str | None,
node_ids: Sequence[str] | None,
include_queues: bool,
) -> dict[str, Any]:
warnings: list[str] = []
try:
rows = await self._execute(
"SELECT * FROM information_schema.file_cache_statistics",
max_rows=_MAX_ROWS,
)
except ClusterRuntimeFailure as exc:
warnings.append(
"File-cache system table is unavailable; using BE metrics "
f"({exc.reason_code})."
)
else:
items = [_normalized_row(row) for row in rows]
if node_ids:
allowed = {str(value) for value in node_ids}
items = [
item
for item in items
if str(item.get("be_id", "")) in allowed
or str(item.get("be_ip", "")) in allowed
]
if scope:
scope_folded = scope.casefold()
items = [
item
for item in items
if scope_folded
in " ".join(str(value) for value in item.values()).casefold()
]
if not include_queues:
items = [
item
for item in items
if "queue" not in str(item.get("metric_name", "")).casefold()
]
return _result(
{"mode": "system_table", "items": items},
source="information_schema.file_cache_statistics",
warnings=warnings,
metadata={"row_count": len(items)},
)
nodes, metric_warnings = await self._metric_family(
markers=_CACHE_MARKERS,
node_ids=node_ids,
)
warnings.extend(metric_warnings)
if not include_queues:
for node in nodes:
node["metrics"] = {
name: value
for name, value in node["metrics"].items()
if "queue" not in name.casefold()
}
if scope:
scope_folded = scope.casefold()
for node in nodes:
node["metrics"] = {
name: value
for name, value in node["metrics"].items()
if scope_folded in name.casefold()
}
if not any(node["metrics"] for node in nodes):
raise ClusterRuntimeFailure(
"No real Doris cache metrics were observed.",
reason_code="CACHE_METRICS_UNAVAILABLE",
status_code=503,
retryable=True,
)
return _result(
{"mode": "metrics", "nodes": nodes},
source="doris_be_metrics",
warnings=warnings,
)
async def get_compaction_status(
self,
*,
database: str | None,
table: str | None,
state: str | None,
limit: int | None,
) -> dict[str, Any]:
max_rows = _bounded_limit(limit, default=100)
try:
rows = await self._execute(
"SELECT * FROM information_schema.doris_be_compaction_tasks",
max_rows=max_rows + 1,
)
except ClusterRuntimeFailure as exc:
native_failure = exc
else:
items = [_normalized_row(row) for row in rows]
items = _filter_rows(
items,
database=database,
table=table,
state=state,
)
truncated = len(items) > max_rows
return _result(
{
"mode": "native_task_tracker",
"items": items[:max_rows],
"truncated": truncated,
},
source="information_schema.doris_be_compaction_tasks",
metadata={"native_tracker": True},
)
nodes, warnings = await self._metric_family(
markers=_COMPACTION_MARKERS,
node_ids=None,
)
if not any(node["metrics"] for node in nodes):
raise ClusterRuntimeFailure(
"Doris compaction evidence is unavailable.",
reason_code=native_failure.reason_code,
status_code=native_failure.status_code,
retryable=native_failure.retryable,
)
warnings.insert(
0,
"Native CompactionTaskTracker is unavailable; returning aggregate "
"metrics without database, table, state, or task-level detail.",
)
return _result(
{
"mode": "legacy_summary",
"nodes": nodes,
"native_tracker": False,
},
source="doris_metrics_compaction_summary",
warnings=warnings,
metadata={"native_tracker": False},
)
async def get_workload_group_status(
self,
*,
name: str | None,
include_usage: bool,
) -> dict[str, Any]:
rows = await self._execute("SHOW WORKLOAD GROUPS", max_rows=_MAX_ROWS)
items = [_normalized_row(row) for row in rows]
if name:
items = [
item
for item in items
if str(item.get("name", "")).casefold() == name.casefold()
]
if not include_usage:
usage_markers = (
"current_",
"running_",
"waiting_",
"queue_",
"usage",
)
items = [
{
key: value
for key, value in item.items()
if not key.startswith(usage_markers)
}
for item in items
]
return _result(
{"items": items},
source="SHOW WORKLOAD GROUPS",
metadata={"row_count": len(items)},
)
async def get_compute_group_status(
self,
*,
name: str | None,
include_nodes: bool,
include_usage: bool,
) -> dict[str, Any]:
rows = await self._execute("SHOW COMPUTE GROUPS", max_rows=_MAX_ROWS)
items = [_normalized_row(row) for row in rows]
if name:
items = [
item
for item in items
if str(
item.get("name") or item.get("compute_group_name") or ""
).casefold()
== name.casefold()
]
if not include_nodes:
for item in items:
for key in tuple(item):
if "node" in key or "backend" in key:
item.pop(key, None)
if not include_usage:
for item in items:
for key in tuple(item):
if any(
marker in key
for marker in ("usage", "cpu", "memory", "running", "queue")
):
item.pop(key, None)
return _result(
{"items": items},
source="SHOW COMPUTE GROUPS",
metadata={"row_count": len(items)},
)
async def analyze_resource_growth(
self,
*,
resource: str | None,
window_days: int | None,
granularity: str | None,
) -> dict[str, Any]:
days = _bounded_limit(window_days, default=30, maximum=3650)
bucket = granularity or "day"
requested = (
(resource,) if resource else ("storage", "query_volume", "user_activity")
)
series: dict[str, list[dict[str, Any]]] = {}
evidence: list[dict[str, Any]] = []
warnings: list[str] = []
for resource_name in requested:
sql, params = _growth_sql(resource_name, days, bucket)
try:
rows = await self._execute(
sql,
params=params,
max_rows=_MAX_ROWS,
)
except ClusterRuntimeFailure as exc:
warnings.append(
f"{resource_name} history is unavailable ({exc.reason_code})."
)
evidence.append(
{
"resource": resource_name,
"success": False,
"reason_code": exc.reason_code,
}
)
continue
points = [
{
"bucket": _row_lookup(row, "bucket"),
"value": _number(_row_lookup(row, "value")),
}
for row in rows
if _row_lookup(row, "bucket") is not None
]
if len(points) < 2:
warnings.append(
f"{resource_name} history has fewer than two recorded "
"time buckets."
)
evidence.append(
{
"resource": resource_name,
"success": False,
"reason_code": "RESOURCE_HISTORY_INSUFFICIENT",
"points": len(points),
}
)
continue
series[resource_name] = points
evidence.append(
{
"resource": resource_name,
"success": True,
"source": _growth_source(resource_name),
"points": len(points),
}
)
if resource_name == "storage":
warnings.append(
"Storage growth uses partition creation evidence inside the "
"window, not synthesized historical cluster snapshots."
)
if not series:
raise ClusterRuntimeFailure(
"Recorded Doris resource history is unavailable.",
reason_code="RESOURCE_HISTORY_UNAVAILABLE",
status_code=503,
retryable=True,
)
summaries = {name: _growth_summary(points) for name, points in series.items()}
return _result(
{
"window_days": days,
"granularity": bucket,
"series": series,
"summaries": summaries,
},
source="recorded_doris_metadata",
warnings=warnings,
evidence=evidence,
metadata={"invented_history": False},
)
async def _metric_family(
self,
*,
markers: Sequence[str],
node_ids: Sequence[str] | None,
) -> tuple[list[dict[str, Any]], list[str]]:
raw = await self._monitoring_tools.get_monitoring_metrics(
role="be",
monitor_type="all",
priority="all",
info_only=False,
include_raw_metrics=True,
format_type="prometheus",
)
if raw.get("success") is False:
return [], ["BE monitoring metrics are unavailable."]
nodes, warnings = _monitoring_nodes(
raw,
metric_names=None,
node_ids=node_ids,
markers=markers,
)
return nodes, warnings
async def _execute(
self,
sql: str,
*,
params: Mapping[str, Any] | tuple[Any, ...] | None = None,
max_rows: int,
) -> list[Mapping[str, Any]]:
auth_context = get_current_auth_context()
session_id = f"{self._session_prefix}:{uuid.uuid4().hex[:8]}"
try:
async with (
self._connection_manager.get_connection_context_for_auth_context(
session_id,
auth_context,
) as connection
):
result = await connection.execute(
sql,
params=params,
auth_context=auth_context,
mask_result=False,
max_rows=max_rows,
max_bytes=2 * 1024 * 1024,
)
except Exception as exc:
raise _classify_failure(exc) from exc
rows: list[Mapping[str, Any]] = [
row for row in (result.data or ()) if isinstance(row, Mapping)
]
return rows[:max_rows]
def _result(
data: Mapping[str, Any],
*,
source: str,
warnings: Sequence[str] = (),
metadata: Mapping[str, Any] | None = None,
evidence: Sequence[Mapping[str, Any]] | None = None,
) -> dict[str, Any]:
unique_warnings = list(dict.fromkeys(str(item) for item in warnings))
response: dict[str, Any] = {
"status": "partial" if unique_warnings else "success",
"data": redact_sensitive_data(dict(data)),
"warnings": unique_warnings,
"metadata": {
"source": source,
**dict(metadata or {}),
},
}
if evidence is not None:
response["evidence"] = [redact_sensitive_data(dict(item)) for item in evidence]
return response
def _collection(
items: Sequence[Mapping[str, Any]],
*,
source: str,
warnings: Sequence[str] = (),
truncated: bool = False,
) -> dict[str, Any]:
return _result(
{
"items": [dict(item) for item in items],
"next_cursor": None,
"truncated": truncated,
},
source=source,
warnings=warnings,
metadata={"returned_items": len(items)},
)
def _node_item(node_type: str, row: Mapping[str, Any]) -> dict[str, Any]:
normalized = _normalized_row(row)
node_id = next(
(
normalized.get(key)
for key in (
"backend_id",
"frontend_id",
"broker_id",
"name",
"host",
)
if normalized.get(key) not in (None, "")
),
None,
)
host = next(
(
normalized.get(key)
for key in ("host", "ip", "host_name", "hostname")
if normalized.get(key) not in (None, "")
),
None,
)
return {
"node_type": node_type,
"node_id": None if node_id is None else str(node_id),
"host": None if host is None else str(host),
**normalized,
}
def _normalized_row(row: Mapping[str, Any]) -> dict[str, Any]:
return {_normalize_key(str(key)): _safe_value(value) for key, value in row.items()}
def _normalize_key(value: str) -> str:
snake = (
value.casefold()
if value.upper() == value
else _SNAKE_BOUNDARY.sub("_", value).casefold()
)
return _SAFE_KEY.sub("_", snake).strip("_")
def _safe_value(value: Any) -> Any:
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value
def _monitoring_nodes(
raw: Mapping[str, Any],
*,
metric_names: Sequence[str] | None,
node_ids: Sequence[str] | None,
markers: Sequence[str] | None = None,
) -> tuple[list[dict[str, Any]], list[str]]:
data = raw.get("data")
if not isinstance(data, Mapping):
return [], ["Monitoring response did not contain metric data."]
allowed_nodes = {str(value) for value in (node_ids or ())}
requested_names = set(metric_names or ())
nodes: list[dict[str, Any]] = []
warnings: list[str] = []
be_data = data.get("be")
be_candidates = be_data if isinstance(be_data, list) else []
role_values: tuple[tuple[str, list[Any]], ...] = (
("fe", [data.get("fe")]),
("be", be_candidates),
)
for role, candidates in role_values:
for candidate in candidates:
if not isinstance(candidate, Mapping):
continue
if candidate.get("success") is False:
warnings.append(f"{role.upper()} metrics endpoint was unavailable.")
continue
node_info = candidate.get("node_info")
node_info = node_info if isinstance(node_info, Mapping) else {}
node_id = (
node_info.get("backend_id")
or node_info.get("name")
or node_info.get("host")
or role
)
host = node_info.get("host")
if (
allowed_nodes
and not {
str(node_id),
str(host),
}
& allowed_nodes
):
continue
metrics = candidate.get("raw_metrics")
if not isinstance(metrics, Mapping):
metrics = candidate.get("metrics")
if not isinstance(metrics, Mapping):
metrics = {}
selected: dict[str, Any] = {}
for name, value in metrics.items():
metric_name = str(name)
if requested_names and metric_name not in requested_names:
continue
if markers and not any(
marker in metric_name.casefold() for marker in markers
):
continue
selected[metric_name] = _safe_value(value)
if len(selected) >= _MAX_METRICS_PER_NODE:
warnings.append(
f"{role.upper()} metric output was truncated to "
f"{_MAX_METRICS_PER_NODE} names per node."
)
break
nodes.append(
{
"role": role,
"node_id": str(node_id),
"host": None if host is None else str(host),
"metrics": selected,
}
)
return nodes, list(dict.fromkeys(warnings))
def _metric_total(value: Any) -> float:
if isinstance(value, int | float):
return float(value)
if isinstance(value, list):
return sum(
_metric_total(item.get("value"))
for item in value
if isinstance(item, Mapping)
)
return 0.0
def _task_state(item: Mapping[str, Any]) -> str:
return str(
item.get("query_status")
or item.get("state")
or item.get("status")
or item.get("command")
or ""
)
def _filter_rows(
rows: Sequence[dict[str, Any]],
*,
database: str | None,
table: str | None,
state: str | None,
) -> list[dict[str, Any]]:
filters = tuple(value.casefold() for value in (database, table, state) if value)
if not filters:
return list(rows)
return [
row
for row in rows
if all(
expected in " ".join(str(value) for value in row.values()).casefold()
for expected in filters
)
]
def _growth_sql(
resource: str,
days: int,
granularity: str,
) -> tuple[str, tuple[int]]:
try:
return _GROWTH_SQL[(resource, granularity)], (days,)
except KeyError:
if resource not in {"storage", "query_volume", "user_activity"}:
raise _argument_failure(
"resource must be storage, query_volume, or user_activity."
) from None
raise _argument_failure("granularity must be hour, day, or week.") from None
def _growth_source(resource: str) -> str:
if resource == "storage":
return "information_schema.partitions"
return "internal.__internal_schema.audit_log"
def _growth_summary(points: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
values = [
float(value)
for point in points
if isinstance((value := point.get("value")), int | float)
]
if not values:
return {"point_count": 0, "growth_percent": None}
growth = None
if len(values) > 1 and values[0] != 0:
growth = round((values[-1] - values[0]) / values[0] * 100, 4)
return {
"point_count": len(values),
"first_value": values[0],
"last_value": values[-1],
"growth_percent": growth,
}
def _row_lookup(row: Mapping[str, Any], name: str) -> Any:
expected = name.casefold()
return next(
(value for key, value in row.items() if str(key).casefold() == expected),
None,
)
def _number(value: Any) -> int | float | None:
if value is None:
return None
if isinstance(value, int | float):
return value
try:
parsed = float(value)
except (TypeError, ValueError):
return None
return int(parsed) if parsed.is_integer() else parsed
def _bounded_limit(
value: int | None,
*,
default: int,
maximum: int = _MAX_ROWS,
) -> int:
if value is None:
return default
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
raise _argument_failure("limit must be a positive integer.")
return min(value, maximum)
def _argument_failure(message: str) -> ClusterRuntimeFailure:
return ClusterRuntimeFailure(
message,
reason_code="CLUSTER_ARGUMENT_INVALID",
status_code=400,
)
def _classify_failure(exc: Exception) -> ClusterRuntimeFailure:
if isinstance(exc, ClusterRuntimeFailure):
return exc
numeric_code = next(
(value for value in getattr(exc, "args", ()) if isinstance(value, int)),
None,
)
message = str(exc).casefold()
if numeric_code in {1044, 1045, 1142, 1227} or any(
marker in message
for marker in ("access denied", "permission denied", "privilege")
):
return ClusterRuntimeFailure(
"Doris denied access to cluster metadata.",
reason_code="CLUSTER_PERMISSION_DENIED",
status_code=403,
)
if numeric_code in {1064, 1109, 1146} or any(
marker in message
for marker in (
"doesn't exist",
"does not exist",
"not supported",
"unsupported",
"unknown table",
)
):
return ClusterRuntimeFailure(
"The requested Doris cluster evidence source is unsupported.",
reason_code="CLUSTER_SOURCE_UNSUPPORTED",
status_code=501,
)
if isinstance(exc, TimeoutError | ConnectionError | OSError):
return ClusterRuntimeFailure(
"Doris cluster evidence is temporarily unavailable.",
reason_code="CLUSTER_BACKEND_UNAVAILABLE",
status_code=503,
retryable=True,
)
return ClusterRuntimeFailure(
"Doris cluster metadata execution failed.",
reason_code="CLUSTER_EXECUTION_FAILED",
status_code=502,
)
__all__ = ["ClusterRuntimeFailure", "DorisClusterRuntime"]