provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev2__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 (161) hide show
  1. provide/foundation/__init__.py +41 -23
  2. provide/foundation/archive/__init__.py +23 -0
  3. provide/foundation/archive/base.py +70 -0
  4. provide/foundation/archive/bzip2.py +157 -0
  5. provide/foundation/archive/gzip.py +159 -0
  6. provide/foundation/archive/operations.py +334 -0
  7. provide/foundation/archive/tar.py +164 -0
  8. provide/foundation/archive/zip.py +203 -0
  9. provide/foundation/cli/__init__.py +2 -2
  10. provide/foundation/cli/commands/deps.py +13 -7
  11. provide/foundation/cli/commands/logs/__init__.py +1 -1
  12. provide/foundation/cli/commands/logs/query.py +1 -1
  13. provide/foundation/cli/commands/logs/send.py +1 -1
  14. provide/foundation/cli/commands/logs/tail.py +1 -1
  15. provide/foundation/cli/decorators.py +11 -10
  16. provide/foundation/cli/main.py +1 -1
  17. provide/foundation/cli/testing.py +2 -35
  18. provide/foundation/cli/utils.py +21 -17
  19. provide/foundation/config/__init__.py +35 -2
  20. provide/foundation/config/base.py +2 -2
  21. provide/foundation/config/converters.py +479 -0
  22. provide/foundation/config/defaults.py +67 -0
  23. provide/foundation/config/env.py +4 -19
  24. provide/foundation/config/loader.py +9 -3
  25. provide/foundation/config/sync.py +19 -4
  26. provide/foundation/console/input.py +5 -5
  27. provide/foundation/console/output.py +35 -13
  28. provide/foundation/context/__init__.py +8 -4
  29. provide/foundation/context/core.py +85 -109
  30. provide/foundation/core.py +1 -2
  31. provide/foundation/crypto/__init__.py +2 -0
  32. provide/foundation/crypto/certificates/__init__.py +34 -0
  33. provide/foundation/crypto/certificates/base.py +173 -0
  34. provide/foundation/crypto/certificates/certificate.py +290 -0
  35. provide/foundation/crypto/certificates/factory.py +213 -0
  36. provide/foundation/crypto/certificates/generator.py +138 -0
  37. provide/foundation/crypto/certificates/loader.py +130 -0
  38. provide/foundation/crypto/certificates/operations.py +198 -0
  39. provide/foundation/crypto/certificates/trust.py +107 -0
  40. provide/foundation/errors/__init__.py +2 -3
  41. provide/foundation/errors/decorators.py +0 -231
  42. provide/foundation/errors/types.py +0 -97
  43. provide/foundation/eventsets/__init__.py +0 -0
  44. provide/foundation/eventsets/display.py +84 -0
  45. provide/foundation/eventsets/registry.py +160 -0
  46. provide/foundation/eventsets/resolver.py +192 -0
  47. provide/foundation/eventsets/sets/das.py +128 -0
  48. provide/foundation/eventsets/sets/database.py +125 -0
  49. provide/foundation/eventsets/sets/http.py +153 -0
  50. provide/foundation/eventsets/sets/llm.py +139 -0
  51. provide/foundation/eventsets/sets/task_queue.py +107 -0
  52. provide/foundation/eventsets/types.py +70 -0
  53. provide/foundation/file/directory.py +13 -22
  54. provide/foundation/file/lock.py +3 -1
  55. provide/foundation/hub/components.py +77 -515
  56. provide/foundation/hub/config.py +151 -0
  57. provide/foundation/hub/discovery.py +62 -0
  58. provide/foundation/hub/handlers.py +81 -0
  59. provide/foundation/hub/lifecycle.py +194 -0
  60. provide/foundation/hub/manager.py +4 -4
  61. provide/foundation/hub/processors.py +44 -0
  62. provide/foundation/integrations/__init__.py +11 -0
  63. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  64. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  65. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  66. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  67. provide/foundation/integrations/openobserve/config.py +37 -0
  68. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  70. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  71. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  72. provide/foundation/logger/__init__.py +3 -10
  73. provide/foundation/logger/config/logging.py +68 -298
  74. provide/foundation/logger/config/telemetry.py +41 -121
  75. provide/foundation/logger/core.py +0 -2
  76. provide/foundation/logger/custom_processors.py +1 -0
  77. provide/foundation/logger/factories.py +11 -2
  78. provide/foundation/logger/processors/main.py +20 -84
  79. provide/foundation/logger/setup/__init__.py +5 -1
  80. provide/foundation/logger/setup/coordinator.py +76 -24
  81. provide/foundation/logger/setup/processors.py +2 -9
  82. provide/foundation/logger/trace.py +27 -0
  83. provide/foundation/metrics/otel.py +10 -10
  84. provide/foundation/observability/__init__.py +2 -2
  85. provide/foundation/process/__init__.py +9 -0
  86. provide/foundation/process/exit.py +47 -0
  87. provide/foundation/process/lifecycle.py +115 -59
  88. provide/foundation/resilience/__init__.py +35 -0
  89. provide/foundation/resilience/circuit.py +164 -0
  90. provide/foundation/resilience/decorators.py +220 -0
  91. provide/foundation/resilience/fallback.py +193 -0
  92. provide/foundation/resilience/retry.py +325 -0
  93. provide/foundation/streams/config.py +79 -0
  94. provide/foundation/streams/console.py +7 -8
  95. provide/foundation/streams/core.py +6 -3
  96. provide/foundation/streams/file.py +12 -2
  97. provide/foundation/testing/__init__.py +84 -2
  98. provide/foundation/testing/archive/__init__.py +24 -0
  99. provide/foundation/testing/archive/fixtures.py +217 -0
  100. provide/foundation/testing/cli.py +30 -17
  101. provide/foundation/testing/common/__init__.py +32 -0
  102. provide/foundation/testing/common/fixtures.py +236 -0
  103. provide/foundation/testing/file/__init__.py +40 -0
  104. provide/foundation/testing/file/content_fixtures.py +316 -0
  105. provide/foundation/testing/file/directory_fixtures.py +107 -0
  106. provide/foundation/testing/file/fixtures.py +52 -0
  107. provide/foundation/testing/file/special_fixtures.py +153 -0
  108. provide/foundation/testing/logger.py +117 -11
  109. provide/foundation/testing/mocking/__init__.py +46 -0
  110. provide/foundation/testing/mocking/fixtures.py +331 -0
  111. provide/foundation/testing/process/__init__.py +48 -0
  112. provide/foundation/testing/process/async_fixtures.py +405 -0
  113. provide/foundation/testing/process/fixtures.py +56 -0
  114. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  115. provide/foundation/testing/threading/__init__.py +38 -0
  116. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  117. provide/foundation/testing/threading/data_fixtures.py +99 -0
  118. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  119. provide/foundation/testing/threading/fixtures.py +54 -0
  120. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  121. provide/foundation/testing/time/__init__.py +32 -0
  122. provide/foundation/testing/time/fixtures.py +409 -0
  123. provide/foundation/testing/transport/__init__.py +30 -0
  124. provide/foundation/testing/transport/fixtures.py +280 -0
  125. provide/foundation/tools/__init__.py +58 -0
  126. provide/foundation/tools/base.py +348 -0
  127. provide/foundation/tools/cache.py +268 -0
  128. provide/foundation/tools/downloader.py +224 -0
  129. provide/foundation/tools/installer.py +254 -0
  130. provide/foundation/tools/registry.py +223 -0
  131. provide/foundation/tools/resolver.py +321 -0
  132. provide/foundation/tools/verifier.py +186 -0
  133. provide/foundation/tracer/otel.py +7 -11
  134. provide/foundation/tracer/spans.py +2 -2
  135. provide/foundation/transport/__init__.py +155 -0
  136. provide/foundation/transport/base.py +171 -0
  137. provide/foundation/transport/client.py +266 -0
  138. provide/foundation/transport/config.py +140 -0
  139. provide/foundation/transport/errors.py +79 -0
  140. provide/foundation/transport/http.py +232 -0
  141. provide/foundation/transport/middleware.py +360 -0
  142. provide/foundation/transport/registry.py +167 -0
  143. provide/foundation/transport/types.py +45 -0
  144. provide/foundation/utils/deps.py +14 -12
  145. provide/foundation/utils/parsing.py +49 -4
  146. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
  147. provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
  148. provide/foundation/cli/commands/logs/generate_old.py +0 -569
  149. provide/foundation/crypto/certificates.py +0 -896
  150. provide/foundation/logger/emoji/__init__.py +0 -44
  151. provide/foundation/logger/emoji/matrix.py +0 -209
  152. provide/foundation/logger/emoji/sets.py +0 -458
  153. provide/foundation/logger/emoji/types.py +0 -56
  154. provide/foundation/logger/setup/emoji_resolver.py +0 -64
  155. provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
  156. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  157. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  158. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  159. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  160. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  161. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -5,22 +5,39 @@
