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
@@ -0,0 +1,572 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from enum import StrEnum
5
+ from hashlib import sha1
6
+ from typing import Literal
7
+ from uuid import uuid4
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator
10
+
11
+ Scalar = str | int | float | bool | None
12
+
13
+
14
+ class Severity(StrEnum):
15
+ DEBUG = "debug"
16
+ INFO = "info"
17
+ NOTICE = "notice"
18
+ WARNING = "warning"
19
+ ERROR = "error"
20
+ CRITICAL = "critical"
21
+
22
+
23
+ class Outcome(StrEnum):
24
+ UNKNOWN = "unknown"
25
+ SUCCEEDED = "succeeded"
26
+ FAILED = "failed"
27
+ REJECTED = "rejected"
28
+ TIMEOUT = "timeout"
29
+ DEGRADED = "degraded"
30
+
31
+
32
+ class EventKind(StrEnum):
33
+ API = "api"
34
+ EXCEPTION = "exception"
35
+ DB = "db"
36
+ CACHE = "cache"
37
+ INFRA = "infra"
38
+ SECURITY = "security"
39
+ BUSINESS = "business"
40
+ DEPENDENCY = "dependency"
41
+ APPLICATION = "application"
42
+
43
+
44
+ class SpanKind(StrEnum):
45
+ INTERNAL = "internal"
46
+ SERVER = "server"
47
+ CLIENT = "client"
48
+ PRODUCER = "producer"
49
+ CONSUMER = "consumer"
50
+
51
+
52
+ class FailureClass(StrEnum):
53
+ TIMEOUT = "timeout"
54
+ DNS = "dns"
55
+ SSL = "ssl"
56
+ CONNECTION = "connection"
57
+ MALFORMED_RESPONSE = "malformed_response"
58
+ RATE_LIMIT = "rate_limit"
59
+ SERVER_ERROR = "server_error"
60
+ CLIENT_ERROR = "client_error"
61
+ UPSTREAM_DEGRADED = "upstream_degraded"
62
+ SATURATED = "saturated"
63
+ UNKNOWN = "unknown"
64
+
65
+
66
+ class DatabaseRole(StrEnum):
67
+ PRIMARY = "primary"
68
+ REPLICA = "replica"
69
+ UNKNOWN = "unknown"
70
+
71
+
72
+ class ExceptionCategory(StrEnum):
73
+ LOGIC = "logic_failure"
74
+ ASYNC = "async_failure"
75
+ RACE = "race_condition"
76
+ VALIDATION = "validation_failure"
77
+ SERIALIZATION = "serialization_failure"
78
+ ORM = "orm_failure"
79
+ DEPENDENCY = "dependency_failure"
80
+ INFRASTRUCTURE = "infrastructure_failure"
81
+ UNKNOWN = "unknown"
82
+
83
+
84
+ class EventDescriptor(BaseModel):
85
+ model_config = ConfigDict(extra="forbid", frozen=True)
86
+
87
+ name: str
88
+ category: str = "application"
89
+ action: str | None = None
90
+ outcome: Outcome = Outcome.UNKNOWN
91
+
92
+
93
+ class RuntimeContext(BaseModel):
94
+ model_config = ConfigDict(extra="forbid")
95
+
96
+ service_name: str = "unknown-service"
97
+ service_namespace: str | None = None
98
+ service_version: str | None = None
99
+ service_instance_id: str | None = None
100
+ deployment_environment: str = "development"
101
+ deployment_ring: str | None = None
102
+ runtime_name: Literal["cpython", "pypy"] | None = "cpython"
103
+ runtime_version: str | None = None
104
+ process_pid: int | None = None
105
+ host_name: str | None = None
106
+ cloud_provider: str | None = None
107
+ cloud_region: str | None = None
108
+ k8s_cluster_name: str | None = None
109
+ k8s_namespace_name: str | None = None
110
+ k8s_deployment_name: str | None = None
111
+ k8s_pod_name: str | None = None
112
+ k8s_node_name: str | None = None
113
+ container_name: str | None = None
114
+ container_image: str | None = None
115
+
116
+
117
+ class PayloadSummary(BaseModel):
118
+ model_config = ConfigDict(extra="forbid")
119
+
120
+ kind: Literal["empty", "json", "text", "bytes", "form", "unknown"] = "unknown"
121
+ size_bytes: int | None = None
122
+ content_type: str | None = None
123
+ truncated: bool = False
124
+ key_count: int | None = None
125
+ top_level_keys: list[str] = Field(default_factory=list)
126
+ preview: str | None = None
127
+
128
+
129
+ class RequestContext(BaseModel):
130
+ model_config = ConfigDict(extra="forbid")
131
+
132
+ protocol: Literal["http", "grpc", "ws", "internal"] = "http"
133
+ method: str | None = None
134
+ scheme: str | None = None
135
+ host: str | None = None
136
+ server_address: str | None = None
137
+ server_port: int | None = None
138
+ route_template: str | None = None
139
+ path: str | None = None
140
+ query_redacted: str | None = None
141
+ client_address: str | None = None
142
+ forwarded_for_redacted: str | None = None
143
+ user_agent: str | None = None
144
+ referer_redacted: str | None = None
145
+ content_type: str | None = None
146
+ body_size_bytes: int | None = None
147
+ timeout_ms: float | None = None
148
+ headers_allowlist: dict[str, str] = Field(default_factory=dict)
149
+ payload_summary: PayloadSummary | None = None
150
+
151
+
152
+ class ResponseContext(BaseModel):
153
+ model_config = ConfigDict(extra="forbid")
154
+
155
+ status_code: int | None = None
156
+ status_class: Literal["1xx", "2xx", "3xx", "4xx", "5xx"] | None = None
157
+ content_type: str | None = None
158
+ body_size_bytes: int | None = None
159
+ malformed: bool = False
160
+ headers_allowlist: dict[str, str] = Field(default_factory=dict)
161
+ payload_summary: PayloadSummary | None = None
162
+
163
+
164
+ class ExceptionFrame(BaseModel):
165
+ model_config = ConfigDict(extra="forbid", frozen=True)
166
+
167
+ file: str
168
+ line: int
169
+ function: str
170
+ module: str | None = None
171
+ class_name: str | None = None
172
+ locals: list[LocalVariableSnapshot] = Field(default_factory=list)
173
+
174
+
175
+ class LocalVariableSnapshot(BaseModel):
176
+ model_config = ConfigDict(extra="forbid", frozen=True)
177
+
178
+ name: str
179
+ type_name: str
180
+ preview: str | None = None
181
+ size_hint: int | None = None
182
+ redacted: bool = False
183
+
184
+
185
+ class ExceptionCause(BaseModel):
186
+ model_config = ConfigDict(extra="forbid", frozen=True)
187
+
188
+ type: str
189
+ message: str | None = None
190
+ module: str | None = None
191
+
192
+
193
+ class AsyncTaskContext(BaseModel):
194
+ model_config = ConfigDict(extra="forbid", frozen=True)
195
+
196
+ task_id: str | None = None
197
+ task_name: str | None = None
198
+ coroutine_name: str | None = None
199
+ cancelled: bool | None = None
200
+ loop_id: str | None = None
201
+ loop_debug: bool | None = None
202
+
203
+
204
+ class ThreadContext(BaseModel):
205
+ model_config = ConfigDict(extra="forbid", frozen=True)
206
+
207
+ ident: int | None = None
208
+ native_id: int | None = None
209
+ name: str | None = None
210
+ daemon: bool | None = None
211
+
212
+
213
+ class RuntimeStateContext(BaseModel):
214
+ model_config = ConfigDict(extra="forbid", frozen=True)
215
+
216
+ module: str | None = None
217
+ function: str | None = None
218
+ class_name: str | None = None
219
+ frame_count: int = 0
220
+ exception_depth: int = 0
221
+ locals: list[LocalVariableSnapshot] = Field(default_factory=list)
222
+ loop_running: bool | None = None
223
+ active_contexts: list[str] = Field(default_factory=list)
224
+
225
+
226
+ class DeduplicationContext(BaseModel):
227
+ model_config = ConfigDict(extra="forbid", frozen=True)
228
+
229
+ fingerprint: str
230
+ occurrence_count: int = 1
231
+ duplicate: bool = False
232
+ first_seen_at: datetime | None = None
233
+ last_seen_at: datetime | None = None
234
+
235
+
236
+ class ExceptionContext(BaseModel):
237
+ model_config = ConfigDict(extra="forbid")
238
+
239
+ type: str
240
+ message: str | None = None
241
+ error_type: str | None = None
242
+ category: ExceptionCategory | None = None
243
+ handled: bool = False
244
+ escaped: bool = True
245
+ stacktrace: str | None = None
246
+ frames: list[ExceptionFrame] = Field(default_factory=list)
247
+ cause_chain: list[str] = Field(default_factory=list)
248
+ causes: list[ExceptionCause] = Field(default_factory=list)
249
+ failure_class: FailureClass | None = None
250
+ runtime_state: RuntimeStateContext | None = None
251
+ async_context: AsyncTaskContext | None = None
252
+ thread_context: ThreadContext | None = None
253
+ deduplication: DeduplicationContext | None = None
254
+ normalized_message: str | None = None
255
+ ai_hints: list[str] = Field(default_factory=list)
256
+
257
+
258
+ class DatabaseContext(BaseModel):
259
+ model_config = ConfigDict(extra="forbid")
260
+
261
+ system: str | None = None
262
+ namespace: str | None = None
263
+ database_name: str | None = None
264
+ host: str | None = None
265
+ port: int | None = None
266
+ role: DatabaseRole | None = None
267
+ alias: str | None = None
268
+ collection_name: str | None = None
269
+ operation_name: str | None = None
270
+ query_summary: str | None = None
271
+ query_fingerprint: str | None = None
272
+ query_text_redacted: str | None = None
273
+ response_status_code: str | None = None
274
+ rows_affected: int | None = None
275
+ many: bool | None = None
276
+ timeout_ms: float | None = None
277
+ deadlock_detected: bool = False
278
+ lock_contended: bool = False
279
+ retry_count: int | None = None
280
+ retry_storm: bool = False
281
+ pool_name: str | None = None
282
+ pool_size: int | None = None
283
+ pool_checked_out: int | None = None
284
+ pool_available: int | None = None
285
+ pool_waiters: int | None = None
286
+ pool_saturated: bool = False
287
+ connection_leak_suspected: bool = False
288
+ checkout_wait_ms: float | None = None
289
+ connection_reused: bool | None = None
290
+ connection_id: str | None = None
291
+ connection_age_ms: float | None = None
292
+ read_only: bool | None = None
293
+ transaction_id: str | None = None
294
+ transaction_depth: int | None = None
295
+ transaction_active: bool | None = None
296
+ transaction_status: str | None = None
297
+ transaction_isolation_level: str | None = None
298
+ autocommit: bool | None = None
299
+
300
+ @field_validator("autocommit", mode="before")
301
+ @classmethod
302
+ def _coerce_autocommit(cls, value: object) -> bool | None:
303
+ # Some drivers expose legacy/sentinel ints (e.g. sqlite's -1 = legacy
304
+ # transaction control). Normalize to a clean tri-state.
305
+ if value is None or isinstance(value, bool):
306
+ return value
307
+ if isinstance(value, int) and value in (0, 1):
308
+ return bool(value)
309
+ return None
310
+
311
+
312
+ class CacheContext(BaseModel):
313
+ model_config = ConfigDict(extra="forbid")
314
+
315
+ system: Literal["redis", "memcached", "local", "other"] | None = None
316
+ operation: str | None = None
317
+ keyspace: str | None = None
318
+ key_hash: str | None = None
319
+ hit: bool | None = None
320
+ ttl_ms: int | None = None
321
+
322
+
323
+ class InfrastructureContext(BaseModel):
324
+ model_config = ConfigDict(extra="forbid")
325
+
326
+ component_type: Literal["queue", "worker", "scheduler", "filesystem", "network", "k8s"] | None = None
327
+ component_name: str | None = None
328
+ availability_zone: str | None = None
329
+ node_pool: str | None = None
330
+ restart_count: int | None = None
331
+ qos_class: str | None = None
332
+
333
+
334
+ class SecurityContext(BaseModel):
335
+ model_config = ConfigDict(extra="forbid")
336
+
337
+ authn_subject_hash: str | None = None
338
+ authn_method: str | None = None
339
+ authz_decision: Literal["allow", "deny", "challenge"] | None = None
340
+ policy_name: str | None = None
341
+ policy_version: str | None = None
342
+ mfa_present: bool | None = None
343
+ remote_ip_classification: Literal["private", "public", "tor", "proxy", "unknown"] | None = None
344
+ threat_signals: list[str] = Field(default_factory=list)
345
+
346
+
347
+ class BusinessContext(BaseModel):
348
+ model_config = ConfigDict(extra="forbid")
349
+
350
+ tenant_id: str | None = None
351
+ tenant_hash: str | None = None
352
+ tenant_tier: Literal["free", "pro", "enterprise", "internal"] | None = None
353
+ organization_id: str | None = None
354
+ workspace_id: str | None = None
355
+ account_id: str | None = None
356
+ business_operation: str | None = None
357
+ feature: str | None = None
358
+ plan: str | None = None
359
+ transaction_id: str | None = None
360
+ impact_score: int | None = None
361
+
362
+
363
+ class DependencyContext(BaseModel):
364
+ model_config = ConfigDict(extra="forbid")
365
+
366
+ dependency_type: Literal["http", "grpc", "db", "cache", "queue", "third_party"] | None = None
367
+ dependency_name: str | None = None
368
+ operation: str | None = None
369
+ target_namespace: str | None = None
370
+ remote_service_name: str | None = None
371
+ remote_region: str | None = None
372
+ upstream: bool | None = None
373
+ failure_domain: str | None = None
374
+
375
+
376
+ class RetryContext(BaseModel):
377
+ model_config = ConfigDict(extra="forbid")
378
+
379
+ attempt: int | None = None
380
+ max_attempts: int | None = None
381
+ policy: str | None = None
382
+ backoff_ms: int | None = None
383
+ jitter_ms: int | None = None
384
+ retriable: bool | None = None
385
+ exhausted: bool | None = None
386
+ storm_detected: bool = False
387
+
388
+
389
+ class PerformanceContext(BaseModel):
390
+ model_config = ConfigDict(extra="forbid")
391
+
392
+ duration_ms: float | None = None
393
+ queue_time_ms: float | None = None
394
+ db_time_ms: float | None = None
395
+ cpu_time_ms: float | None = None
396
+ memory_rss_bytes: int | None = None
397
+ payload_in_bytes: int | None = None
398
+ payload_out_bytes: int | None = None
399
+ slow_threshold_ms: float | None = None
400
+ sampled: bool | None = None
401
+
402
+
403
+ class TraceContext(BaseModel):
404
+ model_config = ConfigDict(extra="forbid")
405
+
406
+ trace_id: str
407
+ span_id: str | None = None
408
+ parent_span_id: str | None = None
409
+ trace_flags: str | None = None
410
+ tracestate: str | None = None
411
+
412
+
413
+ class TelemetryContext(BaseModel):
414
+ model_config = ConfigDict(extra="forbid")
415
+
416
+ trace: TraceContext | None = None
417
+ baggage: dict[str, str] = Field(default_factory=dict)
418
+ span_kind: SpanKind = SpanKind.INTERNAL
419
+ otel_status_code: Literal["OK", "ERROR", "UNSET"] | None = None
420
+ otel_status_message: str | None = None
421
+ telemetry_sdk_name: str | None = "opentelemetry"
422
+ telemetry_sdk_language: str | None = "python"
423
+ telemetry_sdk_version: str | None = None
424
+
425
+
426
+ class CorrelationContext(BaseModel):
427
+ model_config = ConfigDict(extra="forbid")
428
+
429
+ correlation_id: str
430
+ request_id: str | None = None
431
+ causation_id: str | None = None
432
+ parent_event_id: str | None = None
433
+ workflow_id: str | None = None
434
+ saga_id: str | None = None
435
+ session_id: str | None = None
436
+ tenant_id: str | None = None
437
+ user_id: str | None = None
438
+ operation_id: str | None = None
439
+ transaction_id: str | None = None
440
+ idempotency_key_hash: str | None = None
441
+ job_id: str | None = None
442
+
443
+
444
+ class CanonicalEvent(BaseModel):
445
+ model_config = ConfigDict(extra="forbid")
446
+
447
+ schema_version: Literal["1.0"] = "1.0"
448
+ event_id: str = Field(default_factory=lambda: str(uuid4()))
449
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
450
+ observed_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
451
+ severity: Severity = Severity.INFO
452
+ event_name: str
453
+ event_kind: EventKind = EventKind.APPLICATION
454
+ event_domain: str = "application"
455
+ operation: str | None = None
456
+ outcome: Outcome = Outcome.UNKNOWN
457
+ message: str | None = None
458
+ runtime: RuntimeContext = Field(default_factory=RuntimeContext)
459
+ correlation: CorrelationContext = Field(
460
+ default_factory=lambda: CorrelationContext(correlation_id=str(uuid4()))
461
+ )
462
+ telemetry: TelemetryContext = Field(default_factory=TelemetryContext)
463
+ request: RequestContext | None = None
464
+ response: ResponseContext | None = None
465
+ exception: ExceptionContext | None = None
466
+ database: DatabaseContext | None = None
467
+ cache: CacheContext | None = None
468
+ infrastructure: InfrastructureContext | None = None
469
+ security: SecurityContext | None = None
470
+ business: BusinessContext | None = None
471
+ dependency: DependencyContext | None = None
472
+ retry: RetryContext | None = None
473
+ performance: PerformanceContext | None = None
474
+ tags: dict[str, str] = Field(default_factory=dict)
475
+ attributes: dict[str, Scalar] = Field(default_factory=dict)
476
+ investigation_keys: list[str] = Field(default_factory=list)
477
+ ai_summary: str | None = None
478
+ ai_hints: list[str] = Field(default_factory=list)
479
+ runbook_refs: list[str] = Field(default_factory=list)
480
+
481
+ @property
482
+ def name(self) -> str:
483
+ return self.event_name
484
+
485
+ @property
486
+ def descriptor(self) -> EventDescriptor:
487
+ return EventDescriptor(
488
+ name=self.event_name,
489
+ category=self.event_domain,
490
+ action=self.operation,
491
+ outcome=self.outcome,
492
+ )
493
+
494
+ @property
495
+ def context(self) -> dict[str, Scalar]:
496
+ return self.attributes
497
+
498
+ @context.setter
499
+ def context(self, value: dict[str, Scalar]) -> None:
500
+ self.attributes = value
501
+
502
+ @computed_field # type: ignore[prop-decorator] # pydantic computed property
503
+ @property
504
+ def fingerprint(self) -> str:
505
+ if self.exception is not None and self.exception.deduplication is not None:
506
+ return self.exception.deduplication.fingerprint
507
+ parts = [
508
+ self.runtime.service_name,
509
+ self.event_name,
510
+ self.event_kind.value,
511
+ self.outcome.value,
512
+ self.request.route_template if self.request else None,
513
+ self.response.status_class if self.response else None,
514
+ self.exception.error_type if self.exception else None,
515
+ self.exception.type if self.exception else None,
516
+ self.database.system if self.database else None,
517
+ self.database.operation_name if self.database else None,
518
+ self.dependency.dependency_name if self.dependency else None,
519
+ ]
520
+ stable = "|".join(part for part in parts if part)
521
+ return sha1(stable.encode("utf-8"), usedforsecurity=False).hexdigest()
522
+
523
+ def loki_labels(self) -> dict[str, str]:
524
+ return {
525
+ "service": self.runtime.service_name,
526
+ "service_namespace": self.runtime.service_namespace or "default",
527
+ "environment": self.runtime.deployment_environment,
528
+ "level": self.severity.value,
529
+ "event_kind": self.event_kind.value,
530
+ "event_domain": self.event_domain,
531
+ "outcome": self.outcome.value,
532
+ "k8s_namespace": self.runtime.k8s_namespace_name or "none",
533
+ "k8s_cluster": self.runtime.k8s_cluster_name or "none",
534
+ "tenant_tier": (
535
+ self.business.tenant_tier
536
+ if self.business is not None and self.business.tenant_tier is not None
537
+ else "unknown"
538
+ ),
539
+ "runtime_name": self.runtime.runtime_name or "python",
540
+ }
541
+
542
+ @classmethod
543
+ def named(
544
+ cls,
545
+ *,
546
+ event_name: str,
547
+ severity: Severity = Severity.INFO,
548
+ message: str | None = None,
549
+ context: dict[str, Scalar] | None = None,
550
+ ) -> CanonicalEvent:
551
+ parts = event_name.split(".")
552
+ event_domain = parts[0] if parts else "application"
553
+ operation = parts[1] if len(parts) >= 2 else None
554
+ outcome = (
555
+ Outcome(parts[-1])
556
+ if parts and parts[-1] in Outcome._value2member_map_
557
+ else Outcome.UNKNOWN
558
+ )
559
+ event_kind = EventKind.API if event_domain in {"http", "api"} else EventKind.APPLICATION
560
+ return cls(
561
+ severity=severity,
562
+ message=message,
563
+ event_name=event_name,
564
+ event_kind=event_kind,
565
+ event_domain=event_domain,
566
+ operation=operation,
567
+ outcome=outcome,
568
+ attributes=context or {},
569
+ )
570
+
571
+
572
+ Event = CanonicalEvent
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from obsforge.core.errors import TaxonomyError
6
+
7
+ EVENT_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,5}$")
8
+
9
+
10
+ def validate_event_name(name: str) -> str:
11
+ if not EVENT_NAME_RE.fullmatch(name):
12
+ raise TaxonomyError(
13
+ "Event names must be lowercase dotted paths like 'auth.login.failed'."
14
+ )
15
+ return name
16
+
17
+
18
+ class TaxonomyPolicy:
19
+ """Validates semantic naming and reserved namespaces."""
20
+
21
+ def __init__(self, reserved_namespaces: set[str] | None = None) -> None:
22
+ self._reserved_namespaces = reserved_namespaces or set()
23
+
24
+ def validate(self, name: str) -> str:
25
+ validate_event_name(name)
26
+ namespace = name.split(".", 1)[0]
27
+ if self._reserved_namespaces and namespace not in self._reserved_namespaces:
28
+ raise TaxonomyError(f"Namespace '{namespace}' is not registered.")
29
+ return name
@@ -0,0 +1,3 @@
1
+ from obsforge.encoding.json import OrjsonEventEncoder
2
+
3
+ __all__ = ["OrjsonEventEncoder"]
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+ try:
8
+ import orjson
9
+ except ImportError: # pragma: no cover - fallback depends on local env
10
+ orjson = None # type: ignore[assignment]
11
+
12
+ from obsforge.core.models import Event
13
+ from obsforge.encoding.serializers import serialize_datetime
14
+ from obsforge.runtime.policies.loki_policy import LokiSerializer
15
+ from obsforge.transport.sink import SinkEnvelope
16
+
17
+
18
+ class OrjsonEventEncoder:
19
+ """Encodes a canonical event into a Loki-governed :class:`SinkEnvelope`."""
20
+
21
+ def __init__(self, serializer: LokiSerializer | None = None) -> None:
22
+ self._serializer = serializer or LokiSerializer()
23
+
24
+ def encode(self, event: Event) -> SinkEnvelope:
25
+ document = self._serializer.build_document(event)
26
+ payload = self._dumps(document)
27
+ return SinkEnvelope(
28
+ payload=payload,
29
+ labels=dict(document["labels"]),
30
+ structured_metadata=dict(document["structured_metadata"]),
31
+ )
32
+
33
+ def _dumps(self, payload: dict[str, Any]) -> bytes:
34
+ if orjson is not None:
35
+ return orjson.dumps(
36
+ payload,
37
+ default=self._default,
38
+ option=orjson.OPT_APPEND_NEWLINE | orjson.OPT_SORT_KEYS,
39
+ )
40
+ return (json.dumps(payload, default=self._default, sort_keys=True) + "\n").encode("utf-8")
41
+
42
+ @staticmethod
43
+ def _default(value: object) -> str:
44
+ if isinstance(value, datetime):
45
+ return serialize_datetime(value)
46
+ raise TypeError(f"Object of type {type(value)!r} is not JSON serializable")
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+
5
+
6
+ def serialize_datetime(value: datetime) -> str:
7
+ return value.isoformat()
@@ -0,0 +1 @@
1
+ """Instrumentation subsystems for framework and transport runtimes."""
@@ -0,0 +1,4 @@
1
+ from obsforge.instrumentation.db.engine import DBObservabilityEngine
2
+ from obsforge.instrumentation.db.state import get_default_db_engine, set_default_db_engine
3
+
4
+ __all__ = ["DBObservabilityEngine", "get_default_db_engine", "set_default_db_engine"]