sentinel-probes 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.
- sentinel_probes-0.1.0/PKG-INFO +127 -0
- sentinel_probes-0.1.0/README.md +107 -0
- sentinel_probes-0.1.0/pyproject.toml +38 -0
- sentinel_probes-0.1.0/sentinel_probes/__init__.py +48 -0
- sentinel_probes-0.1.0/sentinel_probes/autostart.py +67 -0
- sentinel_probes-0.1.0/sentinel_probes/bootstrap/sitecustomize.py +29 -0
- sentinel_probes-0.1.0/sentinel_probes/capture.py +294 -0
- sentinel_probes-0.1.0/sentinel_probes/evaluator.py +649 -0
- sentinel_probes-0.1.0/sentinel_probes/http.py +190 -0
- sentinel_probes-0.1.0/sentinel_probes/instrumenter.py +713 -0
- sentinel_probes-0.1.0/sentinel_probes/pattern.py +597 -0
- sentinel_probes-0.1.0/sentinel_probes/run.py +31 -0
- sentinel_probes-0.1.0/sentinel_probes/runtime.py +555 -0
- sentinel_probes-0.1.0/sentinel_probes/spans.py +66 -0
- sentinel_probes-0.1.0/sentinel_probes.egg-info/PKG-INFO +127 -0
- sentinel_probes-0.1.0/sentinel_probes.egg-info/SOURCES.txt +24 -0
- sentinel_probes-0.1.0/sentinel_probes.egg-info/dependency_links.txt +1 -0
- sentinel_probes-0.1.0/sentinel_probes.egg-info/entry_points.txt +2 -0
- sentinel_probes-0.1.0/sentinel_probes.egg-info/top_level.txt +1 -0
- sentinel_probes-0.1.0/setup.cfg +4 -0
- sentinel_probes-0.1.0/tests/test_capture.py +262 -0
- sentinel_probes-0.1.0/tests/test_conformance_cases.py +82 -0
- sentinel_probes-0.1.0/tests/test_conformance_patterns.py +81 -0
- sentinel_probes-0.1.0/tests/test_hardening.py +299 -0
- sentinel_probes-0.1.0/tests/test_instrumenter.py +364 -0
- sentinel_probes-0.1.0/tests/test_runtime.py +292 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentinel-probes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sentinel Dynamic Instrumentation for Python: metric, span and span-tag probes placed in running code without a redeploy.
|
|
5
|
+
Author-email: A K M Muhibullah Nayem <nayem.drmc@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/MuhibNayem/sightglass/tree/main/clients/python
|
|
8
|
+
Project-URL: Source, https://github.com/MuhibNayem/sightglass
|
|
9
|
+
Project-URL: Issues, https://github.com/MuhibNayem/sightglass/issues
|
|
10
|
+
Keywords: sentinel,dynamic-instrumentation,observability,opentelemetry,apm,probes
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# sentinel-probes: Dynamic Instrumentation for Python
|
|
22
|
+
|
|
23
|
+
Metric, span and span-tag probes on a running Python service, added, changed
|
|
24
|
+
and removed from [Sentinel](https://github.com/MuhibNayem/sightglass) without a deploy. Standard library only.
|
|
25
|
+
|
|
26
|
+
## Start it
|
|
27
|
+
|
|
28
|
+
With no code change, the way `ddtrace-run` and `opentelemetry-instrument` work.
|
|
29
|
+
Every Python process the command starts runs the client, including
|
|
30
|
+
pre-forked workers:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install sentinel-probes
|
|
34
|
+
|
|
35
|
+
SENTINEL_PROBES_URL=https://ingest.example.com/otlp/v1/probes \
|
|
36
|
+
SENTINEL_API_KEY=<ingest key> \
|
|
37
|
+
OTEL_SERVICE_NAME=checkout \
|
|
38
|
+
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=production,service.version=1.4.2 \
|
|
39
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
|
|
40
|
+
sentinel-probes-run gunicorn app:app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`SENTINEL_SERVICE`, `SENTINEL_ENV`, `SENTINEL_VERSION` and
|
|
44
|
+
`SENTINEL_OTLP_ENDPOINT` take precedence over the OpenTelemetry variables.
|
|
45
|
+
Without `SENTINEL_PROBES_URL` the client stays off. With the URL set but the
|
|
46
|
+
rest of the configuration incomplete, it logs one warning and stays off.
|
|
47
|
+
|
|
48
|
+
Or start it from code:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import sentinel_probes
|
|
52
|
+
|
|
53
|
+
sentinel_probes.start_probes(
|
|
54
|
+
service="checkout",
|
|
55
|
+
env="production",
|
|
56
|
+
api_key="<ingest key>",
|
|
57
|
+
version="1.4.2",
|
|
58
|
+
probes_url="https://ingest.example.com/otlp/v1/probes",
|
|
59
|
+
otlp_endpoint="http://otel-collector:4318",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How it stays cheap
|
|
64
|
+
|
|
65
|
+
- **Python 3.12+:** probes are placed with `sys.monitoring` local events, only
|
|
66
|
+
on the targeted code objects. Every other function runs untouched.
|
|
67
|
+
- **Rate limit:** each probe runs at most 50 times a second. Once a probe has
|
|
68
|
+
used up that allowance, its code object's events are switched off for the rest
|
|
69
|
+
of the second, so a hot method runs at full speed between its sampled calls.
|
|
70
|
+
- **Before 3.12:** a method probe wraps the function. The signature is computed
|
|
71
|
+
once, the rate limit is checked before arguments are bound, and once the
|
|
72
|
+
allowance is used up the wrapper skips the lock entirely.
|
|
73
|
+
- **CPU budget:** probes may use at most 2% of a second's CPU time. Beyond
|
|
74
|
+
that, they're refused until the next second.
|
|
75
|
+
- **Off the application's threads:** a daemon thread polls, reports and exports.
|
|
76
|
+
It restarts in forked children with a new runtime id.
|
|
77
|
+
- **Raw-socket HTTP:** the client talks over a plain socket, not urllib,
|
|
78
|
+
http.client or requests, so OpenTelemetry's and ddtrace's instrumentation
|
|
79
|
+
never see its traffic. Each call is time-boxed and responses are capped at
|
|
80
|
+
1 MiB.
|
|
81
|
+
- **Capture runs no application code:**
|
|
82
|
+
- attributes are read through descriptors, never via properties,
|
|
83
|
+
`__getattr__` or `__str__`;
|
|
84
|
+
- an exception's message is read only when its class keeps
|
|
85
|
+
`BaseException.__str__`;
|
|
86
|
+
- only exact standard-library types (`datetime`, `UUID`, `Decimal`, …) are
|
|
87
|
+
turned into text.
|
|
88
|
+
- **Bounded and redacted:** capture is bounded (depth 2, 20 entries, 256
|
|
89
|
+
characters, 30 variables) and redacted by name (`password`, `token`,
|
|
90
|
+
`session`, …, plus the probe's keywords).
|
|
91
|
+
- **Never into the application:** a failure becomes the probe's error.
|
|
92
|
+
|
|
93
|
+
Per call of a probed method (`tests/bench.py`). The probe is a gauge of the
|
|
94
|
+
return value; most calls are past the 50-a-second limit:
|
|
95
|
+
|
|
96
|
+
| Python | plain call | with the probe | added |
|
|
97
|
+
| ----------------------- | ---------: | -------------: | -------: |
|
|
98
|
+
| 3.13 (`sys.monitoring`) | 0.10 µs | 0.10 µs | +0.01 µs |
|
|
99
|
+
| 3.11 (function wrapper) | 0.09 µs | 0.51 µs | +0.43 µs |
|
|
100
|
+
|
|
101
|
+
## What runs where
|
|
102
|
+
|
|
103
|
+
| Python | Method probes | Line probes |
|
|
104
|
+
| -------- | ------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| 3.12+ | yes | yes |
|
|
106
|
+
| 3.8–3.11 | yes | no: the only way is `sys.settrace`, which taxes every call in every thread. A line probe reports an error that says so. |
|
|
107
|
+
|
|
108
|
+
The client advertises what it can place (`locations=line,method` or `method`).
|
|
109
|
+
|
|
110
|
+
Before 3.12, the wrapper replaces the function where it is defined. Code that
|
|
111
|
+
imported it by name before the probe was placed (`from orders import total`)
|
|
112
|
+
keeps calling the original, and a nested function can't be reached at all; the
|
|
113
|
+
probe reports which.
|
|
114
|
+
|
|
115
|
+
## Tests
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
python -m pytest -q # unit, conformance (79 cases, 3,030 patterns), hardening
|
|
119
|
+
docker run --rm -v "$PWD":/w/python:ro -w /w/python/tests/e2e python:3.11-slim sh run.sh
|
|
120
|
+
docker run --rm -v "$PWD":/w/python:ro -w /w/python/tests/e2e python:3.13-slim sh run.sh
|
|
121
|
+
docker run --rm -v "$PWD":/w/python:ro -w /w/python python:3.13-slim python tests/bench.py
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The end-to-end run starts an unmodified application with
|
|
125
|
+
`python -m sentinel_probes.run` against a fake backend. It checks the poll,
|
|
126
|
+
statuses (including the line-probe refusal on 3.11), a redacted sample and the
|
|
127
|
+
OTLP metrics.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# sentinel-probes: Dynamic Instrumentation for Python
|
|
2
|
+
|
|
3
|
+
Metric, span and span-tag probes on a running Python service, added, changed
|
|
4
|
+
and removed from [Sentinel](https://github.com/MuhibNayem/sightglass) without a deploy. Standard library only.
|
|
5
|
+
|
|
6
|
+
## Start it
|
|
7
|
+
|
|
8
|
+
With no code change, the way `ddtrace-run` and `opentelemetry-instrument` work.
|
|
9
|
+
Every Python process the command starts runs the client, including
|
|
10
|
+
pre-forked workers:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pip install sentinel-probes
|
|
14
|
+
|
|
15
|
+
SENTINEL_PROBES_URL=https://ingest.example.com/otlp/v1/probes \
|
|
16
|
+
SENTINEL_API_KEY=<ingest key> \
|
|
17
|
+
OTEL_SERVICE_NAME=checkout \
|
|
18
|
+
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=production,service.version=1.4.2 \
|
|
19
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
|
|
20
|
+
sentinel-probes-run gunicorn app:app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`SENTINEL_SERVICE`, `SENTINEL_ENV`, `SENTINEL_VERSION` and
|
|
24
|
+
`SENTINEL_OTLP_ENDPOINT` take precedence over the OpenTelemetry variables.
|
|
25
|
+
Without `SENTINEL_PROBES_URL` the client stays off. With the URL set but the
|
|
26
|
+
rest of the configuration incomplete, it logs one warning and stays off.
|
|
27
|
+
|
|
28
|
+
Or start it from code:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import sentinel_probes
|
|
32
|
+
|
|
33
|
+
sentinel_probes.start_probes(
|
|
34
|
+
service="checkout",
|
|
35
|
+
env="production",
|
|
36
|
+
api_key="<ingest key>",
|
|
37
|
+
version="1.4.2",
|
|
38
|
+
probes_url="https://ingest.example.com/otlp/v1/probes",
|
|
39
|
+
otlp_endpoint="http://otel-collector:4318",
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## How it stays cheap
|
|
44
|
+
|
|
45
|
+
- **Python 3.12+:** probes are placed with `sys.monitoring` local events, only
|
|
46
|
+
on the targeted code objects. Every other function runs untouched.
|
|
47
|
+
- **Rate limit:** each probe runs at most 50 times a second. Once a probe has
|
|
48
|
+
used up that allowance, its code object's events are switched off for the rest
|
|
49
|
+
of the second, so a hot method runs at full speed between its sampled calls.
|
|
50
|
+
- **Before 3.12:** a method probe wraps the function. The signature is computed
|
|
51
|
+
once, the rate limit is checked before arguments are bound, and once the
|
|
52
|
+
allowance is used up the wrapper skips the lock entirely.
|
|
53
|
+
- **CPU budget:** probes may use at most 2% of a second's CPU time. Beyond
|
|
54
|
+
that, they're refused until the next second.
|
|
55
|
+
- **Off the application's threads:** a daemon thread polls, reports and exports.
|
|
56
|
+
It restarts in forked children with a new runtime id.
|
|
57
|
+
- **Raw-socket HTTP:** the client talks over a plain socket, not urllib,
|
|
58
|
+
http.client or requests, so OpenTelemetry's and ddtrace's instrumentation
|
|
59
|
+
never see its traffic. Each call is time-boxed and responses are capped at
|
|
60
|
+
1 MiB.
|
|
61
|
+
- **Capture runs no application code:**
|
|
62
|
+
- attributes are read through descriptors, never via properties,
|
|
63
|
+
`__getattr__` or `__str__`;
|
|
64
|
+
- an exception's message is read only when its class keeps
|
|
65
|
+
`BaseException.__str__`;
|
|
66
|
+
- only exact standard-library types (`datetime`, `UUID`, `Decimal`, …) are
|
|
67
|
+
turned into text.
|
|
68
|
+
- **Bounded and redacted:** capture is bounded (depth 2, 20 entries, 256
|
|
69
|
+
characters, 30 variables) and redacted by name (`password`, `token`,
|
|
70
|
+
`session`, …, plus the probe's keywords).
|
|
71
|
+
- **Never into the application:** a failure becomes the probe's error.
|
|
72
|
+
|
|
73
|
+
Per call of a probed method (`tests/bench.py`). The probe is a gauge of the
|
|
74
|
+
return value; most calls are past the 50-a-second limit:
|
|
75
|
+
|
|
76
|
+
| Python | plain call | with the probe | added |
|
|
77
|
+
| ----------------------- | ---------: | -------------: | -------: |
|
|
78
|
+
| 3.13 (`sys.monitoring`) | 0.10 µs | 0.10 µs | +0.01 µs |
|
|
79
|
+
| 3.11 (function wrapper) | 0.09 µs | 0.51 µs | +0.43 µs |
|
|
80
|
+
|
|
81
|
+
## What runs where
|
|
82
|
+
|
|
83
|
+
| Python | Method probes | Line probes |
|
|
84
|
+
| -------- | ------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| 3.12+ | yes | yes |
|
|
86
|
+
| 3.8–3.11 | yes | no: the only way is `sys.settrace`, which taxes every call in every thread. A line probe reports an error that says so. |
|
|
87
|
+
|
|
88
|
+
The client advertises what it can place (`locations=line,method` or `method`).
|
|
89
|
+
|
|
90
|
+
Before 3.12, the wrapper replaces the function where it is defined. Code that
|
|
91
|
+
imported it by name before the probe was placed (`from orders import total`)
|
|
92
|
+
keeps calling the original, and a nested function can't be reached at all; the
|
|
93
|
+
probe reports which.
|
|
94
|
+
|
|
95
|
+
## Tests
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
python -m pytest -q # unit, conformance (79 cases, 3,030 patterns), hardening
|
|
99
|
+
docker run --rm -v "$PWD":/w/python:ro -w /w/python/tests/e2e python:3.11-slim sh run.sh
|
|
100
|
+
docker run --rm -v "$PWD":/w/python:ro -w /w/python/tests/e2e python:3.13-slim sh run.sh
|
|
101
|
+
docker run --rm -v "$PWD":/w/python:ro -w /w/python python:3.13-slim python tests/bench.py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The end-to-end run starts an unmodified application with
|
|
105
|
+
`python -m sentinel_probes.run` against a fake backend. It checks the poll,
|
|
106
|
+
statuses (including the line-probe refusal on 3.11), a redacted sample and the
|
|
107
|
+
OTLP metrics.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sentinel-probes"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Sentinel Dynamic Instrumentation for Python: metric, span and span-tag probes placed in running code without a redeploy."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = []
|
|
12
|
+
license = "Apache-2.0"
|
|
13
|
+
authors = [{ name = "A K M Muhibullah Nayem", email = "nayem.drmc@gmail.com" }]
|
|
14
|
+
keywords = ["sentinel", "dynamic-instrumentation", "observability", "opentelemetry", "apm", "probes"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Topic :: System :: Monitoring",
|
|
22
|
+
"Topic :: Software Development :: Debuggers",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/MuhibNayem/sightglass/tree/main/clients/python"
|
|
27
|
+
Source = "https://github.com/MuhibNayem/sightglass"
|
|
28
|
+
Issues = "https://github.com/MuhibNayem/sightglass/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
sentinel-probes-run = "sentinel_probes.run:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
packages = ["sentinel_probes", "sentinel_probes.bootstrap"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
pythonpath = ["."]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentinel Dynamic Instrumentation Client for Python.
|
|
3
|
+
|
|
4
|
+
Zero external dependencies, ReDoS-safe linear regex engine, safe variable capture
|
|
5
|
+
without descriptor/property execution, CPU governor, and PEP 669 sys.monitoring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .runtime import start_probes, CLIENT_VERSION, Runtime
|
|
9
|
+
from .evaluator import (
|
|
10
|
+
evaluate,
|
|
11
|
+
validate,
|
|
12
|
+
render_tag,
|
|
13
|
+
reads,
|
|
14
|
+
text,
|
|
15
|
+
ProbeError,
|
|
16
|
+
Capture,
|
|
17
|
+
AST_VERSION,
|
|
18
|
+
)
|
|
19
|
+
from .pattern import compile_pattern, compiled, run_pattern, code_points
|
|
20
|
+
from .capture import capture_frame, capture_locals, value_of, scrubbed
|
|
21
|
+
from .instrumenter import Instrumenter
|
|
22
|
+
from .spans import active_span
|
|
23
|
+
from . import http
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"start_probes",
|
|
27
|
+
"CLIENT_VERSION",
|
|
28
|
+
"Runtime",
|
|
29
|
+
"evaluate",
|
|
30
|
+
"validate",
|
|
31
|
+
"render_tag",
|
|
32
|
+
"reads",
|
|
33
|
+
"text",
|
|
34
|
+
"ProbeError",
|
|
35
|
+
"Capture",
|
|
36
|
+
"AST_VERSION",
|
|
37
|
+
"compile_pattern",
|
|
38
|
+
"compiled",
|
|
39
|
+
"run_pattern",
|
|
40
|
+
"code_points",
|
|
41
|
+
"capture_frame",
|
|
42
|
+
"capture_locals",
|
|
43
|
+
"value_of",
|
|
44
|
+
"scrubbed",
|
|
45
|
+
"Instrumenter",
|
|
46
|
+
"active_span",
|
|
47
|
+
"http",
|
|
48
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Start the client from the environment, with no code change: what
|
|
3
|
+
`sentinel-probes-run` (and the sitecustomize it installs) calls.
|
|
4
|
+
|
|
5
|
+
SENTINEL_PROBES_URL https://sightglass.example.com/otlp/v1/probes
|
|
6
|
+
SENTINEL_API_KEY the ingest key
|
|
7
|
+
SENTINEL_SERVICE or OTEL_SERVICE_NAME
|
|
8
|
+
SENTINEL_ENV or deployment.environment(.name) in OTEL_RESOURCE_ATTRIBUTES
|
|
9
|
+
SENTINEL_VERSION or service.version in OTEL_RESOURCE_ATTRIBUTES (optional)
|
|
10
|
+
SENTINEL_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT (default http://localhost:4318)
|
|
11
|
+
|
|
12
|
+
Missing configuration means the client stays off, with one warning; it never
|
|
13
|
+
raises into the application's start-up.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
_runtime: Any = None
|
|
23
|
+
_log = logging.getLogger("sentinel_probes")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resource_attribute(*names: str) -> str:
|
|
27
|
+
raw = os.environ.get("OTEL_RESOURCE_ATTRIBUTES", "")
|
|
28
|
+
for pair in raw.split(","):
|
|
29
|
+
key, sep, value = pair.partition("=")
|
|
30
|
+
if sep and key.strip() in names:
|
|
31
|
+
return value.strip()
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def start_from_env() -> Any:
|
|
36
|
+
"""Start once per process; None when not configured. Forked children restart
|
|
37
|
+
their own poller (Runtime registers an at-fork handler)."""
|
|
38
|
+
global _runtime
|
|
39
|
+
if _runtime is not None:
|
|
40
|
+
return _runtime
|
|
41
|
+
env = os.environ
|
|
42
|
+
url = env.get("SENTINEL_PROBES_URL", "")
|
|
43
|
+
if not url:
|
|
44
|
+
return None
|
|
45
|
+
service = env.get("SENTINEL_SERVICE") or env.get("OTEL_SERVICE_NAME", "")
|
|
46
|
+
environment = env.get("SENTINEL_ENV") or _resource_attribute("deployment.environment.name", "deployment.environment")
|
|
47
|
+
api_key = env.get("SENTINEL_API_KEY", "")
|
|
48
|
+
if not (service and environment and api_key):
|
|
49
|
+
_log.warning(
|
|
50
|
+
"[sentinel-probes] not started: set SENTINEL_SERVICE (or OTEL_SERVICE_NAME), SENTINEL_ENV and SENTINEL_API_KEY"
|
|
51
|
+
)
|
|
52
|
+
return None
|
|
53
|
+
try:
|
|
54
|
+
from .runtime import start_probes
|
|
55
|
+
|
|
56
|
+
_runtime = start_probes(
|
|
57
|
+
service=service,
|
|
58
|
+
env=environment,
|
|
59
|
+
api_key=api_key,
|
|
60
|
+
version=env.get("SENTINEL_VERSION") or _resource_attribute("service.version"),
|
|
61
|
+
probes_url=url,
|
|
62
|
+
otlp_endpoint=env.get("SENTINEL_OTLP_ENDPOINT") or env.get("OTEL_EXPORTER_OTLP_ENDPOINT") or "http://localhost:4318",
|
|
63
|
+
)
|
|
64
|
+
except Exception as e: # never into the application
|
|
65
|
+
_log.warning("[sentinel-probes] not started: %s", e)
|
|
66
|
+
return None
|
|
67
|
+
return _runtime
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Put on PYTHONPATH by `sentinel-probes-run`: starts the client as the interpreter
|
|
3
|
+
starts, then hands over to any sitecustomize the environment already had.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from sentinel_probes.autostart import start_from_env
|
|
12
|
+
|
|
13
|
+
start_from_env()
|
|
14
|
+
except Exception: # never into the application
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
# Chain to the next sitecustomize on the path, as if this one were not here.
|
|
18
|
+
_here = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
for _entry in sys.path:
|
|
20
|
+
if _entry and os.path.abspath(_entry) != _here:
|
|
21
|
+
_candidate = os.path.join(_entry, "sitecustomize.py")
|
|
22
|
+
if os.path.isfile(_candidate):
|
|
23
|
+
_loader_spec = importlib.util.spec_from_file_location("_sentinel_chained_sitecustomize", _candidate)
|
|
24
|
+
if _loader_spec and _loader_spec.loader:
|
|
25
|
+
try:
|
|
26
|
+
_loader_spec.loader.exec_module(importlib.util.module_from_spec(_loader_spec))
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
break
|