debugbundle-python 0.1.0__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.
- debugbundle_python-0.1.0/LICENSE +17 -0
- debugbundle_python-0.1.0/PKG-INFO +66 -0
- debugbundle_python-0.1.0/README.md +27 -0
- debugbundle_python-0.1.0/pyproject.toml +82 -0
- debugbundle_python-0.1.0/setup.cfg +4 -0
- debugbundle_python-0.1.0/src/debugbundle/__init__.py +165 -0
- debugbundle_python-0.1.0/src/debugbundle/config.py +192 -0
- debugbundle_python-0.1.0/src/debugbundle/core.py +811 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/__init__.py +16 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/common.py +77 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/django.py +53 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/fastapi.py +94 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/flask.py +67 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/relay_django.py +50 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/relay_fastapi.py +46 -0
- debugbundle_python-0.1.0/src/debugbundle/integrations/relay_flask.py +46 -0
- debugbundle_python-0.1.0/src/debugbundle/logger_integrations.py +154 -0
- debugbundle_python-0.1.0/src/debugbundle/py.typed +0 -0
- debugbundle_python-0.1.0/src/debugbundle/redaction.py +29 -0
- debugbundle_python-0.1.0/src/debugbundle/relay.py +261 -0
- debugbundle_python-0.1.0/src/debugbundle/suppression.py +124 -0
- debugbundle_python-0.1.0/src/debugbundle/transport.py +59 -0
- debugbundle_python-0.1.0/src/debugbundle/trigger_token.py +143 -0
- debugbundle_python-0.1.0/src/debugbundle_python.egg-info/PKG-INFO +66 -0
- debugbundle_python-0.1.0/src/debugbundle_python.egg-info/SOURCES.txt +36 -0
- debugbundle_python-0.1.0/src/debugbundle_python.egg-info/dependency_links.txt +1 -0
- debugbundle_python-0.1.0/src/debugbundle_python.egg-info/requires.txt +15 -0
- debugbundle_python-0.1.0/src/debugbundle_python.egg-info/top_level.txt +1 -0
- debugbundle_python-0.1.0/tests/test_contracts.py +169 -0
- debugbundle_python-0.1.0/tests/test_framework_integrations.py +279 -0
- debugbundle_python-0.1.0/tests/test_hooks.py +306 -0
- debugbundle_python-0.1.0/tests/test_http_integration.py +165 -0
- debugbundle_python-0.1.0/tests/test_relay.py +195 -0
- debugbundle_python-0.1.0/tests/test_remote_config.py +226 -0
- debugbundle_python-0.1.0/tests/test_repository_metadata.py +48 -0
- debugbundle_python-0.1.0/tests/test_schema_validation.py +100 -0
- debugbundle_python-0.1.0/tests/test_sdk.py +364 -0
- debugbundle_python-0.1.0/tests/test_trigger_token.py +174 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2024-present DebugBundle
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as
|
|
8
|
+
published by the Free Software Foundation, either version 3 of the
|
|
9
|
+
License, or (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debugbundle-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DebugBundle SDK for Python
|
|
5
|
+
Author: DebugBundle
|
|
6
|
+
License-Expression: AGPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://debugbundle.com/docs/sdk-python
|
|
8
|
+
Project-URL: Repository, https://github.com/debugbundle/debugbundle-python
|
|
9
|
+
Project-URL: Issues, https://github.com/debugbundle/debugbundle-python/issues
|
|
10
|
+
Keywords: debugbundle,debugging,ai-agent,error-tracking
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Framework :: Flask
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: httpx<0.29,>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build<2,>=1; extra == "dev"
|
|
27
|
+
Requires-Dist: django<6,>=5; extra == "dev"
|
|
28
|
+
Requires-Dist: fastapi<1,>=0.115; extra == "dev"
|
|
29
|
+
Requires-Dist: flask<4,>=3; extra == "dev"
|
|
30
|
+
Requires-Dist: jsonschema<5,>=4.23; extra == "dev"
|
|
31
|
+
Requires-Dist: loguru<1,>=0.7; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy<2,>=1.15; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest<9,>=8.3; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov<7,>=5; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff<0.12,>=0.11; extra == "dev"
|
|
36
|
+
Requires-Dist: structlog<26,>=24; extra == "dev"
|
|
37
|
+
Requires-Dist: twine<7,>=5; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# debugbundle-python
|
|
41
|
+
|
|
42
|
+
DebugBundle SDK for Python.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install debugbundle-python
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import debugbundle
|
|
54
|
+
|
|
55
|
+
debugbundle.init(project_token="dbundle_proj_test", service="checkout-api")
|
|
56
|
+
debugbundle.capture_exception(RuntimeError("boom"))
|
|
57
|
+
debugbundle.flush()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Status
|
|
61
|
+
|
|
62
|
+
This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
|
|
63
|
+
|
|
64
|
+
## Docs
|
|
65
|
+
|
|
66
|
+
https://debugbundle.com/docs/sdk-python
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# debugbundle-python
|
|
2
|
+
|
|
3
|
+
DebugBundle SDK for Python.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install debugbundle-python
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import debugbundle
|
|
15
|
+
|
|
16
|
+
debugbundle.init(project_token="dbundle_proj_test", service="checkout-api")
|
|
17
|
+
debugbundle.capture_exception(RuntimeError("boom"))
|
|
18
|
+
debugbundle.flush()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Status
|
|
22
|
+
|
|
23
|
+
This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
|
|
24
|
+
|
|
25
|
+
## Docs
|
|
26
|
+
|
|
27
|
+
https://debugbundle.com/docs/sdk-python
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "debugbundle-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "DebugBundle SDK for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "AGPL-3.0-only"
|
|
12
|
+
authors = [{ name = "DebugBundle" }]
|
|
13
|
+
keywords = ["debugbundle", "debugging", "ai-agent", "error-tracking"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: Django",
|
|
17
|
+
"Framework :: FastAPI",
|
|
18
|
+
"Framework :: Flask",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Typing :: Typed"
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27,<0.29"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"build>=1,<2",
|
|
33
|
+
"django>=5,<6",
|
|
34
|
+
"fastapi>=0.115,<1",
|
|
35
|
+
"flask>=3,<4",
|
|
36
|
+
"jsonschema>=4.23,<5",
|
|
37
|
+
"loguru>=0.7,<1",
|
|
38
|
+
"mypy>=1.15,<2",
|
|
39
|
+
"pytest>=8.3,<9",
|
|
40
|
+
"pytest-cov>=5,<7",
|
|
41
|
+
"ruff>=0.11,<0.12",
|
|
42
|
+
"structlog>=24,<26",
|
|
43
|
+
"twine>=5,<7"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://debugbundle.com/docs/sdk-python"
|
|
48
|
+
Repository = "https://github.com/debugbundle/debugbundle-python"
|
|
49
|
+
Issues = "https://github.com/debugbundle/debugbundle-python/issues"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
package-dir = {"" = "src"}
|
|
53
|
+
include-package-data = true
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
pythonpath = ["src"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 120
|
|
64
|
+
target-version = "py310"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "I"]
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
python_version = "3.10"
|
|
71
|
+
files = ["src"]
|
|
72
|
+
check_untyped_defs = true
|
|
73
|
+
no_implicit_optional = true
|
|
74
|
+
warn_unused_ignores = true
|
|
75
|
+
warn_redundant_casts = true
|
|
76
|
+
pretty = true
|
|
77
|
+
|
|
78
|
+
[tool.coverage.run]
|
|
79
|
+
source = ["src/debugbundle"]
|
|
80
|
+
|
|
81
|
+
[tool.coverage.report]
|
|
82
|
+
show_missing = true
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""DebugBundle Python SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from contextvars import Token
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .config import RemoteProbeDirective
|
|
11
|
+
from .core import ConfigFetchResponse, DebugBundleSdk
|
|
12
|
+
from .relay import BrowserRelayAcceptedBatch, BrowserRelayHandler, BrowserRelayResponse
|
|
13
|
+
|
|
14
|
+
_sdk = DebugBundleSdk()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def init(
|
|
18
|
+
project_token: str,
|
|
19
|
+
environment: str | None = None,
|
|
20
|
+
service: str | None = None,
|
|
21
|
+
enabled: bool = True,
|
|
22
|
+
redact_fields: list[str] | None = None,
|
|
23
|
+
sample_rate: float = 1.0,
|
|
24
|
+
batch_size: int = 25,
|
|
25
|
+
flush_interval: float = 5.0,
|
|
26
|
+
endpoint: str = "https://api.debugbundle.com/v1/events",
|
|
27
|
+
log_level: str = "warning",
|
|
28
|
+
max_probe_labels: int = 50,
|
|
29
|
+
max_probe_entries_per_label: int = 10,
|
|
30
|
+
probe_flush_on_error: bool = True,
|
|
31
|
+
fetch_impl: Callable[[str, dict[str, object]], ConfigFetchResponse] | None = None,
|
|
32
|
+
on_diagnostic: Callable[[dict[str, object]], None] | None = None,
|
|
33
|
+
probes_poll_interval: int = 60_000,
|
|
34
|
+
) -> None:
|
|
35
|
+
_sdk.init(
|
|
36
|
+
project_token=project_token,
|
|
37
|
+
environment=environment,
|
|
38
|
+
service=service,
|
|
39
|
+
enabled=enabled,
|
|
40
|
+
redact_fields=redact_fields,
|
|
41
|
+
sample_rate=sample_rate,
|
|
42
|
+
batch_size=batch_size,
|
|
43
|
+
flush_interval=flush_interval,
|
|
44
|
+
endpoint=endpoint,
|
|
45
|
+
log_level=log_level,
|
|
46
|
+
max_probe_labels=max_probe_labels,
|
|
47
|
+
max_probe_entries_per_label=max_probe_entries_per_label,
|
|
48
|
+
probe_flush_on_error=probe_flush_on_error,
|
|
49
|
+
fetch_impl=fetch_impl,
|
|
50
|
+
on_diagnostic=on_diagnostic,
|
|
51
|
+
probes_poll_interval=probes_poll_interval,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def capture_exception(error: BaseException, context: dict[str, object] | None = None) -> None:
|
|
56
|
+
_sdk.capture_exception(error, context=context)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def capture_error(error: BaseException, context: dict[str, object] | None = None) -> None:
|
|
60
|
+
_sdk.capture_error(error, context=context)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def capture_log(message: str, level: str = "warning", context: dict[str, object] | None = None) -> None:
|
|
64
|
+
_sdk.capture_log(message, level=level, context=context)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def capture_request(
|
|
68
|
+
request: Mapping[str, object],
|
|
69
|
+
response: Mapping[str, object] | None = None,
|
|
70
|
+
context: Mapping[str, object] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
_sdk.capture_request(request, response=response, context=context)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def capture_message(message: str, level: str | None = None, context: dict[str, object] | None = None) -> None:
|
|
76
|
+
_sdk.capture_message(message, level=level, context=context)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_context(key: str, value: object) -> None:
|
|
80
|
+
_sdk.set_context(key, value)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def flush() -> None:
|
|
84
|
+
_sdk.flush()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_status() -> str:
|
|
88
|
+
return _sdk.status
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_last_event_at() -> float | None:
|
|
92
|
+
return _sdk.last_event_at
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def probe(label: str, data: object | Callable[[], object], opts: Mapping[str, object] | None = None) -> None:
|
|
96
|
+
_sdk.probe(label, data, opts=opts)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def capture_exceptions() -> None:
|
|
100
|
+
_sdk.capture_exceptions()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def capture_logging(logger: logging.Logger | None = None) -> None:
|
|
104
|
+
_sdk.capture_logging(logger=logger)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def capture_async(loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
108
|
+
_sdk.capture_async(loop=loop)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def begin_request(request: dict) -> object:
|
|
112
|
+
return _sdk.begin_request(request)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def end_request(token: Token[list[RemoteProbeDirective] | None]) -> None:
|
|
116
|
+
_sdk.end_request(token)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_OPTIONAL_EXPORTS = {
|
|
120
|
+
"DebugBundleDjangoMiddleware": (".integrations.django", "DebugBundleDjangoMiddleware"),
|
|
121
|
+
"DebugBundleFastAPIMiddleware": (".integrations.fastapi", "DebugBundleFastAPIMiddleware"),
|
|
122
|
+
"create_django_relay_view": (".integrations.relay_django", "create_django_relay_view"),
|
|
123
|
+
"create_fastapi_relay_handler": (".integrations.relay_fastapi", "create_fastapi_relay_handler"),
|
|
124
|
+
"create_flask_relay_handler": (".integrations.relay_flask", "create_flask_relay_handler"),
|
|
125
|
+
"instrument_fastapi": (".integrations.fastapi", "instrument_fastapi"),
|
|
126
|
+
"instrument_flask": (".integrations.flask", "instrument_flask"),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def __getattr__(name: str) -> Any:
|
|
131
|
+
if name not in _OPTIONAL_EXPORTS:
|
|
132
|
+
raise AttributeError(f"module 'debugbundle' has no attribute {name!r}")
|
|
133
|
+
|
|
134
|
+
module_name, attribute_name = _OPTIONAL_EXPORTS[name]
|
|
135
|
+
module = importlib.import_module(module_name, __name__)
|
|
136
|
+
return getattr(module, attribute_name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"BrowserRelayAcceptedBatch",
|
|
141
|
+
"BrowserRelayHandler",
|
|
142
|
+
"BrowserRelayResponse",
|
|
143
|
+
"DebugBundleSdk",
|
|
144
|
+
"DebugBundleDjangoMiddleware",
|
|
145
|
+
"DebugBundleFastAPIMiddleware",
|
|
146
|
+
"begin_request",
|
|
147
|
+
"capture_async",
|
|
148
|
+
"capture_error",
|
|
149
|
+
"capture_exception",
|
|
150
|
+
"capture_exceptions",
|
|
151
|
+
"capture_log",
|
|
152
|
+
"capture_logging",
|
|
153
|
+
"capture_message",
|
|
154
|
+
"capture_request",
|
|
155
|
+
"create_django_relay_view",
|
|
156
|
+
"create_fastapi_relay_handler",
|
|
157
|
+
"create_flask_relay_handler",
|
|
158
|
+
"end_request",
|
|
159
|
+
"flush",
|
|
160
|
+
"init",
|
|
161
|
+
"instrument_fastapi",
|
|
162
|
+
"instrument_flask",
|
|
163
|
+
"probe",
|
|
164
|
+
"set_context",
|
|
165
|
+
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
DEFAULT_PROBES_POLL_INTERVAL_MS = 60_000
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CapturePolicy:
|
|
11
|
+
preset: str
|
|
12
|
+
capture_logs: str
|
|
13
|
+
capture_request_events: str
|
|
14
|
+
capture_breadcrumbs: str
|
|
15
|
+
capture_probe_events: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RemoteProbeDirective:
|
|
20
|
+
id: str
|
|
21
|
+
label_pattern: str
|
|
22
|
+
service: str
|
|
23
|
+
environment: str
|
|
24
|
+
expires_at: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RemoteConfigSnapshot:
|
|
29
|
+
probes_enabled: bool
|
|
30
|
+
remote_probes_enabled: bool
|
|
31
|
+
directives: list[RemoteProbeDirective]
|
|
32
|
+
poll_interval_ms: int
|
|
33
|
+
capture_policy: CapturePolicy
|
|
34
|
+
trigger_token_key: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
BALANCED_CAPTURE_POLICY = CapturePolicy(
|
|
38
|
+
preset="balanced",
|
|
39
|
+
capture_logs="warning",
|
|
40
|
+
capture_request_events="all",
|
|
41
|
+
capture_breadcrumbs="local_only",
|
|
42
|
+
capture_probe_events="standalone_when_activated",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
MINIMAL_CAPTURE_POLICY = CapturePolicy(
|
|
46
|
+
preset="minimal",
|
|
47
|
+
capture_logs="warning",
|
|
48
|
+
capture_request_events="off",
|
|
49
|
+
capture_breadcrumbs="local_only",
|
|
50
|
+
capture_probe_events="buffer_only",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_remote_config(payload: object, fallback_poll_interval_ms: int, now_ms: int) -> RemoteConfigSnapshot | None:
|
|
55
|
+
if not isinstance(payload, dict):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
probes_enabled = payload.get("probes_enabled") is True
|
|
59
|
+
remote_probes_enabled = payload.get("remote_probes_enabled") is True
|
|
60
|
+
poll_interval_candidate = payload.get("poll_interval_ms")
|
|
61
|
+
poll_interval_ms = (
|
|
62
|
+
int(poll_interval_candidate)
|
|
63
|
+
if isinstance(poll_interval_candidate, (int, float)) and int(poll_interval_candidate) > 0
|
|
64
|
+
else fallback_poll_interval_ms
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
directives: list[RemoteProbeDirective] = []
|
|
68
|
+
active_probes = payload.get("active_probes")
|
|
69
|
+
if isinstance(active_probes, list):
|
|
70
|
+
for directive in active_probes:
|
|
71
|
+
parsed = _parse_directive(directive)
|
|
72
|
+
if parsed is not None and _expires_at_ms(parsed.expires_at) > now_ms:
|
|
73
|
+
directives.append(parsed)
|
|
74
|
+
|
|
75
|
+
capture_policy = _parse_capture_policy(payload.get("capture_policy"))
|
|
76
|
+
if capture_policy is None:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return RemoteConfigSnapshot(
|
|
80
|
+
probes_enabled=probes_enabled,
|
|
81
|
+
remote_probes_enabled=remote_probes_enabled,
|
|
82
|
+
directives=directives,
|
|
83
|
+
poll_interval_ms=poll_interval_ms if remote_probes_enabled else DEFAULT_PROBES_POLL_INTERVAL_MS,
|
|
84
|
+
capture_policy=capture_policy,
|
|
85
|
+
trigger_token_key=_as_non_empty_string(payload.get("trigger_token_key")),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def find_matching_remote_probe_directives(
|
|
90
|
+
directives: list[RemoteProbeDirective],
|
|
91
|
+
label: str,
|
|
92
|
+
service: str,
|
|
93
|
+
environment: str,
|
|
94
|
+
now_ms: int,
|
|
95
|
+
) -> list[RemoteProbeDirective]:
|
|
96
|
+
matches: list[RemoteProbeDirective] = []
|
|
97
|
+
for directive in directives:
|
|
98
|
+
if _expires_at_ms(directive.expires_at) <= now_ms:
|
|
99
|
+
continue
|
|
100
|
+
if directive.service != "*" and directive.service != service:
|
|
101
|
+
continue
|
|
102
|
+
if directive.environment != "*" and directive.environment != environment:
|
|
103
|
+
continue
|
|
104
|
+
if _matches_label_pattern(directive.label_pattern, label):
|
|
105
|
+
matches.append(directive)
|
|
106
|
+
return matches
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
110
|
+
if payload is None:
|
|
111
|
+
return BALANCED_CAPTURE_POLICY
|
|
112
|
+
if not isinstance(payload, dict):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
preset = _as_non_empty_string(payload.get("preset")) or BALANCED_CAPTURE_POLICY.preset
|
|
116
|
+
capture_logs = _as_non_empty_string(payload.get("capture_logs"))
|
|
117
|
+
capture_request_events = _as_non_empty_string(payload.get("capture_request_events"))
|
|
118
|
+
capture_breadcrumbs = _as_non_empty_string(payload.get("capture_breadcrumbs"))
|
|
119
|
+
capture_probe_events = _as_non_empty_string(payload.get("capture_probe_events"))
|
|
120
|
+
|
|
121
|
+
if capture_logs not in {"off", "error", "warning", "info"}:
|
|
122
|
+
return None
|
|
123
|
+
if capture_request_events not in {"off", "failures_only", "filtered", "all"}:
|
|
124
|
+
return None
|
|
125
|
+
if capture_breadcrumbs not in {"local_only", "exception_only", "standalone"}:
|
|
126
|
+
return None
|
|
127
|
+
if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
return CapturePolicy(
|
|
131
|
+
preset=preset,
|
|
132
|
+
capture_logs=capture_logs,
|
|
133
|
+
capture_request_events=capture_request_events,
|
|
134
|
+
capture_breadcrumbs=capture_breadcrumbs,
|
|
135
|
+
capture_probe_events=capture_probe_events,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_directive(payload: object) -> RemoteProbeDirective | None:
|
|
140
|
+
if not isinstance(payload, dict):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
directive_id = _as_non_empty_string(payload.get("id"))
|
|
144
|
+
label_pattern = _as_non_empty_string(payload.get("label_pattern"))
|
|
145
|
+
service = _as_non_empty_string(payload.get("service"))
|
|
146
|
+
environment = _as_non_empty_string(payload.get("environment"))
|
|
147
|
+
expires_at = _as_non_empty_string(payload.get("expires_at"))
|
|
148
|
+
|
|
149
|
+
if directive_id is None:
|
|
150
|
+
return None
|
|
151
|
+
if label_pattern is None:
|
|
152
|
+
return None
|
|
153
|
+
if service is None:
|
|
154
|
+
return None
|
|
155
|
+
if environment is None:
|
|
156
|
+
return None
|
|
157
|
+
if expires_at is None:
|
|
158
|
+
return None
|
|
159
|
+
if _expires_at_ms(expires_at) == 0:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
return RemoteProbeDirective(
|
|
163
|
+
id=directive_id,
|
|
164
|
+
label_pattern=label_pattern,
|
|
165
|
+
service=service,
|
|
166
|
+
environment=environment,
|
|
167
|
+
expires_at=expires_at,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _matches_label_pattern(pattern: str, label: str) -> bool:
|
|
172
|
+
if pattern == "*":
|
|
173
|
+
return True
|
|
174
|
+
if pattern.endswith(".*"):
|
|
175
|
+
prefix = pattern[:-2]
|
|
176
|
+
return label == prefix or label.startswith(f"{prefix}.")
|
|
177
|
+
return pattern == label
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _expires_at_ms(value: str) -> int:
|
|
181
|
+
from datetime import datetime
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if value.endswith("Z"):
|
|
185
|
+
value = value[:-1] + "+00:00"
|
|
186
|
+
return int(datetime.fromisoformat(value).timestamp() * 1000)
|
|
187
|
+
except ValueError:
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _as_non_empty_string(value: Any) -> str | None:
|
|
192
|
+
return value if isinstance(value, str) and len(value) > 0 else None
|