ergon-framework-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ergon/__init__.py +13 -0
- ergon/bootstrap/src/__project__/__init__.py +0 -0
- ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +124 -0
- ergon/bootstrap/src/__project__/_observability/grafana.yaml +17 -0
- ergon/bootstrap/src/__project__/_observability/loki.yaml +48 -0
- ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +53 -0
- ergon/bootstrap/src/__project__/_observability/prometheus.yaml +11 -0
- ergon/bootstrap/src/__project__/_observability/tempo.yaml +24 -0
- ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
- ergon/bootstrap/src/__project__/main.py +9 -0
- ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
- ergon/bootstrap/src/__project__/tasks/constants.py +13 -0
- ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
- ergon/bootstrap/src/__project__/tasks/example_task/config.py +4 -0
- ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +4 -0
- ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +4 -0
- ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +5 -0
- ergon/bootstrap/src/__project__/tasks/example_task/task.py +1 -0
- ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
- ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
- ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
- ergon/bootstrap/src/__project__/tasks/settings.py +5 -0
- ergon/cli.py +174 -0
- ergon/connector/__init__.py +64 -0
- ergon/connector/connector.py +97 -0
- ergon/connector/excel/__init__.py +18 -0
- ergon/connector/excel/connector.py +175 -0
- ergon/connector/excel/models.py +24 -0
- ergon/connector/excel/service.py +98 -0
- ergon/connector/pipefy/__init__.py +21 -0
- ergon/connector/pipefy/async_connector.py +48 -0
- ergon/connector/pipefy/async_service.py +907 -0
- ergon/connector/pipefy/connector.py +36 -0
- ergon/connector/pipefy/models.py +48 -0
- ergon/connector/pipefy/service.py +1016 -0
- ergon/connector/pipefy/version.py +1 -0
- ergon/connector/postgres/__init__.py +11 -0
- ergon/connector/postgres/async_connector.py +119 -0
- ergon/connector/postgres/async_service.py +116 -0
- ergon/connector/postgres/models.py +34 -0
- ergon/connector/rabbitmq/__init__.py +25 -0
- ergon/connector/rabbitmq/async_connector.py +120 -0
- ergon/connector/rabbitmq/async_service.py +417 -0
- ergon/connector/rabbitmq/connector.py +54 -0
- ergon/connector/rabbitmq/helper.py +14 -0
- ergon/connector/rabbitmq/models.py +92 -0
- ergon/connector/rabbitmq/service.py +199 -0
- ergon/connector/sqs/__init__.py +15 -0
- ergon/connector/sqs/async_connector.py +120 -0
- ergon/connector/sqs/async_service.py +246 -0
- ergon/connector/sqs/connector.py +120 -0
- ergon/connector/sqs/models.py +36 -0
- ergon/connector/sqs/service.py +219 -0
- ergon/connector/transaction.py +14 -0
- ergon/py.typed +0 -0
- ergon/service/__init__.py +5 -0
- ergon/service/service.py +17 -0
- ergon/task/__init__.py +13 -0
- ergon/task/base.py +222 -0
- ergon/task/exceptions.py +217 -0
- ergon/task/helpers.py +691 -0
- ergon/task/manager.py +85 -0
- ergon/task/mixins/__init__.py +13 -0
- ergon/task/mixins/consumer.py +858 -0
- ergon/task/mixins/metrics.py +457 -0
- ergon/task/mixins/producer.py +486 -0
- ergon/task/policies.py +229 -0
- ergon/task/runner.py +386 -0
- ergon/task/utils.py +64 -0
- ergon/telemetry/__init__.py +7 -0
- ergon/telemetry/_resource.py +13 -0
- ergon/telemetry/logging.py +370 -0
- ergon/telemetry/metrics.py +101 -0
- ergon/telemetry/tracing.py +152 -0
- ergon/utils/__init__.py +5 -0
- ergon/utils/env.py +26 -0
- ergon_framework_python-0.1.0.dist-info/METADATA +449 -0
- ergon_framework_python-0.1.0.dist-info/RECORD +82 -0
- ergon_framework_python-0.1.0.dist-info/WHEEL +5 -0
- ergon_framework_python-0.1.0.dist-info/entry_points.txt +2 -0
- ergon_framework_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- ergon_framework_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
|
6
|
+
|
|
7
|
+
from opentelemetry._logs import set_logger_provider
|
|
8
|
+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
|
9
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
|
10
|
+
from opentelemetry.sdk._logs.export import (
|
|
11
|
+
BatchLogRecordProcessor,
|
|
12
|
+
ConsoleLogExporter,
|
|
13
|
+
SimpleLogRecordProcessor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# ---------------------------
|
|
17
|
+
# OpenTelemetry imports
|
|
18
|
+
# ---------------------------
|
|
19
|
+
from opentelemetry.sdk.resources import Resource
|
|
20
|
+
from pydantic import BaseModel, Field, model_validator
|
|
21
|
+
|
|
22
|
+
from ._resource import _inject_otel_resource_attributes
|
|
23
|
+
|
|
24
|
+
# ============================================================
|
|
25
|
+
# Pydantic CONFIG OBJECTS
|
|
26
|
+
# ============================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LogFormatter(BaseModel):
|
|
30
|
+
name: str = "default"
|
|
31
|
+
fmt: str = "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s"
|
|
32
|
+
datefmt: str = "%Y-%m-%d %H:%M:%S"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------
|
|
36
|
+
# Filter config
|
|
37
|
+
# ---------------------------
|
|
38
|
+
class LogFilter(BaseModel):
|
|
39
|
+
name: str # filter name in dictConfig
|
|
40
|
+
filter: Any # class inheriting from logging.Filter
|
|
41
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------
|
|
45
|
+
# Base Handler config
|
|
46
|
+
# ---------------------------
|
|
47
|
+
class BaseLogHandler(BaseModel):
|
|
48
|
+
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
49
|
+
formatter: str = "default"
|
|
50
|
+
filters: List[str] = Field(default_factory=list) # MUST be names
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ConsoleLogHandler(BaseLogHandler):
|
|
54
|
+
type: Literal["console"] = "console"
|
|
55
|
+
stream: Literal["stdout", "stderr"] = "stdout"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FileLogHandler(BaseLogHandler):
|
|
59
|
+
type: Literal["file"] = "file"
|
|
60
|
+
filename: str
|
|
61
|
+
mode: Literal["a", "w"] = "a"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class JSONLogHandler(BaseLogHandler):
|
|
65
|
+
filename: str
|
|
66
|
+
type: Literal["json"] = "json"
|
|
67
|
+
formatter: str = "json"
|
|
68
|
+
encoding: str = "utf-8"
|
|
69
|
+
mode: Literal["a", "w"] = "a"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------
|
|
73
|
+
# EXPORTER CONFIG (for lazy instantiation)
|
|
74
|
+
# ---------------------------
|
|
75
|
+
class ExporterConfig(BaseModel):
|
|
76
|
+
"""
|
|
77
|
+
Configuration for lazy exporter instantiation.
|
|
78
|
+
Required for multiprocessing support since gRPC channels cannot be pickled.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
exporter: Any # Exporter class (e.g., OTLPLogExporter)
|
|
82
|
+
args: Dict[str, Any] = Field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------
|
|
86
|
+
# PROCESSOR CONFIG
|
|
87
|
+
# ---------------------------
|
|
88
|
+
class LogProcessor(BaseModel):
|
|
89
|
+
processor: Any
|
|
90
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
91
|
+
# Exporters can be either instances or ExporterConfig for lazy instantiation
|
|
92
|
+
exporters: List[Any] = Field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
@model_validator(mode="after")
|
|
95
|
+
def validate_exporters(self):
|
|
96
|
+
if not isinstance(self.exporters, list):
|
|
97
|
+
raise ValueError("exporters must be a list of exporter instances or ExporterConfig")
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------
|
|
102
|
+
# OTLP HANDLER CONFIG
|
|
103
|
+
# ---------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class OTLPLogHandler(BaseLogHandler):
|
|
107
|
+
type: Literal["otlp"] = "otlp"
|
|
108
|
+
resource: Dict[str, Any] = Field(default_factory=dict)
|
|
109
|
+
processors: List[LogProcessor] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
@model_validator(mode="after")
|
|
112
|
+
def confirm_processors(self):
|
|
113
|
+
if not self.processors:
|
|
114
|
+
raise ValueError("OTLPHandlerConfig requires at least one processor.")
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RotatingFileLogHandler(BaseLogHandler):
|
|
119
|
+
type: Literal["rotating_file"] = "rotating_file"
|
|
120
|
+
filename: str
|
|
121
|
+
max_bytes: int = 10_000_000
|
|
122
|
+
backup_count: int = 5
|
|
123
|
+
callback: Optional[Callable[[Any, str], str]] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TimedRotatingFileLogHandler(BaseLogHandler):
|
|
127
|
+
type: Literal["timed_rotating_file"] = "timed_rotating_file"
|
|
128
|
+
filename: str
|
|
129
|
+
when: Literal["S", "M", "H", "D", "midnight"] = "midnight"
|
|
130
|
+
interval: int = 1
|
|
131
|
+
backupCount: int = 7
|
|
132
|
+
callback: Optional[Callable[[Any, str], str]] = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------
|
|
136
|
+
# Handlers union
|
|
137
|
+
# ---------------------------
|
|
138
|
+
LogHandlers = Union[
|
|
139
|
+
ConsoleLogHandler,
|
|
140
|
+
FileLogHandler,
|
|
141
|
+
JSONLogHandler,
|
|
142
|
+
OTLPLogHandler,
|
|
143
|
+
RotatingFileLogHandler,
|
|
144
|
+
TimedRotatingFileLogHandler,
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------
|
|
149
|
+
# Main logging config object
|
|
150
|
+
# ---------------------------
|
|
151
|
+
class LoggingConfig(BaseModel):
|
|
152
|
+
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
153
|
+
handlers: List[LogHandlers] = Field(default_factory=list)
|
|
154
|
+
filters: List[LogFilter] = Field(default_factory=list)
|
|
155
|
+
formatters: List[LogFormatter] = Field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ============================================================
|
|
159
|
+
# Handler builders
|
|
160
|
+
# ============================================================
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _default_formatter_dict():
|
|
164
|
+
return {
|
|
165
|
+
"format": "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
|
|
166
|
+
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _build_console_handler_dict(cfg: ConsoleLogHandler, *args, **kwargs):
|
|
171
|
+
return {
|
|
172
|
+
"class": "logging.StreamHandler",
|
|
173
|
+
"level": cfg.level,
|
|
174
|
+
"formatter": cfg.formatter,
|
|
175
|
+
"stream": "ext://sys.stdout" if cfg.stream == "stdout" else "ext://sys.stderr",
|
|
176
|
+
"filters": cfg.filters,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _resolve_filename(cfg, *args, **kwargs):
|
|
181
|
+
template = cfg.filename
|
|
182
|
+
task = kwargs["task"]
|
|
183
|
+
metadata = kwargs["metadata"]
|
|
184
|
+
task_name = metadata["task_name"]
|
|
185
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
186
|
+
|
|
187
|
+
final = template.format(
|
|
188
|
+
pid=metadata["pid"],
|
|
189
|
+
task=task_name,
|
|
190
|
+
timestamp=timestamp,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if getattr(cfg, "callback", None):
|
|
194
|
+
return cfg.callback(task=task, filename=final)
|
|
195
|
+
|
|
196
|
+
return final
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _build_file_handler_dict(cfg: FileLogHandler, *args, **kwargs):
|
|
200
|
+
filename = _resolve_filename(cfg, *args, **kwargs)
|
|
201
|
+
Path(filename).parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"class": "logging.FileHandler",
|
|
205
|
+
"level": cfg.level,
|
|
206
|
+
"formatter": cfg.formatter,
|
|
207
|
+
"filename": filename,
|
|
208
|
+
"mode": cfg.mode,
|
|
209
|
+
"encoding": "utf-8",
|
|
210
|
+
"filters": cfg.filters,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _build_rotating_handler_dict(cfg: RotatingFileLogHandler, *args, **kwargs):
|
|
215
|
+
filename = _resolve_filename(cfg, *args, **kwargs)
|
|
216
|
+
Path(filename).parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
220
|
+
"level": cfg.level,
|
|
221
|
+
"formatter": cfg.formatter,
|
|
222
|
+
"filename": filename,
|
|
223
|
+
"maxBytes": cfg.max_bytes,
|
|
224
|
+
"backupCount": cfg.backup_count,
|
|
225
|
+
"encoding": "utf-8",
|
|
226
|
+
"filters": cfg.filters,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _build_timed_rotating_handler_dict(cfg: TimedRotatingFileLogHandler, *args, **kwargs):
|
|
231
|
+
filename = _resolve_filename(cfg, *args, **kwargs)
|
|
232
|
+
Path(filename).parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"class": "logging.handlers.TimedRotatingFileHandler",
|
|
236
|
+
"level": cfg.level,
|
|
237
|
+
"formatter": cfg.formatter,
|
|
238
|
+
"filename": filename,
|
|
239
|
+
"when": cfg.when,
|
|
240
|
+
"interval": cfg.interval,
|
|
241
|
+
"backupCount": cfg.backupCount,
|
|
242
|
+
"encoding": "utf-8",
|
|
243
|
+
"filters": cfg.filters,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _build_json_handler_dict(cfg: JSONLogHandler, *args, **kwargs):
|
|
248
|
+
if cfg.filename:
|
|
249
|
+
filename = _resolve_filename(cfg, *args, **kwargs)
|
|
250
|
+
Path(filename).parent.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
handler_class = "logging.FileHandler"
|
|
252
|
+
handler_args = {"filename": filename, **cfg.model_dump(exclude={"filename", "type", "formatter"})}
|
|
253
|
+
else:
|
|
254
|
+
handler_class = "logging.StreamHandler"
|
|
255
|
+
handler_args = {"stream": "ext://sys.stdout"}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"class": handler_class,
|
|
259
|
+
"level": cfg.level,
|
|
260
|
+
"formatter": cfg.formatter,
|
|
261
|
+
"filters": cfg.filters,
|
|
262
|
+
**handler_args,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------
|
|
267
|
+
# REAL OTel logging pipeline
|
|
268
|
+
# ---------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _build_otlp_handler_dict(cfg: OTLPLogHandler, *args, **kwargs):
|
|
272
|
+
"""Build an OTLP log handler dictionary."""
|
|
273
|
+
|
|
274
|
+
metadata = kwargs["metadata"]
|
|
275
|
+
|
|
276
|
+
cfg.resource = _inject_otel_resource_attributes(resource=cfg.resource, metadata=metadata)
|
|
277
|
+
|
|
278
|
+
resource = Resource(attributes=cfg.resource)
|
|
279
|
+
provider = LoggerProvider(resource=resource)
|
|
280
|
+
|
|
281
|
+
for p in cfg.processors:
|
|
282
|
+
for exporter in p.exporters:
|
|
283
|
+
# Handle lazy configuration for multiprocessing support
|
|
284
|
+
if isinstance(exporter, ExporterConfig):
|
|
285
|
+
exporter_instance = exporter.exporter(**exporter.args)
|
|
286
|
+
else:
|
|
287
|
+
exporter_instance = exporter
|
|
288
|
+
|
|
289
|
+
provider.add_log_record_processor(p.processor(**p.config, exporter=exporter_instance))
|
|
290
|
+
|
|
291
|
+
set_logger_provider(provider)
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"class": "opentelemetry.sdk._logs.LoggingHandler",
|
|
295
|
+
"level": cfg.level,
|
|
296
|
+
"formatter": cfg.formatter,
|
|
297
|
+
"filters": cfg.filters,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Registry
|
|
302
|
+
_LOG_HANDLER_BUILDERS_DICT = {
|
|
303
|
+
"console": lambda cfg, *args, **kwargs: _build_console_handler_dict(cfg, *args, **kwargs),
|
|
304
|
+
"file": lambda cfg, *args, **kwargs: _build_file_handler_dict(cfg, *args, **kwargs),
|
|
305
|
+
"json": lambda cfg, *args, **kwargs: _build_json_handler_dict(cfg, *args, **kwargs),
|
|
306
|
+
"otlp": lambda cfg, *args, **kwargs: _build_otlp_handler_dict(cfg, *args, **kwargs),
|
|
307
|
+
"rotating_file": lambda cfg, *args, **kwargs: _build_rotating_handler_dict(cfg, *args, **kwargs),
|
|
308
|
+
"timed_rotating_file": lambda cfg, *args, **kwargs: _build_timed_rotating_handler_dict(cfg, *args, **kwargs),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# ============================================================
|
|
312
|
+
# Apply entire LoggingConfig to dictConfig
|
|
313
|
+
# ============================================================
|
|
314
|
+
|
|
315
|
+
_LOGGING_CONFIGURED = False
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _apply_logging_config(cfg: LoggingConfig, task: object, metadata: dict):
|
|
319
|
+
global _LOGGING_CONFIGURED
|
|
320
|
+
if _LOGGING_CONFIGURED:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
formatters = {f.name: {"format": f.fmt, "datefmt": f.datefmt} for f in cfg.formatters}
|
|
324
|
+
formatters["default"] = _default_formatter_dict()
|
|
325
|
+
formatters["json"] = {
|
|
326
|
+
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
|
327
|
+
"fmt": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
|
328
|
+
}
|
|
329
|
+
formatters["default"] = _default_formatter_dict()
|
|
330
|
+
|
|
331
|
+
# Build filters dictionary:
|
|
332
|
+
filters = {f.name: {"()": f.filter, **f.config} for f in cfg.filters}
|
|
333
|
+
|
|
334
|
+
handlers_dict = {}
|
|
335
|
+
handlers_order = []
|
|
336
|
+
|
|
337
|
+
for idx, handler_cfg in enumerate(cfg.handlers):
|
|
338
|
+
builder = _LOG_HANDLER_BUILDERS_DICT[handler_cfg.type]
|
|
339
|
+
handler_dict = builder(cfg=handler_cfg, task=task, metadata=metadata)
|
|
340
|
+
|
|
341
|
+
handler_name = f"handler_{idx}"
|
|
342
|
+
handlers_dict[handler_name] = handler_dict
|
|
343
|
+
handlers_order.append(handler_name)
|
|
344
|
+
|
|
345
|
+
config = {
|
|
346
|
+
"version": 1,
|
|
347
|
+
"disable_existing_loggers": False,
|
|
348
|
+
"formatters": formatters,
|
|
349
|
+
"filters": filters,
|
|
350
|
+
"handlers": handlers_dict,
|
|
351
|
+
"root": {"level": cfg.level, "handlers": handlers_order},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
logging.config.dictConfig(config)
|
|
355
|
+
_LOGGING_CONFIGURED = True
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def get_logger(name: str) -> logging.Logger:
|
|
359
|
+
return logging.getLogger(name)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
__all__ = [
|
|
363
|
+
"OTLPLogExporter",
|
|
364
|
+
"BatchLogRecordProcessor",
|
|
365
|
+
"ConsoleLogExporter",
|
|
366
|
+
"SimpleLogRecordProcessor",
|
|
367
|
+
"ExporterConfig",
|
|
368
|
+
"set_logger_provider",
|
|
369
|
+
"LoggerProvider",
|
|
370
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# metrics.py
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
|
6
|
+
from opentelemetry.metrics import get_meter_provider, set_meter_provider
|
|
7
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
8
|
+
from opentelemetry.sdk.metrics.export import (
|
|
9
|
+
ConsoleMetricExporter,
|
|
10
|
+
PeriodicExportingMetricReader,
|
|
11
|
+
)
|
|
12
|
+
from opentelemetry.sdk.resources import Resource
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from ._resource import _inject_otel_resource_attributes
|
|
16
|
+
|
|
17
|
+
# ============================================================
|
|
18
|
+
# CONFIG OBJECTS
|
|
19
|
+
# ============================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MetricReader(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
Defines how a metric reader should be constructed.
|
|
25
|
+
Only push-based readers are supported (PeriodicExportingMetricReader).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
reader: Any
|
|
29
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
30
|
+
exporters: List[Any] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MetricsConfig(BaseModel):
|
|
34
|
+
"""
|
|
35
|
+
Root configuration object for the metrics subsystem.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
resource: Dict[str, Any] = Field(default_factory=dict)
|
|
39
|
+
readers: List[MetricReader] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# internal global flag
|
|
43
|
+
_CONFIGURED_METRICS = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================
|
|
47
|
+
# APPLY METRICS CONFIGURATION
|
|
48
|
+
# ============================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _apply_metrics_config(cfg: MetricsConfig, metadata: dict):
|
|
52
|
+
global _CONFIGURED_METRICS
|
|
53
|
+
if _CONFIGURED_METRICS:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
cfg.resource = _inject_otel_resource_attributes(resource=cfg.resource, metadata=metadata)
|
|
57
|
+
|
|
58
|
+
# 1. Create Resource
|
|
59
|
+
resource = Resource(attributes=cfg.resource)
|
|
60
|
+
|
|
61
|
+
# 2. Create MeterProvider
|
|
62
|
+
provider = MeterProvider(resource=resource)
|
|
63
|
+
|
|
64
|
+
# 3. Register metric readers (PUSH-BASED ONLY)
|
|
65
|
+
for r in cfg.readers:
|
|
66
|
+
reader_cls = r.reader
|
|
67
|
+
|
|
68
|
+
# Create reader instance
|
|
69
|
+
# Each reader may have multiple exporters
|
|
70
|
+
# For push-based metrics, exporters are passed inside PeriodicExportingMetricReader
|
|
71
|
+
for exporter in r.exporters:
|
|
72
|
+
reader = reader_cls(exporter=exporter, **r.config)
|
|
73
|
+
provider.add_metric_reader(reader) # type: ignore[attr-defined]
|
|
74
|
+
|
|
75
|
+
# 4. Register globally
|
|
76
|
+
set_meter_provider(provider)
|
|
77
|
+
|
|
78
|
+
_CONFIGURED_METRICS = True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================
|
|
82
|
+
# ACCESSOR
|
|
83
|
+
# ============================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_metric_meter(name: str):
|
|
87
|
+
"""
|
|
88
|
+
Returns a Meter, similar to get_logger for logging.
|
|
89
|
+
"""
|
|
90
|
+
provider = get_meter_provider()
|
|
91
|
+
return provider.get_meter(name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"OTLPMetricExporter",
|
|
96
|
+
"ConsoleMetricExporter",
|
|
97
|
+
"PeriodicExportingMetricReader",
|
|
98
|
+
"MetricReader",
|
|
99
|
+
"MetricsConfig",
|
|
100
|
+
"get_metric_meter",
|
|
101
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
5
|
+
from opentelemetry.sdk.resources import Resource
|
|
6
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
7
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
|
|
8
|
+
from opentelemetry.trace import set_tracer_provider
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from ._resource import _inject_otel_resource_attributes
|
|
12
|
+
|
|
13
|
+
# ============================================================
|
|
14
|
+
# Pydantic CONFIG OBJECTS
|
|
15
|
+
# ============================================================
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SamplerConfig(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Configures sampling strategy such as:
|
|
21
|
+
- AlwaysOnSampler
|
|
22
|
+
- AlwaysOffSampler
|
|
23
|
+
- TraceIdRatioBased
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
sampler: Any # class or callable
|
|
27
|
+
args: Dict[str, Any] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExporterConfig(BaseModel):
|
|
31
|
+
"""
|
|
32
|
+
Configuration for lazy exporter instantiation.
|
|
33
|
+
Required for multiprocessing support since gRPC channels cannot be pickled.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
37
|
+
|
|
38
|
+
exporter: Any # Exporter class (e.g., OTLPSpanExporter)
|
|
39
|
+
args: Dict[str, Any] = Field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SpanProcessor(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
Mirrors logging.ProcessorConfig.
|
|
45
|
+
Defines a span processor with one or multiple span exporters.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
49
|
+
|
|
50
|
+
processor: Any # BatchSpanProcessor, SimpleSpanProcessor, custom
|
|
51
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
52
|
+
# Exporters can be either instances or ExporterConfig for lazy instantiation
|
|
53
|
+
exporters: List[Any] = Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TracingConfig(BaseModel):
|
|
57
|
+
"""
|
|
58
|
+
Root configuration object for tracing.
|
|
59
|
+
Mirrors LoggingConfig structure.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
resource: Dict[str, Any] = Field(default_factory=dict)
|
|
63
|
+
sampler: Optional[SamplerConfig] = None
|
|
64
|
+
processors: List[SpanProcessor] = Field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ============================================================
|
|
68
|
+
# INTERNAL STATE
|
|
69
|
+
# ============================================================
|
|
70
|
+
|
|
71
|
+
_TRACING_CONFIGURED = False
|
|
72
|
+
_TRACING_LOCK = threading.Lock()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ============================================================
|
|
76
|
+
# APPLY TRACING CONFIG
|
|
77
|
+
# ============================================================
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _apply_tracing_config(cfg: TracingConfig, metadata: dict):
|
|
81
|
+
"""
|
|
82
|
+
Initializes global OpenTelemetry TracerProvider.
|
|
83
|
+
- Builds Resource
|
|
84
|
+
- Applies sampler
|
|
85
|
+
- Instantiates span processors
|
|
86
|
+
- Registers exporters
|
|
87
|
+
- Sets provider globally
|
|
88
|
+
|
|
89
|
+
Mirrors logging.apply_logger_config but adapted for tracing.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
global _TRACING_CONFIGURED
|
|
93
|
+
|
|
94
|
+
with _TRACING_LOCK:
|
|
95
|
+
if _TRACING_CONFIGURED:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
cfg.resource = _inject_otel_resource_attributes(resource=cfg.resource, metadata=metadata)
|
|
99
|
+
|
|
100
|
+
# Build Resource
|
|
101
|
+
resource = Resource(attributes=cfg.resource)
|
|
102
|
+
|
|
103
|
+
# Build Sampler
|
|
104
|
+
if cfg.sampler:
|
|
105
|
+
sampler_instance = cfg.sampler.sampler(**cfg.sampler.args)
|
|
106
|
+
provider = TracerProvider(resource=resource, sampler=sampler_instance)
|
|
107
|
+
else:
|
|
108
|
+
provider = TracerProvider(resource=resource)
|
|
109
|
+
|
|
110
|
+
# Build processors
|
|
111
|
+
for p in cfg.processors:
|
|
112
|
+
for exporter in p.exporters:
|
|
113
|
+
# Handle lazy configuration for multiprocessing support
|
|
114
|
+
if isinstance(exporter, ExporterConfig):
|
|
115
|
+
exporter_instance = exporter.exporter(**exporter.args)
|
|
116
|
+
else:
|
|
117
|
+
exporter_instance = exporter
|
|
118
|
+
|
|
119
|
+
processor = p.processor(exporter_instance, **p.config)
|
|
120
|
+
provider.add_span_processor(processor)
|
|
121
|
+
|
|
122
|
+
# Register provider globally
|
|
123
|
+
set_tracer_provider(provider)
|
|
124
|
+
|
|
125
|
+
_TRACING_CONFIGURED = True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ============================================================
|
|
129
|
+
# GET TRACER
|
|
130
|
+
# ============================================================
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_tracer(name: str = "ergon"):
|
|
134
|
+
"""
|
|
135
|
+
Returns a tracer with the given name.
|
|
136
|
+
Mirrors get_logger(name).
|
|
137
|
+
"""
|
|
138
|
+
from opentelemetry.trace import get_tracer
|
|
139
|
+
|
|
140
|
+
return get_tracer(name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = [
|
|
144
|
+
"OTLPSpanExporter",
|
|
145
|
+
"BatchSpanProcessor",
|
|
146
|
+
"SimpleSpanProcessor",
|
|
147
|
+
"SamplerConfig",
|
|
148
|
+
"SpanProcessor",
|
|
149
|
+
"TracingConfig",
|
|
150
|
+
"ExporterConfig",
|
|
151
|
+
"get_tracer",
|
|
152
|
+
]
|
ergon/utils/__init__.py
ADDED
ergon/utils/env.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
ENV_LOADED = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_env():
|
|
9
|
+
global ENV_LOADED
|
|
10
|
+
if ENV_LOADED:
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
env_file = os.environ.get("ENV_FILE")
|
|
14
|
+
|
|
15
|
+
if not env_file:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
"ENV_FILE is not set. You must set the ENV_FILE environment variable. "
|
|
18
|
+
"Example: ENV_FILE=.env.local | .env.dev | .env.prod | .env.staging"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# If env vars already exist, we are likely in Docker/K8s
|
|
22
|
+
# dotenv should NOT override them
|
|
23
|
+
if os.path.exists(env_file):
|
|
24
|
+
load_dotenv(env_file, encoding="utf-8", override=True)
|
|
25
|
+
|
|
26
|
+
ENV_LOADED = True
|