5
5
  TelemetryConfig class for Foundation telemetry configuration.
6
6
  """
7
7
 
8
- import os
9
-
10
8
  from attrs import define
11
9
 
12
- from provide.foundation.config import BaseConfig, field
10
+ from provide.foundation.config.env import RuntimeConfig
11
+ from provide.foundation.config.base import field
12
+ from provide.foundation.config.defaults import (
13
+ DEFAULT_TELEMETRY_GLOBALLY_DISABLED,
14
+ DEFAULT_TRACING_ENABLED,
15
+ DEFAULT_METRICS_ENABLED,
16
+ DEFAULT_OTLP_PROTOCOL,
17
+ DEFAULT_TRACE_SAMPLE_RATE,
18
+ )
19
+ import os
20
+ from provide.foundation.config.converters import (
21
+ parse_bool_extended,
22
+ parse_headers,
23
+ parse_sample_rate,
24
+ validate_sample_rate,
25
+ )
13
26
  from provide.foundation.logger.config.logging import LoggingConfig
14
27
 
15
28
 
29
+ def _get_service_name() -> str | None:
30
+ """Get service name from OTEL_SERVICE_NAME or PROVIDE_SERVICE_NAME (OTEL takes precedence)."""
31
+ return os.getenv("OTEL_SERVICE_NAME") or os.getenv("PROVIDE_SERVICE_NAME")
32
+
33
+
16
34
  @define(slots=True, repr=False)
17
- class TelemetryConfig(BaseConfig):
35
+ class TelemetryConfig(RuntimeConfig):
18
36
  """Main configuration object for the Foundation Telemetry system."""
19
37
 
20
38
  service_name: str | None = field(
21
- default=None,
22
- env_var="PROVIDE_SERVICE_NAME",
23
- description="Service name for telemetry",
39
+ factory=_get_service_name,
40
+ description="Service name for telemetry (from OTEL_SERVICE_NAME or PROVIDE_SERVICE_NAME)",
24
41
  )
25
42
  service_version: str | None = field(
26
43
  default=None,
@@ -28,23 +45,27 @@ class TelemetryConfig(BaseConfig):
28
45
  description="Service version for telemetry",
29
46
  )
30
47
  logging: LoggingConfig = field(
31
- factory=LoggingConfig, description="Logging configuration"
48
+ factory=lambda: LoggingConfig.from_env(),
49
+ description="Logging configuration"
32
50
  )
33
51
  globally_disabled: bool = field(
34
- default=False,
52
+ default=DEFAULT_TELEMETRY_GLOBALLY_DISABLED,
35
53
  env_var="PROVIDE_TELEMETRY_DISABLED",
54
+ converter=parse_bool_extended,
36
55
  description="Globally disable telemetry",
37
56
  )
38
57
 
39
58
  # OpenTelemetry configuration
40
59
  tracing_enabled: bool = field(
41
- default=True,
60
+ default=DEFAULT_TRACING_ENABLED,
42
61
  env_var="OTEL_TRACING_ENABLED",
62
+ converter=parse_bool_extended,
43
63
  description="Enable OpenTelemetry tracing",
44
64
  )
45
65
  metrics_enabled: bool = field(
46
- default=True,
66
+ default=DEFAULT_METRICS_ENABLED,
47
67
  env_var="OTEL_METRICS_ENABLED",
68
+ converter=parse_bool_extended,
48
69
  description="Enable OpenTelemetry metrics",
49
70
  )
50
71
  otlp_endpoint: str | None = field(
@@ -57,132 +78,31 @@ class TelemetryConfig(BaseConfig):
57
78
  env_var="OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
58
79
  description="OTLP endpoint specifically for traces",
59
80
  )
60
- otlp_headers: str | None = field(
61
- default=None,
81
+ otlp_headers: dict[str, str] = field(
82
+ factory=lambda: {},
62
83
  env_var="OTEL_EXPORTER_OTLP_HEADERS",
84
+ converter=parse_headers,
63
85
  description="Headers to send with OTLP requests (key1=value1,key2=value2)",
64
86
  )
65
87
  otlp_protocol: str = field(
66
- default="http/protobuf",
88
+ default=DEFAULT_OTLP_PROTOCOL,
67
89
  env_var="OTEL_EXPORTER_OTLP_PROTOCOL",
68
90
  description="OTLP protocol (grpc, http/protobuf)",
69
91
  )
70
92
  trace_sample_rate: float = field(
71
- default=1.0,
93
+ default=DEFAULT_TRACE_SAMPLE_RATE,
72
94
  env_var="OTEL_TRACE_SAMPLE_RATE",
95
+ converter=parse_sample_rate,
96
+ validator=validate_sample_rate,
73
97
  description="Sampling rate for traces (0.0 to 1.0)",
74
98
  )
75
99
 
76
- # OpenObserve configuration
77
- openobserve_url: str | None = field(
78
- default=None,
79
- env_var="OPENOBSERVE_URL",
80
- description="OpenObserve API endpoint URL",
81
- )
82
- openobserve_org: str = field(
83
- default="default",
84
- env_var="OPENOBSERVE_ORG",
85
- description="OpenObserve organization name",
86
- )
87
- openobserve_user: str | None = field(
88
- default=None,
89
- env_var="OPENOBSERVE_USER",
90
- description="OpenObserve username for authentication",
91
- )
92
- openobserve_password: str | None = field(
93
- default=None,
94
- env_var="OPENOBSERVE_PASSWORD",
95
- description="OpenObserve password for authentication",
96
- )
97
- openobserve_stream: str = field(
98
- default="default",
99
- env_var="OPENOBSERVE_STREAM",
100
- description="Default OpenObserve stream name",
101
- )
102
-
103
- @classmethod
104
- def from_env(cls, strict: bool = True) -> "TelemetryConfig":
105
- """Creates a TelemetryConfig instance from environment variables."""
106
- config_dict = {}
107
-
108
- # Check OTEL_SERVICE_NAME first, then PROVIDE_SERVICE_NAME
109
- service_name = os.getenv("OTEL_SERVICE_NAME") or os.getenv(
110
- "PROVIDE_SERVICE_NAME"
111
- )
112
- if service_name:
113
- config_dict["service_name"] = service_name
114
100
 
115
- # Service version
116
- service_version = os.getenv("OTEL_SERVICE_VERSION") or os.getenv(
117
- "PROVIDE_SERVICE_VERSION"
118
- )
119
- if service_version:
120
- config_dict["service_version"] = service_version
121
-
122
- # Telemetry disable flag
123
- if disabled := os.getenv("PROVIDE_TELEMETRY_DISABLED"):
124
- config_dict["globally_disabled"] = disabled.lower() == "true"
125
-
126
- # OpenTelemetry specific configuration
127
- if tracing_enabled := os.getenv("OTEL_TRACING_ENABLED"):
128
- config_dict["tracing_enabled"] = tracing_enabled.lower() == "true"
129
-
130
- if metrics_enabled := os.getenv("OTEL_METRICS_ENABLED"):
131
- config_dict["metrics_enabled"] = metrics_enabled.lower() == "true"
132
-
133
- if otlp_endpoint := os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
134
- config_dict["otlp_endpoint"] = otlp_endpoint
135
-
136
- if otlp_traces_endpoint := os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"):
137
- config_dict["otlp_traces_endpoint"] = otlp_traces_endpoint
138
-
139
- if otlp_headers := os.getenv("OTEL_EXPORTER_OTLP_HEADERS"):
140
- config_dict["otlp_headers"] = otlp_headers
141
-
142
- if otlp_protocol := os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL"):
143
- config_dict["otlp_protocol"] = otlp_protocol
144
-
145
- if trace_sample_rate := os.getenv("OTEL_TRACE_SAMPLE_RATE"):
146
- try:
147
- config_dict["trace_sample_rate"] = float(trace_sample_rate)
148
- except ValueError:
149
- if strict:
150
- raise
151
- # Use default value in non-strict mode
152
-
153
- # OpenObserve configuration
154
- if openobserve_url := os.getenv("OPENOBSERVE_URL"):
155
- config_dict["openobserve_url"] = openobserve_url
156
-
157
- if openobserve_org := os.getenv("OPENOBSERVE_ORG"):
158
- config_dict["openobserve_org"] = openobserve_org
159
-
160
- if openobserve_user := os.getenv("OPENOBSERVE_USER"):
161
- config_dict["openobserve_user"] = openobserve_user
162
-
163
- if openobserve_password := os.getenv("OPENOBSERVE_PASSWORD"):
164
- config_dict["openobserve_password"] = openobserve_password
165
-
166
- if openobserve_stream := os.getenv("OPENOBSERVE_STREAM"):
167
- config_dict["openobserve_stream"] = openobserve_stream
168
-
169
- # Load logging config from env
170
- config_dict["logging"] = LoggingConfig.from_env(strict=strict)
171
-
172
- return cls(**config_dict)
173
101
 
174
102
  def get_otlp_headers_dict(self) -> dict[str, str]:
175
- """Parse OTLP headers string into dictionary.
103
+ """Get OTLP headers dictionary.
176
104
 
177
105
  Returns:
178
106
  Dictionary of header key-value pairs
179
107
  """
