Files
2026-07-13 12:24:33 +08:00

209 lines
7.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from enum import Enum
from typing import Optional
# Third Party
from prometheus_client import REGISTRY
import prometheus_client
# First Party
from lmcache.logging import init_logger
logger = init_logger(__name__)
class SocketType(Enum):
"""Enum for socket types to ensure type safety."""
PULL = "pull"
REPLY = "reply"
class PrometheusLogger:
"""
Prometheus logger for cache controller metrics.
Provides dynamic metrics for monitoring KV pool and worker registration.
"""
_instance = None
_gauge_cls = prometheus_client.Gauge
def __init__(self, labels: dict):
self.labels = labels
labelnames = list(self.labels.keys())
# Dynamic metrics for cache controller
self._init_dynamic_metrics(labelnames)
def _init_dynamic_metrics(self, labelnames):
"""
Initialize dynamic metrics that will be updated by lambda functions.
"""
# KV Pool metrics
self.kv_pool_keys_count = self._gauge_cls(
name="lmcache:cache_controller_kv_pool_keys_count",
documentation="The number of keys in the KV pool",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
# Registration Controller metrics
self.registered_workers_count = self._gauge_cls(
name="lmcache:cache_controller_registered_workers_count",
documentation="The total number of registered workers",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
# Socket message count metrics
self.pull_socket_message_count = self._gauge_cls(
name="lmcache:cache_controller_pull_socket_message_count",
documentation="The total number of messages received on PULL socket",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
self.reply_socket_message_count = self._gauge_cls(
name="lmcache:cache_controller_reply_socket_message_count",
documentation="The total number of messages received on REPLY socket",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
# Socket queue/backlog metrics
self.pull_socket_has_pending = self._gauge_cls(
name="lmcache:cache_controller_pull_socket_has_pending",
documentation="Whether PULL socket has pending messages (1=yes, 0=no)",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
self.reply_socket_has_pending = self._gauge_cls(
name="lmcache:cache_controller_reply_socket_has_pending",
documentation="Whether REPLY socket has pending messages (1=yes, 0=no)",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
# Active request metrics
self.pull_socket_active_requests = self._gauge_cls(
name="lmcache:cache_controller_pull_socket_active_requests",
documentation="Number of requests being processed from PULL socket",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
self.reply_socket_active_requests = self._gauge_cls(
name="lmcache:cache_controller_reply_socket_active_requests",
documentation="Number of requests being processed from REPLY socket",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
# Sequence number discontinuity metrics
self.kv_op_seq_discontinuity_count = self._gauge_cls(
name="lmcache:cache_controller_kv_op_seq_discontinuity_count",
documentation="Total count of KV operation sequence number discontinuities",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
# Full sync metrics
self.full_sync_workers_syncing = self._gauge_cls(
name="lmcache:cache_controller_full_sync_workers_syncing",
documentation="Number of workers currently in full sync",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
self.full_sync_workers_completed = self._gauge_cls(
name="lmcache:cache_controller_full_sync_workers_completed",
documentation="Number of workers that have completed full sync",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
self.full_sync_global_progress = self._gauge_cls(
name="lmcache:cache_controller_full_sync_global_progress",
documentation="Global full sync progress (0.0 to 1.0)",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
self.full_sync_missing_batches_total = self._gauge_cls(
name="lmcache:cache_controller_full_sync_missing_batches_total",
documentation="Total count of missing batches across all syncing workers",
labelnames=labelnames,
multiprocess_mode="livemostrecent",
).labels(**self.labels)
@staticmethod
def GetOrCreate(
labels: dict,
) -> "PrometheusLogger":
if PrometheusLogger._instance is None:
PrometheusLogger._instance = PrometheusLogger(labels)
if PrometheusLogger._instance.labels != labels:
logger.error(
"CacheControllerPrometheusLogger instance already created with "
"different metadata. This should not happen except in test"
)
return PrometheusLogger._instance
@staticmethod
def GetInstance() -> "PrometheusLogger":
assert PrometheusLogger._instance is not None, (
"CacheControllerPrometheusLogger instance not created yet"
)
return PrometheusLogger._instance
@staticmethod
def GetInstanceOrNone() -> Optional["PrometheusLogger"]:
"""
Returns the singleton instance of CacheControllerPrometheusLogger if it exists,
otherwise returns None.
"""
return PrometheusLogger._instance
@staticmethod
def DestroyInstance():
PrometheusLogger._instance = None
@staticmethod
def unregister_all_metrics():
collectors = list(REGISTRY._collector_to_names.keys())
for collector in collectors:
try:
REGISTRY.unregister(collector)
except KeyError:
pass
class SocketMetricsContext:
"""Context manager for socket message counting and error handling."""
def __init__(self, manager, socket_type: SocketType, message_count: int = 1):
self.manager = manager
self.socket_type = socket_type
self.message_count = message_count
self.counter_attr = f"{socket_type.value}_socket_message_count"
self.active_attr = f"{socket_type.value}_socket_active_requests"
def __enter__(self):
setattr(
self.manager,
self.counter_attr,
getattr(self.manager, self.counter_attr) + self.message_count,
)
setattr(
self.manager,
self.active_attr,
getattr(self.manager, self.active_attr) + self.message_count,
)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
setattr(
self.manager,
self.active_attr,
getattr(self.manager, self.active_attr) - self.message_count,
)
if exc_type is not None:
logger.error(
"Controller Manager error", exc_info=(exc_type, exc_val, exc_tb)
)
return False