mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 20:35:22 +08:00
138 lines
5.5 KiB
Python
138 lines
5.5 KiB
Python
"""Binding-local telemetry backed exclusively by the SDK Host storage API.
|
|
|
|
The plugin and its Page can be singletons: no tenant event, error, fence, or
|
|
initialization flag is retained in this object between invocations.
|
|
"""
|
|
import json
|
|
import logging
|
|
|
|
from components.shared_state import SerialState
|
|
from .telemetry import LangRAGTelemetry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
KEY = 'langrag.telemetry.v1'
|
|
MAX_BYTES = 192 * 1024
|
|
MAX_EVENTS = 100
|
|
|
|
|
|
class InstallationTelemetry:
|
|
start_timer = staticmethod(LangRAGTelemetry.start_timer)
|
|
elapsed_ms = staticmethod(LangRAGTelemetry.elapsed_ms)
|
|
new_trace_id = staticmethod(LangRAGTelemetry.new_trace_id)
|
|
correlation_summary = staticmethod(LangRAGTelemetry.correlation_summary)
|
|
|
|
def __init__(self, plugin):
|
|
self.plugin = plugin
|
|
self._state = SerialState()
|
|
|
|
async def _transaction(self, operation):
|
|
# Telemetry is installation-scoped, not knowledge-base-scoped.
|
|
return await self._state.run(self.plugin, None, operation)
|
|
|
|
async def initialize(self):
|
|
"""Process-scoped initialization cannot read tenant storage."""
|
|
# The runtime dispatches installation revocation to the plugin instance,
|
|
# which holds no back-reference to this helper; hand it the state so the
|
|
# revocation hook can drop per-binding locks. Without the hook
|
|
# (langbot-plugin 0.7.4) this attribute is simply never read.
|
|
plugin = getattr(self, 'plugin', None)
|
|
if plugin is not None:
|
|
plugin.telemetry_serial_state = self._state
|
|
|
|
async def _load(self):
|
|
events = []
|
|
if KEY in await self.plugin.get_plugin_storage_keys():
|
|
raw = await self.plugin.get_plugin_storage(KEY)
|
|
if len(raw) > MAX_BYTES:
|
|
raise ValueError('Telemetry snapshot too large')
|
|
events = json.loads(raw)
|
|
if not isinstance(events, list) or len(events) > MAX_EVENTS:
|
|
raise ValueError('Invalid telemetry snapshot')
|
|
store = LangRAGTelemetry(max_history_events=MAX_EVENTS)
|
|
for event in events:
|
|
if not isinstance(event, dict) or not event.get('operation'):
|
|
raise ValueError('Invalid telemetry event')
|
|
store._record_event(event, persist=False)
|
|
store._loaded_event_count = len(events)
|
|
return store
|
|
|
|
async def _persist(self, store):
|
|
events = list(store._events)[-MAX_EVENTS:]
|
|
while True:
|
|
raw = json.dumps(events, ensure_ascii=False).encode()
|
|
if len(raw) <= MAX_BYTES:
|
|
break
|
|
events.pop(0)
|
|
try:
|
|
await self.plugin.set_plugin_storage(KEY, raw)
|
|
except BaseException as exc:
|
|
# An interrupted (caller cancelled) or failed durable write leaves the
|
|
# stored history unknown, so this diagnostic state fails closed from
|
|
# here on. The marker stays in process: persisting it under the
|
|
# installation-wide identity would fence every knowledge base of the
|
|
# installation, including the ones this telemetry must never block.
|
|
self._state.mark_fenced(self.plugin, None)
|
|
if isinstance(exc, Exception):
|
|
logger.warning('Could not persist LangRAG telemetry', exc_info=True)
|
|
raise
|
|
|
|
async def _record(self, method, kwargs):
|
|
async def record():
|
|
try:
|
|
store = await self._load()
|
|
getattr(store, method)(**kwargs)
|
|
await self._persist(store)
|
|
except Exception:
|
|
# Diagnostic failure must not retry an acknowledged vector mutation.
|
|
# No installation-specific error is retained on the singleton.
|
|
pass
|
|
await self._transaction(record)
|
|
|
|
async def record_ingest(self, **kwargs):
|
|
await self._record('record_ingest', kwargs)
|
|
|
|
async def record_retrieval(self, **kwargs):
|
|
await self._record('record_retrieval', kwargs)
|
|
|
|
async def record_embedding_batch(self, **kwargs):
|
|
await self._record('record_embedding_batch', kwargs)
|
|
|
|
async def record_delete(self, **kwargs):
|
|
await self._record('record_delete', kwargs)
|
|
|
|
async def snapshot(self):
|
|
return await self._transaction(self._snapshot)
|
|
|
|
async def _snapshot(self):
|
|
store = await self._load()
|
|
result = store.snapshot()
|
|
result['persistence'] = {
|
|
'enabled': True, 'path': 'sdk:plugin-storage',
|
|
'loaded_events': store._loaded_event_count,
|
|
'history_events': len(store._events), 'error': None,
|
|
}
|
|
result['alerts'] = [a for a in result['alerts'] if a['code'] != 'persistence_disabled']
|
|
result['health'] = store._health(result['alerts'])
|
|
return result
|
|
|
|
async def prometheus(self):
|
|
return await self._transaction(self._prometheus)
|
|
|
|
async def _prometheus(self):
|
|
store = await self._load()
|
|
return store.prometheus(await self._snapshot())
|
|
|
|
async def clear(self):
|
|
async def clear():
|
|
try:
|
|
await self.plugin.set_plugin_storage(KEY, b'[]')
|
|
except BaseException as exc:
|
|
# A clear that did not land cannot acknowledge deletion, so this
|
|
# diagnostic state refuses further work from here on.
|
|
self._state.mark_fenced(self.plugin, None)
|
|
if isinstance(exc, Exception):
|
|
logger.warning('Could not clear LangRAG telemetry', exc_info=True)
|
|
raise
|
|
await self._transaction(clear)
|