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/core/models.py
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
"""Instrumentation subsystems for framework and transport runtimes."""
|