ForcePilot/backend/package/yuxi/channel/metrics.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

198 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""多渠道网关关键指标(纯内存计数器实现,不依赖 prometheus_client"""
from __future__ import annotations
import threading
from collections import defaultdict
from typing import Any
class Counter:
"""按标签维度累加的计数器。"""
def __init__(self, name: str, description: str = "") -> None:
self.name = name
self.description = description
self._values: dict[tuple[tuple[str, str], ...], int | float] = defaultdict(int)
self._lock = threading.Lock()
def _key(self, labels: dict[str, str] | None) -> tuple[tuple[str, str], ...]:
labels = labels or {}
return tuple(sorted(labels.items()))
def inc(self, labels: dict[str, str] | None = None, amount: int | float = 1) -> None:
if amount < 0:
raise ValueError("Counter increment amount must be non-negative")
key = self._key(labels)
with self._lock:
self._values[key] += amount
def get(self, labels: dict[str, str] | None = None) -> int | float:
key = self._key(labels)
with self._lock:
return self._values[key]
class Gauge:
"""按标签维度可增可减的仪表盘。"""
def __init__(self, name: str, description: str = "") -> None:
self.name = name
self.description = description
self._values: dict[tuple[tuple[str, str], ...], int | float] = defaultdict(int)
self._lock = threading.Lock()
def _key(self, labels: dict[str, str] | None) -> tuple[tuple[str, str], ...]:
labels = labels or {}
return tuple(sorted(labels.items()))
def inc(self, labels: dict[str, str] | None = None, amount: int | float = 1) -> None:
key = self._key(labels)
with self._lock:
self._values[key] += amount
def dec(self, labels: dict[str, str] | None = None, amount: int | float = 1) -> None:
key = self._key(labels)
with self._lock:
self._values[key] -= amount
def set(self, labels: dict[str, str] | None = None, value: int | float = 0) -> None:
key = self._key(labels)
with self._lock:
self._values[key] = value
def get(self, labels: dict[str, str] | None = None) -> int | float:
key = self._key(labels)
with self._lock:
return self._values[key]
class Histogram:
"""按标签维度记录数值分布的直方图。"""
def __init__(
self,
name: str,
description: str = "",
buckets: tuple[float, ...] = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
) -> None:
self.name = name
self.description = description
self.buckets = buckets
self._counts: dict[tuple[tuple[str, str], ...], int] = defaultdict(int)
self._sums: dict[tuple[tuple[str, str], ...], float] = defaultdict(float)
self._buckets: dict[tuple[tuple[str, str], ...], dict[int, int]] = defaultdict(
lambda: {i: 0 for i in range(len(buckets) + 1)}
)
self._lock = threading.Lock()
def _key(self, labels: dict[str, str] | None) -> tuple[tuple[str, str], ...]:
labels = labels or {}
return tuple(sorted(labels.items()))
def observe(self, labels: dict[str, str] | None = None, value: float = 0) -> None:
key = self._key(labels)
with self._lock:
self._counts[key] += 1
self._sums[key] += value
for idx, bucket in enumerate(self.buckets):
if value <= bucket:
self._buckets[key][idx] += 1
break
else:
self._buckets[key][len(self.buckets)] += 1
def get(self, labels: dict[str, str] | None = None) -> dict[str, Any]:
key = self._key(labels)
with self._lock:
return {
"count": self._counts[key],
"sum": self._sums[key],
"buckets": dict(self._buckets[key]),
}
channel_messages_received_total = Counter(
"channel_messages_received_total",
"接收的渠道消息总数(按 channel_type/account_id 计数)",
)
channel_messages_delivered_total = Counter(
"channel_messages_delivered_total",
"成功投递的渠道消息总数",
)
channel_messages_failed_total = Counter(
"channel_messages_failed_total",
"投递失败的渠道消息总数",
)
channel_delivery_duration_seconds = Histogram(
"channel_delivery_duration_seconds",
"从 Worker 生成消息到渠道投递完成的耗时(秒)",
)
channel_reconnect_total = Counter(
"channel_reconnect_total",
"渠道重连次数",
)
channel_active_connections = Gauge(
"channel_active_connections",
"当前活跃 WS/Polling 连接数",
)
channel_rate_limited_total = Counter(
"channel_rate_limited_total",
"被限流的消息数",
)
_COUNTERS = [
channel_messages_received_total,
channel_messages_delivered_total,
channel_messages_failed_total,
channel_reconnect_total,
channel_rate_limited_total,
]
_GAUGES = [channel_active_connections]
_HISTOGRAMS = [channel_delivery_duration_seconds]
def _format_labels(key: tuple[tuple[str, str], ...]) -> str:
return ",".join(f'{k}="{v}"' for k, v in key)
def dump() -> dict[str, Any]:
"""导出所有指标的当前快照。"""
counters: dict[str, dict[str, Any]] = {}
for counter in _COUNTERS:
with counter._lock:
counters[counter.name] = {
"description": counter.description,
"values": {_format_labels(key): value for key, value in counter._values.items()},
}
gauges: dict[str, dict[str, Any]] = {}
for gauge in _GAUGES:
with gauge._lock:
gauges[gauge.name] = {
"description": gauge.description,
"values": {_format_labels(key): value for key, value in gauge._values.items()},
}
histograms: dict[str, dict[str, Any]] = {}
for histogram in _HISTOGRAMS:
with histogram._lock:
histograms[histogram.name] = {
"description": histogram.description,
"buckets": list(histogram.buckets),
"values": {
_format_labels(key): {
"count": histogram._counts[key],
"sum": histogram._sums[key],
"buckets": dict(histogram._buckets[key]),
}
for key in histogram._counts
},
}
return {
"counters": counters,
"gauges": gauges,
"histograms": histograms,
}