elven-logs-interceptor-python 0.1.9__tar.gz → 0.1.11__tar.gz
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.
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/PKG-INFO +15 -6
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/README.md +9 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/pyproject.toml +11 -11
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/__init__.py +2 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/application/config_service.py +12 -1
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/config.py +5 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/__init__.py +2 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/transport/__init__.py +2 -0
- elven_logs_interceptor_python-0.1.11/src/logs_interceptor/infrastructure/transport/otlp_json_transport.py +260 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/transport/transport_factory.py +5 -1
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/utils.py +55 -1
- elven_logs_interceptor_python-0.1.9/examples/basic_app.py +0 -18
- elven_logs_interceptor_python-0.1.9/examples/fastapi_integration.py +0 -26
- elven_logs_interceptor_python-0.1.9/examples/full_config_reference.py +0 -67
- elven_logs_interceptor_python-0.1.9/examples/high_volume.py +0 -30
- elven_logs_interceptor_python-0.1.9/examples/tracking_usage.py +0 -14
- elven_logs_interceptor_python-0.1.9/scripts/publish.sh +0 -180
- elven_logs_interceptor_python-0.1.9/test-apps/elven-live-demo/.env.example +0 -49
- elven_logs_interceptor_python-0.1.9/test-apps/elven-live-demo/README.md +0 -17
- elven_logs_interceptor_python-0.1.9/test-apps/elven-live-demo/app.py +0 -97
- elven_logs_interceptor_python-0.1.9/test-apps/elven-live-demo/run.sh +0 -21
- elven_logs_interceptor_python-0.1.9/test-apps/elven-observability-smoke/.env.example +0 -167
- elven_logs_interceptor_python-0.1.9/test-apps/elven-observability-smoke/README.md +0 -27
- elven_logs_interceptor_python-0.1.9/test-apps/elven-observability-smoke/app.py +0 -90
- elven_logs_interceptor_python-0.1.9/test-apps/elven-observability-smoke/run.sh +0 -21
- elven_logs_interceptor_python-0.1.9/tests/integration/test_api.py +0 -122
- elven_logs_interceptor_python-0.1.9/tests/unit/test_circuit_breaker_extra.py +0 -48
- elven_logs_interceptor_python-0.1.9/tests/unit/test_config_service.py +0 -144
- elven_logs_interceptor_python-0.1.9/tests/unit/test_core_components.py +0 -77
- elven_logs_interceptor_python-0.1.9/tests/unit/test_env_config.py +0 -59
- elven_logs_interceptor_python-0.1.9/tests/unit/test_integration_filters.py +0 -119
- elven_logs_interceptor_python-0.1.9/tests/unit/test_log_filter_extra.py +0 -68
- elven_logs_interceptor_python-0.1.9/tests/unit/test_log_service_unit.py +0 -578
- elven_logs_interceptor_python-0.1.9/tests/unit/test_loguru_sink.py +0 -137
- elven_logs_interceptor_python-0.1.9/tests/unit/test_loki_json_transport.py +0 -179
- elven_logs_interceptor_python-0.1.9/tests/unit/test_memory_buffer_extra.py +0 -145
- elven_logs_interceptor_python-0.1.9/tests/unit/test_protobuf_transport_safety.py +0 -141
- elven_logs_interceptor_python-0.1.9/tests/unit/test_resilient_transport.py +0 -211
- elven_logs_interceptor_python-0.1.9/tests/unit/test_runtime_interceptor.py +0 -88
- elven_logs_interceptor_python-0.1.9/tests/unit/test_utils_extra.py +0 -276
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/.gitignore +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/ARCHITECTURE.md +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/application/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/application/log_service.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/domain/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/domain/entities.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/domain/interfaces.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/domain/value_objects.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/internal_capture_guard.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/log_noise_filter.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/log_record_extra.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/celery.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/django.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/fastapi.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/flask.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/logging_handler.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/loguru.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/integrations/structlog.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/preload.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/presentation/__init__.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/presentation/factory.py +0 -0
- {elven_logs_interceptor_python-0.1.9 → elven_logs_interceptor_python-0.1.11}/src/logs_interceptor/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: elven-logs-interceptor-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations.
|
|
5
5
|
Author: Elven Observability
|
|
6
6
|
License: MIT
|
|
@@ -14,20 +14,20 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Topic :: System :: Logging
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
|
-
Requires-Dist: httpx
|
|
17
|
+
Requires-Dist: httpx<0.29.0,>=0.24.1
|
|
18
18
|
Requires-Dist: typing-extensions>=4.12.0
|
|
19
19
|
Provides-Extra: all
|
|
20
20
|
Requires-Dist: brotli>=1.1.0; extra == 'all'
|
|
21
21
|
Requires-Dist: celery>=5.4.0; extra == 'all'
|
|
22
22
|
Requires-Dist: django>=4.2; extra == 'all'
|
|
23
|
-
Requires-Dist: fastapi>=0.
|
|
23
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'all'
|
|
24
24
|
Requires-Dist: flask>=3.0.0; extra == 'all'
|
|
25
25
|
Requires-Dist: loguru>=0.7.2; extra == 'all'
|
|
26
26
|
Requires-Dist: opentelemetry-api>=1.24.0; extra == 'all'
|
|
27
27
|
Requires-Dist: orjson>=3.10.0; extra == 'all'
|
|
28
28
|
Requires-Dist: protobuf>=5.0.0; extra == 'all'
|
|
29
29
|
Requires-Dist: python-snappy>=0.7.1; extra == 'all'
|
|
30
|
-
Requires-Dist: starlette>=0.
|
|
30
|
+
Requires-Dist: starlette>=0.27.0; extra == 'all'
|
|
31
31
|
Requires-Dist: structlog>=24.0.0; extra == 'all'
|
|
32
32
|
Provides-Extra: celery
|
|
33
33
|
Requires-Dist: celery>=5.4.0; extra == 'celery'
|
|
@@ -42,8 +42,8 @@ Requires-Dist: twine>=5.1.1; extra == 'dev'
|
|
|
42
42
|
Provides-Extra: django
|
|
43
43
|
Requires-Dist: django>=4.2; extra == 'django'
|
|
44
44
|
Provides-Extra: fastapi
|
|
45
|
-
Requires-Dist: fastapi>=0.
|
|
46
|
-
Requires-Dist: starlette>=0.
|
|
45
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
46
|
+
Requires-Dist: starlette>=0.27.0; extra == 'fastapi'
|
|
47
47
|
Provides-Extra: flask
|
|
48
48
|
Requires-Dist: flask>=3.0.0; extra == 'flask'
|
|
49
49
|
Provides-Extra: loguru
|
|
@@ -138,6 +138,12 @@ Required:
|
|
|
138
138
|
- `LOGS_TENANT`
|
|
139
139
|
- `LOGS_APP_NAME`
|
|
140
140
|
|
|
141
|
+
For collector-first mode, set `LOGS_EXPORTER=otlp` (or `collector`) and use
|
|
142
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` or `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. In this
|
|
143
|
+
mode `LOGS_TENANT` and `LOGS_TOKEN` are not required by the library; put tenant
|
|
144
|
+
or auth headers in `OTEL_EXPORTER_OTLP_HEADERS`/`LOGS_OTLP_HEADERS` if your
|
|
145
|
+
collector gateway requires them.
|
|
146
|
+
|
|
141
147
|
Core:
|
|
142
148
|
|
|
143
149
|
- `LOGS_TOKEN`
|
|
@@ -146,6 +152,9 @@ Core:
|
|
|
146
152
|
|
|
147
153
|
Transport:
|
|
148
154
|
|
|
155
|
+
- `LOGS_EXPORTER` (`loki|otlp`, aliases: `collector`, `otel`)
|
|
156
|
+
- `LOGS_OTLP_ENDPOINT` (explicit OTLP logs endpoint, e.g. `http://collector:4318/v1/logs`)
|
|
157
|
+
- `LOGS_OTLP_HEADERS` (comma-separated headers, e.g. `x-tenant=team-a`)
|
|
149
158
|
- `LOGS_COMPRESSION` (`none|gzip|brotli|snappy`)
|
|
150
159
|
- `LOGS_COMPRESSION_LEVEL`
|
|
151
160
|
- `LOGS_COMPRESSION_THRESHOLD`
|
|
@@ -77,6 +77,12 @@ Required:
|
|
|
77
77
|
- `LOGS_TENANT`
|
|
78
78
|
- `LOGS_APP_NAME`
|
|
79
79
|
|
|
80
|
+
For collector-first mode, set `LOGS_EXPORTER=otlp` (or `collector`) and use
|
|
81
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` or `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. In this
|
|
82
|
+
mode `LOGS_TENANT` and `LOGS_TOKEN` are not required by the library; put tenant
|
|
83
|
+
or auth headers in `OTEL_EXPORTER_OTLP_HEADERS`/`LOGS_OTLP_HEADERS` if your
|
|
84
|
+
collector gateway requires them.
|
|
85
|
+
|
|
80
86
|
Core:
|
|
81
87
|
|
|
82
88
|
- `LOGS_TOKEN`
|
|
@@ -85,6 +91,9 @@ Core:
|
|
|
85
91
|
|
|
86
92
|
Transport:
|
|
87
93
|
|
|
94
|
+
- `LOGS_EXPORTER` (`loki|otlp`, aliases: `collector`, `otel`)
|
|
95
|
+
- `LOGS_OTLP_ENDPOINT` (explicit OTLP logs endpoint, e.g. `http://collector:4318/v1/logs`)
|
|
96
|
+
- `LOGS_OTLP_HEADERS` (comma-separated headers, e.g. `x-tenant=team-a`)
|
|
88
97
|
- `LOGS_COMPRESSION` (`none|gzip|brotli|snappy`)
|
|
89
98
|
- `LOGS_COMPRESSION_LEVEL`
|
|
90
99
|
- `LOGS_COMPRESSION_THRESHOLD`
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "elven-logs-interceptor-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.11"
|
|
8
8
|
description = "Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -32,7 +32,7 @@ classifiers = [
|
|
|
32
32
|
"Topic :: System :: Logging",
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
|
-
"httpx>=0.
|
|
35
|
+
"httpx>=0.24.1,<0.29.0",
|
|
36
36
|
"typing-extensions>=4.12.0",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -47,8 +47,8 @@ otel = [
|
|
|
47
47
|
"opentelemetry-api>=1.24.0",
|
|
48
48
|
]
|
|
49
49
|
fastapi = [
|
|
50
|
-
"fastapi>=0.
|
|
51
|
-
"starlette>=0.
|
|
50
|
+
"fastapi>=0.100.0",
|
|
51
|
+
"starlette>=0.27.0",
|
|
52
52
|
]
|
|
53
53
|
django = [
|
|
54
54
|
"django>=4.2",
|
|
@@ -71,8 +71,8 @@ all = [
|
|
|
71
71
|
"python-snappy>=0.7.1",
|
|
72
72
|
"brotli>=1.1.0",
|
|
73
73
|
"opentelemetry-api>=1.24.0",
|
|
74
|
-
"fastapi>=0.
|
|
75
|
-
"starlette>=0.
|
|
74
|
+
"fastapi>=0.100.0",
|
|
75
|
+
"starlette>=0.27.0",
|
|
76
76
|
"django>=4.2",
|
|
77
77
|
"flask>=3.0.0",
|
|
78
78
|
"celery>=5.4.0",
|
|
@@ -93,22 +93,22 @@ dev = [
|
|
|
93
93
|
packages = ["src/logs_interceptor"]
|
|
94
94
|
|
|
95
95
|
[tool.hatch.build.targets.sdist]
|
|
96
|
-
include = [
|
|
96
|
+
only-include = [
|
|
97
97
|
"/ARCHITECTURE.md",
|
|
98
98
|
"/README.md",
|
|
99
|
-
"/examples",
|
|
100
99
|
"/pyproject.toml",
|
|
101
|
-
"/scripts",
|
|
102
100
|
"/src",
|
|
103
|
-
"/test-apps",
|
|
104
|
-
"/tests",
|
|
105
101
|
]
|
|
106
102
|
exclude = [
|
|
103
|
+
"/.mypy_cache",
|
|
104
|
+
"/.pytest_cache",
|
|
105
|
+
"/.ruff_cache",
|
|
107
106
|
"/.venv*",
|
|
108
107
|
"/.logs-dlq",
|
|
109
108
|
"/build",
|
|
110
109
|
"/collector",
|
|
111
110
|
"/dist",
|
|
111
|
+
"/test-apps",
|
|
112
112
|
]
|
|
113
113
|
|
|
114
114
|
[tool.pytest.ini_options]
|
|
@@ -88,9 +88,11 @@ def _coerce_config(user_config: LogsInterceptorConfig | dict[str, Any] | None) -
|
|
|
88
88
|
|
|
89
89
|
return LogsInterceptorConfig(
|
|
90
90
|
transport=TransportConfig(
|
|
91
|
+
exporter=_pick(transport_raw, "exporter", "exporter"),
|
|
91
92
|
url=_pick(transport_raw, "url", "url", ""),
|
|
92
93
|
tenant_id=_pick(transport_raw, "tenant_id", "tenantId", ""),
|
|
93
94
|
auth_token=_pick(transport_raw, "auth_token", "authToken"),
|
|
95
|
+
headers=_pick(transport_raw, "headers", "headers"),
|
|
94
96
|
timeout=_pick(transport_raw, "timeout", "timeout"),
|
|
95
97
|
max_retries=_pick(transport_raw, "max_retries", "maxRetries"),
|
|
96
98
|
retry_delay=_pick(transport_raw, "retry_delay", "retryDelay"),
|
|
@@ -7,6 +7,7 @@ from ..config import (
|
|
|
7
7
|
CircuitBreakerConfig,
|
|
8
8
|
FilterConfig,
|
|
9
9
|
IntegrationsConfig,
|
|
10
|
+
LogsExporterType,
|
|
10
11
|
LogsInterceptorConfig,
|
|
11
12
|
PerformanceConfig,
|
|
12
13
|
ResolvedBufferConfig,
|
|
@@ -40,7 +41,7 @@ class ConfigService:
|
|
|
40
41
|
|
|
41
42
|
if not config.transport.url:
|
|
42
43
|
errors.append("Transport URL is required")
|
|
43
|
-
if not config.transport.tenant_id:
|
|
44
|
+
if ConfigService._resolve_exporter(config.transport.exporter) == "loki" and not config.transport.tenant_id:
|
|
44
45
|
errors.append("Tenant ID is required")
|
|
45
46
|
if not config.app_name:
|
|
46
47
|
errors.append("App name is required")
|
|
@@ -125,6 +126,7 @@ class ConfigService:
|
|
|
125
126
|
transport: TransportConfig,
|
|
126
127
|
performance: PerformanceConfig | None,
|
|
127
128
|
) -> ResolvedTransportConfig:
|
|
129
|
+
exporter = ConfigService._resolve_exporter(transport.exporter)
|
|
128
130
|
compression = "gzip"
|
|
129
131
|
if transport.compression in (False, "none"):
|
|
130
132
|
compression = "none"
|
|
@@ -139,6 +141,7 @@ class ConfigService:
|
|
|
139
141
|
url=transport.url,
|
|
140
142
|
tenant_id=transport.tenant_id,
|
|
141
143
|
auth_token=transport.auth_token or "",
|
|
144
|
+
headers=transport.headers or {},
|
|
142
145
|
timeout=transport.timeout if transport.timeout is not None else 10_000,
|
|
143
146
|
max_retries=transport.max_retries if transport.max_retries is not None else 3,
|
|
144
147
|
retry_delay=transport.retry_delay if transport.retry_delay is not None else 1_000,
|
|
@@ -175,8 +178,16 @@ class ConfigService:
|
|
|
175
178
|
if transport.enable_structured_metadata is None
|
|
176
179
|
else transport.enable_structured_metadata
|
|
177
180
|
),
|
|
181
|
+
exporter=exporter,
|
|
178
182
|
)
|
|
179
183
|
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _resolve_exporter(value: str | None) -> LogsExporterType:
|
|
186
|
+
normalized = (value or "loki").strip().lower()
|
|
187
|
+
if normalized in {"otlp", "collector", "otel"}:
|
|
188
|
+
return "otlp"
|
|
189
|
+
return "loki"
|
|
190
|
+
|
|
180
191
|
@staticmethod
|
|
181
192
|
def _resolve_buffer(buffer: BufferConfig | None) -> ResolvedBufferConfig:
|
|
182
193
|
source = buffer or BufferConfig()
|
|
@@ -8,13 +8,16 @@ from .types import LogLevel
|
|
|
8
8
|
|
|
9
9
|
CompressionType = Literal["none", "gzip", "brotli", "snappy"]
|
|
10
10
|
DLQType = Literal["memory", "file"]
|
|
11
|
+
LogsExporterType = Literal["loki", "otlp"]
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass(slots=True)
|
|
14
15
|
class TransportConfig:
|
|
16
|
+
exporter: LogsExporterType | str | None = None
|
|
15
17
|
url: str = ""
|
|
16
18
|
tenant_id: str = ""
|
|
17
19
|
auth_token: str | None = None
|
|
20
|
+
headers: dict[str, str] | None = None
|
|
18
21
|
timeout: int | None = None
|
|
19
22
|
max_retries: int | None = None
|
|
20
23
|
retry_delay: int | None = None
|
|
@@ -133,6 +136,8 @@ class ResolvedTransportConfig:
|
|
|
133
136
|
enable_connection_pooling: bool
|
|
134
137
|
max_sockets: int
|
|
135
138
|
enable_structured_metadata: bool = False
|
|
139
|
+
exporter: LogsExporterType = "loki"
|
|
140
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
136
141
|
|
|
137
142
|
|
|
138
143
|
@dataclass(slots=True)
|
|
@@ -17,6 +17,7 @@ from .metrics import MetricsCollector
|
|
|
17
17
|
from .transport import (
|
|
18
18
|
LokiJsonTransport,
|
|
19
19
|
LokiProtobufTransport,
|
|
20
|
+
OtlpJsonTransport,
|
|
20
21
|
ResilientTransport,
|
|
21
22
|
ResilientTransportConfig,
|
|
22
23
|
TransportFactory,
|
|
@@ -41,6 +42,7 @@ __all__ = [
|
|
|
41
42
|
"MetricsCollector",
|
|
42
43
|
"LokiJsonTransport",
|
|
43
44
|
"LokiProtobufTransport",
|
|
45
|
+
"OtlpJsonTransport",
|
|
44
46
|
"ResilientTransport",
|
|
45
47
|
"ResilientTransportConfig",
|
|
46
48
|
"TransportFactory",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from .loki_json_transport import LokiJsonTransport
|
|
2
2
|
from .loki_protobuf_transport import LokiProtobufTransport
|
|
3
|
+
from .otlp_json_transport import OtlpJsonTransport
|
|
3
4
|
from .resilient_transport import ResilientTransport, ResilientTransportConfig
|
|
4
5
|
from .transport_factory import TransportFactory
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"LokiJsonTransport",
|
|
8
9
|
"LokiProtobufTransport",
|
|
10
|
+
"OtlpJsonTransport",
|
|
9
11
|
"ResilientTransport",
|
|
10
12
|
"ResilientTransportConfig",
|
|
11
13
|
"TransportFactory",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from ...config import ResolvedLogsInterceptorConfig
|
|
14
|
+
from ...domain.entities import LogEntryEntity
|
|
15
|
+
from ...types import TransportHealth, TransportMetrics
|
|
16
|
+
from ...utils import get_distribution_version
|
|
17
|
+
from ..internal_capture_guard import suppress_internal_log_capture
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
orjson = cast(Any, import_module("orjson"))
|
|
21
|
+
except Exception: # pragma: no cover - optional dependency
|
|
22
|
+
orjson = None
|
|
23
|
+
|
|
24
|
+
_HEX_RE = re.compile(r"^[0-9a-fA-F]+$")
|
|
25
|
+
_SEVERITY_NUMBER = {
|
|
26
|
+
"debug": 5,
|
|
27
|
+
"info": 9,
|
|
28
|
+
"warn": 13,
|
|
29
|
+
"error": 17,
|
|
30
|
+
"fatal": 21,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class RetryableTransportError(Exception):
|
|
36
|
+
message: str
|
|
37
|
+
status_code: int | None = None
|
|
38
|
+
retryable: bool = False
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.message
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class OtlpJsonTransport:
|
|
45
|
+
"""OTLP/HTTP JSON logs transport for collector-first deployments."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, config: ResolvedLogsInterceptorConfig) -> None:
|
|
48
|
+
self._config = config
|
|
49
|
+
self._transport = config.transport
|
|
50
|
+
self._timeout = self._transport.timeout / 1000
|
|
51
|
+
limits = httpx.Limits(
|
|
52
|
+
max_keepalive_connections=self._transport.max_sockets,
|
|
53
|
+
max_connections=self._transport.max_sockets,
|
|
54
|
+
)
|
|
55
|
+
self._client: httpx.Client | None = None
|
|
56
|
+
if self._transport.enable_connection_pooling:
|
|
57
|
+
self._client = httpx.Client(timeout=self._timeout, limits=limits)
|
|
58
|
+
|
|
59
|
+
self._health: TransportHealth = {
|
|
60
|
+
"healthy": True,
|
|
61
|
+
"consecutive_failures": 0,
|
|
62
|
+
}
|
|
63
|
+
self._metrics: TransportMetrics = {
|
|
64
|
+
"total_sends": 0,
|
|
65
|
+
"successful_sends": 0,
|
|
66
|
+
"failed_sends": 0,
|
|
67
|
+
"avg_latency": 0.0,
|
|
68
|
+
"avg_compression_time": 0.0,
|
|
69
|
+
"avg_compression_ratio": 0.0,
|
|
70
|
+
"total_bytes_sent": 0,
|
|
71
|
+
"total_bytes_compressed": 0,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def send(self, entries: list[LogEntryEntity]) -> None:
|
|
75
|
+
if not entries:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
start = time.perf_counter()
|
|
79
|
+
self._metrics["total_sends"] = self._metrics.get("total_sends", 0) + 1
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
body = self._dumps(self._format_for_otlp(entries))
|
|
83
|
+
headers = {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"User-Agent": (
|
|
86
|
+
"elven-logs-interceptor-python/"
|
|
87
|
+
f"{get_distribution_version('elven-logs-interceptor-python')}"
|
|
88
|
+
),
|
|
89
|
+
**self._transport.headers,
|
|
90
|
+
}
|
|
91
|
+
if self._transport.auth_token:
|
|
92
|
+
headers["Authorization"] = f"Bearer {self._transport.auth_token}"
|
|
93
|
+
|
|
94
|
+
with suppress_internal_log_capture():
|
|
95
|
+
response = self._request(headers, body)
|
|
96
|
+
|
|
97
|
+
if response.status_code >= 300:
|
|
98
|
+
raise RetryableTransportError(
|
|
99
|
+
message=f"OTLP logs endpoint responded with {response.status_code}: {response.text}",
|
|
100
|
+
status_code=response.status_code,
|
|
101
|
+
retryable=response.status_code == 429 or response.status_code >= 500,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
latency = (time.perf_counter() - start) * 1000
|
|
105
|
+
self._record_success(latency)
|
|
106
|
+
self._metrics["total_bytes_sent"] = self._metrics.get("total_bytes_sent", 0) + len(body)
|
|
107
|
+
self._metrics["total_bytes_compressed"] = self._metrics.get("total_bytes_compressed", 0) + len(body)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
self._record_failure(exc)
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
def _request(self, headers: dict[str, str], body: bytes) -> httpx.Response:
|
|
113
|
+
if self._client is not None:
|
|
114
|
+
return self._client.post(self._transport.url, headers=headers, content=body)
|
|
115
|
+
|
|
116
|
+
with httpx.Client(timeout=self._timeout) as client:
|
|
117
|
+
return client.post(self._transport.url, headers=headers, content=body)
|
|
118
|
+
|
|
119
|
+
def is_available(self) -> bool:
|
|
120
|
+
return bool(self._health.get("healthy", False))
|
|
121
|
+
|
|
122
|
+
def get_health(self) -> TransportHealth:
|
|
123
|
+
return cast(TransportHealth, dict(self._health))
|
|
124
|
+
|
|
125
|
+
def get_metrics(self) -> TransportMetrics:
|
|
126
|
+
return cast(TransportMetrics, dict(self._metrics))
|
|
127
|
+
|
|
128
|
+
def destroy(self) -> None:
|
|
129
|
+
if self._client is not None:
|
|
130
|
+
self._client.close()
|
|
131
|
+
self._client = None
|
|
132
|
+
|
|
133
|
+
def _record_success(self, duration_ms: float) -> None:
|
|
134
|
+
self._health = {
|
|
135
|
+
"healthy": True,
|
|
136
|
+
"consecutive_failures": 0,
|
|
137
|
+
"last_successful_send": time.time(),
|
|
138
|
+
}
|
|
139
|
+
successful = self._metrics.get("successful_sends", 0) + 1
|
|
140
|
+
self._metrics["successful_sends"] = successful
|
|
141
|
+
current_avg = float(self._metrics.get("avg_latency", 0.0))
|
|
142
|
+
self._metrics["avg_latency"] = ((current_avg * (successful - 1)) + duration_ms) / successful
|
|
143
|
+
|
|
144
|
+
def _record_failure(self, error: Exception) -> None:
|
|
145
|
+
failures = int(self._health.get("consecutive_failures", 0)) + 1
|
|
146
|
+
self._health = {
|
|
147
|
+
"healthy": False,
|
|
148
|
+
"consecutive_failures": failures,
|
|
149
|
+
"error_message": str(error),
|
|
150
|
+
}
|
|
151
|
+
self._metrics["failed_sends"] = self._metrics.get("failed_sends", 0) + 1
|
|
152
|
+
|
|
153
|
+
def _format_for_otlp(self, entries: list[LogEntryEntity]) -> dict[str, Any]:
|
|
154
|
+
return {
|
|
155
|
+
"resourceLogs": [
|
|
156
|
+
{
|
|
157
|
+
"resource": {"attributes": self._resource_attributes()},
|
|
158
|
+
"scopeLogs": [
|
|
159
|
+
{
|
|
160
|
+
"scope": {
|
|
161
|
+
"name": "elven-logs-interceptor-python",
|
|
162
|
+
"version": get_distribution_version("elven-logs-interceptor-python"),
|
|
163
|
+
},
|
|
164
|
+
"logRecords": [self._format_record(entry) for entry in entries],
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def _resource_attributes(self) -> list[dict[str, Any]]:
|
|
172
|
+
attrs: dict[str, Any] = {
|
|
173
|
+
"service.name": self._config.app_name,
|
|
174
|
+
"service.version": self._config.version,
|
|
175
|
+
"deployment.environment": self._config.environment,
|
|
176
|
+
**self._config.labels,
|
|
177
|
+
}
|
|
178
|
+
return self._attributes(attrs)
|
|
179
|
+
|
|
180
|
+
def _format_record(self, entry: LogEntryEntity) -> dict[str, Any]:
|
|
181
|
+
record: dict[str, Any] = {
|
|
182
|
+
"timeUnixNano": str(self._timestamp_to_ns(entry.timestamp)),
|
|
183
|
+
"observedTimeUnixNano": str(int(time.time() * 1_000_000_000)),
|
|
184
|
+
"severityText": entry.level.upper(),
|
|
185
|
+
"severityNumber": _SEVERITY_NUMBER.get(entry.level, 9),
|
|
186
|
+
"body": {"stringValue": entry.message},
|
|
187
|
+
"attributes": self._entry_attributes(entry),
|
|
188
|
+
}
|
|
189
|
+
if entry.trace_id and self._valid_hex(entry.trace_id, 32):
|
|
190
|
+
record["traceId"] = entry.trace_id.lower()
|
|
191
|
+
if entry.span_id and self._valid_hex(entry.span_id, 16):
|
|
192
|
+
record["spanId"] = entry.span_id.lower()
|
|
193
|
+
return record
|
|
194
|
+
|
|
195
|
+
def _entry_attributes(self, entry: LogEntryEntity) -> list[dict[str, Any]]:
|
|
196
|
+
attrs: dict[str, Any] = {
|
|
197
|
+
"log.id": entry.id,
|
|
198
|
+
"log.level": entry.level,
|
|
199
|
+
}
|
|
200
|
+
if entry.request_id:
|
|
201
|
+
attrs["request.id"] = entry.request_id
|
|
202
|
+
if entry.context:
|
|
203
|
+
attrs["context"] = entry.context
|
|
204
|
+
if entry.metadata:
|
|
205
|
+
attrs["metadata"] = entry.metadata
|
|
206
|
+
if entry.labels:
|
|
207
|
+
attrs["labels"] = entry.labels
|
|
208
|
+
return self._attributes(attrs)
|
|
209
|
+
|
|
210
|
+
def _attributes(self, attrs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
211
|
+
return [
|
|
212
|
+
{"key": str(key), "value": self._any_value(value)}
|
|
213
|
+
for key, value in attrs.items()
|
|
214
|
+
if value is not None
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
def _any_value(self, value: Any) -> dict[str, Any]:
|
|
218
|
+
if isinstance(value, bool):
|
|
219
|
+
return {"boolValue": value}
|
|
220
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
221
|
+
return {"intValue": str(value)}
|
|
222
|
+
if isinstance(value, float):
|
|
223
|
+
return {"doubleValue": value}
|
|
224
|
+
if isinstance(value, str):
|
|
225
|
+
return {"stringValue": value}
|
|
226
|
+
if isinstance(value, (list, tuple)):
|
|
227
|
+
return {"arrayValue": {"values": [self._any_value(item) for item in value]}}
|
|
228
|
+
if isinstance(value, dict):
|
|
229
|
+
return {
|
|
230
|
+
"kvlistValue": {
|
|
231
|
+
"values": [
|
|
232
|
+
{"key": str(key), "value": self._any_value(item)}
|
|
233
|
+
for key, item in value.items()
|
|
234
|
+
if item is not None
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {"stringValue": str(value)}
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _valid_hex(value: str, length: int) -> bool:
|
|
242
|
+
return len(value) == length and bool(_HEX_RE.match(value)) and set(value) != {"0"}
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _timestamp_to_ns(iso_timestamp: str) -> int:
|
|
246
|
+
try:
|
|
247
|
+
normalized = iso_timestamp.replace("Z", "+00:00")
|
|
248
|
+
dt_obj = datetime.fromisoformat(normalized)
|
|
249
|
+
if dt_obj.tzinfo is None:
|
|
250
|
+
dt_obj = dt_obj.replace(tzinfo=timezone.utc)
|
|
251
|
+
seconds = int(dt_obj.timestamp())
|
|
252
|
+
return (seconds * 1_000_000_000) + (dt_obj.microsecond * 1000)
|
|
253
|
+
except Exception:
|
|
254
|
+
return int(time.time() * 1_000_000_000)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _dumps(payload: Any) -> bytes:
|
|
258
|
+
if orjson is not None:
|
|
259
|
+
return cast(bytes, orjson.dumps(payload))
|
|
260
|
+
return json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
@@ -5,6 +5,7 @@ from ...domain.interfaces import ICircuitBreaker, IDeadLetterQueue, ILogTranspor
|
|
|
5
5
|
from ...utils import internal_debug, internal_warn
|
|
6
6
|
from .loki_json_transport import LokiJsonTransport
|
|
7
7
|
from .loki_protobuf_transport import LokiProtobufTransport
|
|
8
|
+
from .otlp_json_transport import OtlpJsonTransport
|
|
8
9
|
from .resilient_transport import ResilientTransport, ResilientTransportConfig
|
|
9
10
|
|
|
10
11
|
|
|
@@ -17,7 +18,10 @@ class TransportFactory:
|
|
|
17
18
|
) -> ILogTransport:
|
|
18
19
|
base_transport: ILogTransport
|
|
19
20
|
|
|
20
|
-
if config.transport.
|
|
21
|
+
if config.transport.exporter == "otlp":
|
|
22
|
+
internal_debug("Selected OtlpJsonTransport")
|
|
23
|
+
base_transport = OtlpJsonTransport(config)
|
|
24
|
+
elif config.transport.compression == "snappy":
|
|
21
25
|
try:
|
|
22
26
|
internal_debug("Selected LokiProtobufTransport")
|
|
23
27
|
base_transport = LokiProtobufTransport(config.transport)
|
|
@@ -75,6 +75,49 @@ def parse_float_range(value: str | None, default: float, min_value: float, max_v
|
|
|
75
75
|
return parsed
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _normalize_logs_exporter(value: str | None) -> str:
|
|
79
|
+
normalized = (value or "loki").strip().lower()
|
|
80
|
+
if normalized in {"otlp", "collector", "otel"}:
|
|
81
|
+
return "otlp"
|
|
82
|
+
return "loki"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_kv_csv(raw: str | None) -> dict[str, str]:
|
|
86
|
+
headers: dict[str, str] = {}
|
|
87
|
+
if not raw:
|
|
88
|
+
return headers
|
|
89
|
+
for item in raw.split(","):
|
|
90
|
+
if "=" not in item:
|
|
91
|
+
continue
|
|
92
|
+
key, value = item.split("=", 1)
|
|
93
|
+
key = key.strip()
|
|
94
|
+
if key:
|
|
95
|
+
headers[key] = value.strip()
|
|
96
|
+
return headers
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _otlp_logs_endpoint_from_base(endpoint: str | None) -> str:
|
|
100
|
+
if not endpoint:
|
|
101
|
+
return ""
|
|
102
|
+
normalized = endpoint.rstrip("/")
|
|
103
|
+
if normalized.endswith("/v1/logs"):
|
|
104
|
+
return normalized
|
|
105
|
+
if normalized.endswith("/v1/traces") or normalized.endswith("/v1/metrics"):
|
|
106
|
+
return normalized.rsplit("/", 1)[0] + "/logs"
|
|
107
|
+
return normalized + "/v1/logs"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_logs_url(env: Mapping[str, str], exporter: str) -> str:
|
|
111
|
+
if exporter != "otlp":
|
|
112
|
+
return env.get("LOGS_URL", "")
|
|
113
|
+
return (
|
|
114
|
+
env.get("LOGS_URL")
|
|
115
|
+
or env.get("LOGS_OTLP_ENDPOINT")
|
|
116
|
+
or env.get("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT")
|
|
117
|
+
or _otlp_logs_endpoint_from_base(env.get("OTEL_EXPORTER_OTLP_ENDPOINT"))
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
78
121
|
def is_debug_enabled() -> bool:
|
|
79
122
|
return parse_bool(os.getenv("LOGS_DEBUG"), False)
|
|
80
123
|
|
|
@@ -507,6 +550,15 @@ def load_config_from_env() -> LogsInterceptorConfig:
|
|
|
507
550
|
if compression_value not in {"none", "gzip", "brotli", "snappy"}:
|
|
508
551
|
compression_value = "gzip"
|
|
509
552
|
|
|
553
|
+
exporter = _normalize_logs_exporter(
|
|
554
|
+
env.get("LOGS_EXPORTER") or env.get("LOGS_TRANSPORT") or env.get("OTEL_LOGS_EXPORTER")
|
|
555
|
+
)
|
|
556
|
+
headers = {
|
|
557
|
+
**_parse_kv_csv(env.get("LOGS_HEADERS")),
|
|
558
|
+
**_parse_kv_csv(env.get("LOGS_OTLP_HEADERS")),
|
|
559
|
+
**(_parse_kv_csv(env.get("OTEL_EXPORTER_OTLP_HEADERS")) if exporter == "otlp" else {}),
|
|
560
|
+
}
|
|
561
|
+
|
|
510
562
|
dlq_type_raw = (env.get("LOGS_DLQ_TYPE") or "memory").lower()
|
|
511
563
|
dlq_type = dlq_type_raw
|
|
512
564
|
if dlq_type not in {"memory", "file"}:
|
|
@@ -514,9 +566,11 @@ def load_config_from_env() -> LogsInterceptorConfig:
|
|
|
514
566
|
|
|
515
567
|
cfg = LogsInterceptorConfig(
|
|
516
568
|
transport=TransportConfig(
|
|
517
|
-
|
|
569
|
+
exporter=exporter,
|
|
570
|
+
url=_resolve_logs_url(env, exporter),
|
|
518
571
|
tenant_id=env.get("LOGS_TENANT", ""),
|
|
519
572
|
auth_token=env.get("LOGS_TOKEN"),
|
|
573
|
+
headers=headers or None,
|
|
520
574
|
timeout=parse_int_range(env.get("LOGS_TIMEOUT"), 10_000, 0, 600_000),
|
|
521
575
|
max_retries=parse_int_range(env.get("LOGS_MAX_RETRIES"), 3, 0, 20),
|
|
522
576
|
retry_delay=parse_int_range(env.get("LOGS_RETRY_DELAY"), 1_000, 0, 120_000),
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from logs_interceptor import init
|
|
2
|
-
|
|
3
|
-
init(
|
|
4
|
-
{
|
|
5
|
-
"appName": "my-service",
|
|
6
|
-
"interceptConsole": True,
|
|
7
|
-
"transport": {
|
|
8
|
-
"url": "http://localhost:3100/loki/api/v1/push",
|
|
9
|
-
"tenantId": "my-tenant",
|
|
10
|
-
"compression": "gzip",
|
|
11
|
-
"enableConnectionPooling": True,
|
|
12
|
-
},
|
|
13
|
-
"deadLetterQueue": {"enabled": True, "type": "file"},
|
|
14
|
-
"circuitBreaker": {"enabled": True},
|
|
15
|
-
}
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
print("service started")
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
from logs_interceptor import init, logger
|
|
4
|
-
from logs_interceptor.integrations import FastAPIMiddleware
|
|
5
|
-
|
|
6
|
-
init(
|
|
7
|
-
{
|
|
8
|
-
"transport": {
|
|
9
|
-
"url": "http://localhost:3100/loki/api/v1/push",
|
|
10
|
-
"tenantId": "my-tenant",
|
|
11
|
-
"compression": "brotli",
|
|
12
|
-
},
|
|
13
|
-
"appName": "my-api",
|
|
14
|
-
"interceptConsole": True,
|
|
15
|
-
"deadLetterQueue": {"enabled": True, "type": "file"},
|
|
16
|
-
}
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
app = FastAPI()
|
|
20
|
-
app.add_middleware(FastAPIMiddleware, logger=logger)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@app.get("/ping")
|
|
24
|
-
def ping() -> dict[str, str]:
|
|
25
|
-
logger.info("ping called")
|
|
26
|
-
return {"message": "pong"}
|