containarium-telemetry 0.22.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.
- containarium_telemetry-0.22.0/PKG-INFO +60 -0
- containarium_telemetry-0.22.0/pyproject.toml +103 -0
- containarium_telemetry-0.22.0/setup.cfg +4 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/__init__.py +16 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_config.py +54 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_distro.py +66 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_dry_run.py +64 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_exec_shim.py +92 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_init.py +156 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_instrumentations.py +93 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_resource.py +71 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/_version.py +16 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry/py.typed +0 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry.egg-info/PKG-INFO +60 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry.egg-info/SOURCES.txt +25 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry.egg-info/dependency_links.txt +1 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry.egg-info/entry_points.txt +8 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry.egg-info/requires.txt +52 -0
- containarium_telemetry-0.22.0/src/containarium_telemetry.egg-info/top_level.txt +1 -0
- containarium_telemetry-0.22.0/tests/test_config.py +60 -0
- containarium_telemetry-0.22.0/tests/test_distro.py +53 -0
- containarium_telemetry-0.22.0/tests/test_dry_run.py +64 -0
- containarium_telemetry-0.22.0/tests/test_exec_shim.py +63 -0
- containarium_telemetry-0.22.0/tests/test_init.py +78 -0
- containarium_telemetry-0.22.0/tests/test_instrumentations.py +79 -0
- containarium_telemetry-0.22.0/tests/test_integration_flask.py +61 -0
- containarium_telemetry-0.22.0/tests/test_resource.py +102 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: containarium-telemetry
|
|
3
|
+
Version: 0.22.0
|
|
4
|
+
Summary: Containarium telemetry distro for Python — opinionated OpenTelemetry init that pairs with the Containarium platform's monitoring infrastructure.
|
|
5
|
+
Author: FootprintAI
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/footprintai/containarium
|
|
8
|
+
Project-URL: Documentation, https://github.com/footprintai/containarium/blob/main/docs/TELEMETRY-DISTRO-DESIGN.md
|
|
9
|
+
Project-URL: Repository, https://github.com/footprintai/containarium
|
|
10
|
+
Project-URL: Issues, https://github.com/footprintai/containarium/issues
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: opentelemetry-api>=1.27.0
|
|
22
|
+
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
23
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation>=0.48b0
|
|
25
|
+
Provides-Extra: flask
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.48b0; extra == "flask"
|
|
27
|
+
Provides-Extra: fastapi
|
|
28
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.48b0; extra == "fastapi"
|
|
29
|
+
Provides-Extra: django
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.48b0; extra == "django"
|
|
31
|
+
Provides-Extra: requests
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.48b0; extra == "requests"
|
|
33
|
+
Provides-Extra: httpx
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.48b0; extra == "httpx"
|
|
35
|
+
Provides-Extra: sqlalchemy
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.48b0; extra == "sqlalchemy"
|
|
37
|
+
Provides-Extra: asyncpg
|
|
38
|
+
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.48b0; extra == "asyncpg"
|
|
39
|
+
Provides-Extra: psycopg
|
|
40
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg>=0.48b0; extra == "psycopg"
|
|
41
|
+
Provides-Extra: redis
|
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-redis>=0.48b0; extra == "redis"
|
|
43
|
+
Provides-Extra: logging
|
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-logging>=0.48b0; extra == "logging"
|
|
45
|
+
Provides-Extra: all
|
|
46
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.48b0; extra == "all"
|
|
47
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.48b0; extra == "all"
|
|
48
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.48b0; extra == "all"
|
|
49
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.48b0; extra == "all"
|
|
50
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.48b0; extra == "all"
|
|
51
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.48b0; extra == "all"
|
|
52
|
+
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.48b0; extra == "all"
|
|
53
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg>=0.48b0; extra == "all"
|
|
54
|
+
Requires-Dist: opentelemetry-instrumentation-redis>=0.48b0; extra == "all"
|
|
55
|
+
Requires-Dist: opentelemetry-instrumentation-logging>=0.48b0; extra == "all"
|
|
56
|
+
Provides-Extra: dev
|
|
57
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
58
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
59
|
+
Requires-Dist: flask>=2.0; extra == "dev"
|
|
60
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.48b0; extra == "dev"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "containarium-telemetry"
|
|
7
|
+
version = "0.22.0"
|
|
8
|
+
description = "Containarium telemetry distro for Python — opinionated OpenTelemetry init that pairs with the Containarium platform's monitoring infrastructure."
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "FootprintAI" },
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: System :: Monitoring",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"opentelemetry-api>=1.27.0",
|
|
27
|
+
"opentelemetry-sdk>=1.27.0",
|
|
28
|
+
"opentelemetry-exporter-otlp-proto-http>=1.27.0",
|
|
29
|
+
# Required for ContainariumDistro / Configurator entry points and
|
|
30
|
+
# the auto-instrumentation runtime used by `containarium-instrument`.
|
|
31
|
+
"opentelemetry-instrumentation>=0.48b0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/footprintai/containarium"
|
|
36
|
+
Documentation = "https://github.com/footprintai/containarium/blob/main/docs/TELEMETRY-DISTRO-DESIGN.md"
|
|
37
|
+
Repository = "https://github.com/footprintai/containarium"
|
|
38
|
+
Issues = "https://github.com/footprintai/containarium/issues"
|
|
39
|
+
|
|
40
|
+
# Per-framework extras pull in the upstream OTel instrumentation package
|
|
41
|
+
# for that framework. The instrumentation packages themselves declare
|
|
42
|
+
# the framework as a transitive dep, so e.g. `pip install
|
|
43
|
+
# containarium-telemetry[flask]` installs flask too. Tenants who already
|
|
44
|
+
# pin their framework get no version conflict because the OTel
|
|
45
|
+
# instrumentation packages declare ranges, not pins.
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
flask = ["opentelemetry-instrumentation-flask>=0.48b0"]
|
|
48
|
+
fastapi = ["opentelemetry-instrumentation-fastapi>=0.48b0"]
|
|
49
|
+
django = ["opentelemetry-instrumentation-django>=0.48b0"]
|
|
50
|
+
requests = ["opentelemetry-instrumentation-requests>=0.48b0"]
|
|
51
|
+
httpx = ["opentelemetry-instrumentation-httpx>=0.48b0"]
|
|
52
|
+
sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy>=0.48b0"]
|
|
53
|
+
asyncpg = ["opentelemetry-instrumentation-asyncpg>=0.48b0"]
|
|
54
|
+
psycopg = ["opentelemetry-instrumentation-psycopg>=0.48b0"]
|
|
55
|
+
redis = ["opentelemetry-instrumentation-redis>=0.48b0"]
|
|
56
|
+
logging = ["opentelemetry-instrumentation-logging>=0.48b0"]
|
|
57
|
+
# `all` is the convenience bundle — recommended default for greenfield
|
|
58
|
+
# apps. Tenants with image-size sensitivity pin individual extras.
|
|
59
|
+
all = [
|
|
60
|
+
"opentelemetry-instrumentation-flask>=0.48b0",
|
|
61
|
+
"opentelemetry-instrumentation-fastapi>=0.48b0",
|
|
62
|
+
"opentelemetry-instrumentation-django>=0.48b0",
|
|
63
|
+
"opentelemetry-instrumentation-requests>=0.48b0",
|
|
64
|
+
"opentelemetry-instrumentation-httpx>=0.48b0",
|
|
65
|
+
"opentelemetry-instrumentation-sqlalchemy>=0.48b0",
|
|
66
|
+
"opentelemetry-instrumentation-asyncpg>=0.48b0",
|
|
67
|
+
"opentelemetry-instrumentation-psycopg>=0.48b0",
|
|
68
|
+
"opentelemetry-instrumentation-redis>=0.48b0",
|
|
69
|
+
"opentelemetry-instrumentation-logging>=0.48b0",
|
|
70
|
+
]
|
|
71
|
+
dev = [
|
|
72
|
+
"pytest>=7.0",
|
|
73
|
+
"pytest-cov>=4.0",
|
|
74
|
+
# Used by integration tests for the instrumentor auto-discovery path.
|
|
75
|
+
"flask>=2.0",
|
|
76
|
+
"opentelemetry-instrumentation-flask>=0.48b0",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# `containarium-instrument` is always installed (D8) — it's a console
|
|
80
|
+
# script alias over `opentelemetry-instrument` that adds --dry-run +
|
|
81
|
+
# branding.
|
|
82
|
+
[project.scripts]
|
|
83
|
+
containarium-instrument = "containarium_telemetry._exec_shim:main"
|
|
84
|
+
|
|
85
|
+
# Entry points the OTel auto-instrumentation runtime discovers. When
|
|
86
|
+
# `opentelemetry-instrument` (or our `containarium-instrument` alias)
|
|
87
|
+
# runs, it loads our Distro + Configurator so that the init we run via
|
|
88
|
+
# direct API call also runs via the auto-instrument path.
|
|
89
|
+
[project.entry-points."opentelemetry_distro"]
|
|
90
|
+
containarium = "containarium_telemetry._distro:ContainariumDistro"
|
|
91
|
+
|
|
92
|
+
[project.entry-points."opentelemetry_configurator"]
|
|
93
|
+
containarium = "containarium_telemetry._distro:ContainariumConfigurator"
|
|
94
|
+
|
|
95
|
+
[tool.setuptools.packages.find]
|
|
96
|
+
where = ["src"]
|
|
97
|
+
|
|
98
|
+
[tool.setuptools.package-data]
|
|
99
|
+
containarium_telemetry = ["py.typed"]
|
|
100
|
+
|
|
101
|
+
[tool.pytest.ini_options]
|
|
102
|
+
testpaths = ["tests"]
|
|
103
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Containarium telemetry distro for Python.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
from containarium_telemetry import init, Shutdown
|
|
6
|
+
|
|
7
|
+
handle = init()
|
|
8
|
+
...
|
|
9
|
+
handle.shutdown(timeout_s=5.0)
|
|
10
|
+
|
|
11
|
+
See docs/TELEMETRY-DISTRO-DESIGN.md for the full contract.
|
|
12
|
+
"""
|
|
13
|
+
from ._init import Shutdown, init
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
|
|
16
|
+
__all__ = ["init", "Shutdown", "__version__"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Env-driven configuration for the distro.
|
|
2
|
+
|
|
3
|
+
The dataclass exists so tests can construct it directly without
|
|
4
|
+
monkey-patching os.environ — every other module in the distro reads
|
|
5
|
+
from a DistroConfig, never from os.getenv().
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class DistroConfig:
|
|
16
|
+
# OTel-standard env vars (the SDK also reads these directly; we
|
|
17
|
+
# capture them here for diagnostics and for code paths that need
|
|
18
|
+
# to branch on their presence).
|
|
19
|
+
endpoint: Optional[str]
|
|
20
|
+
service_name: Optional[str]
|
|
21
|
+
resource_attributes: Optional[str]
|
|
22
|
+
headers: Optional[str]
|
|
23
|
+
protocol: Optional[str]
|
|
24
|
+
|
|
25
|
+
# Containarium-stamped env vars (split form per
|
|
26
|
+
# docs/OTEL-AGENT-RELAY-DESIGN.md decision #5).
|
|
27
|
+
container_id: Optional[str]
|
|
28
|
+
backend_id: Optional[str]
|
|
29
|
+
tenant_id: Optional[str]
|
|
30
|
+
|
|
31
|
+
# Tenant-controlled, used for service.version stamping.
|
|
32
|
+
service_version: Optional[str]
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_env(cls, env: Optional[dict] = None) -> "DistroConfig":
|
|
36
|
+
e = env if env is not None else os.environ
|
|
37
|
+
|
|
38
|
+
def get(key: str) -> Optional[str]:
|
|
39
|
+
v = e.get(key)
|
|
40
|
+
if v is None or v == "":
|
|
41
|
+
return None
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
return cls(
|
|
45
|
+
endpoint=get("OTEL_EXPORTER_OTLP_ENDPOINT"),
|
|
46
|
+
service_name=get("OTEL_SERVICE_NAME"),
|
|
47
|
+
resource_attributes=get("OTEL_RESOURCE_ATTRIBUTES"),
|
|
48
|
+
headers=get("OTEL_EXPORTER_OTLP_HEADERS"),
|
|
49
|
+
protocol=get("OTEL_EXPORTER_OTLP_PROTOCOL"),
|
|
50
|
+
container_id=get("CONTAINARIUM_CONTAINER_ID"),
|
|
51
|
+
backend_id=get("CONTAINARIUM_BACKEND_ID"),
|
|
52
|
+
tenant_id=get("CONTAINARIUM_TENANT_ID"),
|
|
53
|
+
service_version=get("SERVICE_VERSION"),
|
|
54
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""OTel Distro + Configurator entry points for the auto-instrument path.
|
|
2
|
+
|
|
3
|
+
When the user runs `containarium-instrument python app.py` (or the
|
|
4
|
+
upstream `opentelemetry-instrument` with this package installed), the
|
|
5
|
+
OTel runtime loads:
|
|
6
|
+
|
|
7
|
+
1. ContainariumDistro._configure() — sets exporter env defaults
|
|
8
|
+
2. ContainariumConfigurator._configure() — runs our init()
|
|
9
|
+
3. Then iterates registered opentelemetry_instrumentor entry points
|
|
10
|
+
and calls .instrument() on each.
|
|
11
|
+
|
|
12
|
+
Our init() detects the auto-instrument context via the
|
|
13
|
+
_CONTAINARIUM_TELEMETRY_AUTO_INSTRUMENT env sentinel and skips its own
|
|
14
|
+
instrumentation-registration step so we don't double-instrument.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from opentelemetry.instrumentation.distro import BaseDistro
|
|
22
|
+
from opentelemetry.sdk._configuration import _BaseConfigurator
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("containarium_telemetry")
|
|
25
|
+
|
|
26
|
+
# Sentinel set by ContainariumConfigurator. init() reads it to decide
|
|
27
|
+
# whether to register instrumentors itself or defer to the runtime.
|
|
28
|
+
AUTO_INSTRUMENT_ENV_KEY = "_CONTAINARIUM_TELEMETRY_AUTO_INSTRUMENT"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ContainariumDistro(BaseDistro):
|
|
32
|
+
"""OTel Distro plugin — sets exporter env defaults.
|
|
33
|
+
|
|
34
|
+
setdefault throughout: the user's explicit env always wins. We're a
|
|
35
|
+
distro, not a policy enforcer.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def _configure(self, **kwargs) -> None:
|
|
39
|
+
# OTLP metrics by default — matches the central collector.
|
|
40
|
+
os.environ.setdefault("OTEL_METRICS_EXPORTER", "otlp")
|
|
41
|
+
# v1 collector is metrics-only (decision D4); muting trace + log
|
|
42
|
+
# export avoids the SDK fighting an empty endpoint. v2 flips
|
|
43
|
+
# these to "otlp" when Tempo + Loki land.
|
|
44
|
+
os.environ.setdefault("OTEL_TRACES_EXPORTER", "none")
|
|
45
|
+
os.environ.setdefault("OTEL_LOGS_EXPORTER", "none")
|
|
46
|
+
# Pin HTTP/protobuf — the protocol the central collector and
|
|
47
|
+
# otel-sidecar both speak.
|
|
48
|
+
os.environ.setdefault("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ContainariumConfigurator(_BaseConfigurator):
|
|
52
|
+
"""OTel Configurator plugin — installs MeterProvider via init().
|
|
53
|
+
|
|
54
|
+
Configurators are responsible for actually wiring up the SDK
|
|
55
|
+
providers (TracerProvider, MeterProvider, LoggerProvider). Only
|
|
56
|
+
one configurator runs in the auto-instrument flow — ours wins
|
|
57
|
+
because we register an entry point.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def _configure(self, **kwargs) -> None:
|
|
61
|
+
os.environ[AUTO_INSTRUMENT_ENV_KEY] = "1"
|
|
62
|
+
# Lazy import so the OTLP exporter module isn't loaded when
|
|
63
|
+
# only the Distro half of the entry-point pair fires.
|
|
64
|
+
from ._init import init
|
|
65
|
+
|
|
66
|
+
init()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Pretty-printer for the resolved telemetry config.
|
|
2
|
+
|
|
3
|
+
Used by `containarium-instrument --dry-run` (D12). Reports endpoint,
|
|
4
|
+
protocol, resource attrs, headers (bearer redacted by default), and
|
|
5
|
+
whether the pipeline would actually wire up given the env.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from ._config import DistroConfig
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
|
|
14
|
+
_REDACTED_HEADER_KEYS = frozenset({"authorization", "x-api-key"})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_config(config: DistroConfig, *, redact_bearer: bool = True) -> str:
|
|
18
|
+
"""Return a human-readable rendering of the resolved config."""
|
|
19
|
+
lines = [
|
|
20
|
+
"# containarium-telemetry resolved config",
|
|
21
|
+
f"distro_version : {__version__}",
|
|
22
|
+
f"endpoint : {config.endpoint or '<unset>'}",
|
|
23
|
+
f"protocol : {config.protocol or '<default: http/protobuf>'}",
|
|
24
|
+
f"service_name : {config.service_name or '<unset — SDK will default to unknown_service>'}",
|
|
25
|
+
f"resource_attributes : {config.resource_attributes or '<unset>'}",
|
|
26
|
+
f"headers : {_format_headers(config.headers, redact_bearer)}",
|
|
27
|
+
"",
|
|
28
|
+
"# Containarium identity (CONTAINARIUM_* env)",
|
|
29
|
+
f"container.id : {config.container_id or '<unset>'}",
|
|
30
|
+
f"backend.id : {config.backend_id or '<unset>'}",
|
|
31
|
+
f"tenant.id : {config.tenant_id or '<unset>'}",
|
|
32
|
+
f"service.version : {config.service_version or '<unset>'}",
|
|
33
|
+
"",
|
|
34
|
+
"# Defended distro stamp (always present, never overridable)",
|
|
35
|
+
f"containarium.distro : py/{__version__}",
|
|
36
|
+
"",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if config.endpoint:
|
|
40
|
+
lines.append("Status: telemetry pipeline will be configured.")
|
|
41
|
+
else:
|
|
42
|
+
lines.append("Status: TELEMETRY WILL BE NO-OP. Endpoint env not set; init() will fail-open.")
|
|
43
|
+
lines.append("Enable monitoring on this LXC with:")
|
|
44
|
+
lines.append(" containarium monitoring enable <username>")
|
|
45
|
+
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_headers(raw: Optional[str], redact: bool) -> str:
|
|
50
|
+
if not raw:
|
|
51
|
+
return "<unset>"
|
|
52
|
+
if not redact:
|
|
53
|
+
return raw
|
|
54
|
+
parts = []
|
|
55
|
+
for kv in raw.split(","):
|
|
56
|
+
if "=" not in kv:
|
|
57
|
+
parts.append(kv)
|
|
58
|
+
continue
|
|
59
|
+
k, _ = kv.split("=", 1)
|
|
60
|
+
if k.strip().lower() in _REDACTED_HEADER_KEYS:
|
|
61
|
+
parts.append(f"{k.strip()}=<redacted>")
|
|
62
|
+
else:
|
|
63
|
+
parts.append(kv)
|
|
64
|
+
return ",".join(parts)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""containarium-instrument console script.
|
|
2
|
+
|
|
3
|
+
Thin alias over `opentelemetry-instrument` (decision D8 — always
|
|
4
|
+
installed). Adds:
|
|
5
|
+
|
|
6
|
+
- `--dry-run`: prints resolved config and exits without launching
|
|
7
|
+
the app (D12).
|
|
8
|
+
- `--version` / `--help`: branding + usage.
|
|
9
|
+
|
|
10
|
+
Anything else execvp()s `opentelemetry-instrument` with the same args
|
|
11
|
+
and lets the upstream runtime drive the auto-instrument flow. Our
|
|
12
|
+
Distro + Configurator are registered as opentelemetry entry points, so
|
|
13
|
+
the upstream `opentelemetry-instrument` picks up our customizations
|
|
14
|
+
without us forking its argv parser.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
24
|
+
if argv is None:
|
|
25
|
+
argv = sys.argv[1:]
|
|
26
|
+
|
|
27
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
28
|
+
_print_help()
|
|
29
|
+
return 0 if argv else 2
|
|
30
|
+
|
|
31
|
+
if argv[0] in ("-V", "--version"):
|
|
32
|
+
from ._version import __version__
|
|
33
|
+
|
|
34
|
+
print(f"containarium-instrument {__version__}")
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
if argv[0] == "--dry-run":
|
|
38
|
+
return _dry_run()
|
|
39
|
+
|
|
40
|
+
# Delegate to upstream. execvp replaces the process so any state
|
|
41
|
+
# we set on the way in (e.g. env defaults from importing this
|
|
42
|
+
# module's deps) carries forward.
|
|
43
|
+
cmd = "opentelemetry-instrument"
|
|
44
|
+
try:
|
|
45
|
+
os.execvp(cmd, [cmd] + argv)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
print(
|
|
48
|
+
f"containarium-instrument: '{cmd}' not found in PATH. "
|
|
49
|
+
"Reinstall containarium-telemetry to pull the "
|
|
50
|
+
"opentelemetry-instrumentation dep, or pip install "
|
|
51
|
+
"opentelemetry-instrumentation directly.",
|
|
52
|
+
file=sys.stderr,
|
|
53
|
+
)
|
|
54
|
+
return 127
|
|
55
|
+
# Unreachable on success — execvp replaces the process.
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _dry_run() -> int:
|
|
60
|
+
from ._config import DistroConfig
|
|
61
|
+
from ._dry_run import format_config
|
|
62
|
+
|
|
63
|
+
config = DistroConfig.from_env()
|
|
64
|
+
print(format_config(config))
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _print_help() -> None:
|
|
69
|
+
print(
|
|
70
|
+
"""containarium-instrument — Run a Python app with the Containarium telemetry distro.
|
|
71
|
+
|
|
72
|
+
Usage:
|
|
73
|
+
containarium-instrument [--dry-run | --version | --help] <command> [args ...]
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--dry-run Print the resolved telemetry config (endpoint, resource
|
|
77
|
+
attrs, redacted headers, distro version) and exit. Does
|
|
78
|
+
NOT launch the wrapped command.
|
|
79
|
+
--version Print distro version and exit.
|
|
80
|
+
--help Show this help and exit.
|
|
81
|
+
|
|
82
|
+
Without a flag, exec()s opentelemetry-instrument with the same args.
|
|
83
|
+
Our Distro + Configurator are registered as OTel entry points, so
|
|
84
|
+
auto-instrumentation picks up the Containarium identity stamping and
|
|
85
|
+
OTLP/HTTP defaults transparently.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
containarium-instrument python app.py
|
|
89
|
+
containarium-instrument --dry-run
|
|
90
|
+
containarium-instrument python -m flask run
|
|
91
|
+
"""
|
|
92
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Distro init — the public entry point.
|
|
2
|
+
|
|
3
|
+
Contract (per docs/TELEMETRY-DISTRO-DESIGN.md):
|
|
4
|
+
- Always fail-open. Missing endpoint → WARN + no-op handle (D10).
|
|
5
|
+
- Idempotent. Repeat calls log at DEBUG and return the existing handle.
|
|
6
|
+
- The OTLPMetricExporter reads OTEL_EXPORTER_OTLP_{ENDPOINT,HEADERS,
|
|
7
|
+
PROTOCOL} from env directly, so we do not pass them as constructor
|
|
8
|
+
args — that would shadow user overrides we're meant to honor.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from typing import Dict, Optional
|
|
15
|
+
|
|
16
|
+
from opentelemetry import metrics, trace
|
|
17
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
18
|
+
OTLPMetricExporter,
|
|
19
|
+
)
|
|
20
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
21
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
22
|
+
from opentelemetry.trace import NoOpTracerProvider
|
|
23
|
+
|
|
24
|
+
from ._config import DistroConfig
|
|
25
|
+
from ._distro import AUTO_INSTRUMENT_ENV_KEY
|
|
26
|
+
from ._instrumentations import InstrumentationsArg, register_instrumentations
|
|
27
|
+
from ._resource import build_resource
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("containarium_telemetry")
|
|
30
|
+
|
|
31
|
+
_initialized: bool = False
|
|
32
|
+
_shutdown_handle: Optional["Shutdown"] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Shutdown:
|
|
36
|
+
"""Idempotent shutdown handle returned by init()."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, provider: Optional[MeterProvider]):
|
|
39
|
+
self._provider = provider
|
|
40
|
+
self._done = False
|
|
41
|
+
|
|
42
|
+
def shutdown(self, timeout_s: float = 5.0) -> None:
|
|
43
|
+
if self._done:
|
|
44
|
+
return
|
|
45
|
+
self._done = True
|
|
46
|
+
if self._provider is None:
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
self._provider.shutdown(timeout_millis=int(timeout_s * 1000))
|
|
50
|
+
except Exception as e: # noqa: BLE001 — never raise from shutdown
|
|
51
|
+
logger.warning("containarium_telemetry shutdown failed: %s", e)
|
|
52
|
+
|
|
53
|
+
def __call__(self, timeout_s: float = 5.0) -> None:
|
|
54
|
+
# Lets callers use the handle directly as `handle()` instead of
|
|
55
|
+
# `handle.shutdown()` — convenient with atexit.register().
|
|
56
|
+
self.shutdown(timeout_s)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def init(
|
|
60
|
+
service_name: Optional[str] = None,
|
|
61
|
+
extra_attrs: Optional[Dict[str, str]] = None,
|
|
62
|
+
instrumentations: InstrumentationsArg = "auto",
|
|
63
|
+
metric_export_interval_ms: int = 5_000,
|
|
64
|
+
metric_export_timeout_ms: int = 10_000,
|
|
65
|
+
) -> Shutdown:
|
|
66
|
+
"""Initialize the distro. Returns an idempotent Shutdown handle.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
service_name: Override OTEL_SERVICE_NAME if not already set.
|
|
70
|
+
extra_attrs: Extra resource attributes — win over env attrs
|
|
71
|
+
(precedence #5 in TELEMETRY-DISTRO-DESIGN.md).
|
|
72
|
+
instrumentations: "auto" (default — every installed
|
|
73
|
+
opentelemetry_instrumentor), "off", or a list of names.
|
|
74
|
+
Skipped when invoked from `containarium-instrument` /
|
|
75
|
+
`opentelemetry-instrument` (the runtime handles it).
|
|
76
|
+
metric_export_interval_ms: Periodic export tick. Default 5s,
|
|
77
|
+
matching the sidecar's batch processor.
|
|
78
|
+
metric_export_timeout_ms: Per-export timeout. Default 10s.
|
|
79
|
+
|
|
80
|
+
Fail-open: missing OTEL_EXPORTER_OTLP_ENDPOINT logs WARN and returns
|
|
81
|
+
a no-op handle. The app never crashes because telemetry isn't wired.
|
|
82
|
+
"""
|
|
83
|
+
global _initialized, _shutdown_handle
|
|
84
|
+
|
|
85
|
+
if _initialized:
|
|
86
|
+
logger.debug("init() called twice — returning existing handle")
|
|
87
|
+
return _shutdown_handle # type: ignore[return-value]
|
|
88
|
+
|
|
89
|
+
if service_name:
|
|
90
|
+
# setdefault — explicit user env still wins over the arg.
|
|
91
|
+
os.environ.setdefault("OTEL_SERVICE_NAME", service_name)
|
|
92
|
+
|
|
93
|
+
config = DistroConfig.from_env()
|
|
94
|
+
|
|
95
|
+
if not config.endpoint:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"containarium_telemetry: OTEL_EXPORTER_OTLP_ENDPOINT not set; "
|
|
98
|
+
"telemetry will be a no-op. Enable monitoring on the LXC with "
|
|
99
|
+
"`containarium monitoring enable <username>`."
|
|
100
|
+
)
|
|
101
|
+
_shutdown_handle = Shutdown(None)
|
|
102
|
+
_initialized = True
|
|
103
|
+
return _shutdown_handle
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
resource = build_resource(config, extra_attrs=extra_attrs)
|
|
107
|
+
exporter = OTLPMetricExporter()
|
|
108
|
+
reader = PeriodicExportingMetricReader(
|
|
109
|
+
exporter,
|
|
110
|
+
export_interval_millis=metric_export_interval_ms,
|
|
111
|
+
export_timeout_millis=metric_export_timeout_ms,
|
|
112
|
+
)
|
|
113
|
+
provider = MeterProvider(resource=resource, metric_readers=[reader])
|
|
114
|
+
metrics.set_meter_provider(provider)
|
|
115
|
+
except Exception as e: # noqa: BLE001 — fail-open per contract
|
|
116
|
+
logger.warning("containarium_telemetry init failed: %s", e)
|
|
117
|
+
_shutdown_handle = Shutdown(None)
|
|
118
|
+
_initialized = True
|
|
119
|
+
return _shutdown_handle
|
|
120
|
+
|
|
121
|
+
# No-op tracer provider so apps that call trace.get_tracer(...)
|
|
122
|
+
# don't crash — v1 collector accepts metrics only (D4). The v2
|
|
123
|
+
# traces pipeline will replace this with a real provider.
|
|
124
|
+
_set_noop_tracer_provider_if_unset()
|
|
125
|
+
|
|
126
|
+
# Skip instrumentation registration when invoked from the
|
|
127
|
+
# auto-instrument runtime (containarium-instrument /
|
|
128
|
+
# opentelemetry-instrument) — the runtime registers them itself
|
|
129
|
+
# after we return.
|
|
130
|
+
if os.environ.get(AUTO_INSTRUMENT_ENV_KEY) != "1":
|
|
131
|
+
register_instrumentations(instrumentations)
|
|
132
|
+
|
|
133
|
+
_shutdown_handle = Shutdown(provider)
|
|
134
|
+
_initialized = True
|
|
135
|
+
return _shutdown_handle
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _set_noop_tracer_provider_if_unset() -> None:
|
|
139
|
+
"""Install NoOpTracerProvider only if no real one is configured.
|
|
140
|
+
|
|
141
|
+
OTel's default `ProxyTracerProvider` already returns no-op spans
|
|
142
|
+
when nothing has been set, so this is mostly belt-and-braces — but
|
|
143
|
+
it makes the v1→v2 transition cleaner (one call to flip).
|
|
144
|
+
"""
|
|
145
|
+
current = trace.get_tracer_provider()
|
|
146
|
+
# Only stamp if nothing real has been installed yet. Don't clobber
|
|
147
|
+
# an app that already wired its own tracer.
|
|
148
|
+
if type(current).__name__ == "ProxyTracerProvider":
|
|
149
|
+
trace.set_tracer_provider(NoOpTracerProvider())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _reset_for_tests() -> None:
|
|
153
|
+
"""Reset module-level init state. Tests only — not part of the API."""
|
|
154
|
+
global _initialized, _shutdown_handle
|
|
155
|
+
_initialized = False
|
|
156
|
+
_shutdown_handle = None
|