provide-foundation 0.0.0.dev0__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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,239 @@
|
|
1
|
+
#
|
2
|
+
# core.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Core FoundationLogger implementation.
|
6
|
+
Contains the main logging class with all logging methods.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import contextlib
|
10
|
+
import threading
|
11
|
+
from typing import TYPE_CHECKING, Any
|
12
|
+
|
13
|
+
import structlog
|
14
|
+
|
15
|
+
from provide.foundation.types import TRACE_LEVEL_NAME
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from provide.foundation.logger.config import TelemetryConfig
|
19
|
+
from provide.foundation.logger.setup.emoji_resolver import ResolvedEmojiConfig
|
20
|
+
|
21
|
+
_LAZY_SETUP_LOCK = threading.Lock()
|
22
|
+
_LAZY_SETUP_STATE: dict[str, Any] = {"done": False, "error": None, "in_progress": False}
|
23
|
+
|
24
|
+
|
25
|
+
class FoundationLogger:
|
26
|
+
"""A `structlog`-based logger providing a standardized logging interface."""
|
27
|
+
|
28
|
+
def __init__(self) -> None:
|
29
|
+
self._internal_logger = structlog.get_logger().bind(
|
30
|
+
logger_name=f"{self.__class__.__module__}.{self.__class__.__name__}"
|
31
|
+
)
|
32
|
+
self._is_configured_by_setup: bool = False
|
33
|
+
self._active_config: TelemetryConfig | None = None
|
34
|
+
self._active_resolved_emoji_config: ResolvedEmojiConfig | None = None
|
35
|
+
|
36
|
+
def _check_structlog_already_disabled(self) -> bool:
|
37
|
+
try:
|
38
|
+
current_config = structlog.get_config()
|
39
|
+
if current_config and isinstance(
|
40
|
+
current_config.get("logger_factory"), structlog.ReturnLoggerFactory
|
41
|
+
):
|
42
|
+
with _LAZY_SETUP_LOCK:
|
43
|
+
_LAZY_SETUP_STATE["done"] = True
|
44
|
+
return True
|
45
|
+
except Exception:
|
46
|
+
pass
|
47
|
+
return False
|
48
|
+
|
49
|
+
def _ensure_configured(self) -> None:
|
50
|
+
"""
|
51
|
+
Ensures the logger is configured, performing lazy setup if necessary.
|
52
|
+
This method is thread-safe and handles setup failures gracefully.
|
53
|
+
"""
|
54
|
+
# Fast path for already configured loggers.
|
55
|
+
if self._is_configured_by_setup or (
|
56
|
+
_LAZY_SETUP_STATE["done"] and not _LAZY_SETUP_STATE["error"]
|
57
|
+
):
|
58
|
+
return
|
59
|
+
|
60
|
+
# If setup is in progress by another thread, or failed previously, use fallback.
|
61
|
+
if _LAZY_SETUP_STATE["in_progress"] or _LAZY_SETUP_STATE["error"]:
|
62
|
+
self._setup_emergency_fallback()
|
63
|
+
return
|
64
|
+
|
65
|
+
# If structlog is already configured to be a no-op, we're done.
|
66
|
+
if self._check_structlog_already_disabled():
|
67
|
+
return
|
68
|
+
|
69
|
+
# Acquire lock to perform setup.
|
70
|
+
with _LAZY_SETUP_LOCK:
|
71
|
+
# Double-check state after acquiring lock, as another thread might have finished.
|
72
|
+
if self._is_configured_by_setup or (
|
73
|
+
_LAZY_SETUP_STATE["done"] and not _LAZY_SETUP_STATE["error"]
|
74
|
+
):
|
75
|
+
return
|
76
|
+
|
77
|
+
# If error was set while waiting for lock, use fallback.
|
78
|
+
if _LAZY_SETUP_STATE["error"]:
|
79
|
+
self._setup_emergency_fallback()
|
80
|
+
return
|
81
|
+
|
82
|
+
# If still needs setup, perform lazy setup.
|
83
|
+
if not _LAZY_SETUP_STATE["done"]:
|
84
|
+
self._perform_lazy_setup()
|
85
|
+
|
86
|
+
def _perform_lazy_setup(self) -> None:
|
87
|
+
"""Perform the actual lazy setup of the logging system."""
|
88
|
+
from provide.foundation.logger.setup.coordinator import internal_setup
|
89
|
+
|
90
|
+
try:
|
91
|
+
_LAZY_SETUP_STATE["in_progress"] = True
|
92
|
+
internal_setup(is_explicit_call=False)
|
93
|
+
except Exception as e:
|
94
|
+
_LAZY_SETUP_STATE["error"] = e
|
95
|
+
self._setup_emergency_fallback()
|
96
|
+
finally:
|
97
|
+
_LAZY_SETUP_STATE["in_progress"] = False
|
98
|
+
|
99
|
+
def _setup_emergency_fallback(self) -> None:
|
100
|
+
"""Set up emergency fallback logging when normal setup fails."""
|
101
|
+
from provide.foundation.utils.streams import get_safe_stderr
|
102
|
+
|
103
|
+
with contextlib.suppress(Exception):
|
104
|
+
structlog.configure(
|
105
|
+
processors=[structlog.dev.ConsoleRenderer()],
|
106
|
+
logger_factory=structlog.PrintLoggerFactory(file=get_safe_stderr()),
|
107
|
+
wrapper_class=structlog.BoundLogger,
|
108
|
+
cache_logger_on_first_use=True,
|
109
|
+
)
|
110
|
+
|
111
|
+
def get_logger(self, name: str | None = None) -> Any:
|
112
|
+
self._ensure_configured()
|
113
|
+
effective_name = name if name is not None else "pyvider.default"
|
114
|
+
return structlog.get_logger().bind(logger_name=effective_name)
|
115
|
+
|
116
|
+
def _log_with_level(
|
117
|
+
self, level_method_name: str, event: str, **kwargs: Any
|
118
|
+
) -> None:
|
119
|
+
self._ensure_configured()
|
120
|
+
|
121
|
+
# Use the logger name from kwargs if provided, otherwise default
|
122
|
+
logger_name = kwargs.pop("_foundation_logger_name", "pyvider.dynamic_call")
|
123
|
+
log = self.get_logger(logger_name)
|
124
|
+
|
125
|
+
# Handle trace level specially since PrintLogger doesn't have trace method
|
126
|
+
if level_method_name == "trace":
|
127
|
+
kwargs["_foundation_level_hint"] = TRACE_LEVEL_NAME.lower()
|
128
|
+
log.msg(event, **kwargs)
|
129
|
+
else:
|
130
|
+
getattr(log, level_method_name)(event, **kwargs)
|
131
|
+
|
132
|
+
def _format_message_with_args(self, event: str | Any, args: tuple[Any, ...]) -> str:
|
133
|
+
"""Format a log message with positional arguments using % formatting."""
|
134
|
+
if args:
|
135
|
+
try:
|
136
|
+
return str(event) % args
|
137
|
+
except (TypeError, ValueError):
|
138
|
+
return f"{event} {args}"
|
139
|
+
return str(event)
|
140
|
+
|
141
|
+
def trace(
|
142
|
+
self,
|
143
|
+
event: str,
|
144
|
+
*args: Any,
|
145
|
+
_foundation_logger_name: str | None = None,
|
146
|
+
**kwargs: Any,
|
147
|
+
) -> None:
|
148
|
+
"""Log trace-level event for detailed debugging."""
|
149
|
+
formatted_event = self._format_message_with_args(event, args)
|
150
|
+
if _foundation_logger_name is not None:
|
151
|
+
kwargs["_foundation_logger_name"] = _foundation_logger_name
|
152
|
+
self._log_with_level(TRACE_LEVEL_NAME.lower(), formatted_event, **kwargs)
|
153
|
+
|
154
|
+
def debug(self, event: str, *args: Any, **kwargs: Any) -> None:
|
155
|
+
"""Log debug-level event."""
|
156
|
+
formatted_event = self._format_message_with_args(event, args)
|
157
|
+
self._log_with_level("debug", formatted_event, **kwargs)
|
158
|
+
|
159
|
+
def info(self, event: str, *args: Any, **kwargs: Any) -> None:
|
160
|
+
"""Log info-level event."""
|
161
|
+
formatted_event = self._format_message_with_args(event, args)
|
162
|
+
self._log_with_level("info", formatted_event, **kwargs)
|
163
|
+
|
164
|
+
def warning(self, event: str, *args: Any, **kwargs: Any) -> None:
|
165
|
+
"""Log warning-level event."""
|
166
|
+
formatted_event = self._format_message_with_args(event, args)
|
167
|
+
self._log_with_level("warning", formatted_event, **kwargs)
|
168
|
+
|
169
|
+
warn = warning # Alias for compatibility
|
170
|
+
|
171
|
+
def error(self, event: str, *args: Any, **kwargs: Any) -> None:
|
172
|
+
"""Log error-level event."""
|
173
|
+
formatted_event = self._format_message_with_args(event, args)
|
174
|
+
self._log_with_level("error", formatted_event, **kwargs)
|
175
|
+
|
176
|
+
def exception(self, event: str, *args: Any, **kwargs: Any) -> None:
|
177
|
+
"""Log error-level event with exception traceback."""
|
178
|
+
formatted_event = self._format_message_with_args(event, args)
|
179
|
+
kwargs["exc_info"] = True
|
180
|
+
self._log_with_level("error", formatted_event, **kwargs)
|
181
|
+
|
182
|
+
def critical(self, event: str, *args: Any, **kwargs: Any) -> None:
|
183
|
+
"""Log critical-level event."""
|
184
|
+
formatted_event = self._format_message_with_args(event, args)
|
185
|
+
self._log_with_level("critical", formatted_event, **kwargs)
|
186
|
+
|
187
|
+
def bind(self, **kwargs: Any) -> Any:
|
188
|
+
"""
|
189
|
+
Create a new logger with additional context bound to it.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
**kwargs: Key-value pairs to bind to the logger
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
A new logger instance with the bound context
|
196
|
+
"""
|
197
|
+
self._ensure_configured()
|
198
|
+
log = self.get_logger("pyvider.context_bind")
|
199
|
+
return log.bind(**kwargs)
|
200
|
+
|
201
|
+
def unbind(self, *keys: str) -> Any:
|
202
|
+
"""
|
203
|
+
Create a new logger with specified keys removed from context.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
*keys: Context keys to remove
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
A new logger instance without the specified keys
|
210
|
+
"""
|
211
|
+
self._ensure_configured()
|
212
|
+
log = self.get_logger("pyvider.context_unbind")
|
213
|
+
return log.unbind(*keys)
|
214
|
+
|
215
|
+
def try_unbind(self, *keys: str) -> Any:
|
216
|
+
"""
|
217
|
+
Create a new logger with specified keys removed from context.
|
218
|
+
Does not raise an error if keys don't exist.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
*keys: Context keys to remove
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
A new logger instance without the specified keys
|
225
|
+
"""
|
226
|
+
self._ensure_configured()
|
227
|
+
log = self.get_logger("pyvider.context_try_unbind")
|
228
|
+
return log.try_unbind(*keys)
|
229
|
+
|
230
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
231
|
+
"""Override setattr to prevent accidental modification of logger state."""
|
232
|
+
if hasattr(self, name) and name.startswith("_"):
|
233
|
+
super().__setattr__(name, value)
|
234
|
+
else:
|
235
|
+
super().__setattr__(name, value)
|
236
|
+
|
237
|
+
|
238
|
+
# Global logger instance
|
239
|
+
logger: FoundationLogger = FoundationLogger()
|
@@ -0,0 +1,172 @@
|
|
1
|
+
#
|
2
|
+
# custom_processors.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Foundation Telemetry Custom Structlog Processors.
|
6
|
+
Includes processors for log level normalization, level-based filtering,
|
7
|
+
and logger name emoji prefixes. The semantic field emoji prefix processor
|
8
|
+
is now created as a closure in config.py.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import logging as stdlib_logging
|
12
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast
|
13
|
+
|
14
|
+
import structlog
|
15
|
+
|
16
|
+
from provide.foundation.types import TRACE_LEVEL_NAME, TRACE_LEVEL_NUM, LogLevelStr
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
pass
|
20
|
+
|
21
|
+
_NUMERIC_TO_LEVEL_NAME_CUSTOM: dict[int, str] = {
|
22
|
+
stdlib_logging.CRITICAL: "critical",
|
23
|
+
stdlib_logging.ERROR: "error",
|
24
|
+
stdlib_logging.WARNING: "warning",
|
25
|
+
stdlib_logging.INFO: "info",
|
26
|
+
stdlib_logging.DEBUG: "debug",
|
27
|
+
TRACE_LEVEL_NUM: TRACE_LEVEL_NAME.lower(),
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
class StructlogProcessor(Protocol):
|
32
|
+
def __call__(
|
33
|
+
self, logger: Any, method_name: str, event_dict: structlog.types.EventDict
|
34
|
+
) -> structlog.types.EventDict: ... # pragma: no cover
|
35
|
+
|
36
|
+
|
37
|
+
def add_log_level_custom(
|
38
|
+
_logger: Any, method_name: str, event_dict: structlog.types.EventDict
|
39
|
+
) -> structlog.types.EventDict:
|
40
|
+
level_hint: str | None = event_dict.pop("_foundation_level_hint", None)
|
41
|
+
if level_hint is not None:
|
42
|
+
event_dict["level"] = level_hint.lower()
|
43
|
+
elif "level" not in event_dict:
|
44
|
+
match method_name:
|
45
|
+
case "exception":
|
46
|
+
event_dict["level"] = "error"
|
47
|
+
case "warn":
|
48
|
+
event_dict["level"] = "warning"
|
49
|
+
case "msg":
|
50
|
+
event_dict["level"] = "info"
|
51
|
+
case _:
|
52
|
+
event_dict["level"] = method_name.lower()
|
53
|
+
return event_dict
|
54
|
+
|
55
|
+
|
56
|
+
class _LevelFilter:
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
default_level_str: LogLevelStr,
|
60
|
+
module_levels: dict[str, LogLevelStr],
|
61
|
+
level_to_numeric_map: dict[LogLevelStr, int],
|
62
|
+
) -> None:
|
63
|
+
self.default_numeric_level: int = level_to_numeric_map[default_level_str]
|
64
|
+
self.module_numeric_levels: dict[str, int] = {
|
65
|
+
module: level_to_numeric_map[level_str]
|
66
|
+
for module, level_str in module_levels.items()
|
67
|
+
}
|
68
|
+
self.level_to_numeric_map = level_to_numeric_map
|
69
|
+
self.sorted_module_paths: list[str] = sorted(
|
70
|
+
self.module_numeric_levels.keys(), key=len, reverse=True
|
71
|
+
)
|
72
|
+
|
73
|
+
def __call__(
|
74
|
+
self, _logger: Any, _method_name: str, event_dict: structlog.types.EventDict
|
75
|
+
) -> structlog.types.EventDict:
|
76
|
+
logger_name: str = event_dict.get("logger_name", "unnamed_filter_target")
|
77
|
+
event_level_str_from_dict = str(event_dict.get("level", "info")).upper()
|
78
|
+
event_level_text: LogLevelStr = cast(LogLevelStr, event_level_str_from_dict)
|
79
|
+
event_num_level: int = self.level_to_numeric_map.get(
|
80
|
+
event_level_text, self.level_to_numeric_map["INFO"]
|
81
|
+
)
|
82
|
+
threshold_num_level: int = self.default_numeric_level
|
83
|
+
for path_prefix in self.sorted_module_paths:
|
84
|
+
if logger_name.startswith(path_prefix):
|
85
|
+
threshold_num_level = self.module_numeric_levels[path_prefix]
|
86
|
+
break
|
87
|
+
if event_num_level < threshold_num_level:
|
88
|
+
raise structlog.DropEvent
|
89
|
+
return event_dict
|
90
|
+
|
91
|
+
|
92
|
+
def filter_by_level_custom(
|
93
|
+
default_level_str: LogLevelStr,
|
94
|
+
module_levels: dict[str, LogLevelStr],
|
95
|
+
level_to_numeric_map: dict[LogLevelStr, int],
|
96
|
+
) -> _LevelFilter:
|
97
|
+
return _LevelFilter(default_level_str, module_levels, level_to_numeric_map)
|
98
|
+
|
99
|
+
|
100
|
+
_LOGGER_NAME_EMOJI_PREFIXES: dict[str, str] = {
|
101
|
+
"provide.foundation.core.test": "âī¸",
|
102
|
+
"provide.foundation.core_setup": "đ ī¸",
|
103
|
+
"provide.foundation.emoji_matrix_display": "đĄ",
|
104
|
+
"provide.foundation": "âī¸",
|
105
|
+
"provide.foundation.logger": "đ",
|
106
|
+
"provide.foundation.logger.config": "đŠ",
|
107
|
+
"pyvider.dynamic_call_trace": "đŖ",
|
108
|
+
"pyvider.dynamic_call": "đŖī¸",
|
109
|
+
"pyvider.default": "đĻ",
|
110
|
+
"formatter.test": "đ¨",
|
111
|
+
"service.alpha": "đĻ",
|
112
|
+
"service.beta": "đ§",
|
113
|
+
"service.beta.child": "đļ",
|
114
|
+
"service.gamma.trace_enabled": "đŦ",
|
115
|
+
"service.delta": "đŠ",
|
116
|
+
"das.test": "đ",
|
117
|
+
"json.exc.test": "đĨ",
|
118
|
+
"service.name.test": "đ",
|
119
|
+
"simple": "đ",
|
120
|
+
"test.basic": "đ§Ē",
|
121
|
+
"unknown": "â",
|
122
|
+
"test": "đ§Ē",
|
123
|
+
"default": "đš",
|
124
|
+
"emoji.test": "đ",
|
125
|
+
}
|
126
|
+
_SORTED_LOGGER_NAME_EMOJI_KEYWORDS: list[str] = sorted(
|
127
|
+
_LOGGER_NAME_EMOJI_PREFIXES.keys(), key=len, reverse=True
|
128
|
+
)
|
129
|
+
_EMOJI_LOOKUP_CACHE: dict[str, str] = {}
|
130
|
+
_EMOJI_CACHE_SIZE_LIMIT: int = 1000
|
131
|
+
|
132
|
+
|
133
|
+
def _compute_emoji_for_logger_name(logger_name: str) -> str:
|
134
|
+
for keyword in _SORTED_LOGGER_NAME_EMOJI_KEYWORDS:
|
135
|
+
if keyword == "default":
|
136
|
+
continue
|
137
|
+
if logger_name.startswith(keyword):
|
138
|
+
return _LOGGER_NAME_EMOJI_PREFIXES[keyword]
|
139
|
+
return _LOGGER_NAME_EMOJI_PREFIXES.get("default", "đš")
|
140
|
+
|
141
|
+
|
142
|
+
def add_logger_name_emoji_prefix(
|
143
|
+
_logger: Any, _method_name: str, event_dict: structlog.types.EventDict
|
144
|
+
) -> structlog.types.EventDict:
|
145
|
+
logger_name = event_dict.get("logger_name", "default")
|
146
|
+
if logger_name in _EMOJI_LOOKUP_CACHE:
|
147
|
+
chosen_emoji = _EMOJI_LOOKUP_CACHE[logger_name]
|
148
|
+
else:
|
149
|
+
chosen_emoji = _compute_emoji_for_logger_name(logger_name)
|
150
|
+
if len(_EMOJI_LOOKUP_CACHE) < _EMOJI_CACHE_SIZE_LIMIT:
|
151
|
+
_EMOJI_LOOKUP_CACHE[logger_name] = chosen_emoji
|
152
|
+
event_msg = event_dict.get("event")
|
153
|
+
if event_msg is not None:
|
154
|
+
event_dict["event"] = f"{chosen_emoji} {event_msg}"
|
155
|
+
elif chosen_emoji:
|
156
|
+
event_dict["event"] = chosen_emoji
|
157
|
+
return event_dict
|
158
|
+
|
159
|
+
|
160
|
+
def get_emoji_cache_stats() -> dict[str, Any]: # pragma: no cover
|
161
|
+
return {
|
162
|
+
"cache_size": len(_EMOJI_LOOKUP_CACHE),
|
163
|
+
"cache_limit": _EMOJI_CACHE_SIZE_LIMIT,
|
164
|
+
"cache_utilization": len(_EMOJI_LOOKUP_CACHE) / _EMOJI_CACHE_SIZE_LIMIT * 100
|
165
|
+
if _EMOJI_CACHE_SIZE_LIMIT > 0
|
166
|
+
else 0,
|
167
|
+
}
|
168
|
+
|
169
|
+
|
170
|
+
def clear_emoji_cache() -> None: # pragma: no cover
|
171
|
+
global _EMOJI_LOOKUP_CACHE
|
172
|
+
_EMOJI_LOOKUP_CACHE.clear()
|
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
Emoji configuration and mapping for Foundation logger.
|
3
|
+
|
4
|
+
This module provides emoji sets and mappings for visual enhancement
|
5
|
+
of structured logs in various domains (HTTP, LLM, Database, etc.).
|
6
|
+
"""
|
7
|
+
|
8
|
+
from provide.foundation.logger.emoji.matrix import (
|
9
|
+
PRIMARY_EMOJI,
|
10
|
+
SECONDARY_EMOJI,
|
11
|
+
TERTIARY_EMOJI,
|
12
|
+
show_emoji_matrix,
|
13
|
+
)
|
14
|
+
from provide.foundation.logger.emoji.sets import (
|
15
|
+
DATABASE_EMOJI_SET,
|
16
|
+
HTTP_EMOJI_SET,
|
17
|
+
LEGACY_DAS_EMOJI_SETS,
|
18
|
+
LEGACY_DAS_FIELD_DEFINITIONS,
|
19
|
+
LLM_EMOJI_SET,
|
20
|
+
)
|
21
|
+
from provide.foundation.logger.emoji.types import (
|
22
|
+
EmojiSet,
|
23
|
+
EmojiSetConfig,
|
24
|
+
FieldToEmojiMapping,
|
25
|
+
)
|
26
|
+
|
27
|
+
__all__ = [
|
28
|
+
# Core emoji dictionaries
|
29
|
+
"PRIMARY_EMOJI",
|
30
|
+
"SECONDARY_EMOJI",
|
31
|
+
"TERTIARY_EMOJI",
|
32
|
+
# Emoji sets
|
33
|
+
"DATABASE_EMOJI_SET",
|
34
|
+
"HTTP_EMOJI_SET",
|
35
|
+
"LLM_EMOJI_SET",
|
36
|
+
"LEGACY_DAS_EMOJI_SETS",
|
37
|
+
"LEGACY_DAS_FIELD_DEFINITIONS",
|
38
|
+
# Types
|
39
|
+
"EmojiSet",
|
40
|
+
"EmojiSetConfig",
|
41
|
+
"FieldToEmojiMapping",
|
42
|
+
# Utilities
|
43
|
+
"show_emoji_matrix",
|
44
|
+
]
|
@@ -0,0 +1,209 @@
|
|
1
|
+
#
|
2
|
+
# emoji_matrix.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Foundation Telemetry Emoji Matrix and Display Utilities.
|
6
|
+
Defines the core DAS emoji mappings and provides utilities to display
|
7
|
+
active emoji configurations (DAS or emoji set-based).
|
8
|
+
"""
|
9
|
+
|
10
|
+
from provide.foundation.logger import (
|
11
|
+
base as foundation_logger_base, # For accessing the global logger instance
|
12
|
+
)
|
13
|
+
|
14
|
+
# Import types for resolved config structure
|
15
|
+
from provide.foundation.logger.emoji.types import EmojiSet, FieldToEmojiMapping
|
16
|
+
|
17
|
+
PRIMARY_EMOJI: dict[str, str] = {
|
18
|
+
"system": "âī¸",
|
19
|
+
"server": "đī¸",
|
20
|
+
"client": "đ",
|
21
|
+
"network": "đ",
|
22
|
+
"security": "đ",
|
23
|
+
"config": "đŠ",
|
24
|
+
"database": "đī¸",
|
25
|
+
"cache": "đž",
|
26
|
+
"task": "đ",
|
27
|
+
"plugin": "đ",
|
28
|
+
"telemetry": "đ°ī¸",
|
29
|
+
"di": "đ",
|
30
|
+
"protocol": "đĄ",
|
31
|
+
"file": "đ",
|
32
|
+
"user": "đ¤",
|
33
|
+
"test": "đ§Ē",
|
34
|
+
"utils": "đ§°",
|
35
|
+
"core": "đ",
|
36
|
+
"auth": "đ",
|
37
|
+
"entity": "đĻ",
|
38
|
+
"report": "đ",
|
39
|
+
"payment": "đŗ",
|
40
|
+
"default": "â",
|
41
|
+
}
|
42
|
+
|
43
|
+
SECONDARY_EMOJI: dict[str, str] = {
|
44
|
+
"init": "đą",
|
45
|
+
"start": "đ",
|
46
|
+
"stop": "đ",
|
47
|
+
"connect": "đ",
|
48
|
+
"disconnect": "đ",
|
49
|
+
"listen": "đ",
|
50
|
+
"send": "đ¤",
|
51
|
+
"receive": "đĨ",
|
52
|
+
"read": "đ",
|
53
|
+
"write": "đ",
|
54
|
+
"process": "âī¸",
|
55
|
+
"validate": "đĄī¸",
|
56
|
+
"execute": "âļī¸",
|
57
|
+
"query": "đ",
|
58
|
+
"update": "đ",
|
59
|
+
"delete": "đī¸",
|
60
|
+
"login": "âĄī¸",
|
61
|
+
"logout": "âŦ
ī¸",
|
62
|
+
"auth": "đ",
|
63
|
+
"error": "đĨ",
|
64
|
+
"encrypt": "đĄī¸",
|
65
|
+
"decrypt": "đ",
|
66
|
+
"parse": "đ§Š",
|
67
|
+
"transmit": "đĄ",
|
68
|
+
"build": "đī¸",
|
69
|
+
"schedule": "đ
",
|
70
|
+
"emit": "đĸ",
|
71
|
+
"load": "đĄ",
|
72
|
+
"observe": "đ§",
|
73
|
+
"request": "đŖī¸",
|
74
|
+
"interrupt": "đĻ",
|
75
|
+
"register": "âī¸",
|
76
|
+
"default": "â",
|
77
|
+
}
|
78
|
+
|
79
|
+
TERTIARY_EMOJI: dict[str, str] = {
|
80
|
+
"success": "â
",
|
81
|
+
"failure": "â",
|
82
|
+
"error": "đĨ",
|
83
|
+
"warning": "â ī¸",
|
84
|
+
"info": "âšī¸",
|
85
|
+
"debug": "đ",
|
86
|
+
"trace": "đŖ",
|
87
|
+
"attempt": "âŗ",
|
88
|
+
"retry": "đ",
|
89
|
+
"skip": "âī¸",
|
90
|
+
"complete": "đ",
|
91
|
+
"timeout": "âąī¸",
|
92
|
+
"notfound": "â",
|
93
|
+
"unauthorized": "đĢ",
|
94
|
+
"invalid": "đĸ",
|
95
|
+
"cached": "đ¯",
|
96
|
+
"ongoing": "đ",
|
97
|
+
"idle": "đ¤",
|
98
|
+
"ready": "đ",
|
99
|
+
"default": "âĄī¸",
|
100
|
+
}
|
101
|
+
|
102
|
+
|
103
|
+
def _format_emoji_set_for_display(emoji_set: EmojiSet) -> list[str]:
|
104
|
+
lines = [
|
105
|
+
f" Emoji Set: '{emoji_set.name}' (Default Key: '{emoji_set.default_emoji_key}')"
|
106
|
+
]
|
107
|
+
for key, emoji in sorted(emoji_set.emojis.items()):
|
108
|
+
lines.append(f" {emoji} -> {key.capitalize()}")
|
109
|
+
return lines
|
110
|
+
|
111
|
+
|
112
|
+
def _format_field_definition_for_display(field_def: FieldToEmojiMapping) -> str:
|
113
|
+
parts = [f" Log Key: '{field_def.log_key}'"]
|
114
|
+
if field_def.description:
|
115
|
+
parts.append(f" Desc: {field_def.description}")
|
116
|
+
if field_def.value_type:
|
117
|
+
parts.append(f" Type: {field_def.value_type}")
|
118
|
+
if field_def.emoji_set_name:
|
119
|
+
parts.append(f" Emoji Set: '{field_def.emoji_set_name}'")
|
120
|
+
if field_def.default_emoji_override_key:
|
121
|
+
parts.append(
|
122
|
+
f" Default Emoji Key (Override): '{field_def.default_emoji_override_key}'"
|
123
|
+
)
|
124
|
+
return "\n".join(parts)
|
125
|
+
|
126
|
+
|
127
|
+
def show_emoji_matrix() -> None: # pragma: no cover
|
128
|
+
"""
|
129
|
+
Prints the active Foundation emoji logging contract to the console.
|
130
|
+
If emoji sets are active, it displays their configuration.
|
131
|
+
Otherwise, it displays the core DAS emoji mappings.
|
132
|
+
Activated by `PROVIDE_SHOW_EMOJI_MATRIX` environment variable.
|
133
|
+
"""
|
134
|
+
# Ensure the logger is configured to access the config
|
135
|
+
foundation_logger_base.logger._ensure_configured()
|
136
|
+
|
137
|
+
# Get the show_emoji_matrix flag from the logging config
|
138
|
+
telemetry_config = getattr(foundation_logger_base.logger, "_active_config", None)
|
139
|
+
if not telemetry_config or not telemetry_config.logging.show_emoji_matrix:
|
140
|
+
return
|
141
|
+
|
142
|
+
matrix_logger = foundation_logger_base.logger.get_logger(
|
143
|
+
"provide.foundation.emoji_matrix_display"
|
144
|
+
)
|
145
|
+
|
146
|
+
# Access the resolved emoji config from the global logger instance
|
147
|
+
# This assumes the logger has been configured (explicitly or lazily)
|
148
|
+
foundation_logger_base.logger._ensure_configured() # Ensure config is loaded
|
149
|
+
resolved_config_tuple = getattr(
|
150
|
+
foundation_logger_base.logger, "_active_resolved_emoji_config", None
|
151
|
+
)
|
152
|
+
|
153
|
+
lines: list[str] = []
|
154
|
+
|
155
|
+
if resolved_config_tuple:
|
156
|
+
resolved_field_definitions, resolved_emoji_sets_lookup = resolved_config_tuple
|
157
|
+
|
158
|
+
if resolved_field_definitions: # New emoji sets are active
|
159
|
+
lines.append("Foundation Telemetry: Active Emoji Set Contract")
|
160
|
+
lines.append("=" * 70)
|
161
|
+
lines.append(
|
162
|
+
"Active Field-to-Emoji Mappings (Order determines prefix sequence):"
|
163
|
+
)
|
164
|
+
if not resolved_field_definitions:
|
165
|
+
lines.append(" (No field-to-emoji mappings are active)")
|
166
|
+
for i, field_def in enumerate(resolved_field_definitions):
|
167
|
+
lines.append(f"\nField {i + 1}:")
|
168
|
+
lines.append(_format_field_definition_for_display(field_def))
|
169
|
+
|
170
|
+
lines.append("\n" + "=" * 70)
|
171
|
+
lines.append(
|
172
|
+
"Available Emoji Sets (Referenced by Field-to-Emoji Mappings):"
|
173
|
+
)
|
174
|
+
if not resolved_emoji_sets_lookup:
|
175
|
+
lines.append(" (No emoji sets are defined/active)")
|
176
|
+
for set_name in sorted(resolved_emoji_sets_lookup.keys()):
|
177
|
+
emoji_set = resolved_emoji_sets_lookup[set_name]
|
178
|
+
lines.extend(_format_emoji_set_for_display(emoji_set))
|
179
|
+
lines.append("") # Spacer
|
180
|
+
|
181
|
+
else: # No custom fields resolved, means core DAS is active
|
182
|
+
lines.append("Foundation Telemetry: Core DAS Emoji Contract")
|
183
|
+
lines.append("=" * 70)
|
184
|
+
lines.append("Primary Emojis ('domain' key):")
|
185
|
+
lines.extend(
|
186
|
+
f" {e} -> {k.capitalize()}" for k, e in PRIMARY_EMOJI.items()
|
187
|
+
)
|
188
|
+
lines.append("\nSecondary Emojis ('action' key):")
|
189
|
+
lines.extend(
|
190
|
+
f" {e} -> {k.capitalize()}" for k, e in SECONDARY_EMOJI.items()
|
191
|
+
)
|
192
|
+
lines.append("\nTertiary Emojis ('status' key):")
|
193
|
+
lines.extend(
|
194
|
+
f" {e} -> {k.capitalize()}" for k, e in TERTIARY_EMOJI.items()
|
195
|
+
)
|
196
|
+
else:
|
197
|
+
lines.append(
|
198
|
+
"Foundation Telemetry: Emoji configuration not yet resolved or available."
|
199
|
+
)
|
200
|
+
|
201
|
+
if lines:
|
202
|
+
matrix_logger.info("\n".join(lines))
|
203
|
+
else: # Should not happen if _ensure_configured works
|
204
|
+
matrix_logger.warning(
|
205
|
+
"Could not determine active emoji configuration to display."
|
206
|
+
)
|
207
|
+
|
208
|
+
|
209
|
+
# đĄđ§ą
|