provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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 (163) hide show
  1. provide/foundation/__init__.py +36 -10
  2. provide/foundation/archive/__init__.py +1 -1
  3. provide/foundation/archive/base.py +15 -14
  4. provide/foundation/archive/bzip2.py +40 -40
  5. provide/foundation/archive/gzip.py +42 -42
  6. provide/foundation/archive/operations.py +93 -96
  7. provide/foundation/archive/tar.py +33 -31
  8. provide/foundation/archive/zip.py +52 -50
  9. provide/foundation/asynctools/__init__.py +20 -0
  10. provide/foundation/asynctools/core.py +126 -0
  11. provide/foundation/cli/__init__.py +2 -2
  12. provide/foundation/cli/commands/deps.py +15 -9
  13. provide/foundation/cli/commands/logs/__init__.py +3 -3
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +4 -4
  16. provide/foundation/cli/commands/logs/send.py +3 -3
  17. provide/foundation/cli/commands/logs/tail.py +3 -3
  18. provide/foundation/cli/decorators.py +11 -11
  19. provide/foundation/cli/main.py +1 -1
  20. provide/foundation/cli/testing.py +2 -40
  21. provide/foundation/cli/utils.py +21 -18
  22. provide/foundation/config/__init__.py +35 -2
  23. provide/foundation/config/base.py +2 -2
  24. provide/foundation/config/converters.py +477 -0
  25. provide/foundation/config/defaults.py +67 -0
  26. provide/foundation/config/env.py +6 -20
  27. provide/foundation/config/loader.py +10 -4
  28. provide/foundation/config/sync.py +8 -6
  29. provide/foundation/config/types.py +5 -5
  30. provide/foundation/config/validators.py +4 -4
  31. provide/foundation/console/input.py +5 -5
  32. provide/foundation/console/output.py +36 -14
  33. provide/foundation/context/__init__.py +8 -4
  34. provide/foundation/context/core.py +88 -110
  35. provide/foundation/crypto/certificates/__init__.py +9 -5
  36. provide/foundation/crypto/certificates/base.py +2 -2
  37. provide/foundation/crypto/certificates/certificate.py +48 -19
  38. provide/foundation/crypto/certificates/factory.py +26 -18
  39. provide/foundation/crypto/certificates/generator.py +24 -23
  40. provide/foundation/crypto/certificates/loader.py +24 -16
  41. provide/foundation/crypto/certificates/operations.py +17 -10
  42. provide/foundation/crypto/certificates/trust.py +21 -21
  43. provide/foundation/env/__init__.py +28 -0
  44. provide/foundation/env/core.py +218 -0
  45. provide/foundation/errors/__init__.py +3 -3
  46. provide/foundation/errors/decorators.py +0 -234
  47. provide/foundation/errors/types.py +0 -98
  48. provide/foundation/eventsets/display.py +13 -14
  49. provide/foundation/eventsets/registry.py +61 -31
  50. provide/foundation/eventsets/resolver.py +50 -46
  51. provide/foundation/eventsets/sets/das.py +8 -8
  52. provide/foundation/eventsets/sets/database.py +14 -14
  53. provide/foundation/eventsets/sets/http.py +21 -21
  54. provide/foundation/eventsets/sets/llm.py +16 -16
  55. provide/foundation/eventsets/sets/task_queue.py +13 -13
  56. provide/foundation/eventsets/types.py +7 -7
  57. provide/foundation/file/directory.py +14 -23
  58. provide/foundation/file/lock.py +4 -3
  59. provide/foundation/hub/components.py +75 -389
  60. provide/foundation/hub/config.py +157 -0
  61. provide/foundation/hub/discovery.py +63 -0
  62. provide/foundation/hub/handlers.py +89 -0
  63. provide/foundation/hub/lifecycle.py +195 -0
  64. provide/foundation/hub/manager.py +7 -4
  65. provide/foundation/hub/processors.py +49 -0
  66. provide/foundation/integrations/__init__.py +11 -0
  67. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  68. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
  70. provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
  71. provide/foundation/integrations/openobserve/config.py +37 -0
  72. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  73. provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
  74. provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
  75. provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
  76. provide/foundation/logger/__init__.py +0 -1
  77. provide/foundation/logger/config/base.py +1 -1
  78. provide/foundation/logger/config/logging.py +69 -299
  79. provide/foundation/logger/config/telemetry.py +39 -121
  80. provide/foundation/logger/factories.py +2 -2
  81. provide/foundation/logger/processors/main.py +12 -10
  82. provide/foundation/logger/ratelimit/limiters.py +4 -4
  83. provide/foundation/logger/ratelimit/processor.py +1 -1
  84. provide/foundation/logger/setup/coordinator.py +39 -25
  85. provide/foundation/logger/setup/processors.py +3 -3
  86. provide/foundation/logger/setup/testing.py +14 -0
  87. provide/foundation/logger/trace.py +5 -5
  88. provide/foundation/metrics/__init__.py +1 -1
  89. provide/foundation/metrics/otel.py +3 -1
  90. provide/foundation/observability/__init__.py +3 -3
  91. provide/foundation/process/__init__.py +9 -0
  92. provide/foundation/process/exit.py +48 -0
  93. provide/foundation/process/lifecycle.py +69 -46
  94. provide/foundation/resilience/__init__.py +36 -0
  95. provide/foundation/resilience/circuit.py +166 -0
  96. provide/foundation/resilience/decorators.py +236 -0
  97. provide/foundation/resilience/fallback.py +208 -0
  98. provide/foundation/resilience/retry.py +327 -0
  99. provide/foundation/serialization/__init__.py +16 -0
  100. provide/foundation/serialization/core.py +70 -0
  101. provide/foundation/streams/config.py +78 -0
  102. provide/foundation/streams/console.py +4 -5
  103. provide/foundation/streams/core.py +5 -2
  104. provide/foundation/streams/file.py +12 -2
  105. provide/foundation/testing/__init__.py +29 -9
  106. provide/foundation/testing/archive/__init__.py +7 -7
  107. provide/foundation/testing/archive/fixtures.py +58 -54
  108. provide/foundation/testing/cli.py +30 -20
  109. provide/foundation/testing/common/__init__.py +13 -15
  110. provide/foundation/testing/common/fixtures.py +27 -57
  111. provide/foundation/testing/file/__init__.py +15 -15
  112. provide/foundation/testing/file/content_fixtures.py +289 -0
  113. provide/foundation/testing/file/directory_fixtures.py +107 -0
  114. provide/foundation/testing/file/fixtures.py +42 -516
  115. provide/foundation/testing/file/special_fixtures.py +145 -0
  116. provide/foundation/testing/logger.py +89 -8
  117. provide/foundation/testing/mocking/__init__.py +21 -21
  118. provide/foundation/testing/mocking/fixtures.py +80 -67
  119. provide/foundation/testing/process/__init__.py +23 -23
  120. provide/foundation/testing/process/async_fixtures.py +414 -0
  121. provide/foundation/testing/process/fixtures.py +48 -571
  122. provide/foundation/testing/process/subprocess_fixtures.py +210 -0
  123. provide/foundation/testing/threading/__init__.py +17 -17
  124. provide/foundation/testing/threading/basic_fixtures.py +105 -0
  125. provide/foundation/testing/threading/data_fixtures.py +101 -0
  126. provide/foundation/testing/threading/execution_fixtures.py +278 -0
  127. provide/foundation/testing/threading/fixtures.py +32 -502
  128. provide/foundation/testing/threading/sync_fixtures.py +100 -0
  129. provide/foundation/testing/time/__init__.py +11 -11
  130. provide/foundation/testing/time/fixtures.py +95 -83
  131. provide/foundation/testing/transport/__init__.py +9 -9
  132. provide/foundation/testing/transport/fixtures.py +54 -54
  133. provide/foundation/time/__init__.py +18 -0
  134. provide/foundation/time/core.py +63 -0
  135. provide/foundation/tools/__init__.py +2 -2
  136. provide/foundation/tools/base.py +68 -67
  137. provide/foundation/tools/cache.py +69 -74
  138. provide/foundation/tools/downloader.py +68 -62
  139. provide/foundation/tools/installer.py +51 -57
  140. provide/foundation/tools/registry.py +38 -45
  141. provide/foundation/tools/resolver.py +70 -68
  142. provide/foundation/tools/verifier.py +39 -50
  143. provide/foundation/tracer/spans.py +2 -14
  144. provide/foundation/transport/__init__.py +26 -33
  145. provide/foundation/transport/base.py +32 -30
  146. provide/foundation/transport/client.py +44 -49
  147. provide/foundation/transport/config.py +36 -107
  148. provide/foundation/transport/errors.py +13 -27
  149. provide/foundation/transport/http.py +69 -55
  150. provide/foundation/transport/middleware.py +113 -114
  151. provide/foundation/transport/registry.py +29 -27
  152. provide/foundation/transport/types.py +6 -6
  153. provide/foundation/utils/deps.py +17 -14
  154. provide/foundation/utils/parsing.py +49 -4
  155. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  156. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  157. provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
  158. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  159. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  160. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  161. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  162. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  163. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -5,390 +5,160 @@
