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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. 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
+ # 💡🧱