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

177 lines
5.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Context reporting: snapshot messages describing a deployment.
Context reports are sent once each (at startup, or at KV-cache registration
for the MP path in :mod:`.mp`), in contrast to the periodic interval
counters in :mod:`.continuous`.
"""
# Future
from __future__ import annotations
# Standard
from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING
import threading
# First Party
from lmcache.logging import init_logger
from lmcache.usage_telemetry.env_probe import collect_env_message
from lmcache.usage_telemetry.guard import swallow_telemetry_errors
from lmcache.usage_telemetry.identity import (
get_usage_identity,
is_usage_tracking_enabled,
)
from lmcache.usage_telemetry.messages import (
DeploymentMode,
EngineMessage,
MetadataMessage,
UsageMessage,
)
from lmcache.usage_telemetry.transport import (
DEFAULT_SENDER,
UsageMessageSender,
build_usage_payload,
usage_server_url,
)
if TYPE_CHECKING:
# First Party
from lmcache.v1.config import LMCacheEngineConfig
from lmcache.v1.metadata import LMCacheMetadata
logger = init_logger(__name__)
class UsageContextBase(ABC):
"""Shared plumbing for one-shot usage reporting.
Subclasses define which messages the report contains via
:meth:`_collect_messages`; this base owns the identity, transport, and
optional local logging. Each message is POSTed to the endpoint it
declares in the schema (:mod:`lmcache.usage_telemetry.messages`).
"""
def __init__(
self,
mode: DeploymentMode,
local_log: str | None,
sender: UsageMessageSender | None,
) -> None:
"""Initialize shared reporting state.
Args:
mode: Deployment mode stamped on every payload this reporter
sends.
local_log: Path of a human-readable local log of every sent
payload; ``None`` disables local logging.
sender: Message transport; ``None`` selects the default HTTP
sender.
"""
self._mode = mode
self._local_log = local_log
self._sender = sender if sender is not None else DEFAULT_SENDER
self._identity = get_usage_identity()
self._start_time = datetime.now()
@abstractmethod
def _collect_messages(self) -> list[UsageMessage]:
"""Return the messages of the one-shot report."""
raise NotImplementedError
@swallow_telemetry_errors
def report_once(self) -> None:
"""Collect and send every one-shot message on the calling thread.
Failures are swallowed; never raises.
"""
for message in self._collect_messages():
payload = build_usage_payload(message, self._identity, self._mode)
self._sender.send(usage_server_url(message.ENDPOINT), payload)
self._write_local(payload)
def _write_local(self, payload: dict[str, object]) -> None:
"""Append *payload* to the local log file, if one is configured."""
if self._local_log is None:
return
text = "".join(f"{key}: {value}\n" for key, value in payload.items()) + "\n"
try:
with open(self._local_log, "a") as f:
f.write(text)
except OSError:
logger.debug("Unable to write usage log to %s", self._local_log)
class UsageContext(UsageContextBase):
"""One-shot usage reporter for the single-process LMCacheEngine path.
Sends an ``EnvMessage``, an ``EngineMessage``, and a
``MetadataMessage`` to the stats server.
"""
def __init__(
self,
config: LMCacheEngineConfig,
metadata: LMCacheMetadata,
local_log: str | None = None,
sender: UsageMessageSender | None = None,
) -> None:
"""Initialize the reporter.
Args:
config: The engine configuration to snapshot.
metadata: The engine metadata (model, world size, kv layout).
local_log: Path of a local log of sent payloads; ``None``
disables local logging.
sender: Message transport; ``None`` selects the default HTTP
sender.
"""
super().__init__(DeploymentMode.SINGLE_PROCESS, local_log, sender)
self._config = config
self._metadata = metadata
def _collect_messages(self) -> list[UsageMessage]:
metadata_message = MetadataMessage(
start_time=self._start_time.strftime("%Y-%m-%d %H:%M:%S"),
duration=(datetime.now() - self._start_time).total_seconds(),
)
return [
collect_env_message(),
EngineMessage.from_config(self._config, self._metadata),
metadata_message,
]
@swallow_telemetry_errors
def InitializeUsageContext(
config: LMCacheEngineConfig,
metadata: LMCacheMetadata,
local_log: str | None = None,
sender: UsageMessageSender | None = None,
) -> UsageContext | None:
"""Start one-shot usage reporting for a single-process engine.
Returns immediately; the report is sent in the background. Never
blocks or raises.
Args:
config: The engine configuration to snapshot.
metadata: The engine metadata (model, world size, kv layout).
local_log: Path of a local log of sent payloads; ``None`` disables
local logging.
sender: Message transport; ``None`` selects the default HTTP sender.
Returns:
The usage context, or ``None`` when usage tracking is disabled or
initialization failed.
"""
if not is_usage_tracking_enabled():
return None
logger.info("Initializing usage context.")
context = UsageContext(config, metadata, local_log, sender)
threading.Thread(
target=context.report_once, daemon=True, name="lmcache-usage-report"
).start()
return context