obsforge 0.1.1__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.
- obsforge-0.1.1/.gitignore +12 -0
- obsforge-0.1.1/CHANGELOG.md +101 -0
- obsforge-0.1.1/LICENSE +21 -0
- obsforge-0.1.1/PKG-INFO +383 -0
- obsforge-0.1.1/README.md +323 -0
- obsforge-0.1.1/pyproject.toml +149 -0
- obsforge-0.1.1/src/obsforge/__init__.py +44 -0
- obsforge-0.1.1/src/obsforge/api/__init__.py +5 -0
- obsforge-0.1.1/src/obsforge/api/context.py +32 -0
- obsforge-0.1.1/src/obsforge/api/decorators.py +42 -0
- obsforge-0.1.1/src/obsforge/api/events.py +25 -0
- obsforge-0.1.1/src/obsforge/api/logger.py +45 -0
- obsforge-0.1.1/src/obsforge/config/__init__.py +4 -0
- obsforge-0.1.1/src/obsforge/config/bootstrap.py +190 -0
- obsforge-0.1.1/src/obsforge/config/settings.py +176 -0
- obsforge-0.1.1/src/obsforge/config/state.py +53 -0
- obsforge-0.1.1/src/obsforge/core/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/core/contracts.py +52 -0
- obsforge-0.1.1/src/obsforge/core/errors.py +10 -0
- obsforge-0.1.1/src/obsforge/core/models.py +572 -0
- obsforge-0.1.1/src/obsforge/core/taxonomy.py +29 -0
- obsforge-0.1.1/src/obsforge/encoding/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/encoding/json.py +46 -0
- obsforge-0.1.1/src/obsforge/encoding/serializers.py +7 -0
- obsforge-0.1.1/src/obsforge/instrumentation/__init__.py +1 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/__init__.py +4 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/aiomysql.py +106 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/asyncpg.py +107 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/django.py +70 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/engine.py +324 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/pool.py +123 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/psycopg.py +168 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/sql.py +65 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/sqlalchemy.py +161 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/state.py +28 -0
- obsforge-0.1.1/src/obsforge/instrumentation/db/transactions.py +73 -0
- obsforge-0.1.1/src/obsforge/instrumentation/exceptions/__init__.py +15 -0
- obsforge-0.1.1/src/obsforge/instrumentation/exceptions/classification.py +108 -0
- obsforge-0.1.1/src/obsforge/instrumentation/exceptions/dedupe.py +66 -0
- obsforge-0.1.1/src/obsforge/instrumentation/exceptions/engine.py +373 -0
- obsforge-0.1.1/src/obsforge/instrumentation/exceptions/sanitization.py +64 -0
- obsforge-0.1.1/src/obsforge/instrumentation/exceptions/state.py +101 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/__init__.py +13 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/aiohttp.py +57 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/classification.py +98 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/dependency.py +92 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/engine.py +516 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/httpx.py +130 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/requests.py +98 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/sanitization.py +134 -0
- obsforge-0.1.1/src/obsforge/instrumentation/http/state.py +24 -0
- obsforge-0.1.1/src/obsforge/integrations/__init__.py +1 -0
- obsforge-0.1.1/src/obsforge/integrations/asyncio.py +43 -0
- obsforge-0.1.1/src/obsforge/integrations/celery/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/integrations/celery/signals.py +76 -0
- obsforge-0.1.1/src/obsforge/integrations/django/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/integrations/django/middleware.py +105 -0
- obsforge-0.1.1/src/obsforge/integrations/django/settings.py +1 -0
- obsforge-0.1.1/src/obsforge/integrations/django/signals.py +5 -0
- obsforge-0.1.1/src/obsforge/integrations/drf/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/integrations/drf/exception_handler.py +31 -0
- obsforge-0.1.1/src/obsforge/integrations/fastapi/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/integrations/fastapi/dependencies.py +7 -0
- obsforge-0.1.1/src/obsforge/integrations/fastapi/exception_handlers.py +29 -0
- obsforge-0.1.1/src/obsforge/integrations/fastapi/middleware.py +142 -0
- obsforge-0.1.1/src/obsforge/integrations/kafka.py +27 -0
- obsforge-0.1.1/src/obsforge/integrations/logging_bridge.py +122 -0
- obsforge-0.1.1/src/obsforge/integrations/rabbitmq.py +29 -0
- obsforge-0.1.1/src/obsforge/integrations/workers.py +60 -0
- obsforge-0.1.1/src/obsforge/plugins/__init__.py +4 -0
- obsforge-0.1.1/src/obsforge/plugins/builtin.py +24 -0
- obsforge-0.1.1/src/obsforge/plugins/manager.py +23 -0
- obsforge-0.1.1/src/obsforge/plugins/registry.py +35 -0
- obsforge-0.1.1/src/obsforge/plugins/spec.py +27 -0
- obsforge-0.1.1/src/obsforge/propagation/__init__.py +26 -0
- obsforge-0.1.1/src/obsforge/propagation/baggage.py +42 -0
- obsforge-0.1.1/src/obsforge/propagation/correlation.py +98 -0
- obsforge-0.1.1/src/obsforge/propagation/distributed.py +389 -0
- obsforge-0.1.1/src/obsforge/propagation/state.py +24 -0
- obsforge-0.1.1/src/obsforge/propagation/tracecontext.py +56 -0
- obsforge-0.1.1/src/obsforge/py.typed +0 -0
- obsforge-0.1.1/src/obsforge/runtime/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/runtime/pii.py +53 -0
- obsforge-0.1.1/src/obsforge/runtime/pipeline.py +102 -0
- obsforge-0.1.1/src/obsforge/runtime/policies/__init__.py +0 -0
- obsforge-0.1.1/src/obsforge/runtime/policies/loki_policy.py +180 -0
- obsforge-0.1.1/src/obsforge/runtime/processors.py +78 -0
- obsforge-0.1.1/src/obsforge/runtime/redaction.py +101 -0
- obsforge-0.1.1/src/obsforge/runtime/sampling.py +21 -0
- obsforge-0.1.1/src/obsforge/telemetry/__init__.py +5 -0
- obsforge-0.1.1/src/obsforge/telemetry/mapping.py +67 -0
- obsforge-0.1.1/src/obsforge/telemetry/otel_logs.py +96 -0
- obsforge-0.1.1/src/obsforge/telemetry/otel_traces.py +78 -0
- obsforge-0.1.1/src/obsforge/testing/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/testing/capture.py +11 -0
- obsforge-0.1.1/src/obsforge/testing/fakes.py +40 -0
- obsforge-0.1.1/src/obsforge/transport/__init__.py +3 -0
- obsforge-0.1.1/src/obsforge/transport/sink.py +19 -0
- obsforge-0.1.1/src/obsforge/transport/stdout.py +20 -0
- obsforge-0.1.1/tests/conftest.py +71 -0
- obsforge-0.1.1/tests/integration/db/test_sqlalchemy_sqlite.py +74 -0
- obsforge-0.1.1/tests/integration/http/test_fastapi_middleware.py +98 -0
- obsforge-0.1.1/tests/integration/http/test_http_clients.py +94 -0
- obsforge-0.1.1/tests/integration/otel/test_otel_export.py +268 -0
- obsforge-0.1.1/tests/integration/propagation/test_carriers.py +185 -0
- obsforge-0.1.1/tests/integration/propagation/test_context_isolation.py +207 -0
- obsforge-0.1.1/tests/integration/propagation/test_workers.py +111 -0
- obsforge-0.1.1/tests/integration/test_celery_correlation.py +72 -0
- obsforge-0.1.1/tests/integration/test_logging_bridge.py +187 -0
- obsforge-0.1.1/tests/regression/test_baggage_caps.py +156 -0
- obsforge-0.1.1/tests/regression/test_dedupe_eviction.py +215 -0
- obsforge-0.1.1/tests/regression/test_no_high_cardinality_labels.py +118 -0
- obsforge-0.1.1/tests/regression/test_pii_never_serialized.py +181 -0
- obsforge-0.1.1/tests/regression/test_pipeline_fail_open.py +251 -0
- obsforge-0.1.1/tests/regression/test_pipeline_sync_async_parity.py +140 -0
- obsforge-0.1.1/tests/test_architecture_smoke.py +240 -0
- obsforge-0.1.1/tests/test_benchmark_smoke.py +14 -0
- obsforge-0.1.1/tests/unit/test_default_engine_state.py +52 -0
- obsforge-0.1.1/tests/unit/test_exception_sanitization.py +129 -0
- obsforge-0.1.1/tests/unit/test_http_classification.py +119 -0
- obsforge-0.1.1/tests/unit/test_loki_policy.py +254 -0
- obsforge-0.1.1/tests/unit/test_payload_sanitizer.py +234 -0
- obsforge-0.1.1/tests/unit/test_pii_scrubber.py +47 -0
- obsforge-0.1.1/tests/unit/test_pool_tracker.py +75 -0
- obsforge-0.1.1/tests/unit/test_settings.py +29 -0
- obsforge-0.1.1/tests/unit/test_sql_normalizer.py +82 -0
- obsforge-0.1.1/tests/unit/test_telemetry_mapping.py +274 -0
- obsforge-0.1.1/tests/unit/test_transactions.py +79 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to obsforge are documented here. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); versions follow SemVer.
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
_Nothing yet._
|
|
9
|
+
|
|
10
|
+
## [0.1.1] — 2026-06-06
|
|
11
|
+
|
|
12
|
+
First published release to PyPI (0.1.0 was tagged but never published). Bundles
|
|
13
|
+
adoption ergonomics and three integration fixes found by an end-to-end scenario
|
|
14
|
+
sweep over previously-uncovered paths.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`drf` and `celery` install extras**: `pip install "obsforge[drf]"` (Django REST
|
|
18
|
+
Framework exception handler) and `"obsforge[celery]"` (task correlation). CI now
|
|
19
|
+
installs them so their integration tests run.
|
|
20
|
+
- **One-liner drop-in**: `install_logging_bridge()` now works with no arguments —
|
|
21
|
+
it reuses the client registered by the last `bootstrap()`, or initializes a
|
|
22
|
+
default SDK on first use. Passing `event_client` explicitly is still supported.
|
|
23
|
+
- `service_name` now defaults from the OpenTelemetry-standard `OTEL_SERVICE_NAME`
|
|
24
|
+
environment variable (falling back to `unknown-service`), so logs are
|
|
25
|
+
attributable without code changes.
|
|
26
|
+
- Benchmark suite extended with a fair **stdlib `logging` baseline** (obsforge is
|
|
27
|
+
~8–9× plain stdlib formatting) and a **sustained concurrent-load** sweep
|
|
28
|
+
(thread-safety + aggregate throughput); see `docs/operational/performance.md`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **httpx sync instrumentation crashed every request**: the sync transport read
|
|
32
|
+
`response.content` on an unread streaming response, raising
|
|
33
|
+
`httpx.ResponseNotRead`. Now reads the body first (mirrors the async path).
|
|
34
|
+
Adds end-to-end client tests (httpx sync/async + requests) — this path had no
|
|
35
|
+
coverage.
|
|
36
|
+
- **Celery correlation never propagated**: the `before_task_publish` handler
|
|
37
|
+
discarded the injected carrier instead of merging it into the outbound headers
|
|
38
|
+
(Celery sends the dict it hands the signal), so worker logs never inherited the
|
|
39
|
+
producer's correlation id. Now updates the headers in place. Adds a no-broker
|
|
40
|
+
round-trip test (producer → worker).
|
|
41
|
+
- **OTel export opt-in was a no-op**: `bootstrap()` built the OTel exporter without
|
|
42
|
+
a provider, so `otel.logs_enabled=True` silently emitted nothing.
|
|
43
|
+
`bootstrap()` now accepts `otel_logger_provider=` / `otel_tracer_provider=` and
|
|
44
|
+
raises `ConfigurationError` if export is enabled without a usable provider.
|
|
45
|
+
`traces_enabled` is now wired too (it was previously ignored).
|
|
46
|
+
- Docs reconciled with reality: corrected core dependency list (just `orjson` +
|
|
47
|
+
`pydantic`), Python support (3.11/3.12/3.13, matching pyproject and CI), and the
|
|
48
|
+
internal progress table (re-scored against the zero-infra logging scope).
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- **`bootstrap()` signature**: added keyword-only `otel_logger_provider` and
|
|
52
|
+
`otel_tracer_provider`. Non-OTel callers are unaffected.
|
|
53
|
+
|
|
54
|
+
## [0.1.0] — 2026-06-05
|
|
55
|
+
|
|
56
|
+
First public release.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- **Drop-in stdlib `logging` bridge**: `ObsforgeLoggingHandler` and
|
|
60
|
+
`install_logging_bridge()` turn existing `logging` calls into structured
|
|
61
|
+
events with no code changes. Records inherit the active correlation context;
|
|
62
|
+
`logger.exception(...)` captures an exception context; `obsforge.*` loggers are
|
|
63
|
+
skipped to prevent recursion.
|
|
64
|
+
- **Loki label governance**: `LokiLabelPolicy` + `StructuredMetadataPolicy` and a
|
|
65
|
+
three-section `{labels, structured_metadata, body}` serializer. High-cardinality
|
|
66
|
+
identifiers (trace_id, user_id, tenant_id, ...) are never emitted as labels.
|
|
67
|
+
- **Deep PII redaction**: value-level scrubber (email, bearer/JWT, card, SSN, IP)
|
|
68
|
+
applied across all string fields incl. `event.message`, headers, payload
|
|
69
|
+
previews, exception messages/locals, and DB query text.
|
|
70
|
+
- **Fail-open pipeline** with severity sampling; telemetry faults never reach the
|
|
71
|
+
application's hot path.
|
|
72
|
+
- **Optional OpenTelemetry export** (`otel` extra): real log exporter + trace
|
|
73
|
+
bridge, off by default, no collector required for the default path.
|
|
74
|
+
- Public API: `Severity`, `Outcome`, `EventKind`, `ObsforgeSettings`, `Event`,
|
|
75
|
+
`instrument`, plus the logging bridge.
|
|
76
|
+
- Benchmark suite (`benchmarks/run.py`), CI (lint + mypy --strict + tests on
|
|
77
|
+
3.11/3.12/3.13), runnable examples, and operational/adoption docs.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
- Reoriented around the zero-infra finality: structured logs to stdout consumed
|
|
81
|
+
by any stack's shipper. Removed the eager `or bootstrap()` integration
|
|
82
|
+
fallbacks in favor of `require_default_*_engine()` (fail clearly if not
|
|
83
|
+
initialized).
|
|
84
|
+
- **Python support widened to `>=3.11`** (was 3.12): PEP 695 generics converted
|
|
85
|
+
to `TypeVar`; CI runs 3.11/3.12/3.13.
|
|
86
|
+
- **Leaner dependency surface**: core now depends only on `orjson` + `pydantic`
|
|
87
|
+
(removed unused `structlog` and `typing-extensions`).
|
|
88
|
+
- **`import obsforge` no longer imports any optional dependency** (requests,
|
|
89
|
+
opentelemetry, ...). Client adapters and the OTel exporter are imported lazily.
|
|
90
|
+
- Exposed `obsforge.__version__`.
|
|
91
|
+
|
|
92
|
+
### Removed
|
|
93
|
+
- **AI Incident Intelligence / OpenRouter subsystem** — the only real
|
|
94
|
+
external-service dependency. Out of scope for a zero-infra logging library.
|
|
95
|
+
- Unwired batching scaffolding (`BatchDispatcher`, `RingBuffer`).
|
|
96
|
+
- Eager re-export of `instrument_requests_session` from
|
|
97
|
+
`obsforge.instrumentation.http` (import it directly from the submodule).
|
|
98
|
+
|
|
99
|
+
### Quality
|
|
100
|
+
- `ruff` clean, `mypy --strict` clean, layered unit/integration/regression test
|
|
101
|
+
suite green.
|
obsforge-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthony Grullon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
obsforge-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obsforge
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Zero-infra drop-in structured logging & observability for Python (Grafana/Loki, Elastic, any stack)
|
|
5
|
+
Project-URL: Homepage, https://github.com/AnthonyGrullonA/OBSFORGE
|
|
6
|
+
Project-URL: Repository, https://github.com/AnthonyGrullonA/OBSFORGE
|
|
7
|
+
Project-URL: Issues, https://github.com/AnthonyGrullonA/OBSFORGE/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Anthony Grullon
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: django,fastapi,logging,observability,opentelemetry,structured-logging
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: System :: Logging
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: orjson>=3.10.0
|
|
26
|
+
Requires-Dist: pydantic>=2.8.0
|
|
27
|
+
Provides-Extra: celery
|
|
28
|
+
Requires-Dist: celery>=5.3.0; extra == 'celery'
|
|
29
|
+
Provides-Extra: db
|
|
30
|
+
Requires-Dist: aiomysql>=0.2.0; extra == 'db'
|
|
31
|
+
Requires-Dist: asyncpg>=0.29.0; extra == 'db'
|
|
32
|
+
Requires-Dist: psycopg>=3.1.0; extra == 'db'
|
|
33
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'db'
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: anyio>=4.4.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: mypy>=1.11.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: twine>=5.1.0; extra == 'dev'
|
|
43
|
+
Provides-Extra: django
|
|
44
|
+
Requires-Dist: django>=5.1; extra == 'django'
|
|
45
|
+
Provides-Extra: drf
|
|
46
|
+
Requires-Dist: django>=5.1; extra == 'drf'
|
|
47
|
+
Requires-Dist: djangorestframework>=3.15.0; extra == 'drf'
|
|
48
|
+
Provides-Extra: fastapi
|
|
49
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'fastapi'
|
|
50
|
+
Requires-Dist: starlette>=0.40.0; extra == 'fastapi'
|
|
51
|
+
Provides-Extra: http
|
|
52
|
+
Requires-Dist: aiohttp>=3.10.0; extra == 'http'
|
|
53
|
+
Requires-Dist: httpx>=0.27.0; extra == 'http'
|
|
54
|
+
Requires-Dist: requests>=2.32.0; extra == 'http'
|
|
55
|
+
Provides-Extra: otel
|
|
56
|
+
Requires-Dist: opentelemetry-api>=1.27.0; extra == 'otel'
|
|
57
|
+
Requires-Dist: opentelemetry-sdk>=1.27.0; extra == 'otel'
|
|
58
|
+
Requires-Dist: opentelemetry-semantic-conventions>=0.48b0; extra == 'otel'
|
|
59
|
+
Description-Content-Type: text/markdown
|
|
60
|
+
|
|
61
|
+
# obsforge
|
|
62
|
+
|
|
63
|
+
[](https://pypi.org/project/obsforge/)
|
|
64
|
+
[](https://pypi.org/project/obsforge/)
|
|
65
|
+
[](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/LICENSE)
|
|
66
|
+
|
|
67
|
+
**Zero-infra, drop-in structured logging & observability for Python.**
|
|
68
|
+
|
|
69
|
+
Add it to your code and your logs come out as structured JSON on **stdout** —
|
|
70
|
+
ready for your existing shipper (promtail, filebeat, fluent-bit, otel-collector)
|
|
71
|
+
to deliver to **Grafana/Loki, Elastic, or any stack**. obsforge **does not run,
|
|
72
|
+
require, or manage any extra infrastructure**. It writes to stdout; your platform
|
|
73
|
+
already knows how to collect that.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install obsforge
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import logging, obsforge
|
|
81
|
+
|
|
82
|
+
obsforge.install_logging_bridge() # one line, no bootstrap, no code rewrite
|
|
83
|
+
|
|
84
|
+
logging.getLogger("checkout.auth").warning("login failed", extra={"reason": "bad_password"})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
That's the whole integration. Set the service name once via the OpenTelemetry-standard
|
|
88
|
+
env var and every event is attributable:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export OTEL_SERVICE_NAME=checkout # otherwise logs show "unknown-service"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## What problem does it solve?
|
|
97
|
+
|
|
98
|
+
Most apps log unstructured strings, then teams bolt on regex parsing, inconsistent
|
|
99
|
+
fields, and accidental high-cardinality labels that blow up Loki. Tracing and
|
|
100
|
+
logs live in separate worlds. And "observability SDKs" often drag in collectors,
|
|
101
|
+
agents, or external services you have to operate.
|
|
102
|
+
|
|
103
|
+
obsforge takes the opposite stance:
|
|
104
|
+
|
|
105
|
+
- **Semantic events, not strings.** Every log is a canonical event with a stable
|
|
106
|
+
schema (`event`, `severity`, `service`, `correlation`, `trace`, ...), not a
|
|
107
|
+
free-form `message`.
|
|
108
|
+
- **Cardinality-safe by construction.** Output is a three-section document where
|
|
109
|
+
low-cardinality keys become labels and high-cardinality identifiers
|
|
110
|
+
(`trace_id`, `user_id`, `tenant_id`, ...) are **never** labels.
|
|
111
|
+
- **Correlation built in.** W3C trace context + correlation ids flow across HTTP,
|
|
112
|
+
Celery, Kafka, RabbitMQ, asyncio and background workers — so logs and traces
|
|
113
|
+
share ids.
|
|
114
|
+
- **Zero extra infrastructure.** stdout-first, JSON-first. No collector, agent, or
|
|
115
|
+
service is required by the library.
|
|
116
|
+
- **Drop-in.** Works with your existing `logging` calls; no rewrite.
|
|
117
|
+
|
|
118
|
+
## What it is *not*
|
|
119
|
+
|
|
120
|
+
It is not an agent, a daemon, or a hosted service. It doesn't ship logs itself —
|
|
121
|
+
your platform's collector does. It has no external service dependency in the
|
|
122
|
+
default path.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Features
|
|
127
|
+
|
|
128
|
+
- 🪵 **stdlib `logging` bridge** — route existing `logger.*` calls through obsforge with one line.
|
|
129
|
+
- 🧱 **Canonical event model** — typed (pydantic), with HTTP / DB / exception / cache / security / business / dependency context.
|
|
130
|
+
- 🏷️ **Loki label governance** — `LokiLabelPolicy` + `StructuredMetadataPolicy`; no cardinality footguns.
|
|
131
|
+
- 🔗 **Distributed correlation** — W3C `traceparent` / `baggage`, propagated across services, queues, and tasks.
|
|
132
|
+
- 🛡️ **Security by default** — deep PII scrubbing (email, JWT/bearer, card, SSN, IP) across every string field; identity validation on ingress.
|
|
133
|
+
- 🧯 **Fail-open pipeline** — a telemetry fault never breaks your request.
|
|
134
|
+
- 🧩 **Framework adapters** — FastAPI/Starlette, Django/DRF middleware; HTTP-client and DB instrumentation.
|
|
135
|
+
- 🔭 **Optional OpenTelemetry** — opt-in log export + trace bridge (off by default; no collector needed otherwise).
|
|
136
|
+
- ✅ **Typed & tested** — `mypy --strict` clean, layered test suite, benchmarked.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Installation
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pip install obsforge # core (orjson, pydantic — no other runtime deps)
|
|
144
|
+
pip install "obsforge[fastapi]" # FastAPI / Starlette middleware
|
|
145
|
+
pip install "obsforge[django]" # Django middleware
|
|
146
|
+
pip install "obsforge[drf]" # Django REST Framework exception handler
|
|
147
|
+
pip install "obsforge[celery]" # Celery task correlation
|
|
148
|
+
pip install "obsforge[db]" # SQLAlchemy / psycopg / asyncpg / aiomysql
|
|
149
|
+
pip install "obsforge[http]" # httpx / requests / aiohttp client instrumentation
|
|
150
|
+
pip install "obsforge[otel]" # optional OpenTelemetry export
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Python **3.11+** (CI runs 3.11, 3.12, 3.13).
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Quickstart
|
|
158
|
+
|
|
159
|
+
### 1) Drop-in for existing `logging` code (recommended start)
|
|
160
|
+
|
|
161
|
+
The minimal integration is a single call — no `bootstrap()`, no objects to thread
|
|
162
|
+
through your code. The bridge initializes a default SDK on first use:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import logging, obsforge
|
|
166
|
+
|
|
167
|
+
obsforge.install_logging_bridge() # set OTEL_SERVICE_NAME in the environment
|
|
168
|
+
|
|
169
|
+
log = logging.getLogger("checkout.orders")
|
|
170
|
+
log.info("order placed", extra={"order_id": "o_123", "amount_cents": 4200})
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
charge(order)
|
|
174
|
+
except PaymentError:
|
|
175
|
+
log.exception("charge failed", extra={"order_id": "o_123"}) # traceback captured
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
When you need explicit configuration (environment, Loki preset, redaction), bootstrap
|
|
179
|
+
once and the bridge reuses that SDK automatically:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
sdk = obsforge.bootstrap(obsforge.ObsforgeSettings(
|
|
183
|
+
service_name="checkout",
|
|
184
|
+
environment="production",
|
|
185
|
+
loki={"preset": "prod"}, # dev | staging | prod label policy
|
|
186
|
+
))
|
|
187
|
+
obsforge.install_logging_bridge() # reuses the client registered by bootstrap()
|
|
188
|
+
# (or pass it explicitly: obsforge.install_logging_bridge(sdk.event_client))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`logger.exception(...)` captures a structured exception context. Logs emitted
|
|
192
|
+
inside a request automatically inherit its correlation id.
|
|
193
|
+
|
|
194
|
+
### 2) Explicit semantic events
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
sdk.logger.log_sync("auth.login.failed", severity=obsforge.Severity.WARNING, reason="bad_password")
|
|
198
|
+
await sdk.logger.log("billing.invoice.paid", amount_cents=4200)
|
|
199
|
+
|
|
200
|
+
@obsforge.instrument(sdk.event_client, "checkout.order.place")
|
|
201
|
+
def place_order(...): ...
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 3) FastAPI
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
import obsforge
|
|
208
|
+
from fastapi import FastAPI
|
|
209
|
+
from obsforge.integrations.fastapi.middleware import FastAPIObservabilityMiddleware
|
|
210
|
+
|
|
211
|
+
sdk = obsforge.bootstrap(obsforge.ObsforgeSettings(service_name="api"))
|
|
212
|
+
app = FastAPI()
|
|
213
|
+
app.add_middleware(FastAPIObservabilityMiddleware, api_engine=sdk.api_engine)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
See [`examples/`](examples/) for FastAPI, Django, worker, and plain-logging apps.
|
|
217
|
+
|
|
218
|
+
### 4) OpenTelemetry (optional)
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install "obsforge[otel]"
|
|
222
|
+
```
|
|
223
|
+
```python
|
|
224
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
|
225
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
226
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
227
|
+
|
|
228
|
+
provider = LoggerProvider()
|
|
229
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
|
|
230
|
+
|
|
231
|
+
sdk = obsforge.bootstrap(
|
|
232
|
+
obsforge.ObsforgeSettings(service_name="api", otel={"logs_enabled": True}),
|
|
233
|
+
otel_logger_provider=provider, # inject the configured provider to actually export
|
|
234
|
+
)
|
|
235
|
+
# Events are also emitted as OTel LogRecords with trace_id in the record context.
|
|
236
|
+
```
|
|
237
|
+
OTel export is off by default — stdout remains the default path. Opting in
|
|
238
|
+
(`logs_enabled=True` / `traces_enabled=True`) requires injecting the matching
|
|
239
|
+
provider (`otel_logger_provider=` / `otel_tracer_provider=`); enabling it without
|
|
240
|
+
one raises `ConfigurationError` rather than silently dropping every record.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## What the output looks like
|
|
245
|
+
|
|
246
|
+
Each event is one JSON line on stdout, split into three sections:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"labels": { "service": "checkout", "environment": "production", "level": "warning", "event_kind": "api", "outcome": "failed" },
|
|
251
|
+
"structured_metadata": { "trace_id": "0af7...", "correlation_id": "...", "user_id": "u_1", "fingerprint": "..." },
|
|
252
|
+
"body": { "event_name": "auth.login.failed", "message": "login failed", "severity": "warning", "...": "..." }
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- **`labels`** → low-cardinality, safe to index in Loki.
|
|
257
|
+
- **`structured_metadata`** → high-cardinality ids; queryable, **never** labels.
|
|
258
|
+
- **`body`** → the full canonical event (your log line).
|
|
259
|
+
|
|
260
|
+
### How your stack consumes it (no library infra)
|
|
261
|
+
|
|
262
|
+
| Backend | How |
|
|
263
|
+
|---|---|
|
|
264
|
+
| **Grafana / Loki** | promtail or Alloy maps `labels`→labels, `structured_metadata`→structured metadata, `body`→log line ([config](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/operational/loki_otel_setup.md)) |
|
|
265
|
+
| **Elastic** | filebeat / fluent-bit ingest the JSON line; query by `body.*` / `structured_metadata.*` |
|
|
266
|
+
| **Anything** | it's JSON on stdout — if your platform collects stdout, it just works |
|
|
267
|
+
|
|
268
|
+
### Grafana / Loki (promtail snippet)
|
|
269
|
+
|
|
270
|
+
Map the three sections explicitly — never auto-flatten the whole document into labels:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
pipeline_stages:
|
|
274
|
+
- json:
|
|
275
|
+
expressions: { labels: labels, structured_metadata: structured_metadata, body: body }
|
|
276
|
+
- labels:
|
|
277
|
+
service:
|
|
278
|
+
environment:
|
|
279
|
+
level:
|
|
280
|
+
event_kind:
|
|
281
|
+
outcome:
|
|
282
|
+
- structured_metadata:
|
|
283
|
+
trace_id:
|
|
284
|
+
correlation_id:
|
|
285
|
+
user_id:
|
|
286
|
+
- output:
|
|
287
|
+
source: body
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Full promtail/Alloy + OTLP reference: [docs/operational/loki_otel_setup.md](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/operational/loki_otel_setup.md).
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Governance & safety (defaults you get for free)
|
|
295
|
+
|
|
296
|
+
- **No cardinality footguns:** `trace_id`, `user_id`, `tenant_id`, `request_id`,
|
|
297
|
+
`correlation_id`, `session_id`, `event_id` can never be emitted as labels.
|
|
298
|
+
- **PII scrubbing:** emails, bearer/JWT tokens, card numbers, SSNs and IPs are
|
|
299
|
+
redacted from every string field — message, payload previews, headers,
|
|
300
|
+
exception text, DB query text.
|
|
301
|
+
- **Fail-open:** any error inside the pipeline is logged-and-dropped, never raised
|
|
302
|
+
into your hot path.
|
|
303
|
+
- **Trusted propagation:** inbound identifiers are length- and control-char-validated;
|
|
304
|
+
baggage is capped.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Configuration
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
obsforge.ObsforgeSettings(
|
|
312
|
+
service_name="checkout",
|
|
313
|
+
environment="production",
|
|
314
|
+
min_severity=obsforge.Severity.INFO, # sampler drops below-threshold events
|
|
315
|
+
loki={"preset": "prod"}, # label policy preset
|
|
316
|
+
security={"scrub_pii": True, "trust_inbound_identity": True},
|
|
317
|
+
otel={"logs_enabled": False}, # opt-in; requires the otel extra + a provider
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Per-subsystem settings exist for HTTP, DB, exceptions, and distributed
|
|
322
|
+
correlation — see `obsforge.config.settings`.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Performance
|
|
327
|
+
|
|
328
|
+
~28k–37k events/sec/core single-threaded; ~35 µs per `logger.info(...)` through
|
|
329
|
+
the bridge — about **8–9× plain stdlib formatting**, and that delta buys the
|
|
330
|
+
typed event, PII scrubbing, correlation, and Loki-governed JSON. Throughput holds
|
|
331
|
+
steady under concurrent load (Python 3.13, no-op sink). Run it yourself:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
python benchmarks/run.py # overhead + stdlib baseline + concurrency sweep
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Details and methodology: [docs/operational/performance.md](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/operational/performance.md).
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Documentation
|
|
342
|
+
|
|
343
|
+
- [Adoption guide](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/adoption-guide.md) — full integration walkthrough
|
|
344
|
+
- [Architecture](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/architecture.md) — layers and canonical event shape
|
|
345
|
+
- [Loki / OTel setup](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/operational/loki_otel_setup.md) — promtail/Alloy + OTLP config
|
|
346
|
+
- [Performance](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/operational/performance.md)
|
|
347
|
+
- [Support matrix](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/support-matrix.md)
|
|
348
|
+
- [Publishing](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/docs/publishing.md)
|
|
349
|
+
- [Changelog](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/CHANGELOG.md)
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Compatibility
|
|
354
|
+
|
|
355
|
+
| | |
|
|
356
|
+
|---|---|
|
|
357
|
+
| Python | 3.11, 3.12, 3.13 (CI matrix) |
|
|
358
|
+
| Frameworks | FastAPI/Starlette, Django/DRF (via extras) |
|
|
359
|
+
| DB drivers | SQLAlchemy, psycopg, asyncpg, aiomysql (via `db` extra) |
|
|
360
|
+
| HTTP clients | httpx, requests, aiohttp (via `http` extra) |
|
|
361
|
+
| Backends | Grafana/Loki, Elastic, any stdout collector |
|
|
362
|
+
|
|
363
|
+
Postgres, Kafka, RabbitMQ, Celery brokers and an OTLP collector are **never run or
|
|
364
|
+
required** by obsforge — they're only relevant to instrumentation tests you opt into.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Development
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
python3 -m venv .venv && source .venv/bin/activate # Python 3.11+
|
|
372
|
+
pip install -e ".[dev,fastapi,django,db,http,otel]"
|
|
373
|
+
|
|
374
|
+
ruff check src tests # lint
|
|
375
|
+
mypy src # strict type-check
|
|
376
|
+
pytest -m "not requires_postgres and not requires_mysql and not requires_kafka and not requires_rabbitmq and not requires_broker"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT — see [LICENSE](https://github.com/AnthonyGrullonA/OBSFORGE/blob/main/LICENSE).
|