tigrcorn-observability 0.3.16.dev12__tar.gz → 0.3.17.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/PKG-INFO +3 -3
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/pyproject.toml +3 -3
- tigrcorn_observability-0.3.17.dev1/src/tigrcorn_observability/__init__.py +35 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability/logging.py +113 -3
- tigrcorn_observability-0.3.17.dev1/src/tigrcorn_observability/protocol.py +225 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability.egg-info/PKG-INFO +3 -3
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability.egg-info/SOURCES.txt +1 -0
- tigrcorn_observability-0.3.17.dev1/src/tigrcorn_observability.egg-info/requires.txt +2 -0
- tigrcorn_observability-0.3.16.dev12/src/tigrcorn_observability/__init__.py +0 -1
- tigrcorn_observability-0.3.16.dev12/src/tigrcorn_observability.egg-info/requires.txt +0 -2
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/LICENSE +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/README.md +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/setup.cfg +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability/events.py +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability/metrics.py +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability/py.typed +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability/tracing.py +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability.egg-info/dependency_links.txt +0 -0
- {tigrcorn_observability-0.3.16.dev12 → tigrcorn_observability-0.3.17.dev1}/src/tigrcorn_observability.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-observability
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.17.dev1
|
|
4
4
|
Summary: Logging, metrics, tracing, DoS warning events, and release-evidence metadata for the Tigrcorn Python web server.
|
|
5
5
|
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
6
|
License: Apache License
|
|
@@ -186,8 +186,8 @@ Classifier: Typing :: Typed
|
|
|
186
186
|
Requires-Python: <3.15,>=3.10
|
|
187
187
|
Description-Content-Type: text/markdown
|
|
188
188
|
License-File: LICENSE
|
|
189
|
-
Requires-Dist: tigrcorn-core==0.3.
|
|
190
|
-
Requires-Dist: tigrcorn-config==0.3.
|
|
189
|
+
Requires-Dist: tigrcorn-core==0.3.17.dev1
|
|
190
|
+
Requires-Dist: tigrcorn-config==0.3.17.dev1
|
|
191
191
|
Dynamic: license-file
|
|
192
192
|
|
|
193
193
|
<div align="center">
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tigrcorn-observability"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.17.dev1"
|
|
8
8
|
description = "Logging, metrics, tracing, DoS warning events, and release-evidence metadata for the Tigrcorn Python web server."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<3.15"
|
|
@@ -29,8 +29,8 @@ classifiers = [
|
|
|
29
29
|
"Typing :: Typed",
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
|
-
"tigrcorn-core==0.3.
|
|
33
|
-
"tigrcorn-config==0.3.
|
|
32
|
+
"tigrcorn-core==0.3.17.dev1",
|
|
33
|
+
"tigrcorn-config==0.3.17.dev1",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[tool.setuptools]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Logging, metrics, tracing helpers."""
|
|
2
|
+
|
|
3
|
+
from tigrcorn_observability.protocol import (
|
|
4
|
+
ObservabilityRecord,
|
|
5
|
+
ProtocolLabels,
|
|
6
|
+
ProtocolObservabilityError,
|
|
7
|
+
certification_evidence_links,
|
|
8
|
+
lifecycle_event,
|
|
9
|
+
make_observability_record,
|
|
10
|
+
protocol_label_schema,
|
|
11
|
+
protocol_transport_labels,
|
|
12
|
+
propagate_otel_context,
|
|
13
|
+
quic_operational_labels,
|
|
14
|
+
redact_and_bound_labels,
|
|
15
|
+
redact_mapping,
|
|
16
|
+
tls_alpn_labels,
|
|
17
|
+
validate_label_config,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ObservabilityRecord",
|
|
22
|
+
"ProtocolLabels",
|
|
23
|
+
"ProtocolObservabilityError",
|
|
24
|
+
"certification_evidence_links",
|
|
25
|
+
"lifecycle_event",
|
|
26
|
+
"make_observability_record",
|
|
27
|
+
"protocol_label_schema",
|
|
28
|
+
"protocol_transport_labels",
|
|
29
|
+
"propagate_otel_context",
|
|
30
|
+
"quic_operational_labels",
|
|
31
|
+
"redact_and_bound_labels",
|
|
32
|
+
"redact_mapping",
|
|
33
|
+
"tls_alpn_labels",
|
|
34
|
+
"validate_label_config",
|
|
35
|
+
]
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any, Mapping
|
|
11
11
|
|
|
12
12
|
from tigrcorn_config.files import ConfigFileError, load_config_source
|
|
13
|
+
from tigrcorn_observability.protocol import make_observability_record
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class LoggingConfigError(RuntimeError):
|
|
@@ -164,23 +165,132 @@ class AccessLogger:
|
|
|
164
165
|
self.logger = logger
|
|
165
166
|
self.enabled = enabled
|
|
166
167
|
self.fmt = fmt or '{peer} "{method} {path} {proto}" {status}'
|
|
168
|
+
self.observability_records: list[dict[str, Any]] = []
|
|
167
169
|
|
|
168
170
|
def _peer(self, client: tuple[str, int] | None) -> str:
|
|
169
171
|
return f"{client[0]}:{client[1]}" if client else '-'
|
|
170
172
|
|
|
171
|
-
def log_http(
|
|
173
|
+
def log_http(
|
|
174
|
+
self,
|
|
175
|
+
client: tuple[str, int] | None,
|
|
176
|
+
method: str,
|
|
177
|
+
path: str,
|
|
178
|
+
status: int,
|
|
179
|
+
proto: str,
|
|
180
|
+
*,
|
|
181
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...] | None = None,
|
|
182
|
+
tls_version: str | None = None,
|
|
183
|
+
alpn: str | None = None,
|
|
184
|
+
transport: str | None = None,
|
|
185
|
+
) -> None:
|
|
172
186
|
if not self.enabled:
|
|
173
187
|
return
|
|
174
188
|
peer = self._peer(client)
|
|
175
189
|
message = self.fmt.format(peer=peer, method=method, path=path, status=status, proto=proto)
|
|
176
|
-
|
|
190
|
+
observability = self._http_observability_record(
|
|
191
|
+
peer=peer,
|
|
192
|
+
method=method,
|
|
193
|
+
path=path,
|
|
194
|
+
status=status,
|
|
195
|
+
proto=proto,
|
|
196
|
+
request_headers=request_headers,
|
|
197
|
+
tls_version=tls_version,
|
|
198
|
+
alpn=alpn,
|
|
199
|
+
transport=transport,
|
|
200
|
+
)
|
|
201
|
+
self.observability_records.append(observability)
|
|
202
|
+
self.logger.info(
|
|
203
|
+
message,
|
|
204
|
+
extra={
|
|
205
|
+
'event': 'access.http',
|
|
206
|
+
'peer': peer,
|
|
207
|
+
'method': method,
|
|
208
|
+
'path': path,
|
|
209
|
+
'status': status,
|
|
210
|
+
'proto': proto,
|
|
211
|
+
'observability': observability,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
177
214
|
|
|
178
215
|
def log_ws(self, client: tuple[str, int] | None, path: str, result: str) -> None:
|
|
179
216
|
if not self.enabled:
|
|
180
217
|
return
|
|
181
218
|
peer = self._peer(client)
|
|
182
219
|
message = f'{peer} "WEBSOCKET {path}" {result}'
|
|
183
|
-
|
|
220
|
+
observability = make_observability_record(
|
|
221
|
+
kind='event',
|
|
222
|
+
name=f'websocket.{result}',
|
|
223
|
+
labels={
|
|
224
|
+
'protocol': 'websocket',
|
|
225
|
+
'transport': 'tcp',
|
|
226
|
+
'lifecycle': result,
|
|
227
|
+
'peer': peer,
|
|
228
|
+
'path': path,
|
|
229
|
+
},
|
|
230
|
+
attributes={'result': result},
|
|
231
|
+
)
|
|
232
|
+
self.observability_records.append(observability)
|
|
233
|
+
self.logger.info(
|
|
234
|
+
message,
|
|
235
|
+
extra={
|
|
236
|
+
'event': 'access.websocket',
|
|
237
|
+
'peer': peer,
|
|
238
|
+
'path': path,
|
|
239
|
+
'result': result,
|
|
240
|
+
'observability': observability,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _http_observability_record(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
peer: str,
|
|
248
|
+
method: str,
|
|
249
|
+
path: str,
|
|
250
|
+
status: int,
|
|
251
|
+
proto: str,
|
|
252
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...] | None,
|
|
253
|
+
tls_version: str | None,
|
|
254
|
+
alpn: str | None,
|
|
255
|
+
transport: str | None,
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
protocol = _protocol_from_http_proto(proto)
|
|
258
|
+
labels: dict[str, Any] = {
|
|
259
|
+
'protocol': protocol,
|
|
260
|
+
'transport': transport or ('quic' if protocol == 'http3' else 'tcp'),
|
|
261
|
+
'peer': peer,
|
|
262
|
+
'path': path,
|
|
263
|
+
}
|
|
264
|
+
if tls_version is not None:
|
|
265
|
+
labels['tls_version'] = tls_version
|
|
266
|
+
if alpn is not None:
|
|
267
|
+
labels['alpn'] = alpn
|
|
268
|
+
return make_observability_record(
|
|
269
|
+
kind='event',
|
|
270
|
+
name='http.request',
|
|
271
|
+
labels=labels,
|
|
272
|
+
attributes={
|
|
273
|
+
'headers': _headers_to_mapping(request_headers or ()),
|
|
274
|
+
'method': method,
|
|
275
|
+
'status': int(status),
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _protocol_from_http_proto(proto: str) -> str:
|
|
281
|
+
normalized = str(proto).lower()
|
|
282
|
+
if normalized.endswith('/3'):
|
|
283
|
+
return 'http3'
|
|
284
|
+
if normalized.endswith('/2'):
|
|
285
|
+
return 'http2'
|
|
286
|
+
return 'http1'
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _headers_to_mapping(headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...]) -> dict[str, str]:
|
|
290
|
+
result: dict[str, str] = {}
|
|
291
|
+
for name, value in headers:
|
|
292
|
+
result[name.decode('latin1').lower()] = value.decode('latin1', 'replace')
|
|
293
|
+
return result
|
|
184
294
|
|
|
185
295
|
|
|
186
296
|
def _coerce_level(level: str) -> int:
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProtocolObservabilityError(ValueError):
|
|
10
|
+
"""Raised when protocol-aware observability would emit unsafe telemetry."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PROTOCOL_LABEL_SCHEMA: tuple[tuple[str, str], ...] = (
|
|
14
|
+
("protocol", "low-cardinality protocol name"),
|
|
15
|
+
("transport", "low-cardinality transport domain"),
|
|
16
|
+
("tls_version", "safe TLS protocol version"),
|
|
17
|
+
("alpn", "safe negotiated ALPN token"),
|
|
18
|
+
("profile", "deployment profile"),
|
|
19
|
+
("listener_id", "bounded listener identifier"),
|
|
20
|
+
("lifecycle", "protocol lifecycle event"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_ALLOWED_LABELS = {name for name, _description in PROTOCOL_LABEL_SCHEMA} | {
|
|
24
|
+
"event_kind",
|
|
25
|
+
"metric",
|
|
26
|
+
"quic_loss_packets",
|
|
27
|
+
"quic_migration",
|
|
28
|
+
"quic_rtt_ms",
|
|
29
|
+
"span_id",
|
|
30
|
+
"trace_id",
|
|
31
|
+
}
|
|
32
|
+
_SENSITIVE_PATTERN = re.compile(r"(authorization|certificate|cookie|key|password|private|qlog|secret|token)", re.I)
|
|
33
|
+
_HIGH_CARDINALITY = {"client_ip", "connection_id", "path", "peer", "peer_id", "qlog_connection_id", "session_id"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class ProtocolLabels:
|
|
38
|
+
protocol: str
|
|
39
|
+
transport: str
|
|
40
|
+
tls_version: str | None = None
|
|
41
|
+
alpn: str | None = None
|
|
42
|
+
profile: str | None = None
|
|
43
|
+
listener_id: str | None = None
|
|
44
|
+
lifecycle: str | None = None
|
|
45
|
+
|
|
46
|
+
def as_dict(self) -> dict[str, str | None]:
|
|
47
|
+
return {
|
|
48
|
+
"alpn": self.alpn,
|
|
49
|
+
"lifecycle": self.lifecycle,
|
|
50
|
+
"listener_id": self.listener_id,
|
|
51
|
+
"profile": self.profile,
|
|
52
|
+
"protocol": self.protocol,
|
|
53
|
+
"tls_version": self.tls_version,
|
|
54
|
+
"transport": self.transport,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class ObservabilityRecord:
|
|
60
|
+
kind: str
|
|
61
|
+
name: str
|
|
62
|
+
labels: dict[str, Any]
|
|
63
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
def as_dict(self) -> dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"attributes": _stable_mapping(self.attributes),
|
|
68
|
+
"kind": self.kind,
|
|
69
|
+
"labels": _stable_mapping(self.labels),
|
|
70
|
+
"name": self.name,
|
|
71
|
+
"schema_version": 1,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def protocol_label_schema() -> dict[str, Any]:
|
|
76
|
+
return {
|
|
77
|
+
"cardinality": {
|
|
78
|
+
"bounded": tuple(sorted(_ALLOWED_LABELS)),
|
|
79
|
+
"hashed": tuple(sorted(_HIGH_CARDINALITY)),
|
|
80
|
+
},
|
|
81
|
+
"labels": tuple({"name": name, "description": description} for name, description in PROTOCOL_LABEL_SCHEMA),
|
|
82
|
+
"redaction": tuple(sorted(("authorization", "certificate", "cookie", "key", "password", "private", "qlog", "secret", "token"))),
|
|
83
|
+
"schema_version": 1,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def make_observability_record(
|
|
88
|
+
*,
|
|
89
|
+
kind: str,
|
|
90
|
+
name: str,
|
|
91
|
+
labels: Mapping[str, Any],
|
|
92
|
+
attributes: Mapping[str, Any] | None = None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
validated = validate_label_config(labels)
|
|
95
|
+
return ObservabilityRecord(
|
|
96
|
+
kind=kind,
|
|
97
|
+
name=name,
|
|
98
|
+
labels=redact_and_bound_labels(validated),
|
|
99
|
+
attributes=redact_mapping(attributes or {}),
|
|
100
|
+
).as_dict()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def protocol_transport_labels(*, protocol: str, transport: str, profile: str = "default") -> dict[str, Any]:
|
|
104
|
+
return ProtocolLabels(protocol=protocol, transport=transport, profile=profile).as_dict()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def tls_alpn_labels(*, protocol: str, transport: str, tls_version: str, alpn: str) -> dict[str, Any]:
|
|
108
|
+
return ProtocolLabels(
|
|
109
|
+
protocol=protocol,
|
|
110
|
+
transport=transport,
|
|
111
|
+
tls_version=tls_version,
|
|
112
|
+
alpn=alpn,
|
|
113
|
+
).as_dict()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def lifecycle_event(*, protocol: str, transport: str, lifecycle: str, session_id: str | None = None) -> dict[str, Any]:
|
|
117
|
+
labels = ProtocolLabels(protocol=protocol, transport=transport, lifecycle=lifecycle).as_dict()
|
|
118
|
+
if session_id is not None:
|
|
119
|
+
labels["session_id"] = session_id
|
|
120
|
+
return make_observability_record(kind="event", name=f"{protocol}.{lifecycle}", labels=labels)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def propagate_otel_context(context: Mapping[str, str], labels: Mapping[str, Any]) -> dict[str, Any]:
|
|
124
|
+
trace_id = context.get("traceparent", "").split("-")[1] if context.get("traceparent", "").count("-") >= 3 else None
|
|
125
|
+
span_id = context.get("traceparent", "").split("-")[2] if context.get("traceparent", "").count("-") >= 3 else None
|
|
126
|
+
merged = dict(labels)
|
|
127
|
+
if trace_id:
|
|
128
|
+
merged["trace_id"] = trace_id
|
|
129
|
+
if span_id:
|
|
130
|
+
merged["span_id"] = span_id
|
|
131
|
+
return make_observability_record(kind="trace", name="otel.context", labels=merged)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def quic_operational_labels(*, rtt_ms: float, loss_packets: int, migration: str, qlog_secret: str | None = None) -> dict[str, Any]:
|
|
135
|
+
labels: dict[str, Any] = {
|
|
136
|
+
"protocol": "quic",
|
|
137
|
+
"transport": "quic",
|
|
138
|
+
"quic_loss_packets": max(0, int(loss_packets)),
|
|
139
|
+
"quic_migration": migration,
|
|
140
|
+
"quic_rtt_ms": round(float(rtt_ms), 3),
|
|
141
|
+
}
|
|
142
|
+
attributes: dict[str, Any] = {}
|
|
143
|
+
if qlog_secret is not None:
|
|
144
|
+
attributes["qlog_secret"] = qlog_secret
|
|
145
|
+
return make_observability_record(kind="metric", name="quic.operational", labels=labels, attributes=attributes)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def redact_mapping(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
149
|
+
result: dict[str, Any] = {}
|
|
150
|
+
for key in sorted(payload):
|
|
151
|
+
value = payload[key]
|
|
152
|
+
if _SENSITIVE_PATTERN.search(str(key)):
|
|
153
|
+
result[key] = "[redacted]"
|
|
154
|
+
elif isinstance(value, Mapping):
|
|
155
|
+
result[key] = redact_mapping(value)
|
|
156
|
+
else:
|
|
157
|
+
result[key] = value
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def redact_and_bound_labels(labels: Mapping[str, Any]) -> dict[str, Any]:
|
|
162
|
+
result: dict[str, Any] = {}
|
|
163
|
+
for key in sorted(labels):
|
|
164
|
+
value = labels[key]
|
|
165
|
+
if _SENSITIVE_PATTERN.search(str(key)):
|
|
166
|
+
result[key] = "[redacted]"
|
|
167
|
+
elif key in _HIGH_CARDINALITY:
|
|
168
|
+
result[f"{key}_hash"] = _hash_label(value)
|
|
169
|
+
else:
|
|
170
|
+
result[key] = value
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def validate_label_config(labels: Mapping[str, Any]) -> dict[str, Any]:
|
|
175
|
+
for key, value in labels.items():
|
|
176
|
+
if _SENSITIVE_PATTERN.search(str(key)):
|
|
177
|
+
continue
|
|
178
|
+
if key in _HIGH_CARDINALITY:
|
|
179
|
+
continue
|
|
180
|
+
if key not in _ALLOWED_LABELS:
|
|
181
|
+
raise ProtocolObservabilityError(f"unsafe observability label: {key}")
|
|
182
|
+
if isinstance(value, str) and len(value) > 128:
|
|
183
|
+
raise ProtocolObservabilityError(f"unsafe observability label value: {key}")
|
|
184
|
+
return dict(labels)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def certification_evidence_links(
|
|
188
|
+
*,
|
|
189
|
+
evidence_id: str,
|
|
190
|
+
protocol_claim_ids: tuple[str, ...],
|
|
191
|
+
transport_claim_ids: tuple[str, ...],
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
if not protocol_claim_ids or not transport_claim_ids:
|
|
194
|
+
raise ProtocolObservabilityError("observability evidence requires protocol and transport claim links")
|
|
195
|
+
return {
|
|
196
|
+
"evidence_id": evidence_id,
|
|
197
|
+
"protocol_claim_ids": tuple(sorted(protocol_claim_ids)),
|
|
198
|
+
"transport_claim_ids": tuple(sorted(transport_claim_ids)),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _hash_label(value: Any) -> str:
|
|
203
|
+
return hashlib.sha256(str(value).encode("utf-8")).hexdigest()[:16]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _stable_mapping(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
207
|
+
return {key: payload[key] for key in sorted(payload)}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
__all__ = [
|
|
211
|
+
"ProtocolLabels",
|
|
212
|
+
"ProtocolObservabilityError",
|
|
213
|
+
"ObservabilityRecord",
|
|
214
|
+
"certification_evidence_links",
|
|
215
|
+
"lifecycle_event",
|
|
216
|
+
"make_observability_record",
|
|
217
|
+
"protocol_label_schema",
|
|
218
|
+
"protocol_transport_labels",
|
|
219
|
+
"propagate_otel_context",
|
|
220
|
+
"quic_operational_labels",
|
|
221
|
+
"redact_and_bound_labels",
|
|
222
|
+
"redact_mapping",
|
|
223
|
+
"tls_alpn_labels",
|
|
224
|
+
"validate_label_config",
|
|
225
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-observability
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.17.dev1
|
|
4
4
|
Summary: Logging, metrics, tracing, DoS warning events, and release-evidence metadata for the Tigrcorn Python web server.
|
|
5
5
|
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
6
|
License: Apache License
|
|
@@ -186,8 +186,8 @@ Classifier: Typing :: Typed
|
|
|
186
186
|
Requires-Python: <3.15,>=3.10
|
|
187
187
|
Description-Content-Type: text/markdown
|
|
188
188
|
License-File: LICENSE
|
|
189
|
-
Requires-Dist: tigrcorn-core==0.3.
|
|
190
|
-
Requires-Dist: tigrcorn-config==0.3.
|
|
189
|
+
Requires-Dist: tigrcorn-core==0.3.17.dev1
|
|
190
|
+
Requires-Dist: tigrcorn-config==0.3.17.dev1
|
|
191
191
|
Dynamic: license-file
|
|
192
192
|
|
|
193
193
|
<div align="center">
|
|
@@ -5,6 +5,7 @@ src/tigrcorn_observability/__init__.py
|
|
|
5
5
|
src/tigrcorn_observability/events.py
|
|
6
6
|
src/tigrcorn_observability/logging.py
|
|
7
7
|
src/tigrcorn_observability/metrics.py
|
|
8
|
+
src/tigrcorn_observability/protocol.py
|
|
8
9
|
src/tigrcorn_observability/py.typed
|
|
9
10
|
src/tigrcorn_observability/tracing.py
|
|
10
11
|
src/tigrcorn_observability.egg-info/PKG-INFO
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Logging, metrics, tracing helpers."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|