obsforge 0.1.1__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.
- obsforge/__init__.py +44 -0
- obsforge/api/__init__.py +5 -0
- obsforge/api/context.py +32 -0
- obsforge/api/decorators.py +42 -0
- obsforge/api/events.py +25 -0
- obsforge/api/logger.py +45 -0
- obsforge/config/__init__.py +4 -0
- obsforge/config/bootstrap.py +190 -0
- obsforge/config/settings.py +176 -0
- obsforge/config/state.py +53 -0
- obsforge/core/__init__.py +3 -0
- obsforge/core/contracts.py +52 -0
- obsforge/core/errors.py +10 -0
- obsforge/core/models.py +572 -0
- obsforge/core/taxonomy.py +29 -0
- obsforge/encoding/__init__.py +3 -0
- obsforge/encoding/json.py +46 -0
- obsforge/encoding/serializers.py +7 -0
- obsforge/instrumentation/__init__.py +1 -0
- obsforge/instrumentation/db/__init__.py +4 -0
- obsforge/instrumentation/db/aiomysql.py +106 -0
- obsforge/instrumentation/db/asyncpg.py +107 -0
- obsforge/instrumentation/db/django.py +70 -0
- obsforge/instrumentation/db/engine.py +324 -0
- obsforge/instrumentation/db/pool.py +123 -0
- obsforge/instrumentation/db/psycopg.py +168 -0
- obsforge/instrumentation/db/sql.py +65 -0
- obsforge/instrumentation/db/sqlalchemy.py +161 -0
- obsforge/instrumentation/db/state.py +28 -0
- obsforge/instrumentation/db/transactions.py +73 -0
- obsforge/instrumentation/exceptions/__init__.py +15 -0
- obsforge/instrumentation/exceptions/classification.py +108 -0
- obsforge/instrumentation/exceptions/dedupe.py +66 -0
- obsforge/instrumentation/exceptions/engine.py +373 -0
- obsforge/instrumentation/exceptions/sanitization.py +64 -0
- obsforge/instrumentation/exceptions/state.py +101 -0
- obsforge/instrumentation/http/__init__.py +13 -0
- obsforge/instrumentation/http/aiohttp.py +57 -0
- obsforge/instrumentation/http/classification.py +98 -0
- obsforge/instrumentation/http/dependency.py +92 -0
- obsforge/instrumentation/http/engine.py +516 -0
- obsforge/instrumentation/http/httpx.py +130 -0
- obsforge/instrumentation/http/requests.py +98 -0
- obsforge/instrumentation/http/sanitization.py +134 -0
- obsforge/instrumentation/http/state.py +24 -0
- obsforge/integrations/__init__.py +1 -0
- obsforge/integrations/asyncio.py +43 -0
- obsforge/integrations/celery/__init__.py +3 -0
- obsforge/integrations/celery/signals.py +76 -0
- obsforge/integrations/django/__init__.py +3 -0
- obsforge/integrations/django/middleware.py +105 -0
- obsforge/integrations/django/settings.py +1 -0
- obsforge/integrations/django/signals.py +5 -0
- obsforge/integrations/drf/__init__.py +3 -0
- obsforge/integrations/drf/exception_handler.py +31 -0
- obsforge/integrations/fastapi/__init__.py +3 -0
- obsforge/integrations/fastapi/dependencies.py +7 -0
- obsforge/integrations/fastapi/exception_handlers.py +29 -0
- obsforge/integrations/fastapi/middleware.py +142 -0
- obsforge/integrations/kafka.py +27 -0
- obsforge/integrations/logging_bridge.py +122 -0
- obsforge/integrations/rabbitmq.py +29 -0
- obsforge/integrations/workers.py +60 -0
- obsforge/plugins/__init__.py +4 -0
- obsforge/plugins/builtin.py +24 -0
- obsforge/plugins/manager.py +23 -0
- obsforge/plugins/registry.py +35 -0
- obsforge/plugins/spec.py +27 -0
- obsforge/propagation/__init__.py +26 -0
- obsforge/propagation/baggage.py +42 -0
- obsforge/propagation/correlation.py +98 -0
- obsforge/propagation/distributed.py +389 -0
- obsforge/propagation/state.py +24 -0
- obsforge/propagation/tracecontext.py +56 -0
- obsforge/py.typed +0 -0
- obsforge/runtime/__init__.py +3 -0
- obsforge/runtime/pii.py +53 -0
- obsforge/runtime/pipeline.py +102 -0
- obsforge/runtime/policies/__init__.py +0 -0
- obsforge/runtime/policies/loki_policy.py +180 -0
- obsforge/runtime/processors.py +78 -0
- obsforge/runtime/redaction.py +101 -0
- obsforge/runtime/sampling.py +21 -0
- obsforge/telemetry/__init__.py +5 -0
- obsforge/telemetry/mapping.py +67 -0
- obsforge/telemetry/otel_logs.py +96 -0
- obsforge/telemetry/otel_traces.py +78 -0
- obsforge/testing/__init__.py +3 -0
- obsforge/testing/capture.py +11 -0
- obsforge/testing/fakes.py +40 -0
- obsforge/transport/__init__.py +3 -0
- obsforge/transport/sink.py +19 -0
- obsforge/transport/stdout.py +20 -0
- obsforge-0.1.1.dist-info/METADATA +383 -0
- obsforge-0.1.1.dist-info/RECORD +98 -0
- obsforge-0.1.1.dist-info/WHEEL +4 -0
- obsforge-0.1.1.dist-info/entry_points.txt +2 -0
- obsforge-0.1.1.dist-info/licenses/LICENSE +21 -0
obsforge/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from obsforge.api.context import ContextManager
|
|
4
|
+
from obsforge.api.decorators import instrument
|
|
5
|
+
from obsforge.api.events import EventClient
|
|
6
|
+
from obsforge.api.logger import Logger
|
|
7
|
+
from obsforge.config.bootstrap import BootstrapResult, bootstrap
|
|
8
|
+
from obsforge.config.settings import ObsforgeSettings
|
|
9
|
+
from obsforge.core.models import CanonicalEvent, Event, EventKind, Outcome, Severity
|
|
10
|
+
from obsforge.instrumentation.db.engine import DBObservabilityEngine
|
|
11
|
+
from obsforge.instrumentation.exceptions.engine import ExceptionIntelligenceEngine
|
|
12
|
+
from obsforge.instrumentation.http.engine import APIRuntimeEngine
|
|
13
|
+
from obsforge.integrations.logging_bridge import (
|
|
14
|
+
ObsforgeLoggingHandler,
|
|
15
|
+
install_logging_bridge,
|
|
16
|
+
)
|
|
17
|
+
from obsforge.propagation.distributed import DistributedCorrelationEngine
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = version("obsforge")
|
|
21
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
22
|
+
__version__ = "0.0.0.dev0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"APIRuntimeEngine",
|
|
26
|
+
"BootstrapResult",
|
|
27
|
+
"CanonicalEvent",
|
|
28
|
+
"ContextManager",
|
|
29
|
+
"DBObservabilityEngine",
|
|
30
|
+
"DistributedCorrelationEngine",
|
|
31
|
+
"Event",
|
|
32
|
+
"EventClient",
|
|
33
|
+
"EventKind",
|
|
34
|
+
"ExceptionIntelligenceEngine",
|
|
35
|
+
"Logger",
|
|
36
|
+
"ObsforgeLoggingHandler",
|
|
37
|
+
"ObsforgeSettings",
|
|
38
|
+
"Outcome",
|
|
39
|
+
"Severity",
|
|
40
|
+
"__version__",
|
|
41
|
+
"bootstrap",
|
|
42
|
+
"install_logging_bridge",
|
|
43
|
+
"instrument",
|
|
44
|
+
]
|
obsforge/api/__init__.py
ADDED
obsforge/api/context.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from obsforge.core.models import CorrelationContext, TraceContext
|
|
7
|
+
from obsforge.propagation.correlation import CorrelationManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContextManager:
|
|
11
|
+
"""Public facade for request-local observability context."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, manager: CorrelationManager | None = None) -> None:
|
|
14
|
+
self._manager = manager or CorrelationManager()
|
|
15
|
+
|
|
16
|
+
def get_correlation(self) -> CorrelationContext | None:
|
|
17
|
+
return self._manager.get_correlation()
|
|
18
|
+
|
|
19
|
+
def set_correlation(self, context: CorrelationContext) -> None:
|
|
20
|
+
self._manager.set_correlation(context)
|
|
21
|
+
|
|
22
|
+
def clear_correlation(self) -> None:
|
|
23
|
+
self._manager.clear()
|
|
24
|
+
|
|
25
|
+
def get_trace(self) -> TraceContext | None:
|
|
26
|
+
return self._manager.get_trace()
|
|
27
|
+
|
|
28
|
+
def set_trace(self, context: TraceContext) -> None:
|
|
29
|
+
self._manager.set_trace(context)
|
|
30
|
+
|
|
31
|
+
def bind_attributes(self, attributes: Mapping[str, Any]) -> None:
|
|
32
|
+
self._manager.bind_attributes(attributes)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from obsforge.api.events import EventClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def instrument(event_client: EventClient, event_name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
12
|
+
"""Attach semantic lifecycle events around a sync or async function."""
|
|
13
|
+
|
|
14
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
15
|
+
if inspect.iscoroutinefunction(func):
|
|
16
|
+
@wraps(func)
|
|
17
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
18
|
+
await event_client.emit_named(event_name=f"{event_name}.started")
|
|
19
|
+
try:
|
|
20
|
+
result = await func(*args, **kwargs)
|
|
21
|
+
except Exception:
|
|
22
|
+
await event_client.emit_named(event_name=f"{event_name}.failed")
|
|
23
|
+
raise
|
|
24
|
+
await event_client.emit_named(event_name=f"{event_name}.succeeded")
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
return async_wrapper
|
|
28
|
+
|
|
29
|
+
@wraps(func)
|
|
30
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
31
|
+
event_client.emit_named_sync(event_name=f"{event_name}.started")
|
|
32
|
+
try:
|
|
33
|
+
result = func(*args, **kwargs)
|
|
34
|
+
except Exception:
|
|
35
|
+
event_client.emit_named_sync(event_name=f"{event_name}.failed")
|
|
36
|
+
raise
|
|
37
|
+
event_client.emit_named_sync(event_name=f"{event_name}.succeeded")
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
return sync_wrapper
|
|
41
|
+
|
|
42
|
+
return decorator
|
obsforge/api/events.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from obsforge.core.models import Event
|
|
6
|
+
from obsforge.runtime.pipeline import EventPipeline
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventClient:
|
|
10
|
+
"""Public API for semantic event emission."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, pipeline: EventPipeline) -> None:
|
|
13
|
+
self._pipeline = pipeline
|
|
14
|
+
|
|
15
|
+
async def emit(self, event: Event) -> None:
|
|
16
|
+
await self._pipeline.process(event)
|
|
17
|
+
|
|
18
|
+
def emit_sync(self, event: Event) -> None:
|
|
19
|
+
self._pipeline.process_sync(event)
|
|
20
|
+
|
|
21
|
+
async def emit_named(self, event_name: str, **context: Any) -> None:
|
|
22
|
+
await self.emit(Event.named(event_name=event_name, context=context))
|
|
23
|
+
|
|
24
|
+
def emit_named_sync(self, event_name: str, **context: Any) -> None:
|
|
25
|
+
self.emit_sync(Event.named(event_name=event_name, context=context))
|
obsforge/api/logger.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from obsforge.api.events import EventClient
|
|
6
|
+
from obsforge.core.models import Event, Severity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Logger:
|
|
10
|
+
"""High-level logger that always writes semantic events."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, event_client: EventClient) -> None:
|
|
13
|
+
self._event_client = event_client
|
|
14
|
+
|
|
15
|
+
async def log(
|
|
16
|
+
self,
|
|
17
|
+
event_name: str,
|
|
18
|
+
*,
|
|
19
|
+
severity: Severity = Severity.INFO,
|
|
20
|
+
message: str | None = None,
|
|
21
|
+
**context: Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
event = Event.named(
|
|
24
|
+
event_name=event_name,
|
|
25
|
+
severity=severity,
|
|
26
|
+
message=message,
|
|
27
|
+
context=context,
|
|
28
|
+
)
|
|
29
|
+
await self._event_client.emit(event)
|
|
30
|
+
|
|
31
|
+
def log_sync(
|
|
32
|
+
self,
|
|
33
|
+
event_name: str,
|
|
34
|
+
*,
|
|
35
|
+
severity: Severity = Severity.INFO,
|
|
36
|
+
message: str | None = None,
|
|
37
|
+
**context: Any,
|
|
38
|
+
) -> None:
|
|
39
|
+
event = Event.named(
|
|
40
|
+
event_name=event_name,
|
|
41
|
+
severity=severity,
|
|
42
|
+
message=message,
|
|
43
|
+
context=context,
|
|
44
|
+
)
|
|
45
|
+
self._event_client.emit_sync(event)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from obsforge.api.context import ContextManager
|
|
6
|
+
from obsforge.api.events import EventClient
|
|
7
|
+
from obsforge.api.logger import Logger
|
|
8
|
+
from obsforge.config.settings import ObsforgeSettings
|
|
9
|
+
from obsforge.config.state import set_default_event_client
|
|
10
|
+
from obsforge.core.contracts import EventProcessor, TelemetryExporter
|
|
11
|
+
from obsforge.core.errors import ConfigurationError
|
|
12
|
+
from obsforge.core.taxonomy import TaxonomyPolicy
|
|
13
|
+
from obsforge.encoding.json import OrjsonEventEncoder
|
|
14
|
+
from obsforge.instrumentation.db.engine import DBObservabilityEngine
|
|
15
|
+
from obsforge.instrumentation.db.state import set_default_db_engine
|
|
16
|
+
from obsforge.instrumentation.exceptions.engine import ExceptionIntelligenceEngine
|
|
17
|
+
from obsforge.instrumentation.exceptions.state import set_default_exception_engine
|
|
18
|
+
from obsforge.instrumentation.http.engine import APIRuntimeEngine
|
|
19
|
+
from obsforge.instrumentation.http.state import set_default_api_engine
|
|
20
|
+
from obsforge.plugins.builtin import DefaultTaxonomyPlugin
|
|
21
|
+
from obsforge.plugins.manager import PluginManager
|
|
22
|
+
from obsforge.plugins.registry import DefaultPluginRegistry
|
|
23
|
+
from obsforge.propagation.correlation import CorrelationManager
|
|
24
|
+
from obsforge.propagation.distributed import DistributedCorrelationEngine
|
|
25
|
+
from obsforge.propagation.state import set_default_distributed_engine
|
|
26
|
+
from obsforge.runtime.pii import PIIScrubber
|
|
27
|
+
from obsforge.runtime.pipeline import EventPipeline
|
|
28
|
+
from obsforge.runtime.policies.loki_policy import (
|
|
29
|
+
LokiLabelPolicy,
|
|
30
|
+
LokiSerializer,
|
|
31
|
+
StructuredMetadataPolicy,
|
|
32
|
+
)
|
|
33
|
+
from obsforge.runtime.processors import (
|
|
34
|
+
CorrelationProcessor,
|
|
35
|
+
RuntimeContextProcessor,
|
|
36
|
+
TaxonomyProcessor,
|
|
37
|
+
TimestampProcessor,
|
|
38
|
+
)
|
|
39
|
+
from obsforge.runtime.redaction import RedactionProcessor
|
|
40
|
+
from obsforge.runtime.sampling import SeveritySamplingPolicy
|
|
41
|
+
from obsforge.transport.stdout import StdoutSink
|
|
42
|
+
|
|
43
|
+
# NOTE: ``obsforge.telemetry.otel_logs`` is imported lazily inside bootstrap()
|
|
44
|
+
# only when OTel is enabled, so a plain ``import obsforge`` never touches the
|
|
45
|
+
# optional opentelemetry import graph.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BootstrapResult:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
logger: Logger,
|
|
53
|
+
event_client: EventClient,
|
|
54
|
+
context_manager: ContextManager,
|
|
55
|
+
pipeline: EventPipeline,
|
|
56
|
+
plugin_manager: PluginManager,
|
|
57
|
+
distributed_engine: DistributedCorrelationEngine,
|
|
58
|
+
api_engine: APIRuntimeEngine,
|
|
59
|
+
db_engine: DBObservabilityEngine,
|
|
60
|
+
exception_engine: ExceptionIntelligenceEngine,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.logger = logger
|
|
63
|
+
self.event_client = event_client
|
|
64
|
+
self.context_manager = context_manager
|
|
65
|
+
self.pipeline = pipeline
|
|
66
|
+
self.plugin_manager = plugin_manager
|
|
67
|
+
self.distributed_engine = distributed_engine
|
|
68
|
+
self.api_engine = api_engine
|
|
69
|
+
self.db_engine = db_engine
|
|
70
|
+
self.exception_engine = exception_engine
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bootstrap(
|
|
74
|
+
settings: ObsforgeSettings | None = None,
|
|
75
|
+
*,
|
|
76
|
+
otel_logger_provider: Any | None = None,
|
|
77
|
+
otel_tracer_provider: Any | None = None,
|
|
78
|
+
) -> BootstrapResult:
|
|
79
|
+
"""Initialize the SDK.
|
|
80
|
+
|
|
81
|
+
To make OTel export actually emit, opt in via settings AND inject a configured
|
|
82
|
+
provider here: ``otel.logs_enabled=True`` needs ``otel_logger_provider=...`` and
|
|
83
|
+
``otel.traces_enabled=True`` needs ``otel_tracer_provider=...``. Enabling export
|
|
84
|
+
without a usable provider raises ``ConfigurationError`` rather than silently
|
|
85
|
+
dropping every record.
|
|
86
|
+
"""
|
|
87
|
+
settings = settings or ObsforgeSettings()
|
|
88
|
+
correlation_manager = CorrelationManager()
|
|
89
|
+
registry = DefaultPluginRegistry()
|
|
90
|
+
DefaultTaxonomyPlugin().setup(registry)
|
|
91
|
+
plugin_manager = PluginManager(registry=registry)
|
|
92
|
+
plugin_manager.load_entry_points()
|
|
93
|
+
|
|
94
|
+
processors: list[EventProcessor] = [
|
|
95
|
+
TimestampProcessor(),
|
|
96
|
+
RuntimeContextProcessor(settings),
|
|
97
|
+
TaxonomyProcessor(TaxonomyPolicy(reserved_namespaces=registry.namespaces())),
|
|
98
|
+
CorrelationProcessor(correlation_manager),
|
|
99
|
+
RedactionProcessor(
|
|
100
|
+
settings.redact_keys,
|
|
101
|
+
scrubber=PIIScrubber(enabled=settings.security.scrub_pii),
|
|
102
|
+
),
|
|
103
|
+
*registry.processors(),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
serializer = LokiSerializer(
|
|
107
|
+
label_policy=LokiLabelPolicy.preset(settings.loki.preset),
|
|
108
|
+
metadata_policy=StructuredMetadataPolicy(),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
exporters: list[TelemetryExporter] = list(registry.exporters())
|
|
112
|
+
if settings.otel.logs_enabled:
|
|
113
|
+
from obsforge.telemetry.otel_logs import OTelLogExporter
|
|
114
|
+
|
|
115
|
+
log_exporter = OTelLogExporter(
|
|
116
|
+
logger_provider=otel_logger_provider,
|
|
117
|
+
logger_name=settings.otel.logger_name,
|
|
118
|
+
)
|
|
119
|
+
if not log_exporter.enabled:
|
|
120
|
+
raise ConfigurationError(
|
|
121
|
+
"otel.logs_enabled=True but no usable OpenTelemetry LoggerProvider was "
|
|
122
|
+
"provided. Pass a configured provider: "
|
|
123
|
+
"bootstrap(settings, otel_logger_provider=provider)."
|
|
124
|
+
)
|
|
125
|
+
exporters.append(log_exporter)
|
|
126
|
+
if settings.otel.traces_enabled:
|
|
127
|
+
from obsforge.telemetry.otel_traces import OTelTraceBridge
|
|
128
|
+
|
|
129
|
+
trace_bridge = OTelTraceBridge(
|
|
130
|
+
tracer_provider=otel_tracer_provider,
|
|
131
|
+
tracer_name=settings.otel.tracer_name,
|
|
132
|
+
)
|
|
133
|
+
if not trace_bridge.enabled:
|
|
134
|
+
raise ConfigurationError(
|
|
135
|
+
"otel.traces_enabled=True but no usable OpenTelemetry TracerProvider was "
|
|
136
|
+
"provided. Pass a configured provider: "
|
|
137
|
+
"bootstrap(settings, otel_tracer_provider=provider)."
|
|
138
|
+
)
|
|
139
|
+
exporters.append(trace_bridge)
|
|
140
|
+
|
|
141
|
+
pipeline = EventPipeline(
|
|
142
|
+
processors=processors,
|
|
143
|
+
encoder=OrjsonEventEncoder(serializer),
|
|
144
|
+
sinks=[StdoutSink(), *registry.sinks()],
|
|
145
|
+
exporters=exporters,
|
|
146
|
+
sampling=SeveritySamplingPolicy(min_severity=settings.min_severity),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
event_client = EventClient(pipeline)
|
|
150
|
+
set_default_event_client(event_client)
|
|
151
|
+
logger = Logger(event_client)
|
|
152
|
+
context_manager = ContextManager(correlation_manager)
|
|
153
|
+
distributed_engine = DistributedCorrelationEngine(
|
|
154
|
+
correlation_manager=correlation_manager,
|
|
155
|
+
settings=settings,
|
|
156
|
+
)
|
|
157
|
+
set_default_distributed_engine(distributed_engine)
|
|
158
|
+
exception_engine = ExceptionIntelligenceEngine(
|
|
159
|
+
event_client=event_client,
|
|
160
|
+
correlation_manager=correlation_manager,
|
|
161
|
+
settings=settings,
|
|
162
|
+
)
|
|
163
|
+
set_default_exception_engine(exception_engine)
|
|
164
|
+
db_engine = DBObservabilityEngine(
|
|
165
|
+
event_client=event_client,
|
|
166
|
+
correlation_manager=correlation_manager,
|
|
167
|
+
settings=settings,
|
|
168
|
+
exception_engine=exception_engine,
|
|
169
|
+
)
|
|
170
|
+
set_default_db_engine(db_engine)
|
|
171
|
+
api_engine = APIRuntimeEngine(
|
|
172
|
+
event_client=event_client,
|
|
173
|
+
correlation_manager=correlation_manager,
|
|
174
|
+
settings=settings,
|
|
175
|
+
exception_engine=exception_engine,
|
|
176
|
+
distributed_engine=distributed_engine,
|
|
177
|
+
)
|
|
178
|
+
set_default_api_engine(api_engine)
|
|
179
|
+
|
|
180
|
+
return BootstrapResult(
|
|
181
|
+
logger=logger,
|
|
182
|
+
event_client=event_client,
|
|
183
|
+
context_manager=context_manager,
|
|
184
|
+
pipeline=pipeline,
|
|
185
|
+
plugin_manager=plugin_manager,
|
|
186
|
+
distributed_engine=distributed_engine,
|
|
187
|
+
api_engine=api_engine,
|
|
188
|
+
db_engine=db_engine,
|
|
189
|
+
exception_engine=exception_engine,
|
|
190
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from obsforge.core.models import Severity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _default_service_name() -> str:
|
|
13
|
+
"""Honor the OpenTelemetry-standard ``OTEL_SERVICE_NAME`` env var so logs are
|
|
14
|
+
attributable out of the box; fall back to a clearly-unset sentinel."""
|
|
15
|
+
env = os.environ.get("OTEL_SERVICE_NAME", "").strip()
|
|
16
|
+
return env or "unknown-service"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTPInstrumentationSettings(BaseModel):
|
|
20
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
21
|
+
|
|
22
|
+
header_allowlist: set[str] = Field(
|
|
23
|
+
default_factory=lambda: {
|
|
24
|
+
"accept",
|
|
25
|
+
"content-type",
|
|
26
|
+
"content-length",
|
|
27
|
+
"user-agent",
|
|
28
|
+
"x-request-id",
|
|
29
|
+
"x-correlation-id",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
header_redactlist: set[str] = Field(
|
|
33
|
+
default_factory=lambda: {
|
|
34
|
+
"authorization",
|
|
35
|
+
"cookie",
|
|
36
|
+
"set-cookie",
|
|
37
|
+
"proxy-authorization",
|
|
38
|
+
"x-api-key",
|
|
39
|
+
"x-auth-token",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
payload_preview_bytes: int = 256
|
|
43
|
+
payload_capture_bytes: int = 4096
|
|
44
|
+
slow_request_threshold_ms: float = 500.0
|
|
45
|
+
anomaly_window_size: int = 50
|
|
46
|
+
anomaly_min_samples: int = 10
|
|
47
|
+
retry_storm_threshold: int = 3
|
|
48
|
+
saturation_latency_threshold_ms: float = 1000.0
|
|
49
|
+
saturation_failure_ratio: float = 0.3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ExceptionInstrumentationSettings(BaseModel):
|
|
53
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
54
|
+
|
|
55
|
+
max_frames: int = 50
|
|
56
|
+
max_locals_per_frame: int = 12
|
|
57
|
+
max_local_preview_chars: int = 160
|
|
58
|
+
max_cause_depth: int = 8
|
|
59
|
+
dedupe_window_seconds: int = 900
|
|
60
|
+
sensitive_local_names: set[str] = Field(
|
|
61
|
+
default_factory=lambda: {
|
|
62
|
+
"password",
|
|
63
|
+
"passwd",
|
|
64
|
+
"secret",
|
|
65
|
+
"token",
|
|
66
|
+
"api_key",
|
|
67
|
+
"authorization",
|
|
68
|
+
"cookie",
|
|
69
|
+
"session",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DatabaseInstrumentationSettings(BaseModel):
|
|
75
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
76
|
+
|
|
77
|
+
slow_query_threshold_ms: float = 250.0
|
|
78
|
+
saturation_latency_threshold_ms: float = 1000.0
|
|
79
|
+
pool_exhaustion_waiters_threshold: int = 1
|
|
80
|
+
leak_checkout_threshold_ms: float = 30000.0
|
|
81
|
+
retry_storm_threshold: int = 3
|
|
82
|
+
max_query_preview_chars: int = 240
|
|
83
|
+
max_normalized_query_chars: int = 2000
|
|
84
|
+
query_sample_rate: float = 1.0
|
|
85
|
+
redacted_literals_placeholder: str = "?"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DistributedCorrelationSettings(BaseModel):
|
|
89
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
90
|
+
|
|
91
|
+
correlation_header: str = "x-correlation-id"
|
|
92
|
+
request_header: str = "x-request-id"
|
|
93
|
+
tenant_header: str = "x-tenant-id"
|
|
94
|
+
user_header: str = "x-user-id"
|
|
95
|
+
operation_header: str = "x-operation-id"
|
|
96
|
+
transaction_header: str = "x-transaction-id"
|
|
97
|
+
celery_header_prefix: str = "obsforge."
|
|
98
|
+
kafka_header_prefix: str = "obsforge."
|
|
99
|
+
rabbitmq_header_prefix: str = "obsforge."
|
|
100
|
+
baggage_keys: tuple[str, ...] = (
|
|
101
|
+
"tenant_id",
|
|
102
|
+
"user_id",
|
|
103
|
+
"operation_id",
|
|
104
|
+
"transaction_id",
|
|
105
|
+
"correlation_id",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SecuritySettings(BaseModel):
|
|
110
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
111
|
+
|
|
112
|
+
scrub_pii: bool = True
|
|
113
|
+
trust_inbound_identity: bool = True
|
|
114
|
+
max_identifier_chars: int = 256
|
|
115
|
+
max_baggage_items: int = 32
|
|
116
|
+
max_baggage_value_chars: int = 1024
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LokiSettings(BaseModel):
|
|
120
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
121
|
+
|
|
122
|
+
preset: Literal["dev", "staging", "prod"] = "prod"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class OTelSettings(BaseModel):
|
|
126
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
127
|
+
|
|
128
|
+
logs_enabled: bool = False
|
|
129
|
+
traces_enabled: bool = False
|
|
130
|
+
logger_name: str = "obsforge"
|
|
131
|
+
tracer_name: str = "obsforge"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ObsforgeSettings(BaseModel):
|
|
135
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
136
|
+
|
|
137
|
+
service_name: str = Field(default_factory=_default_service_name)
|
|
138
|
+
service_version: str = "0.1.0"
|
|
139
|
+
environment: str = "development"
|
|
140
|
+
namespace: str = "application"
|
|
141
|
+
deployment_ring: str | None = None
|
|
142
|
+
runtime_version: str = platform.python_version()
|
|
143
|
+
process_pid: int = os.getpid()
|
|
144
|
+
host_name: str | None = platform.node() or None
|
|
145
|
+
cloud_provider: str | None = None
|
|
146
|
+
cloud_region: str | None = None
|
|
147
|
+
k8s_cluster_name: str | None = None
|
|
148
|
+
k8s_namespace_name: str | None = None
|
|
149
|
+
k8s_deployment_name: str | None = None
|
|
150
|
+
telemetry_sdk_version: str = "0.1.1"
|
|
151
|
+
min_severity: Severity = Severity.INFO
|
|
152
|
+
redact_keys: set[str] = Field(
|
|
153
|
+
default_factory=lambda: {
|
|
154
|
+
"password",
|
|
155
|
+
"passwd",
|
|
156
|
+
"token",
|
|
157
|
+
"secret",
|
|
158
|
+
"api_key",
|
|
159
|
+
"apikey",
|
|
160
|
+
"authorization",
|
|
161
|
+
"auth",
|
|
162
|
+
"cookie",
|
|
163
|
+
"session",
|
|
164
|
+
"credit_card",
|
|
165
|
+
"card_number",
|
|
166
|
+
"ssn",
|
|
167
|
+
"private_key",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
loki: LokiSettings = Field(default_factory=LokiSettings)
|
|
171
|
+
otel: OTelSettings = Field(default_factory=OTelSettings)
|
|
172
|
+
security: SecuritySettings = Field(default_factory=SecuritySettings)
|
|
173
|
+
http: HTTPInstrumentationSettings = Field(default_factory=HTTPInstrumentationSettings)
|
|
174
|
+
exceptions: ExceptionInstrumentationSettings = Field(default_factory=ExceptionInstrumentationSettings)
|
|
175
|
+
database: DatabaseInstrumentationSettings = Field(default_factory=DatabaseInstrumentationSettings)
|
|
176
|
+
distributed: DistributedCorrelationSettings = Field(default_factory=DistributedCorrelationSettings)
|
obsforge/config/state.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Process-wide default :class:`EventClient`.
|
|
2
|
+
|
|
3
|
+
The default is registered by :func:`obsforge.bootstrap` and lets zero-config
|
|
4
|
+
entry points (notably :func:`install_logging_bridge`) work as a true one-liner:
|
|
5
|
+
when no client is passed, the last bootstrapped one is reused, or a default SDK
|
|
6
|
+
is initialized on the spot.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from obsforge.api.events import EventClient
|
|
16
|
+
|
|
17
|
+
_default_event_client: EventClient | None = None
|
|
18
|
+
# Reentrant: get_or_bootstrap_event_client() holds the lock while calling
|
|
19
|
+
# bootstrap(), which re-enters via set_default_event_client().
|
|
20
|
+
_lock = threading.RLock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_default_event_client(client: EventClient) -> None:
|
|
24
|
+
global _default_event_client
|
|
25
|
+
with _lock:
|
|
26
|
+
_default_event_client = client
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_default_event_client() -> EventClient | None:
|
|
30
|
+
return _default_event_client
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def reset_default_event_client() -> None:
|
|
34
|
+
global _default_event_client
|
|
35
|
+
with _lock:
|
|
36
|
+
_default_event_client = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_or_bootstrap_event_client() -> EventClient:
|
|
40
|
+
"""Return the process-default client, bootstrapping a default SDK if none.
|
|
41
|
+
|
|
42
|
+
Thread-safe: the bootstrap-if-absent path uses double-checked locking so
|
|
43
|
+
concurrent first-time callers initialize exactly one default SDK."""
|
|
44
|
+
client = _default_event_client
|
|
45
|
+
if client is not None:
|
|
46
|
+
return client
|
|
47
|
+
with _lock:
|
|
48
|
+
if _default_event_client is not None:
|
|
49
|
+
return _default_event_client
|
|
50
|
+
from obsforge.config.bootstrap import bootstrap
|
|
51
|
+
|
|
52
|
+
# bootstrap() registers itself as the default via set_default_event_client().
|
|
53
|
+
return bootstrap().event_client
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from obsforge.core.models import Event
|
|
7
|
+
from obsforge.transport.sink import SinkEnvelope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventProcessor(Protocol):
|
|
11
|
+
async def process(self, event: Event) -> Event: ...
|
|
12
|
+
|
|
13
|
+
def process_sync(self, event: Event) -> Event: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EventSink(Protocol):
|
|
17
|
+
async def emit(self, envelope: SinkEnvelope) -> None: ...
|
|
18
|
+
|
|
19
|
+
def emit_sync(self, envelope: SinkEnvelope) -> None: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EventEncoder(Protocol):
|
|
23
|
+
def encode(self, event: Event) -> SinkEnvelope: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TelemetryExporter(Protocol):
|
|
27
|
+
async def export(self, event: Event) -> None: ...
|
|
28
|
+
|
|
29
|
+
def export_sync(self, event: Event) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SettingsProvider(Protocol):
|
|
33
|
+
def as_mapping(self) -> Mapping[str, Any]: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Plugin(Protocol):
|
|
37
|
+
name: str
|
|
38
|
+
version: str
|
|
39
|
+
|
|
40
|
+
def setup(self, registry: PluginRegistry) -> None: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PluginRegistry(Protocol):
|
|
44
|
+
def register_processor(self, processor: EventProcessor) -> None: ...
|
|
45
|
+
|
|
46
|
+
def register_sink(self, sink: EventSink) -> None: ...
|
|
47
|
+
|
|
48
|
+
def register_exporter(self, exporter: TelemetryExporter) -> None: ...
|
|
49
|
+
|
|
50
|
+
def register_taxonomy_namespace(self, namespace: str) -> None: ...
|
|
51
|
+
|
|
52
|
+
def processors(self) -> Sequence[EventProcessor]: ...
|
obsforge/core/errors.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class ObsforgeError(Exception):
|
|
2
|
+
"""Base exception for the SDK."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigurationError(ObsforgeError):
|
|
6
|
+
"""Raised when bootstrap or plugin setup is inconsistent."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TaxonomyError(ObsforgeError):
|
|
10
|
+
"""Raised when an event naming contract is invalid."""
|