clue-python-sdk-core 0.0.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.
- clue_python_sdk_core/__init__.py +135 -0
- clue_python_sdk_core/adapters.py +2177 -0
- clue_python_sdk_core/bootstrap.py +166 -0
- clue_python_sdk_core/celery.py +734 -0
- clue_python_sdk_core/client.py +335 -0
- clue_python_sdk_core/contracts.py +224 -0
- clue_python_sdk_core/event_size.py +96 -0
- clue_python_sdk_core/http_extraction.py +146 -0
- clue_python_sdk_core/orm.py +534 -0
- clue_python_sdk_core/otel_bridge.py +164 -0
- clue_python_sdk_core/parameter_snapshot.py +152 -0
- clue_python_sdk_core/privacy.py +124 -0
- clue_python_sdk_core/requests_instrumentation.py +341 -0
- clue_python_sdk_core/resources.py +44 -0
- clue_python_sdk_core/runtime.py +894 -0
- clue_python_sdk_core-0.0.1.dist-info/METADATA +11 -0
- clue_python_sdk_core-0.0.1.dist-info/RECORD +19 -0
- clue_python_sdk_core-0.0.1.dist-info/WHEEL +5 -0
- clue_python_sdk_core-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
|
|
6
|
+
from .adapters import (
|
|
7
|
+
build_summary_event,
|
|
8
|
+
build_outbound_request_failed_event,
|
|
9
|
+
build_outbound_request_finished_event,
|
|
10
|
+
build_outbound_request_started_event,
|
|
11
|
+
)
|
|
12
|
+
from .contracts import DEPENDENCY_SUMMARY_EVENT, SDK_COLLECTION_MODE_STANDARD
|
|
13
|
+
from .http_extraction import _safe_urlsplit, to_path_template
|
|
14
|
+
from .otel_bridge import annotate_current_otel_span, build_otel_request_span_id, resolve_current_otel_span_context
|
|
15
|
+
from .runtime import (
|
|
16
|
+
add_event,
|
|
17
|
+
create_request_span_id,
|
|
18
|
+
create_span_id,
|
|
19
|
+
get_current_client,
|
|
20
|
+
get_current_context,
|
|
21
|
+
load_settings,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_patched = False
|
|
25
|
+
_original_request = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_context_string(context: object, key: str) -> str | None:
|
|
29
|
+
if isinstance(context, dict):
|
|
30
|
+
value = context.get(key)
|
|
31
|
+
return value if isinstance(value, str) and value else None
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _with_propagation_headers(
|
|
36
|
+
headers: object,
|
|
37
|
+
*,
|
|
38
|
+
context: object,
|
|
39
|
+
span_id: str,
|
|
40
|
+
request_span_id: str,
|
|
41
|
+
) -> dict[str, str]:
|
|
42
|
+
propagated: dict[str, str] = {}
|
|
43
|
+
if isinstance(headers, dict):
|
|
44
|
+
propagated.update({str(key): str(value) for key, value in headers.items()})
|
|
45
|
+
|
|
46
|
+
def set_if_absent(name: str, value: str | None) -> None:
|
|
47
|
+
if not value:
|
|
48
|
+
return
|
|
49
|
+
if any(existing.lower() == name.lower() for existing in propagated):
|
|
50
|
+
return
|
|
51
|
+
propagated[name] = value
|
|
52
|
+
|
|
53
|
+
set_if_absent("x-clue-trace-id", _read_context_string(context, "trace_id"))
|
|
54
|
+
set_if_absent("x-clue-parent-span-id", span_id)
|
|
55
|
+
set_if_absent("x-clue-request-id", _read_context_string(context, "request_id"))
|
|
56
|
+
set_if_absent("x-clue-request-span-id", request_span_id)
|
|
57
|
+
set_if_absent("x-clue-interaction-id", _read_context_string(context, "interaction_id"))
|
|
58
|
+
set_if_absent("x-clue-session-id", _read_context_string(context, "session_id"))
|
|
59
|
+
set_if_absent("x-clue-anonymous-id", _read_context_string(context, "anonymous_id"))
|
|
60
|
+
set_if_absent("x-clue-user-id", _read_context_string(context, "user_id"))
|
|
61
|
+
set_if_absent("x-clue-account-id", _read_context_string(context, "account_id"))
|
|
62
|
+
set_if_absent("x-clue-tab-id", _read_context_string(context, "tab_id"))
|
|
63
|
+
return propagated
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _status_class(status_code: int) -> str:
|
|
67
|
+
if status_code >= 500:
|
|
68
|
+
return "5xx"
|
|
69
|
+
if status_code >= 400:
|
|
70
|
+
return "4xx"
|
|
71
|
+
if status_code >= 300:
|
|
72
|
+
return "3xx"
|
|
73
|
+
if status_code >= 200:
|
|
74
|
+
return "2xx"
|
|
75
|
+
return "unknown"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _duration_bucket(duration_ms: int) -> str:
|
|
79
|
+
if duration_ms < 100:
|
|
80
|
+
return "lt_100ms"
|
|
81
|
+
if duration_ms < 500:
|
|
82
|
+
return "100_499ms"
|
|
83
|
+
if duration_ms < 2_000:
|
|
84
|
+
return "500_1999ms"
|
|
85
|
+
if duration_ms < 5_000:
|
|
86
|
+
return "2000_4999ms"
|
|
87
|
+
return "gte_5000ms"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _dependency_context(
|
|
91
|
+
context: object,
|
|
92
|
+
*,
|
|
93
|
+
span_id: str,
|
|
94
|
+
parent_span_id: str | None,
|
|
95
|
+
request_span_id: str,
|
|
96
|
+
) -> dict[str, object]:
|
|
97
|
+
event_context = dict(context) if isinstance(context, dict) else {}
|
|
98
|
+
event_context["span_id"] = span_id
|
|
99
|
+
event_context["parent_span_id"] = parent_span_id
|
|
100
|
+
event_context["request_span_id"] = request_span_id
|
|
101
|
+
return event_context
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_dependency_summary_event(
|
|
105
|
+
*,
|
|
106
|
+
context: object,
|
|
107
|
+
method: str,
|
|
108
|
+
url: str,
|
|
109
|
+
status_code: int,
|
|
110
|
+
duration_ms: int,
|
|
111
|
+
span_id: str,
|
|
112
|
+
parent_span_id: str | None,
|
|
113
|
+
request_span_id: str,
|
|
114
|
+
denied_keys: tuple[str, ...],
|
|
115
|
+
):
|
|
116
|
+
scheme, host, path, _query = _safe_urlsplit(url)
|
|
117
|
+
path_template = to_path_template(path)
|
|
118
|
+
return build_summary_event(
|
|
119
|
+
context=_dependency_context(
|
|
120
|
+
context,
|
|
121
|
+
span_id=span_id,
|
|
122
|
+
parent_span_id=parent_span_id,
|
|
123
|
+
request_span_id=request_span_id,
|
|
124
|
+
),
|
|
125
|
+
event_name=DEPENDENCY_SUMMARY_EVENT,
|
|
126
|
+
collector_name="requests",
|
|
127
|
+
aggregation_kind="request",
|
|
128
|
+
summary_window_ms=0,
|
|
129
|
+
summary_count=1,
|
|
130
|
+
budget_window_ms=60_000,
|
|
131
|
+
rate_limit_key=f"dependency:{host or 'unknown'}:{_status_class(status_code)}",
|
|
132
|
+
properties={
|
|
133
|
+
"target_service": host or None,
|
|
134
|
+
"method": method.upper(),
|
|
135
|
+
"path_template": path_template,
|
|
136
|
+
"protocol": scheme or None,
|
|
137
|
+
"status_class": _status_class(status_code),
|
|
138
|
+
"duration_bucket": _duration_bucket(duration_ms),
|
|
139
|
+
},
|
|
140
|
+
metrics={
|
|
141
|
+
"status_code": status_code,
|
|
142
|
+
"duration_ms": duration_ms,
|
|
143
|
+
"normal_success_count": 1 if 200 <= status_code < 500 else 0,
|
|
144
|
+
},
|
|
145
|
+
denied_keys=denied_keys,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _record_summary_metric(client: object, count: int = 1) -> None:
|
|
150
|
+
record = getattr(client, "record_events_summarized", None)
|
|
151
|
+
if callable(record):
|
|
152
|
+
record(count)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def instrument_requests() -> bool:
|
|
156
|
+
global _patched
|
|
157
|
+
global _original_request
|
|
158
|
+
|
|
159
|
+
if _patched:
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
settings = load_settings()
|
|
163
|
+
if not settings.enabled or not settings.capture_outbound_requests:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
sessions_module = import_module("requests.sessions")
|
|
168
|
+
except ImportError:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
session_class = getattr(sessions_module, "Session", None)
|
|
172
|
+
if session_class is None:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
original_request = getattr(session_class, "request", None)
|
|
176
|
+
if not callable(original_request):
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
def wrapped_request(self, method, url, **kwargs):
|
|
180
|
+
context = get_current_context()
|
|
181
|
+
if context is None:
|
|
182
|
+
return original_request(self, method, url, **kwargs)
|
|
183
|
+
|
|
184
|
+
otel_span = resolve_current_otel_span_context("CLIENT")
|
|
185
|
+
parent_span_id = (
|
|
186
|
+
otel_span.get("parent_span_id") or None
|
|
187
|
+
if otel_span is not None
|
|
188
|
+
else (
|
|
189
|
+
context.get("span_id")
|
|
190
|
+
if isinstance(context.get("span_id"), str)
|
|
191
|
+
else None
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
span_id = (
|
|
195
|
+
otel_span["span_id"]
|
|
196
|
+
if otel_span is not None and otel_span.get("span_id")
|
|
197
|
+
else create_span_id()
|
|
198
|
+
)
|
|
199
|
+
request_span_id = (
|
|
200
|
+
build_otel_request_span_id(otel_span["trace_id"], otel_span["span_id"])
|
|
201
|
+
if otel_span is not None
|
|
202
|
+
else create_request_span_id()
|
|
203
|
+
)
|
|
204
|
+
annotate_current_otel_span(
|
|
205
|
+
{
|
|
206
|
+
"clue.request_span_id": request_span_id,
|
|
207
|
+
},
|
|
208
|
+
expected_kind="CLIENT",
|
|
209
|
+
)
|
|
210
|
+
request_headers = _with_propagation_headers(
|
|
211
|
+
kwargs.get("headers"),
|
|
212
|
+
context=context,
|
|
213
|
+
span_id=span_id,
|
|
214
|
+
request_span_id=request_span_id,
|
|
215
|
+
)
|
|
216
|
+
request_kwargs = {**kwargs, "headers": request_headers}
|
|
217
|
+
started_at = time.perf_counter()
|
|
218
|
+
if settings.sdk_collection_mode != SDK_COLLECTION_MODE_STANDARD:
|
|
219
|
+
add_event(
|
|
220
|
+
build_outbound_request_started_event(
|
|
221
|
+
context=context,
|
|
222
|
+
method=str(method),
|
|
223
|
+
url=str(url),
|
|
224
|
+
request_body=request_kwargs.get("json", request_kwargs.get("data")),
|
|
225
|
+
request_headers=request_headers,
|
|
226
|
+
timeout_ms=_timeout_ms(request_kwargs.get("timeout")),
|
|
227
|
+
allowed_value_paths=settings.allowed_value_paths,
|
|
228
|
+
denied_keys=settings.denied_keys,
|
|
229
|
+
span_id=span_id,
|
|
230
|
+
parent_span_id=parent_span_id,
|
|
231
|
+
request_span_id=request_span_id,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
try:
|
|
235
|
+
response = original_request(self, method, url, **request_kwargs)
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
add_event(
|
|
238
|
+
build_outbound_request_failed_event(
|
|
239
|
+
context=context,
|
|
240
|
+
method=str(method),
|
|
241
|
+
url=str(url),
|
|
242
|
+
failure_type=exc.__class__.__name__,
|
|
243
|
+
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
244
|
+
status_code=0,
|
|
245
|
+
request_body=request_kwargs.get("json", request_kwargs.get("data")),
|
|
246
|
+
request_headers=request_headers,
|
|
247
|
+
allowed_value_paths=settings.allowed_value_paths,
|
|
248
|
+
denied_keys=settings.denied_keys,
|
|
249
|
+
timeout_ms=_timeout_ms(request_kwargs.get("timeout")),
|
|
250
|
+
span_id=span_id,
|
|
251
|
+
parent_span_id=parent_span_id,
|
|
252
|
+
request_span_id=request_span_id,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
status_code = int(getattr(response, "status_code", 0) or 0)
|
|
258
|
+
duration_ms = int((time.perf_counter() - started_at) * 1000)
|
|
259
|
+
if (
|
|
260
|
+
settings.sdk_collection_mode == SDK_COLLECTION_MODE_STANDARD
|
|
261
|
+
and status_code < 500
|
|
262
|
+
and duration_ms < 2_000
|
|
263
|
+
):
|
|
264
|
+
summary_event = _build_dependency_summary_event(
|
|
265
|
+
context=context,
|
|
266
|
+
method=str(method),
|
|
267
|
+
url=str(url),
|
|
268
|
+
status_code=status_code,
|
|
269
|
+
duration_ms=duration_ms,
|
|
270
|
+
span_id=span_id,
|
|
271
|
+
parent_span_id=parent_span_id,
|
|
272
|
+
request_span_id=request_span_id,
|
|
273
|
+
denied_keys=settings.denied_keys,
|
|
274
|
+
)
|
|
275
|
+
if add_event(summary_event):
|
|
276
|
+
_record_summary_metric(get_current_client())
|
|
277
|
+
else:
|
|
278
|
+
add_event(
|
|
279
|
+
build_outbound_request_finished_event(
|
|
280
|
+
context=context,
|
|
281
|
+
method=str(method),
|
|
282
|
+
url=str(url),
|
|
283
|
+
status_code=status_code,
|
|
284
|
+
duration_ms=duration_ms,
|
|
285
|
+
request_body=request_kwargs.get("json", request_kwargs.get("data")),
|
|
286
|
+
request_headers=request_headers,
|
|
287
|
+
response_body=_response_body(response),
|
|
288
|
+
response_headers=getattr(response, "headers", None),
|
|
289
|
+
allowed_value_paths=settings.allowed_value_paths,
|
|
290
|
+
denied_keys=settings.denied_keys,
|
|
291
|
+
timeout_ms=_timeout_ms(request_kwargs.get("timeout")),
|
|
292
|
+
span_id=span_id,
|
|
293
|
+
parent_span_id=parent_span_id,
|
|
294
|
+
request_span_id=request_span_id,
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
return response
|
|
298
|
+
|
|
299
|
+
_original_request = original_request
|
|
300
|
+
session_class.request = wrapped_request
|
|
301
|
+
_patched = True
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def reset_requests_instrumentation() -> None:
|
|
306
|
+
global _patched
|
|
307
|
+
global _original_request
|
|
308
|
+
|
|
309
|
+
if not _patched or _original_request is None:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
sessions_module = import_module("requests.sessions")
|
|
314
|
+
except ImportError:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
session_class = getattr(sessions_module, "Session", None)
|
|
318
|
+
if session_class is None:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
session_class.request = _original_request
|
|
322
|
+
_original_request = None
|
|
323
|
+
_patched = False
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _timeout_ms(value: object) -> int | None:
|
|
327
|
+
if isinstance(value, (int, float)):
|
|
328
|
+
return int(float(value) * 1000)
|
|
329
|
+
if isinstance(value, tuple) and value:
|
|
330
|
+
first = value[0]
|
|
331
|
+
if isinstance(first, (int, float)):
|
|
332
|
+
return int(float(first) * 1000)
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _response_body(response: object) -> object:
|
|
337
|
+
if hasattr(response, "content"):
|
|
338
|
+
return getattr(response, "content")
|
|
339
|
+
if hasattr(response, "text"):
|
|
340
|
+
return getattr(response, "text")
|
|
341
|
+
return None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class CluePythonResourceConfig:
|
|
8
|
+
service_name: str
|
|
9
|
+
project_key: str
|
|
10
|
+
environment: str
|
|
11
|
+
deployment_environment: str
|
|
12
|
+
service_type: str
|
|
13
|
+
producer_id: str = "python-service"
|
|
14
|
+
service_version: str | None = None
|
|
15
|
+
service_namespace: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_resource_attributes(
|
|
19
|
+
config: CluePythonResourceConfig,
|
|
20
|
+
) -> dict[str, str]:
|
|
21
|
+
attributes = {
|
|
22
|
+
"service.name": config.service_name,
|
|
23
|
+
"clue.producer_id": config.producer_id,
|
|
24
|
+
"deployment.environment": config.deployment_environment,
|
|
25
|
+
"clue.project_key": config.project_key,
|
|
26
|
+
"clue.environment": config.environment,
|
|
27
|
+
"clue.runtime_language": "python",
|
|
28
|
+
"clue.service_type": config.service_type,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if config.service_version:
|
|
32
|
+
attributes["service.version"] = config.service_version
|
|
33
|
+
if config.service_namespace:
|
|
34
|
+
attributes["service.namespace"] = config.service_namespace
|
|
35
|
+
|
|
36
|
+
return attributes
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def serialize_resource_attributes(attributes: dict[str, str]) -> str:
|
|
40
|
+
return ",".join(
|
|
41
|
+
f"{key}={value}"
|
|
42
|
+
for key, value in sorted(attributes.items())
|
|
43
|
+
if value
|
|
44
|
+
)
|