Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / services / logging / observability.py: 100%
40 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Provides the concrete implementation of the observability and logging service.
6This module defines the `Observability` class, which implements the
7`ObservabilityProtocol`. It serves as the primary interface for structured
8logging throughout the application, using `structlog` as its underlying engine.
9It can also be configured to forward log entries to a telemetry backend,
10unifying logging and event tracking.
11"""
13from __future__ import annotations
15from typing import Any, Self
17from injector import inject
18import structlog
19from structlog.typing import FilteringBoundLogger
21from bijux_cli.core.enums import LogLevel
22from bijux_cli.services.contracts import ObservabilityProtocol, TelemetryProtocol
23from bijux_cli.services.errors import ServiceError
26class Observability(ObservabilityProtocol):
27 """A structured logging service integrating `structlog` and telemetry.
29 This class wraps a `structlog` logger to produce structured log entries.
30 If configured with a telemetry backend, it also forwards these events for
31 analytics and monitoring.
33 Attributes:
34 _logger (FilteringBoundLogger): The underlying `structlog` logger instance.
35 _telemetry (TelemetryProtocol): The telemetry service for event forwarding.
36 """
38 @inject
39 def __init__(
40 self, *, log_level: LogLevel = LogLevel.INFO, telemetry: TelemetryProtocol
41 ) -> None:
42 """Initializes the observability service.
44 Args:
45 log_level (str): The log level used for this instance.
46 telemetry (TelemetryProtocol): The telemetry sink used for events.
47 """
48 self._logger: FilteringBoundLogger = structlog.get_logger("bijux_cli")
49 self._telemetry = telemetry
50 _ = log_level
52 def set_telemetry(self, telemetry: TelemetryProtocol) -> Self:
53 """Attaches a telemetry backend for forwarding log events.
55 This allows the service to be "upgraded" from a simple logger to a full
56 observability tool after its initial creation.
58 Args:
59 telemetry (TelemetryProtocol): The telemetry service to receive events.
61 Returns:
62 Self: The service instance, allowing for method chaining.
63 """
64 self._telemetry = telemetry
65 return self
67 @classmethod
68 def setup(
69 cls, *, log_level: LogLevel = LogLevel.INFO, telemetry: TelemetryProtocol
70 ) -> Self:
71 """Instantiates and configures an `Observability` service.
73 Args:
74 log_level (str): The configured log level.
75 telemetry (TelemetryProtocol): The telemetry sink for events.
77 Returns:
78 Self: A new, configured `Observability` instance.
79 """
80 return cls(log_level=log_level, telemetry=telemetry)
82 def get_logger(self) -> FilteringBoundLogger:
83 """Retrieves the underlying `structlog` logger instance.
85 Returns:
86 FilteringBoundLogger: The `structlog` logger, which can be used
87 directly if needed.
88 """
89 return self._logger
91 def bind(self, **kwargs: Any) -> Self:
92 """Binds context key-value pairs to all subsequent log entries.
94 Args:
95 **kwargs (Any): Context values to include in each log entry.
97 Returns:
98 Self: The service instance, allowing for method chaining.
99 """
100 self._logger = self._logger.bind(**kwargs)
101 return self
103 def log(
104 self,
105 level: str,
106 msg: str,
107 *,
108 extra: dict[str, Any] | None = None,
109 ) -> Self:
110 """Logs a structured message and emits a corresponding telemetry event.
112 Args:
113 level (str): The severity level of the log (e.g., 'debug', 'info',
114 'warning', 'error', 'critical').
115 msg (str): The log message.
116 extra (dict[str, Any] | None): Additional context to include in the
117 log entry.
119 Returns:
120 Self: The service instance, allowing for method chaining.
122 Raises:
123 ServiceError: If `level` is not a valid log level name.
124 """
125 log_func = getattr(self._logger, level.lower(), None)
126 if not callable(log_func):
127 raise ServiceError(f"Invalid log level: {level}")
129 log_context = extra or {}
130 if log_context:
131 log_func(msg, **log_context)
132 else:
133 log_func(msg)
135 telemetry_payload = {"level": level, "message": msg}
136 telemetry_payload.update(log_context)
137 self._telemetry.event("LOG_EMITTED", telemetry_payload)
139 return self
141 def close(self) -> None:
142 """Logs the shutdown of the observability service.
144 Note:
145 In this implementation, this method only logs a debug message and
146 does not perform resource cleanup like flushing. Flushing is
147 handled by the telemetry service's own lifecycle methods.
148 """
149 self._logger.debug("Observability shutdown")
152__all__ = ["Observability"]