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.
Files changed (98) hide show
  1. obsforge/__init__.py +44 -0
  2. obsforge/api/__init__.py +5 -0
  3. obsforge/api/context.py +32 -0
  4. obsforge/api/decorators.py +42 -0
  5. obsforge/api/events.py +25 -0
  6. obsforge/api/logger.py +45 -0
  7. obsforge/config/__init__.py +4 -0
  8. obsforge/config/bootstrap.py +190 -0
  9. obsforge/config/settings.py +176 -0
  10. obsforge/config/state.py +53 -0
  11. obsforge/core/__init__.py +3 -0
  12. obsforge/core/contracts.py +52 -0
  13. obsforge/core/errors.py +10 -0
  14. obsforge/core/models.py +572 -0
  15. obsforge/core/taxonomy.py +29 -0
  16. obsforge/encoding/__init__.py +3 -0
  17. obsforge/encoding/json.py +46 -0
  18. obsforge/encoding/serializers.py +7 -0
  19. obsforge/instrumentation/__init__.py +1 -0
  20. obsforge/instrumentation/db/__init__.py +4 -0
  21. obsforge/instrumentation/db/aiomysql.py +106 -0
  22. obsforge/instrumentation/db/asyncpg.py +107 -0
  23. obsforge/instrumentation/db/django.py +70 -0
  24. obsforge/instrumentation/db/engine.py +324 -0
  25. obsforge/instrumentation/db/pool.py +123 -0
  26. obsforge/instrumentation/db/psycopg.py +168 -0
  27. obsforge/instrumentation/db/sql.py +65 -0
  28. obsforge/instrumentation/db/sqlalchemy.py +161 -0
  29. obsforge/instrumentation/db/state.py +28 -0
  30. obsforge/instrumentation/db/transactions.py +73 -0
  31. obsforge/instrumentation/exceptions/__init__.py +15 -0
  32. obsforge/instrumentation/exceptions/classification.py +108 -0
  33. obsforge/instrumentation/exceptions/dedupe.py +66 -0
  34. obsforge/instrumentation/exceptions/engine.py +373 -0
  35. obsforge/instrumentation/exceptions/sanitization.py +64 -0
  36. obsforge/instrumentation/exceptions/state.py +101 -0
  37. obsforge/instrumentation/http/__init__.py +13 -0
  38. obsforge/instrumentation/http/aiohttp.py +57 -0
  39. obsforge/instrumentation/http/classification.py +98 -0
  40. obsforge/instrumentation/http/dependency.py +92 -0
  41. obsforge/instrumentation/http/engine.py +516 -0
  42. obsforge/instrumentation/http/httpx.py +130 -0
  43. obsforge/instrumentation/http/requests.py +98 -0
  44. obsforge/instrumentation/http/sanitization.py +134 -0
  45. obsforge/instrumentation/http/state.py +24 -0
  46. obsforge/integrations/__init__.py +1 -0
  47. obsforge/integrations/asyncio.py +43 -0
  48. obsforge/integrations/celery/__init__.py +3 -0
  49. obsforge/integrations/celery/signals.py +76 -0
  50. obsforge/integrations/django/__init__.py +3 -0
  51. obsforge/integrations/django/middleware.py +105 -0
  52. obsforge/integrations/django/settings.py +1 -0
  53. obsforge/integrations/django/signals.py +5 -0
  54. obsforge/integrations/drf/__init__.py +3 -0
  55. obsforge/integrations/drf/exception_handler.py +31 -0
  56. obsforge/integrations/fastapi/__init__.py +3 -0
  57. obsforge/integrations/fastapi/dependencies.py +7 -0
  58. obsforge/integrations/fastapi/exception_handlers.py +29 -0
  59. obsforge/integrations/fastapi/middleware.py +142 -0
  60. obsforge/integrations/kafka.py +27 -0
  61. obsforge/integrations/logging_bridge.py +122 -0
  62. obsforge/integrations/rabbitmq.py +29 -0
  63. obsforge/integrations/workers.py +60 -0
  64. obsforge/plugins/__init__.py +4 -0
  65. obsforge/plugins/builtin.py +24 -0
  66. obsforge/plugins/manager.py +23 -0
  67. obsforge/plugins/registry.py +35 -0
  68. obsforge/plugins/spec.py +27 -0
  69. obsforge/propagation/__init__.py +26 -0
  70. obsforge/propagation/baggage.py +42 -0
  71. obsforge/propagation/correlation.py +98 -0
  72. obsforge/propagation/distributed.py +389 -0
  73. obsforge/propagation/state.py +24 -0
  74. obsforge/propagation/tracecontext.py +56 -0
  75. obsforge/py.typed +0 -0
  76. obsforge/runtime/__init__.py +3 -0
  77. obsforge/runtime/pii.py +53 -0
  78. obsforge/runtime/pipeline.py +102 -0
  79. obsforge/runtime/policies/__init__.py +0 -0
  80. obsforge/runtime/policies/loki_policy.py +180 -0
  81. obsforge/runtime/processors.py +78 -0
  82. obsforge/runtime/redaction.py +101 -0
  83. obsforge/runtime/sampling.py +21 -0
  84. obsforge/telemetry/__init__.py +5 -0
  85. obsforge/telemetry/mapping.py +67 -0
  86. obsforge/telemetry/otel_logs.py +96 -0
  87. obsforge/telemetry/otel_traces.py +78 -0
  88. obsforge/testing/__init__.py +3 -0
  89. obsforge/testing/capture.py +11 -0
  90. obsforge/testing/fakes.py +40 -0
  91. obsforge/transport/__init__.py +3 -0
  92. obsforge/transport/sink.py +19 -0
  93. obsforge/transport/stdout.py +20 -0
  94. obsforge-0.1.1.dist-info/METADATA +383 -0
  95. obsforge-0.1.1.dist-info/RECORD +98 -0
  96. obsforge-0.1.1.dist-info/WHEEL +4 -0
  97. obsforge-0.1.1.dist-info/entry_points.txt +2 -0
  98. 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
+ ]
@@ -0,0 +1,5 @@
1
+ from obsforge.api.context import ContextManager
2
+ from obsforge.api.events import EventClient
3
+ from obsforge.api.logger import Logger
4
+
5
+ __all__ = ["ContextManager", "EventClient", "Logger"]
@@ -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,4 @@
1
+ from obsforge.config.bootstrap import bootstrap
2
+ from obsforge.config.settings import ObsforgeSettings
3
+
4
+ __all__ = ["ObsforgeSettings", "bootstrap"]
@@ -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)
@@ -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,3 @@
1
+ from obsforge.core.models import CanonicalEvent, Event, EventKind, Outcome, Severity
2
+
3
+ __all__ = ["CanonicalEvent", "Event", "EventKind", "Outcome", "Severity"]
@@ -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]: ...
@@ -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."""