180
- if not self.otlp_headers:
181
- return {}
182
-
183
- headers = {}
184
- for pair in self.otlp_headers.split(","):
185
- if "=" in pair:
186
- key, value = pair.split("=", 1)
187
- headers[key.strip()] = value.strip()
188
- return headers
108
+ return self.otlp_headers
@@ -16,7 +16,6 @@ from provide.foundation.types import TRACE_LEVEL_NAME
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from provide.foundation.logger.config import TelemetryConfig
19
- from provide.foundation.logger.setup.emoji_resolver import ResolvedEmojiConfig
20
19
 
21
20
  _LAZY_SETUP_LOCK = threading.Lock()
22
21
  _LAZY_SETUP_STATE: dict[str, Any] = {"done": False, "error": None, "in_progress": False}
@@ -31,7 +30,6 @@ class FoundationLogger:
31
30
  )
32
31
  self._is_configured_by_setup: bool = False
33
32
  self._active_config: TelemetryConfig | None = None
34
- self._active_resolved_emoji_config: ResolvedEmojiConfig | None = None
35
33
 
36
34
  def _check_structlog_already_disabled(self) -> bool:
37
35
  try:
@@ -131,6 +131,7 @@ _EMOJI_CACHE_SIZE_LIMIT: int = 1000
131
131
 
