1"""Observability module for dependency injection."""
2
3from __future__ import annotations
4
5from typing import Any
6
7from lexigram.contracts.observability.ai import AITracerProtocol
8from lexigram.di.module import DynamicModule, Module, module
9
10
11@module()
12class ObservabilityModule(Module):
13 """AI Observability module — registers ObservabilityProvider."""
14
15 @classmethod
16 def configure(cls, config: Any | None = None) -> DynamicModule:
17 """Create an ObservabilityModule with explicit configuration."""
18 return DynamicModule(
19 module=cls,
20 providers=[cls._resolve_provider(config)],
21 exports=[AITracerProtocol],
22 )
23
24 @classmethod
25 def stub(cls, config: Any = None) -> DynamicModule:
26 """Return a no-op ObservabilityModule for testing.
27
28 Honors *config* identically to :meth:`configure`; the provider
29 registers no-op tracers and metrics when the features are disabled.
30
31 Args:
32 config: Optional config override; dicts are coerced.
33
34 Returns:
35 A DynamicModule with noop observability configuration.
36 """
37 return cls.configure(config=config)
38
39 @staticmethod
40 def _resolve_provider(config: Any) -> Any:
41 """Resolve the observability provider honoring typed and dict configs.
42
43 Args:
44 config: ``None``, an ``ObservabilityConfig``, or a plain dict
45 of the same keys.
46
47 Returns:
48 The configured provider instance.
49
50 Raises:
51 TypeError: When *config* is neither ``None``, a dict, nor an
52 ``ObservabilityConfig``.
53 """
54 from lexigram.ai.observability.config import ObservabilityConfig
55 from lexigram.ai.observability.di.provider import ObservabilityProvider
56
57 if config is None:
58 return ObservabilityProvider()
59 if isinstance(config, dict):
60 return ObservabilityProvider(config=ObservabilityConfig(**config))
61 if isinstance(config, ObservabilityConfig):
62 return ObservabilityProvider(config=config)
63 raise TypeError(
64 f"config must be ObservabilityConfig or dict, got {type(config).__name__}"
65 )
66
67
68__all__ = ["ObservabilityModule"]