1"""DI registrations and gateway hooks for relay request logs.
2
3The governance provider root keeps the log store/sink hierarchy
4unbound during ``register()`` (the store needs a database that is only
5resolvable in ``boot()``). :func:`boot_relay_logs` resolves the
6database through its contract, builds the SQL store and usage service,
7and binds them so the relay gateway's best-effort logger and the admin
8read surface resolve the same instances through the container.
9
10Nothing in this module imports gateway implementations.
11"""
12
13from __future__ import annotations
14
15from typing import TYPE_CHECKING
16
17from lexigram.ai.governance import GovernanceConfig
18from lexigram.ai.governance.relay_logs import (
19 RelayUsageService,
20 SqlRelayRequestLogStore,
21)
22from lexigram.contracts.ai.relay import (
23 RelayDailyUsage,
24 RelayModelRank,
25 RelayRequestLogEntry,
26 RelayRequestLogStoreProtocol,
27 RelayUsageServiceProtocol,
28)
29from lexigram.contracts.data import DatabaseProviderProtocol
30from lexigram.logging import get_logger
31
32if TYPE_CHECKING:
33 from lexigram.contracts.core.di import (
34 BootContainerProtocol,
35 ContainerRegistrarProtocol,
36 )
37
38logger = get_logger(__name__)
39
40__all__ = [
41 "NoopRelayRequestLogStore",
42 "NoopRelayUsageService",
43 "boot_relay_logs",
44 "register_relay_logs",
45]
46
47
48class NoopRelayRequestLogStore(RelayRequestLogStoreProtocol):
49 """Drop-everything request-log sink used when logging is disabled."""
50
51 async def append(self, entry: RelayRequestLogEntry) -> None:
52 """Discard *entry*; logging is disabled."""
53 del entry
54
55
56class NoopRelayUsageService(RelayUsageServiceProtocol):
57 """Empty usage read service used when logging is disabled."""
58
59 async def daily_usage(self, user_id: str, days: int) -> list[RelayDailyUsage]:
60 """Return no usage for *user_id*."""
61 del user_id, days
62 return []
63
64 async def model_rank(self, days: int, limit: int) -> list[RelayModelRank]:
65 """Return no ranked models."""
66 del days, limit
67 return []
68
69 async def list_requests(
70 self,
71 days: int,
72 page: int,
73 page_size: int,
74 *,
75 user_id: str | None = None,
76 token_id: str | None = None,
77 ) -> list[RelayRequestLogEntry]:
78 """Return no request-log entries."""
79 del days, page, page_size, user_id, token_id
80 return []
81
82
83def register_relay_logs(
84 container: ContainerRegistrarProtocol,
85 config: object,
86) -> None:
87 """Register the relay request-log hierarchy by contract.
88
89 The root always exposes ``RelayRequestLogStoreProtocol`` and
90 ``RelayUsageServiceProtocol`` behind no-op instances so the gateway
91 and admin surfaces can resolve the same protocol-shaped code path
92 even when logging is disabled or the database is unavailable. When
93 logging is enabled, the durable store is built from the resolved
94 database contract during :func:`boot_relay_logs` and the placeholders
95 are rebound.
96
97 Args:
98 container: The container registrar to bind into.
99 config: Governance configuration; ``enabled`` gates logging.
100 """
101 container.singleton(RelayRequestLogStoreProtocol, NoopRelayRequestLogStore())
102 container.singleton(RelayUsageServiceProtocol, NoopRelayUsageService())
103 if not isinstance(config, GovernanceConfig) or not config.enabled:
104 logger.info("relay_logs_disabled", reason="governance disabled")
105 return
106 logger.info("relay_logs_registered")
107
108
109async def boot_relay_logs(
110 container: BootContainerProtocol,
111 config: object,
112) -> None:
113 """Build the relay log store and usage service and bind them by contract.
114
115 Resolution is contract-scoped (only
116 :class:`~lexigram.contracts.data.DatabaseProviderProtocol`). When
117 the database is missing, nothing is bound and a startup diagnostic
118 is logged so the missing dependency is discoverable; the gateway
119 keeps no-oping on the absent store.
120
121 Args:
122 container: The boot container used to resolve contracts.
123 config: Governance configuration driving the bootstrap.
124 """
125 if not isinstance(config, GovernanceConfig) or not config.enabled:
126 logger.info("relay_logs_boot_skipped", reason="governance disabled")
127 return
128
129 database = await container.resolve_optional(DatabaseProviderProtocol)
130 if database is None:
131 logger.warning(
132 "relay_logs_missing_dependency",
133 missing="DatabaseProviderProtocol",
134 )
135 return
136
137 store: RelayRequestLogStoreProtocol = SqlRelayRequestLogStore(database)
138 service: RelayUsageServiceProtocol = RelayUsageService(database)
139 container.bind(RelayRequestLogStoreProtocol, store) # type: ignore[type-abstract]
140 container.bind(RelayUsageServiceProtocol, service) # type: ignore[type-abstract]
141 logger.info("relay_logs_booted", store="SqlRelayRequestLogStore")