132
132
 
133
133
  def _compute_emoji_for_logger_name(logger_name: str) -> str:
134
+ # Original keyword-based system
134
135
  for keyword in _SORTED_LOGGER_NAME_EMOJI_KEYWORDS:
135
136
  if keyword == "default":
136
137
  continue
@@ -10,18 +10,27 @@ from typing import Any
10
10
  from provide.foundation.logger.core import logger
11
11
 
12
12
 
13
- def get_logger(name: str | None = None) -> Any:
13
+ def get_logger(
14
+ name: str | None = None,
15
+ emoji: str | None = None,
16
+ emoji_hierarchy: dict[str, str] | None = None
17
+ ) -> Any:
14
18
  """
15
- Get a logger instance with the given name.
19
+ Get a logger instance with the given name and optional emoji customization.
16
20
 
17
21
  This is a convenience function that uses the global FoundationLogger.
18
22
 
19
23
  Args:
20
24
  name: Logger name (e.g., __name__ from a module)
25
+ emoji: Override emoji for this specific logger instance
26
+ emoji_hierarchy: Define emoji mapping for module hierarchy patterns
21
27
 
22
28
  Returns:
23
29
  Configured structlog logger instance
24
30
  """
31
+ # Emoji hierarchy removed - using event sets now
32
+ # emoji and emoji_hierarchy parameters are deprecated
33
+
25
34
  return logger.get_logger(name)