5
5
  LoggingConfig class for Foundation logger configuration.
6
6
  """
7
7
 
8
- import json
9
- import os
10
8
  from pathlib import Path
11
9
 
12
10
  from attrs import define
13
11
 
14
- from provide.foundation.config import BaseConfig, field
15
- from provide.foundation.config.types import ConfigSource
16
- from provide.foundation.logger.config.base import get_config_logger
17
- from provide.foundation.eventsets.types import EventMapping, EventSet
12
+ from provide.foundation.config.base import field
13
+ from provide.foundation.config.converters import (
14
+ parse_bool_extended,
15
+ parse_console_formatter,
16
+ parse_float_with_validation,
17
+ parse_foundation_log_output,
18
+ parse_log_level,
19
+ parse_module_levels,
20
+ parse_rate_limits,
21
+ validate_log_level,
22
+ validate_overflow_policy,
23
+ validate_positive,
24
+ )
25
+ from provide.foundation.config.defaults import (
26
+ DEFAULT_CONSOLE_FORMATTER,
27
+ DEFAULT_DAS_EMOJI_ENABLED,
28
+ DEFAULT_FOUNDATION_LOG_OUTPUT,
29
+ DEFAULT_FOUNDATION_SETUP_LOG_LEVEL,
30
+ DEFAULT_LOG_LEVEL,
31
+ DEFAULT_LOGGER_NAME_EMOJI_ENABLED,
32
+ DEFAULT_OMIT_TIMESTAMP,
33
+ DEFAULT_RATE_LIMIT_EMIT_WARNINGS,
34
+ DEFAULT_RATE_LIMIT_ENABLED,
35
+ DEFAULT_RATE_LIMIT_GLOBAL,
36
+ DEFAULT_RATE_LIMIT_GLOBAL_CAPACITY,
37
+ DEFAULT_RATE_LIMIT_OVERFLOW_POLICY,
38
+ )
39
+ from provide.foundation.config.env import RuntimeConfig
18
40
  from provide.foundation.types import (
19
- _VALID_FORMATTER_TUPLE,
20
- _VALID_LOG_LEVEL_TUPLE,
21
41
  ConsoleFormatterStr,
22
42
  LogLevelStr,
23
43
  )
24
44
 
25
45
 
26
46
  @define(slots=True, repr=False)
27
- class LoggingConfig(BaseConfig):
47
+ class LoggingConfig(RuntimeConfig):
28
48
  """Configuration specific to logging behavior within Foundation Telemetry."""
29
49
 
30
50
  default_level: LogLevelStr = field(
31
- default="WARNING",
51
+ default=DEFAULT_LOG_LEVEL,
32
52
  env_var="PROVIDE_LOG_LEVEL",
53
+ converter=parse_log_level,
54
+ validator=validate_log_level,
33
55
  description="Default logging level",
34
56
  )
35
57
  module_levels: dict[str, LogLevelStr] = field(
36
58
  factory=lambda: {},
37
59
  env_var="PROVIDE_LOG_MODULE_LEVELS",
60
+ converter=parse_module_levels,
38
61
  description="Per-module log levels (format: module1:LEVEL,module2:LEVEL)",
39
62
  )
40
63
  console_formatter: ConsoleFormatterStr = field(
41
- default="key_value",
64
+ default=DEFAULT_CONSOLE_FORMATTER,
42
65
  env_var="PROVIDE_LOG_CONSOLE_FORMATTER",
66
+ converter=parse_console_formatter,
43
67
  description="Console output formatter (key_value or json)",
44
68
  )
45
69
  logger_name_emoji_prefix_enabled: bool = field(
46
- default=True,
70
+ default=DEFAULT_LOGGER_NAME_EMOJI_ENABLED,
47
71
  env_var="PROVIDE_LOG_LOGGER_NAME_EMOJI_ENABLED",
72
+ converter=parse_bool_extended,
48
73
  description="Enable emoji prefixes based on logger names",
49
74
  )
50
75
  das_emoji_prefix_enabled: bool = field(
51
- default=True,
76
+ default=DEFAULT_DAS_EMOJI_ENABLED,
52
77
  env_var="PROVIDE_LOG_DAS_EMOJI_ENABLED",
78
+ converter=parse_bool_extended,
53
79
  description="Enable Domain-Action-Status emoji prefixes",
54
80
  )
55
81
  omit_timestamp: bool = field(
56
- default=False,
82
+ default=DEFAULT_OMIT_TIMESTAMP,
57
83
  env_var="PROVIDE_LOG_OMIT_TIMESTAMP",
84
+ converter=parse_bool_extended,
58
85
  description="Omit timestamps from console output",
59
86
  )
60
- enabled_emoji_sets: list[str] = field(
61
- factory=lambda: [],
62
- env_var="PROVIDE_LOG_ENABLED_EMOJI_SETS",
63
- description="Comma-separated list of emoji sets to enable",
64
- )
65
- custom_emoji_sets: list[EventSet] = field(
66
- factory=lambda: [],
67
- env_var="PROVIDE_LOG_CUSTOM_EMOJI_SETS",
68
- description="JSON array of custom emoji set configurations",
69
- )
70
- user_defined_emoji_sets: list[EventMapping] = field(
71
- factory=lambda: [],
72
- env_var="PROVIDE_LOG_USER_DEFINED_EMOJI_SETS",
73
- description="JSON array of user-defined emoji sets",
74
- )
87
+ # Event sets have replaced emoji sets - these fields are deprecated
75
88
  log_file: Path | None = field(
76
- default=None, env_var="PROVIDE_LOG_FILE", description="Path to log file"
89
+ default=None,
90
+ env_var="PROVIDE_LOG_FILE",
91
+ converter=lambda x: Path(x) if x else None,
92
+ description="Path to log file",
77
93
  )
78
94
  foundation_setup_log_level: LogLevelStr = field(
79
- default="INFO",
95
+ default=DEFAULT_FOUNDATION_SETUP_LOG_LEVEL,
80
96
  env_var="FOUNDATION_LOG_LEVEL",
97
+ converter=parse_log_level,
98
+ validator=validate_log_level,
81
99
  description="Log level for Foundation internal setup messages",
82
100
  )
83
101
  foundation_log_output: str = field(
84
- default="stderr",
102
+ default=DEFAULT_FOUNDATION_LOG_OUTPUT,
85
103
  env_var="FOUNDATION_LOG_OUTPUT",
104
+ converter=parse_foundation_log_output,
86
105
  description="Output destination for Foundation internal messages (stderr, stdout, main)",
87
106
  )
88
- show_emoji_matrix: bool = field(
89
- default=False,
90
- env_var="PROVIDE_SHOW_EMOJI_MATRIX",
91
- description="Whether to display emoji matrix on startup",
92
- )
93
107
  rate_limit_enabled: bool = field(
94
- default=False,
108
+ default=DEFAULT_RATE_LIMIT_ENABLED,
95
109
  env_var="PROVIDE_LOG_RATE_LIMIT_ENABLED",
110
+ converter=parse_bool_extended,
96
111
  description="Enable rate limiting for log output",
97
112
  )
98
113
  rate_limit_global: float | None = field(
99
114
  default=None,
100
115
  env_var="PROVIDE_LOG_RATE_LIMIT_GLOBAL",
116
+ converter=lambda x: parse_float_with_validation(x, min_val=0.0) if x else None,
101
117
  description="Global rate limit (logs per second)",
102
118
  )
103
119
  rate_limit_global_capacity: float | None = field(
104
120
  default=None,
105
121
  env_var="PROVIDE_LOG_RATE_LIMIT_GLOBAL_CAPACITY",
122
+ converter=lambda x: parse_float_with_validation(x, min_val=0.0) if x else None,
106
123
  description="Global rate limit burst capacity",
107
124
  )
108
125
  rate_limit_per_logger: dict[str, tuple[float, float]] = field(
109
126
  factory=lambda: {},
110
127
  env_var="PROVIDE_LOG_RATE_LIMIT_PER_LOGGER",
128
+ converter=parse_rate_limits,
111
129
  description="Per-logger rate limits (format: logger1:rate:capacity,logger2:rate:capacity)",
112
130
  )
113
131
  rate_limit_emit_warnings: bool = field(
114
- default=True,
132
+ default=DEFAULT_RATE_LIMIT_EMIT_WARNINGS,
115
133
  env_var="PROVIDE_LOG_RATE_LIMIT_EMIT_WARNINGS",
134
+ converter=parse_bool_extended,
116
135
  description="Emit warnings when logs are rate limited",
117
136
  )
118
137
  rate_limit_summary_interval: float = field(
119
- default=5.0,
138
+ default=DEFAULT_RATE_LIMIT_GLOBAL,
120
139
  env_var="PROVIDE_LOG_RATE_LIMIT_SUMMARY_INTERVAL",
140
+ converter=lambda x: parse_float_with_validation(x, min_val=0.0)
141
+ if x
142
+ else DEFAULT_RATE_LIMIT_GLOBAL,
143
+ validator=validate_positive,
121
144
  description="Seconds between rate limit summary reports",
122
145
  )
123
146
  rate_limit_max_queue_size: int = field(
124
- default=1000,
147
+ default=DEFAULT_RATE_LIMIT_GLOBAL_CAPACITY,
125
148
  env_var="PROVIDE_LOG_RATE_LIMIT_MAX_QUEUE_SIZE",
149
+ converter=int,
150
+ validator=validate_positive,
126
151
  description="Maximum number of logs to queue when rate limited",
127
152
  )
128
153
  rate_limit_max_memory_mb: float | None = field(
129
154
  default=None,
130
155
  env_var="PROVIDE_LOG_RATE_LIMIT_MAX_MEMORY_MB",
156
+ converter=lambda x: parse_float_with_validation(x, min_val=0.0) if x else None,
131
157
  description="Maximum memory (MB) for queued logs",
132
158
  )
133
159
  rate_limit_overflow_policy: str = field(
134
- default="drop_oldest",
160
+ default=DEFAULT_RATE_LIMIT_OVERFLOW_POLICY,
135
161
  env_var="PROVIDE_LOG_RATE_LIMIT_OVERFLOW_POLICY",
162
+ validator=validate_overflow_policy,
136
163
  description="Policy when queue is full: drop_oldest, drop_newest, or block",
137
164
  )
138
-
139
- @classmethod
140
- def from_env(cls, strict: bool = True) -> "LoggingConfig":
141
- """Load LoggingConfig from environment variables."""
142
- config_dict = {}
143
-
144
- # Parse standard fields
145
- if level := os.getenv("PROVIDE_LOG_LEVEL"):
146
- level = level.upper()
147
- if level in _VALID_LOG_LEVEL_TUPLE:
148
- config_dict["default_level"] = level
149
- elif strict:
150
- get_config_logger().warning(
151
- "[Foundation Config Warning] Invalid configuration value, using default",
152
- config_key="PROVIDE_LOG_LEVEL",
153
- invalid_value=level,
154
- valid_options=list(_VALID_LOG_LEVEL_TUPLE),
155
- default_value="DEBUG",
156
- )
157
-
158
- if formatter := os.getenv("PROVIDE_LOG_CONSOLE_FORMATTER"):
159
- formatter = formatter.lower()
160
- if formatter in _VALID_FORMATTER_TUPLE:
161
- config_dict["console_formatter"] = formatter
162
- elif strict:
163
- get_config_logger().warning(
164
- "[Foundation Config Warning] Invalid configuration value, using default",
165
- config_key="PROVIDE_LOG_CONSOLE_FORMATTER",
166
- invalid_value=formatter,
167
- valid_options=list(_VALID_FORMATTER_TUPLE),
168
- default_value="key_value",
169
- )
170
-
171
- if omit_ts := os.getenv("PROVIDE_LOG_OMIT_TIMESTAMP"):
172
- config_dict["omit_timestamp"] = omit_ts.lower() == "true"
173
-
174
- if logger_emoji := os.getenv("PROVIDE_LOG_LOGGER_NAME_EMOJI_ENABLED"):
175
- config_dict["logger_name_emoji_prefix_enabled"] = (
176
- logger_emoji.lower() == "true"
177
- )
178
-
179
- if das_emoji := os.getenv("PROVIDE_LOG_DAS_EMOJI_ENABLED"):
180
- config_dict["das_emoji_prefix_enabled"] = das_emoji.lower() == "true"
181
-
182
- if log_file := os.getenv("PROVIDE_LOG_FILE"):
183
- config_dict["log_file"] = Path(log_file)
184
-
185
- if foundation_level := os.getenv("FOUNDATION_LOG_LEVEL"):
186
- foundation_level = foundation_level.upper()
187
- if foundation_level in _VALID_LOG_LEVEL_TUPLE:
188
- config_dict["foundation_setup_log_level"] = foundation_level
189
- elif strict:
190
- get_config_logger().warning(
191
- "[Foundation Config Warning] Invalid configuration value, using default",
192
- config_key="FOUNDATION_LOG_LEVEL",
193
- invalid_value=foundation_level,
194
- valid_options=list(_VALID_LOG_LEVEL_TUPLE),
195
- default_value="INFO",
196
- )
197
-
198
- if foundation_output := os.getenv("FOUNDATION_LOG_OUTPUT"):
199
- foundation_output = foundation_output.lower()
200
- valid_outputs = ("stderr", "stdout", "main")
201
- if foundation_output in valid_outputs:
202
- config_dict["foundation_log_output"] = foundation_output
203
- elif strict:
204
- get_config_logger().warning(
205
- "[Foundation Config Warning] Invalid configuration value, using default",
206
- config_key="FOUNDATION_LOG_OUTPUT",
207
- invalid_value=foundation_output,
208
- valid_options=list(valid_outputs),
209
- default_value="stderr",
210
- )
211
-
212
- if show_matrix := os.getenv("PROVIDE_SHOW_EMOJI_MATRIX"):
213
- config_dict["show_emoji_matrix"] = show_matrix.strip().lower() in (
214
- "true",
215
- "1",
216
- "yes",
217
- )
218
-
219
- # Parse rate limiting configuration
220
- if rate_limit_enabled := os.getenv("PROVIDE_LOG_RATE_LIMIT_ENABLED"):
221
- config_dict["rate_limit_enabled"] = rate_limit_enabled.lower() == "true"
222
-
223
- if rate_limit_global := os.getenv("PROVIDE_LOG_RATE_LIMIT_GLOBAL"):
224
- try:
225
- config_dict["rate_limit_global"] = float(rate_limit_global)
226
- except ValueError:
227
- if strict:
228
- get_config_logger().warning(
229
- "[Foundation Config Warning] Invalid rate limit value",
230
- config_key="PROVIDE_LOG_RATE_LIMIT_GLOBAL",
231
- invalid_value=rate_limit_global,
232
- )
233
-
234
- if rate_limit_capacity := os.getenv("PROVIDE_LOG_RATE_LIMIT_GLOBAL_CAPACITY"):
235
- try:
236
- config_dict["rate_limit_global_capacity"] = float(rate_limit_capacity)
237
- except ValueError:
238
- if strict:
239
- get_config_logger().warning(
240
- "[Foundation Config Warning] Invalid rate limit capacity",
241
- config_key="PROVIDE_LOG_RATE_LIMIT_GLOBAL_CAPACITY",
242
- invalid_value=rate_limit_capacity,
243
- )
244
-
245
- if per_logger_limits := os.getenv("PROVIDE_LOG_RATE_LIMIT_PER_LOGGER"):
246
- limits_dict = {}
247
- for item in per_logger_limits.split(","):
248
- parts = item.split(":")
249
- if len(parts) == 3:
250
- logger_name, rate, capacity = parts
251
- try:
252
- limits_dict[logger_name.strip()] = (
253
- float(rate.strip()),
254
- float(capacity.strip()),
255
- )
256
- except ValueError:
257
- if strict:
258
- get_config_logger().warning(
259
- "[Foundation Config Warning] Invalid per-logger rate limit",
260
- config_key="PROVIDE_LOG_RATE_LIMIT_PER_LOGGER",
261
- invalid_item=item,
262
- )
263
- if limits_dict:
264
- config_dict["rate_limit_per_logger"] = limits_dict
265
-
266
- if emit_warnings := os.getenv("PROVIDE_LOG_RATE_LIMIT_EMIT_WARNINGS"):
267
- config_dict["rate_limit_emit_warnings"] = emit_warnings.lower() == "true"
268
-
269
- if summary_interval := os.getenv("PROVIDE_LOG_RATE_LIMIT_SUMMARY_INTERVAL"):
270
- try:
271
- config_dict["rate_limit_summary_interval"] = float(summary_interval)
272
- except ValueError:
273
- if strict:
274
- get_config_logger().warning(
275
- "[Foundation Config Warning] Invalid summary interval",
276
- config_key="PROVIDE_LOG_RATE_LIMIT_SUMMARY_INTERVAL",
277
- invalid_value=summary_interval,
278
- )
279
-
280
- if max_queue := os.getenv("PROVIDE_LOG_RATE_LIMIT_MAX_QUEUE_SIZE"):
281
- try:
282
- config_dict["rate_limit_max_queue_size"] = int(max_queue)
283
- except ValueError:
284
- if strict:
285
- get_config_logger().warning(
286
- "[Foundation Config Warning] Invalid max queue size",
287
- config_key="PROVIDE_LOG_RATE_LIMIT_MAX_QUEUE_SIZE",
288
- invalid_value=max_queue,
289
- )
290
-
291
- if max_memory := os.getenv("PROVIDE_LOG_RATE_LIMIT_MAX_MEMORY_MB"):
292
- try:
293
- config_dict["rate_limit_max_memory_mb"] = float(max_memory)
294
- except ValueError:
295
- if strict:
296
- get_config_logger().warning(
297
- "[Foundation Config Warning] Invalid max memory",
298
- config_key="PROVIDE_LOG_RATE_LIMIT_MAX_MEMORY_MB",
299
- invalid_value=max_memory,
300
- )
301
-
302
- if overflow_policy := os.getenv("PROVIDE_LOG_RATE_LIMIT_OVERFLOW_POLICY"):
303
- valid_policies = ("drop_oldest", "drop_newest", "block")
304
- if overflow_policy in valid_policies:
305
- config_dict["rate_limit_overflow_policy"] = overflow_policy
306
- elif strict:
307
- get_config_logger().warning(
308
- "[Foundation Config Warning] Invalid overflow policy",
309
- config_key="PROVIDE_LOG_RATE_LIMIT_OVERFLOW_POLICY",
310
- invalid_value=overflow_policy,
311
- valid_options=list(valid_policies),
312
- )
313
-
314
- # Parse complex fields
315
- if module_levels := os.getenv("PROVIDE_LOG_MODULE_LEVELS"):
316
- levels_dict = {}
317
- for item in module_levels.split(","):
318
- if ":" in item:
319
- module, level = item.split(":", 1)
320
- module = module.strip()
321
- level = level.strip().upper()
322
- if module and level in _VALID_LOG_LEVEL_TUPLE:
323
- levels_dict[module] = level
324
- elif strict and module and level not in _VALID_LOG_LEVEL_TUPLE:
325
- get_config_logger().warning(
326
- "[Foundation Config Warning] Invalid module log level, skipping",
327
- config_key="PROVIDE_LOG_MODULE_LEVELS",
328
- module_name=module,
329
- invalid_level=level,
330
- valid_options=list(_VALID_LOG_LEVEL_TUPLE),
331
- )
332
- if levels_dict:
333
- config_dict["module_levels"] = levels_dict
334
-
335
- if emoji_sets := os.getenv("PROVIDE_LOG_ENABLED_EMOJI_SETS"):
336
- config_dict["enabled_emoji_sets"] = [
337
- s.strip() for s in emoji_sets.split(",") if s.strip()
338
- ]
339
-
340
- if custom_sets := os.getenv("PROVIDE_LOG_CUSTOM_EMOJI_SETS"):
341
- try:
342
- parsed = json.loads(custom_sets)
343
- if isinstance(parsed, list):
344
- config_dict["custom_emoji_sets"] = [
345
- EventSet(**item) if isinstance(item, dict) else item
346
- for item in parsed
347
- ]
348
- except json.JSONDecodeError as e:
349
- if strict:
350
- get_config_logger().warning(
351
- "[Foundation Config Warning] Invalid JSON in configuration",
352
- config_key="PROVIDE_LOG_CUSTOM_EMOJI_SETS",
353
- error=str(e),
354
- config_value=custom_sets[:100] + "..."
355
- if len(custom_sets) > 100
356
- else custom_sets,
357
- )
358
- except (TypeError, ValueError) as e:
359
- if strict:
360
- get_config_logger().warning(
361
- "[Foundation Config Warning] Error parsing custom emoji set configuration",
362
- config_key="PROVIDE_LOG_CUSTOM_EMOJI_SETS",
363
- error=str(e),
364
- error_type=type(e).__name__,
365
- )
366
-
367
- if user_sets := os.getenv("PROVIDE_LOG_USER_DEFINED_EMOJI_SETS"):
368
- try:
369
- parsed = json.loads(user_sets)
370
- if isinstance(parsed, list):
371
- config_dict["user_defined_emoji_sets"] = [
372
- EventMapping(**item) if isinstance(item, dict) else item
373
- for item in parsed
374
- ]
375
- except json.JSONDecodeError as e:
376
- if strict:
377
- get_config_logger().warning(
378
- "[Foundation Config Warning] Invalid JSON in configuration",
379
- config_key="PROVIDE_LOG_USER_DEFINED_EMOJI_SETS",
380
- error=str(e),
381
- config_value=user_sets[:100] + "..."
382
- if len(user_sets) > 100
383
- else user_sets,
384
- )
385
- except (TypeError, ValueError) as e:
386
- if strict:
387
- get_config_logger().warning(
388
- "[Foundation Config Warning] Error parsing user emoji set configuration",
389
- config_key="PROVIDE_LOG_USER_DEFINED_EMOJI_SETS",
390
- error=str(e),
391
- error_type=type(e).__name__,
392
- )
393
-
394
- return cls.from_dict(config_dict, source=ConfigSource.ENV)
@@ -9,18 +9,36 @@ import os
9
9
 
10
10
  from attrs import define
11
11
 
12
- from provide.foundation.config import BaseConfig, field
12
+ from provide.foundation.config.base import field
13
+ from provide.foundation.config.converters import (
14
+ parse_bool_extended,
15
+ parse_headers,
16
+ parse_sample_rate,
17
+ validate_sample_rate,
18
+ )
19
+ from provide.foundation.config.defaults import (
20
+ DEFAULT_METRICS_ENABLED,
21
+ DEFAULT_OTLP_PROTOCOL,
22
+ DEFAULT_TELEMETRY_GLOBALLY_DISABLED,
23
+ DEFAULT_TRACE_SAMPLE_RATE,
24
+ DEFAULT_TRACING_ENABLED,
25
+ )
26
+ from provide.foundation.config.env import RuntimeConfig
13
27
  from provide.foundation.logger.config.logging import LoggingConfig
14
28
 
15
29
 
30
+ def _get_service_name() -> str | None:
31
+ """Get service name from OTEL_SERVICE_NAME or PROVIDE_SERVICE_NAME (OTEL takes precedence)."""
32
+ return os.getenv("OTEL_SERVICE_NAME") or os.getenv("PROVIDE_SERVICE_NAME")
33
+
34
+
16
35
  @define(slots=True, repr=False)
17
- class TelemetryConfig(BaseConfig):
36
+ class TelemetryConfig(RuntimeConfig):
18
37
  """Main configuration object for the Foundation Telemetry system."""
19
38
 
20
39
  service_name: str | None = field(
21
- default=None,
22
- env_var="PROVIDE_SERVICE_NAME",
23
- description="Service name for telemetry",
40
+ factory=_get_service_name,
41
+ description="Service name for telemetry (from OTEL_SERVICE_NAME or PROVIDE_SERVICE_NAME)",
24
42
  )
25
43
  service_version: str | None = field(
26
44
  default=None,
@@ -28,23 +46,26 @@ class TelemetryConfig(BaseConfig):
28
46
  description="Service version for telemetry",
29
47
  )
30
48
  logging: LoggingConfig = field(
31
- factory=LoggingConfig, description="Logging configuration"
49
+ factory=lambda: LoggingConfig.from_env(), 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,29 @@ 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
-
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
-
174
100
  def get_otlp_headers_dict(self) -> dict[str, str]:
175
- """Parse OTLP headers string into dictionary.
101
+ """Get OTLP headers dictionary.
176
102
 
177
103
  Returns:
178
104
  Dictionary of header key-value pairs
179
105
  """
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
106
+ return self.otlp_headers