hexastack-logging 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ from hexastack_logging import adapters, infra
2
+
3
+ __all__ = [
4
+ "adapters",
5
+ "infra",
6
+ ]
@@ -0,0 +1,13 @@
1
+ from hexastack_logging.adapters.logger import (
2
+ LoguruAdapter,
3
+ RichLogger,
4
+ StructlogAdapter,
5
+ StructuredLogger,
6
+ )
7
+
8
+ __all__ = [
9
+ "LoguruAdapter",
10
+ "RichLogger",
11
+ "StructlogAdapter",
12
+ "StructuredLogger",
13
+ ]
@@ -0,0 +1,11 @@
1
+ from hexastack_logging.adapters.logger.loguru import LoguruAdapter
2
+ from hexastack_logging.adapters.logger.rich import RichLogger
3
+ from hexastack_logging.adapters.logger.structlog import StructlogAdapter
4
+ from hexastack_logging.adapters.logger.structured import StructuredLogger
5
+
6
+ __all__ = [
7
+ "LoguruAdapter",
8
+ "RichLogger",
9
+ "StructlogAdapter",
10
+ "StructuredLogger",
11
+ ]
@@ -0,0 +1,133 @@
1
+ import importlib
2
+ from typing import Any
3
+
4
+ from hexastack_core.domain.exceptions import MissingDependencyError
5
+ from hexastack_core.ports.logging import Extras, LoggingPort
6
+ from hexastack_core.utils.context import get_correlation_id, get_user_context
7
+
8
+
9
+ class LoguruAdapter(LoggingPort):
10
+ """Optional Loguru adapter implementing LoggingPort.
11
+
12
+ Notes/Architectural Intent:
13
+ Delegates logging to loguru while binding Hexastack correlation
14
+ and user context.
15
+ """
16
+
17
+ def __init__(self, logger: Any = None) -> None:
18
+ """Initialize LoguruAdapter.
19
+
20
+ Args:
21
+ logger: Optional loguru logger instance.
22
+
23
+ Raises:
24
+ MissingDependencyError: If loguru package is not installed.
25
+ """
26
+ if logger is None:
27
+ try:
28
+ loguru_mod = importlib.import_module("loguru")
29
+ self._logger: Any = loguru_mod.logger
30
+ except ImportError as err:
31
+ raise MissingDependencyError(
32
+ "loguru is required for LoguruAdapter. Install via 'pip install hexastack-logging[loguru]'."
33
+ ) from err
34
+ else:
35
+ self._logger = logger
36
+
37
+ def _get_bound_logger(self, extra: Extras | None = None) -> Any:
38
+ bind_dict: dict[str, Any] = dict(extra) if extra else {}
39
+ cid = get_correlation_id()
40
+ if cid:
41
+ bind_dict["correlation_id"] = cid
42
+ user = get_user_context()
43
+ if user:
44
+ bind_dict["user_id"] = user.user_id
45
+ if user.tenant_id:
46
+ bind_dict["tenant_id"] = user.tenant_id
47
+ return self._logger.bind(**bind_dict)
48
+
49
+ def critical(
50
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
51
+ ) -> None:
52
+ """Log a critical message through loguru.
53
+
54
+ Args:
55
+ message: Text message to log.
56
+ extra: Optional key-value dictionary of contextual metadata.
57
+ exc: Optional exception instance to attach traceback.
58
+
59
+ Returns:
60
+ None.
61
+
62
+ Raises:
63
+ None.
64
+ """
65
+ log = self._get_bound_logger(extra)
66
+ log.opt(exception=exc).critical(message)
67
+
68
+ def debug(self, message: str, extra: Extras | None = None) -> None:
69
+ """Log a debug message through loguru.
70
+
71
+ Args:
72
+ message: Text message to log.
73
+ extra: Optional key-value dictionary of contextual metadata.
74
+
75
+ Returns:
76
+ None.
77
+
78
+ Raises:
79
+ None.
80
+ """
81
+ log = self._get_bound_logger(extra)
82
+ log.debug(message)
83
+
84
+ def error(
85
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
86
+ ) -> None:
87
+ """Log an error message through loguru.
88
+
89
+ Args:
90
+ message: Text message to log.
91
+ extra: Optional key-value dictionary of contextual metadata.
92
+ exc: Optional exception instance to attach traceback.
93
+
94
+ Returns:
95
+ None.
96
+
97
+ Raises:
98
+ None.
99
+ """
100
+ log = self._get_bound_logger(extra)
101
+ log.opt(exception=exc).error(message)
102
+
103
+ def info(self, message: str, extra: Extras | None = None) -> None:
104
+ """Log an informational message through loguru.
105
+
106
+ Args:
107
+ message: Text message to log.
108
+ extra: Optional key-value dictionary of contextual metadata.
109
+
110
+ Returns:
111
+ None.
112
+
113
+ Raises:
114
+ None.
115
+ """
116
+ log = self._get_bound_logger(extra)
117
+ log.info(message)
118
+
119
+ def warning(self, message: str, extra: Extras | None = None) -> None:
120
+ """Log a warning message through loguru.
121
+
122
+ Args:
123
+ message: Text message to log.
124
+ extra: Optional key-value dictionary of contextual metadata.
125
+
126
+ Returns:
127
+ None.
128
+
129
+ Raises:
130
+ None.
131
+ """
132
+ log = self._get_bound_logger(extra)
133
+ log.warning(message)
@@ -0,0 +1,131 @@
1
+ import importlib
2
+ from typing import Any
3
+
4
+ from hexastack_core.domain.exceptions import MissingDependencyError
5
+ from hexastack_core.ports.logging import Extras, LoggingPort
6
+ from hexastack_core.utils.context import get_correlation_id
7
+
8
+
9
+ class RichLogger(LoggingPort):
10
+ """Optional Rich terminal logger implementing LoggingPort.
11
+
12
+ Notes/Architectural Intent:
13
+ Renders rich stylized terminal logs with correlation ID tags when rich is installed.
14
+ """
15
+
16
+ def __init__(self, console: Any = None) -> None:
17
+ """Initialize RichLogger.
18
+
19
+ Args:
20
+ console: Optional rich.console.Console instance.
21
+
22
+ Raises:
23
+ MissingDependencyError: If rich package is not installed.
24
+ """
25
+ if console is None:
26
+ try:
27
+ rich_console_mod = importlib.import_module("rich.console")
28
+ self._console: Any = rich_console_mod.Console()
29
+ except ImportError as err:
30
+ raise MissingDependencyError(
31
+ "rich is required for RichLogger. Install via 'pip install hexastack-logging[rich]'."
32
+ ) from err
33
+ else:
34
+ self._console = console
35
+
36
+ def _render(
37
+ self,
38
+ level: str,
39
+ color: str,
40
+ message: str,
41
+ extra: Extras | None = None,
42
+ exc: Exception | None = None,
43
+ ) -> None:
44
+ cid = get_correlation_id()
45
+ cid_tag = f"[dim][corr:{cid[:8]}][/dim] " if cid else ""
46
+ extra_str = f" [dim]{extra}[/dim]" if extra else ""
47
+ exc_str = f"\n[red]{exc}[/red]" if exc else ""
48
+ self._console.print(
49
+ f"[{color}][{level:<8}][/{color}] {cid_tag}{message}{extra_str}{exc_str}"
50
+ )
51
+
52
+ def critical(
53
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
54
+ ) -> None:
55
+ """Log a critical message with Rich formatting.
56
+
57
+ Args:
58
+ message: Text message to log.
59
+ extra: Optional key-value dictionary of contextual metadata.
60
+ exc: Optional exception instance to attach traceback.
61
+
62
+ Returns:
63
+ None.
64
+
65
+ Raises:
66
+ None.
67
+ """
68
+ self._render("CRITICAL", "bold red", message, extra=extra, exc=exc)
69
+
70
+ def debug(self, message: str, extra: Extras | None = None) -> None:
71
+ """Log a debug message with Rich formatting.
72
+
73
+ Args:
74
+ message: Text message to log.
75
+ extra: Optional key-value dictionary of contextual metadata.
76
+
77
+ Returns:
78
+ None.
79
+
80
+ Raises:
81
+ None.
82
+ """
83
+ self._render("DEBUG", "cyan", message, extra=extra)
84
+
85
+ def error(
86
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
87
+ ) -> None:
88
+ """Log an error message with Rich formatting.
89
+
90
+ Args:
91
+ message: Text message to log.
92
+ extra: Optional key-value dictionary of contextual metadata.
93
+ exc: Optional exception instance to attach traceback.
94
+
95
+ Returns:
96
+ None.
97
+
98
+ Raises:
99
+ None.
100
+ """
101
+ self._render("ERROR", "red", message, extra=extra, exc=exc)
102
+
103
+ def info(self, message: str, extra: Extras | None = None) -> None:
104
+ """Log an informational message with Rich formatting.
105
+
106
+ Args:
107
+ message: Text message to log.
108
+ extra: Optional key-value dictionary of contextual metadata.
109
+
110
+ Returns:
111
+ None.
112
+
113
+ Raises:
114
+ None.
115
+ """
116
+ self._render("INFO", "green", message, extra=extra)
117
+
118
+ def warning(self, message: str, extra: Extras | None = None) -> None:
119
+ """Log a warning message with Rich formatting.
120
+
121
+ Args:
122
+ message: Text message to log.
123
+ extra: Optional key-value dictionary of contextual metadata.
124
+
125
+ Returns:
126
+ None.
127
+
128
+ Raises:
129
+ None.
130
+ """
131
+ self._render("WARNING", "yellow", message, extra=extra)
@@ -0,0 +1,128 @@
1
+ import importlib
2
+ from typing import Any
3
+
4
+ from hexastack_core.domain.exceptions import MissingDependencyError
5
+ from hexastack_core.ports.logging import Extras, LoggingPort
6
+ from hexastack_core.utils.context import get_correlation_id, get_user_context
7
+
8
+
9
+ class StructlogAdapter(LoggingPort):
10
+ """Optional Structlog adapter implementing LoggingPort.
11
+
12
+ Notes/Architectural Intent:
13
+ Delegates logging to structlog BoundLogger while binding Hexastack correlation
14
+ and user context.
15
+ """
16
+
17
+ def __init__(self, logger: Any = None) -> None:
18
+ """Initialize StructlogAdapter.
19
+
20
+ Args:
21
+ logger: Optional structlog logger instance.
22
+
23
+ Raises:
24
+ MissingDependencyError: If structlog package is not installed.
25
+ """
26
+ if logger is None:
27
+ try:
28
+ structlog_mod = importlib.import_module("structlog")
29
+ self._logger: Any = structlog_mod.get_logger()
30
+ except ImportError as err:
31
+ raise MissingDependencyError(
32
+ "structlog is required for StructlogAdapter. Install via 'pip install hexastack-logging[structlog]'."
33
+ ) from err
34
+ else:
35
+ self._logger = logger
36
+
37
+ def _bind_context(self, extra: Extras | None = None) -> dict[str, Any]:
38
+ context: dict[str, Any] = dict(extra) if extra else {}
39
+ cid = get_correlation_id()
40
+ if cid:
41
+ context["correlation_id"] = cid
42
+ user = get_user_context()
43
+ if user:
44
+ context["user_id"] = user.user_id
45
+ if user.tenant_id:
46
+ context["tenant_id"] = user.tenant_id
47
+ return context
48
+
49
+ def critical(
50
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
51
+ ) -> None:
52
+ """Log a critical message through structlog.
53
+
54
+ Args:
55
+ message: Text message to log.
56
+ extra: Optional key-value dictionary of contextual metadata.
57
+ exc: Optional exception instance to attach traceback.
58
+
59
+ Returns:
60
+ None.
61
+
62
+ Raises:
63
+ None.
64
+ """
65
+ self._logger.critical(message, exc_info=exc, **self._bind_context(extra))
66
+
67
+ def debug(self, message: str, extra: Extras | None = None) -> None:
68
+ """Log a debug message through structlog.
69
+
70
+ Args:
71
+ message: Text message to log.
72
+ extra: Optional key-value dictionary of contextual metadata.
73
+
74
+ Returns:
75
+ None.
76
+
77
+ Raises:
78
+ None.
79
+ """
80
+ self._logger.debug(message, **self._bind_context(extra))
81
+
82
+ def error(
83
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
84
+ ) -> None:
85
+ """Log an error message through structlog.
86
+
87
+ Args:
88
+ message: Text message to log.
89
+ extra: Optional key-value dictionary of contextual metadata.
90
+ exc: Optional exception instance to attach traceback.
91
+
92
+ Returns:
93
+ None.
94
+
95
+ Raises:
96
+ None.
97
+ """
98
+ self._logger.error(message, exc_info=exc, **self._bind_context(extra))
99
+
100
+ def info(self, message: str, extra: Extras | None = None) -> None:
101
+ """Log an informational message through structlog.
102
+
103
+ Args:
104
+ message: Text message to log.
105
+ extra: Optional key-value dictionary of contextual metadata.
106
+
107
+ Returns:
108
+ None.
109
+
110
+ Raises:
111
+ None.
112
+ """
113
+ self._logger.info(message, **self._bind_context(extra))
114
+
115
+ def warning(self, message: str, extra: Extras | None = None) -> None:
116
+ """Log a warning message through structlog.
117
+
118
+ Args:
119
+ message: Text message to log.
120
+ extra: Optional key-value dictionary of contextual metadata.
121
+
122
+ Returns:
123
+ None.
124
+
125
+ Raises:
126
+ None.
127
+ """
128
+ self._logger.warning(message, **self._bind_context(extra))
@@ -0,0 +1,139 @@
1
+ import logging
2
+ from logging.handlers import QueueListener
3
+
4
+ from hexastack_core.ports.logging import Extras, LoggingPort
5
+ from hexastack_logging.infra.config import (
6
+ HexastackLoggingConfig,
7
+ configure_logging,
8
+ )
9
+
10
+
11
+ class StructuredLogger(LoggingPort):
12
+ """Production-grade structured logger implementing LoggingPort.
13
+
14
+ Notes/Architectural Intent:
15
+ Delegates to standard library logging.Logger configured with CorrelationIdFilter,
16
+ SanitizerFilter, rotating file handlers, and optional background QueueListener.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ name: str = "hexastack",
22
+ config: HexastackLoggingConfig | None = None,
23
+ logger: logging.Logger | None = None,
24
+ listener: QueueListener | None = None,
25
+ ) -> None:
26
+ """Initialize StructuredLogger with logger name, configuration, and optional listener.
27
+
28
+ Args:
29
+ name: Logger hierarchy name identifier.
30
+ config: Optional configuration model.
31
+ logger: Optional pre-configured Logger instance.
32
+ listener: Optional active QueueListener instance.
33
+ """
34
+ self._logger = logger or logging.getLogger(name)
35
+ if logger is None:
36
+ self._listener = configure_logging(
37
+ config=config, target_logger=self._logger
38
+ )
39
+ else:
40
+ self._listener = listener
41
+
42
+ def close(self) -> None:
43
+ """Stop and flush the background QueueListener if active.
44
+
45
+ Returns:
46
+ None.
47
+
48
+ Raises:
49
+ None.
50
+ """
51
+ if self._listener is not None:
52
+ self._listener.stop()
53
+ self._listener = None
54
+
55
+ def critical(
56
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
57
+ ) -> None:
58
+ """Log a critical message with extra context.
59
+
60
+ Args:
61
+ message: The message string to log.
62
+ extra: Optional key-value dictionary of contextual metadata.
63
+ exc: Optional exception instance to attach traceback.
64
+
65
+ Returns:
66
+ None.
67
+
68
+ Raises:
69
+ None.
70
+ """
71
+ self._logger.critical(message, exc_info=exc, extra=extra)
72
+
73
+ def debug(self, message: str, extra: Extras | None = None) -> None:
74
+ """Log a debug message with extra context.
75
+
76
+ Args:
77
+ message: The message string to log.
78
+ extra: Optional key-value dictionary of contextual metadata.
79
+
80
+ Returns:
81
+ None.
82
+
83
+ Raises:
84
+ None.
85
+ """
86
+ self._logger.debug(message, extra=extra)
87
+
88
+ def error(
89
+ self, message: str, extra: Extras | None = None, exc: Exception | None = None
90
+ ) -> None:
91
+ """Log an error message with extra context.
92
+
93
+ Args:
94
+ message: The message string to log.
95
+ extra: Optional key-value dictionary of contextual metadata.
96
+ exc: Optional exception instance to attach traceback.
97
+
98
+ Returns:
99
+ None.
100
+
101
+ Raises:
102
+ None.
103
+ """
104
+ self._logger.error(message, exc_info=exc, extra=extra)
105
+
106
+ def info(self, message: str, extra: Extras | None = None) -> None:
107
+ """Log an informational message with extra context.
108
+
109
+ Args:
110
+ message: The message string to log.
111
+ extra: Optional key-value dictionary of contextual metadata.
112
+
113
+ Returns:
114
+ None.
115
+
116
+ Raises:
117
+ None.
118
+ """
119
+ self._logger.info(message, extra=extra)
120
+
121
+ @property
122
+ def listener(self) -> QueueListener | None:
123
+ """The active QueueListener instance if async queueing is enabled."""
124
+ return self._listener
125
+
126
+ def warning(self, message: str, extra: Extras | None = None) -> None:
127
+ """Log a warning message with extra context.
128
+
129
+ Args:
130
+ message: The message string to log.
131
+ extra: Optional key-value dictionary of contextual metadata.
132
+
133
+ Returns:
134
+ None.
135
+
136
+ Raises:
137
+ None.
138
+ """
139
+ self._logger.warning(message, extra=extra)
@@ -0,0 +1,31 @@
1
+ from hexastack_logging.infra.config import (
2
+ AsyncQueueConfig,
3
+ FileLoggingConfig,
4
+ HexastackLoggingConfig,
5
+ SanitizerConfig,
6
+ configure_logging,
7
+ register_logging_config,
8
+ )
9
+ from hexastack_logging.infra.filters import (
10
+ CorrelationIdFilter,
11
+ SanitizerFilter,
12
+ )
13
+ from hexastack_logging.infra.formatters import (
14
+ ConsoleFormatter,
15
+ JsonFormatter,
16
+ )
17
+ from hexastack_logging.infra.sanitizer import Sanitizer
18
+
19
+ __all__ = [
20
+ "AsyncQueueConfig",
21
+ "configure_logging",
22
+ "ConsoleFormatter",
23
+ "CorrelationIdFilter",
24
+ "FileLoggingConfig",
25
+ "HexastackLoggingConfig",
26
+ "JsonFormatter",
27
+ "register_logging_config",
28
+ "Sanitizer",
29
+ "SanitizerConfig",
30
+ "SanitizerFilter",
31
+ ]
@@ -0,0 +1,55 @@
1
+ from hexastack_core.infra.bootstrap import BootstrapContext
2
+ from hexastack_core.infra.registries.config import ConfigRegistry
3
+ from hexastack_core.ports.bootstrap import BootstrapperPort
4
+ from hexastack_core.ports.logging import LoggingPort
5
+ from hexastack_logging.adapters.logger.structured import StructuredLogger
6
+ from hexastack_logging.infra.config import (
7
+ HexastackLoggingConfig,
8
+ register_logging_config,
9
+ )
10
+
11
+
12
+ class LoggingBootstrapper(BootstrapperPort):
13
+ """Bootstrap extension initializing logging configuration and LoggingPort adapter.
14
+
15
+ Notes/Architectural Intent:
16
+ Implements BootstrapperPort for hexastack-logging, registering 'logging'
17
+ config section in Phase 1 and binding StructuredLogger into rodi DI in Phase 2.
18
+ """
19
+
20
+ name: str = "logging"
21
+ order: int = 10
22
+
23
+ def configure(self, context: BootstrapContext) -> None:
24
+ """Phase 2: Configure root logging and register StructuredLogger in container.
25
+
26
+ Args:
27
+ context: BootstrapContext containing DI container and loaded config.
28
+
29
+ Returns:
30
+ None.
31
+
32
+ Raises:
33
+ None.
34
+ """
35
+ if LoggingPort not in context.container:
36
+ cfg = context.get_config("logging", HexastackLoggingConfig)
37
+ logger = StructuredLogger(config=cfg)
38
+ context.container.add_instance(logger, declared_class=LoggingPort)
39
+ context.properties["logger"] = logger
40
+ else:
41
+ context.properties["logger"] = context.container.resolve(LoggingPort)
42
+
43
+ def register_config(self, registry: ConfigRegistry) -> None:
44
+ """Phase 1: Register logging configuration schema under 'logging'.
45
+
46
+ Args:
47
+ registry: Target ConfigRegistry instance.
48
+
49
+ Returns:
50
+ None.
51
+
52
+ Raises:
53
+ None.
54
+ """
55
+ register_logging_config(registry)