26
35
 
27
36
 
@@ -26,8 +26,6 @@ from provide.foundation.types import (
26
26
  LogLevelStr,
27
27
  )
28
28
 
29
- if TYPE_CHECKING:
30
- from provide.foundation.logger.setup.emoji_resolver import ResolvedEmojiConfig
31
29
 
32
30
  _LEVEL_TO_NUMERIC: dict[LogLevelStr, int] = {
33
31
  "CRITICAL": stdlib_logging.CRITICAL,
@@ -71,96 +69,34 @@ def _config_create_timestamp_processors(
71
69
  return processors
72
70
 
73
71
 
74
- def _config_create_emoji_processors(
75
- logging_config: LoggingConfig, resolved_emoji_config: "ResolvedEmojiConfig"
72
+ def _config_create_event_enrichment_processors(
73
+ logging_config: LoggingConfig
76
74
  ) -> list[StructlogProcessor]:
77
75
  processors: list[StructlogProcessor] = []
78
76
  if logging_config.logger_name_emoji_prefix_enabled:
79
77
  processors.append(cast(StructlogProcessor, add_logger_name_emoji_prefix))
80
78
  if logging_config.das_emoji_prefix_enabled:
81
- # FIX: Create the processor as a closure with the resolved config
82
- resolved_field_definitions, resolved_emoji_sets_lookup = resolved_emoji_config
83
-
84
- def add_das_emoji_prefix_closure(
79
+ def add_event_enrichment_processor(
85
80
  _logger: Any, _method_name: str, event_dict: structlog.types.EventDict
86
81
  ) -> structlog.types.EventDict:
87
- # This inner function now has access to the resolved config from its closure scope
88
- from provide.foundation.logger.emoji.matrix import (
89
- PRIMARY_EMOJI,
90
- SECONDARY_EMOJI,
91
- TERTIARY_EMOJI,
92
- )
93
-
94
- final_das_prefix_parts: list[str] = []
95
-
96
- if resolved_field_definitions: # New Layered Emoji System is active
97
- for field_def in resolved_field_definitions:
98
- value_from_event = event_dict.get(field_def.log_key)
99
- if value_from_event is not None and field_def.emoji_set_name:
100
- event_dict.pop(field_def.log_key, None)
101
- emoji_set = resolved_emoji_sets_lookup.get(
102
- field_def.emoji_set_name
103
- )
104
- if emoji_set:
105
- value_str_lower = str(value_from_event).lower()
106
- specific_emoji = emoji_set.emojis.get(value_str_lower)
107
- default_key = (
108
- field_def.default_emoji_override_key
109
- or emoji_set.default_emoji_key
110
- )
111
- default_emoji = emoji_set.emojis.get(default_key, "❓")
112
- chosen_emoji = (
113
- specific_emoji
114
- if specific_emoji is not None
115
- else default_emoji
116
- )
117
- final_das_prefix_parts.append(f"[{chosen_emoji}]")
118
- else:
119
- final_das_prefix_parts.append("[❓]")
120
- else: # Fallback to Core DAS System
121
- domain = event_dict.pop("domain", None)
122
- action = event_dict.pop("action", None)
123
- status = event_dict.pop("status", None)
124
- if domain or action or status:
125
- domain_emoji = (
126
- PRIMARY_EMOJI.get(str(domain).lower(), PRIMARY_EMOJI["default"])
127
- if domain
128
- else PRIMARY_EMOJI["default"]
129
- )
130
- action_emoji = (
131
- SECONDARY_EMOJI.get(
132
- str(action).lower(), SECONDARY_EMOJI["default"]
133
- )
134
- if action
135
- else SECONDARY_EMOJI["default"]
136
- )
137
- status_emoji = (
138
- TERTIARY_EMOJI.get(
139
- str(status).lower(), TERTIARY_EMOJI["default"]
140
- )
141
- if status
142
- else TERTIARY_EMOJI["default"]
143
- )
144
- final_das_prefix_parts.extend(
145
- [f"[{domain_emoji}]", f"[{action_emoji}]", f"[{status_emoji}]"]
146
- )
147
-
148
- if final_das_prefix_parts:
149
- final_das_prefix_str = "".join(final_das_prefix_parts)
150
- event_msg = event_dict.get("event")
151
- event_dict["event"] = (
152
- f"{final_das_prefix_str} {event_msg}"
153
- if event_msg is not None
154
- else final_das_prefix_str
155
- )
156
- return event_dict
157
-
158
- processors.append(cast(StructlogProcessor, add_das_emoji_prefix_closure))
82
+ # Lazy import to avoid circular dependency
83
+ from provide.foundation.eventsets.resolver import get_resolver
84
+ from provide.foundation.eventsets.registry import discover_event_sets
85
+
86
+ # Initialize on first use
87
+ if not hasattr(add_event_enrichment_processor, '_initialized'):
88
+ discover_event_sets()
89
+ add_event_enrichment_processor._initialized = True
90
+
91
+ resolver = get_resolver()
92
+ return resolver.enrich_event(event_dict)
93
+
94
+ processors.append(cast(StructlogProcessor, add_event_enrichment_processor))
159
95
  return processors
160
96
 
161
97
 
162
98
  def _build_core_processors_list(
163
- config: TelemetryConfig, resolved_emoji_config: "ResolvedEmojiConfig"
99
+ config: TelemetryConfig
164
100
  ) -> list[StructlogProcessor]:
165
101
  log_cfg = config.logging
166
102
  processors: list[StructlogProcessor] = [
@@ -202,7 +138,7 @@ def _build_core_processors_list(
202
138
  if config.tracing_enabled and not config.globally_disabled:
203
139
  processors.append(cast(StructlogProcessor, inject_trace_context))
204
140
 
205
- processors.extend(_config_create_emoji_processors(log_cfg, resolved_emoji_config))
141
+ processors.extend(_config_create_event_enrichment_processors(log_cfg))
206
142
  return processors
207
143
 
208
144
 
@@ -243,10 +179,10 @@ def _build_formatter_processors_list(
243
179
  # Unknown formatter, warn and default to key_value
244
180
  # Use setup coordinator logger
245
181
  from provide.foundation.logger.setup.coordinator import (
246
- create_core_setup_logger,
182
+ create_foundation_internal_logger,
247
183
  )
248
184
 
249
- setup_logger = create_core_setup_logger()
185
+ setup_logger = create_foundation_internal_logger()
250
186
  setup_logger.warning(
251
187
  f"Unknown formatter '{logging_config.console_formatter}', using default 'key_value'. "
252
188
  f"Valid formatters: ['json', 'key_value']"
@@ -8,7 +8,10 @@ Handles structured logging configuration, processor setup, and emoji resolution.
8
8
  Provides the core setup functionality for the Foundation logging system.
9
9
  """
10
10
 
11
- from provide.foundation.logger.setup.coordinator import internal_setup
11
+ from provide.foundation.logger.setup.coordinator import (
12
+ get_vanilla_logger,
13
+ internal_setup,
14
+ )
12
15
 
13
16
  # Import testing utilities conditionally
14
17
  try:
@@ -22,6 +25,7 @@ except ImportError:
22
25
  reset_for_testing = None
23
26
 
24
27
  __all__ = [
28
+ "get_vanilla_logger",
25
29
  "internal_setup",
26
30
  ]
27
31
 
@@ -17,8 +17,6 @@ from provide.foundation.logger.core import (
17
17
  _LAZY_SETUP_STATE,
18
18
  logger as foundation_logger,
19
19
  )
20
- from provide.foundation.logger.emoji.sets import BUILTIN_EMOJI_SETS
21
- from provide.foundation.logger.setup.emoji_resolver import resolve_active_emoji_config
22
20
  from provide.foundation.logger.setup.processors import (
23
21
  configure_structlog_output,
24
22
  handle_globally_disabled_setup,
@@ -33,22 +31,35 @@ _FOUNDATION_LOG_LEVEL: int | None = None
33
31
 
34
32
 
35
33
  def get_foundation_log_level() -> int:
36
- """Get the Foundation log level from LoggingConfig, checking only once."""
34
+ """Get Foundation log level for setup phase, safely."""
37
35
  global _FOUNDATION_LOG_LEVEL
38
36
  if _FOUNDATION_LOG_LEVEL is None:
39
- # Use the proper config system to get the Foundation setup log level
40
- logging_config = LoggingConfig.from_env(strict=False)
41
- level_str = logging_config.foundation_setup_log_level.upper()
42
- _FOUNDATION_LOG_LEVEL = getattr(
43
- stdlib_logging,
44
- level_str,
45
- stdlib_logging.INFO, # Default fallback
46
- )
37
+ import os
38
+
39
+ # Direct env read - avoid config imports that cause circular deps
40
+ level_str = os.environ.get("FOUNDATION_LOG_LEVEL", "INFO").upper()
41
+
42
+ # Validate and map to numeric level
43
+ valid_levels = {
44
+ "CRITICAL": stdlib_logging.CRITICAL,
45
+ "ERROR": stdlib_logging.ERROR,
46
+ "WARNING": stdlib_logging.WARNING,
47
+ "INFO": stdlib_logging.INFO,
48
+ "DEBUG": stdlib_logging.DEBUG,
49
+ "NOTSET": stdlib_logging.NOTSET,
50
+ }
51
+
52
+ _FOUNDATION_LOG_LEVEL = valid_levels.get(level_str, stdlib_logging.INFO)
47
53
  return _FOUNDATION_LOG_LEVEL
48
54
 
49
55
 
50
- def create_core_setup_logger(globally_disabled: bool = False) -> Any:
51
- """Create a structlog logger for core setup messages."""
56
+ def create_foundation_internal_logger(globally_disabled: bool = False) -> Any:
57
+ """
58
+ Create Foundation's internal setup logger (structlog).
59
+
60
+ This is used internally by Foundation during its own initialization.
61
+ Components should use get_vanilla_logger() instead.
62
+ """
52
63
  if globally_disabled:
53
64
  # Configure structlog to be a no-op for core setup logger
54
65
  structlog.configure(
@@ -61,7 +72,7 @@ def create_core_setup_logger(globally_disabled: bool = False) -> Any:
61
72
  else:
62
73
  # Get the foundation log output stream
63
74
  try:
64
- logging_config = LoggingConfig.from_env(strict=False)
75
+ logging_config = LoggingConfig.from_env()
65
76
  foundation_stream = get_foundation_log_stream(
66
77
  logging_config.foundation_log_output
67
78
  )
@@ -84,6 +95,55 @@ def create_core_setup_logger(globally_disabled: bool = False) -> Any:
84
95
  return structlog.get_logger(_CORE_SETUP_LOGGER_NAME)
85
96
 
86
97
 
98
+ def get_vanilla_logger(name: str):
99
+ """
100
+ Get a vanilla Python logger without Foundation enhancements.
101
+
102
+ This provides a plain Python logger that respects FOUNDATION_LOG_LEVEL
103
+ but doesn't trigger Foundation's initialization. Use this for logging
104
+ during Foundation's setup phase or when you need to avoid circular
105
+ dependencies.
106
+
107
+ Args:
108
+ name: Logger name (e.g., "provide.foundation.otel.setup")
109
+
110
+ Returns:
111
+ A standard Python logging.Logger instance
112
+
113
+ Note:
114
+ "Vanilla" means plain/unmodified Python logging, without
115
+ Foundation's features like emoji prefixes or structured logging.
116
+ """
117
+ import logging
118
+ import sys
119
+ import os
120
+
121
+ slog = logging.getLogger(name)
122
+
123
+ # Configure only once per logger
124
+ if not slog.handlers:
125
+ log_level = get_foundation_log_level()
126
+ slog.setLevel(log_level)
127
+
128
+ # Respect FOUNDATION_LOG_OUTPUT setting
129
+ output = os.environ.get("FOUNDATION_LOG_OUTPUT", "stderr").lower()
130
+ stream = sys.stderr if output != "stdout" else sys.stdout
131
+
132
+ handler = logging.StreamHandler(stream)
133
+ handler.setLevel(log_level)
134
+ formatter = logging.Formatter(
135
+ '%(asctime)s [%(levelname)-5s] %(message)s',
136
+ datefmt='%Y-%m-%dT%H:%M:%S'
137
+ )
138
+ handler.setFormatter(formatter)
139
+ slog.addHandler(handler)
140
+
141
+ # Don't propagate to avoid duplicate messages
142
+ slog.propagate = False
143
+
144
+ return slog
145
+
146
+
87
147
  def internal_setup(
88
148
  config: TelemetryConfig | None = None, is_explicit_call: bool = False
89
149
  ) -> None:
@@ -95,11 +155,10 @@ def internal_setup(
95
155
  structlog.reset_defaults()
96
156
  foundation_logger._is_configured_by_setup = False
97
157
  foundation_logger._active_config = None
98
- foundation_logger._active_resolved_emoji_config = None
99
158
  _LAZY_SETUP_STATE.update({"done": False, "error": None, "in_progress": False})
100
159
 
101
160
  current_config = config if config is not None else TelemetryConfig.from_env()
102
- core_setup_logger = create_core_setup_logger(
161
+ core_setup_logger = create_foundation_internal_logger(
103
162
  globally_disabled=current_config.globally_disabled
104
163
  )
105
164
 
@@ -111,28 +170,21 @@ def internal_setup(
111
170
  formatter=current_config.logging.console_formatter,
112
171
  )
113
172
 
114
- resolved_emoji_config = resolve_active_emoji_config(
115
- current_config.logging, BUILTIN_EMOJI_SETS
116
- )
117
173
 
118
174
  if current_config.globally_disabled:
119
175
  handle_globally_disabled_setup()
120
176
  else:
121
177
  configure_structlog_output(
122
- current_config, resolved_emoji_config, get_log_stream()
178
+ current_config, get_log_stream()
123
179
  )
124
180
 
125
181
  foundation_logger._is_configured_by_setup = is_explicit_call
126
182
  foundation_logger._active_config = current_config
127
- foundation_logger._active_resolved_emoji_config = resolved_emoji_config
128
183
  _LAZY_SETUP_STATE["done"] = True
129
184
 
130
185
  if not current_config.globally_disabled:
131
- field_definitions, emoji_sets = resolved_emoji_config
132
186
  core_setup_logger.debug(
133
187
  "⚙️➡️✅ Foundation (structlog) setup completed",
134
- emoji_sets_enabled=len(field_definitions) > 0,
135
- emoji_sets_count=len(emoji_sets),
136
188
  processors_configured=True,
137
189
  log_file_enabled=current_config.logging.log_file is not None,
138
190
  )
@@ -15,12 +15,8 @@ from provide.foundation.logger.processors import (
15
15
  _build_core_processors_list,
16
16
  _build_formatter_processors_list,
17
17
  )
18
- from provide.foundation.logger.setup.emoji_resolver import ResolvedEmojiConfig
19
-
20
-
21
18
  def build_complete_processor_chain(
22
19
  config: TelemetryConfig,
23
- resolved_emoji_config: ResolvedEmojiConfig,
24
20
  log_stream: TextIO,
25
21
  ) -> list[Any]:
26
22
  """
@@ -28,13 +24,12 @@ def build_complete_processor_chain(
28
24
 
29
25
  Args:
30
26
  config: Telemetry configuration
31
- resolved_emoji_config: Resolved emoji configuration
32
27
  log_stream: Output stream for logging
33
28
 
34
29
  Returns:
35
30
  List of processors for structlog
36
31
  """
37
- core_processors = _build_core_processors_list(config, resolved_emoji_config)
32
+ core_processors = _build_core_processors_list(config)
38
33
  formatter_processors = _build_formatter_processors_list(config.logging, log_stream)
39
34
  return cast(list[Any], core_processors + formatter_processors)
40
35
 
@@ -57,7 +52,6 @@ def apply_structlog_configuration(processors: list[Any], log_stream: TextIO) ->
57
52
 
58
53
  def configure_structlog_output(
59
54
  config: TelemetryConfig,
60
- resolved_emoji_config: ResolvedEmojiConfig,
61
55
  log_stream: TextIO,
62
56
  ) -> None:
63
57
  """
@@ -65,11 +59,10 @@ def configure_structlog_output(
65
59
 
66
60
  Args:
67
61
  config: Telemetry configuration
68
- resolved_emoji_config: Resolved emoji configuration
69
62
  log_stream: Output stream for logging
70
63
  """
71
64
  processors = build_complete_processor_chain(
72
- config, resolved_emoji_config, log_stream
65
+ config, log_stream
73
66
  )
74
67
  apply_structlog_configuration(processors, log_stream)
75
68
 
@@ -36,3 +36,30 @@ if not hasattr(stdlib_logging, TRACE_LEVEL_NAME): # pragma: no cover
36
36
  (cast(Any, stdlib_logging.root)).trace = trace.__get__(
37
37
  stdlib_logging.root, stdlib_logging.Logger
38
38
  )
39
+
40
+ # Also patch PrintLogger from structlog to support trace method
41
+ try:
42
+ import structlog
43
+ from structlog import PrintLogger
44
+
45
+ if not hasattr(PrintLogger, "trace"): # pragma: no cover
46
+ def trace_for_print_logger(
47
+ self: PrintLogger, msg: object, *args: object, **kwargs: object
48
+ ) -> None: # pragma: no cover
49
+ # PrintLogger doesn't have level checking, so just format and print like other methods
50
+ if args:
51
+ try:
52
+ formatted_msg = str(msg) % args
53
+ except (TypeError, ValueError):
54
+ formatted_msg = f"{msg} {args}"
55
+ else:
56
+ formatted_msg = str(msg)
57
+
58
+ # Use the same output mechanism as other PrintLogger methods
59
+ self._file.write(formatted_msg + "\n")
60
+ self._file.flush()
61
+
62
+ PrintLogger.trace = trace_for_print_logger # type: ignore[attr-defined]
63
+
64
+ except ImportError: # pragma: no cover
65
+ pass