argus-obs-sdk 1.0.0a3__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.
- argus_obs_sdk-1.0.0a3/.gitignore +30 -0
- argus_obs_sdk-1.0.0a3/PKG-INFO +132 -0
- argus_obs_sdk-1.0.0a3/README.md +69 -0
- argus_obs_sdk-1.0.0a3/pyproject.toml +59 -0
- argus_obs_sdk-1.0.0a3/src/argus/__init__.py +251 -0
- argus_obs_sdk-1.0.0a3/src/argus/_config.py +162 -0
- argus_obs_sdk-1.0.0a3/src/argus/_logging.py +193 -0
- argus_obs_sdk-1.0.0a3/src/argus/_metrics.py +155 -0
- argus_obs_sdk-1.0.0a3/src/argus/_resource.py +87 -0
- argus_obs_sdk-1.0.0a3/src/argus/_tracing.py +113 -0
- argus_obs_sdk-1.0.0a3/src/argus/asgi.py +166 -0
- argus_obs_sdk-1.0.0a3/src/argus/autoinst.py +121 -0
- argus_obs_sdk-1.0.0a3/src/argus/propagate.py +142 -0
- argus_obs_sdk-1.0.0a3/src/argus/py.typed +0 -0
- argus_obs_sdk-1.0.0a3/tests/test_asgi_trust.py +137 -0
- argus_obs_sdk-1.0.0a3/tests/test_contract.py +302 -0
- argus_obs_sdk-1.0.0a3/tests/test_propagation.py +111 -0
- argus_obs_sdk-1.0.0a3/tests/test_resource.py +42 -0
- argus_obs_sdk-1.0.0a3/tests/test_resource_identity.py +74 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.env.*
|
|
3
|
+
!.env.example
|
|
4
|
+
!.env.*.example
|
|
5
|
+
*.env.local
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
.venv/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.DS_Store
|
|
15
|
+
|
|
16
|
+
# Configuracion del dead man's switch: lleva credenciales de aviso.
|
|
17
|
+
deadman/deadman.json
|
|
18
|
+
|
|
19
|
+
# Secretos montados en contenedores. Nunca al repositorio.
|
|
20
|
+
platform/secrets/
|
|
21
|
+
|
|
22
|
+
# El índice se GENERA desde dist/ con scripts/construir_indice.py.
|
|
23
|
+
# Versionar ruedas es versionar binarios que ya sabemos reproducir.
|
|
24
|
+
platform/indice/simple/
|
|
25
|
+
|
|
26
|
+
# La coordinacion con otros equipos vive FUERA del repositorio, en
|
|
27
|
+
# ~/Documents/Victor/<equipo>_argus/. Aqui hay incidentes suyos, rutas de su
|
|
28
|
+
# codigo y sus identificadores de backlog: es informacion de ellos y este
|
|
29
|
+
# repositorio es publico. Ver docs/coordinacion.md.
|
|
30
|
+
docs/solicitudes/
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: argus-obs-sdk
|
|
3
|
+
Version: 1.0.0a3
|
|
4
|
+
Summary: SDK de observabilidad de Argus. Para APLICACIONES: configura trazas, metricas y logs con una linea.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Root1V/argus-obs
|
|
6
|
+
Project-URL: Repository, https://github.com/Root1V/argus-obs
|
|
7
|
+
Project-URL: Issues, https://github.com/Root1V/argus-obs/issues
|
|
8
|
+
Author: Emeric Espiritu
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: genai,llm,logs,metrics,observability,opentelemetry,tracing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: System :: Monitoring
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: argus-obs-semconv<2,>=1.0.0a3
|
|
21
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2,>=1.30
|
|
22
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.30
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30; extra == 'all'
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.51b0; extra == 'all'
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-asgi>=0.51b0; extra == 'all'
|
|
27
|
+
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.51b0; extra == 'all'
|
|
28
|
+
Requires-Dist: opentelemetry-instrumentation-celery>=0.51b0; extra == 'all'
|
|
29
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.51b0; extra == 'all'
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.51b0; extra == 'all'
|
|
31
|
+
Requires-Dist: opentelemetry-instrumentation-redis>=0.51b0; extra == 'all'
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.51b0; extra == 'all'
|
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.51b0; extra == 'all'
|
|
34
|
+
Requires-Dist: structlog>=25.1; extra == 'all'
|
|
35
|
+
Provides-Extra: asgi
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-asgi>=0.51b0; extra == 'asgi'
|
|
37
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.51b0; extra == 'asgi'
|
|
38
|
+
Provides-Extra: celery
|
|
39
|
+
Requires-Dist: opentelemetry-instrumentation-celery>=0.51b0; extra == 'celery'
|
|
40
|
+
Provides-Extra: client
|
|
41
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.51b0; extra == 'client'
|
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.51b0; extra == 'client'
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
47
|
+
Requires-Dist: pyyaml>=6; extra == 'dev'
|
|
48
|
+
Requires-Dist: starlette>=0.37; extra == 'dev'
|
|
49
|
+
Provides-Extra: genai
|
|
50
|
+
Requires-Dist: openlit>=1.34; extra == 'genai'
|
|
51
|
+
Provides-Extra: http
|
|
52
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30; extra == 'http'
|
|
53
|
+
Provides-Extra: kafka
|
|
54
|
+
Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.51b0; extra == 'kafka'
|
|
55
|
+
Provides-Extra: logging
|
|
56
|
+
Requires-Dist: structlog>=25.1; extra == 'logging'
|
|
57
|
+
Provides-Extra: redis
|
|
58
|
+
Requires-Dist: opentelemetry-instrumentation-redis>=0.51b0; extra == 'redis'
|
|
59
|
+
Provides-Extra: sql
|
|
60
|
+
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.51b0; extra == 'sql'
|
|
61
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.51b0; extra == 'sql'
|
|
62
|
+
Description-Content-Type: text/markdown
|
|
63
|
+
|
|
64
|
+
# argus-sdk
|
|
65
|
+
|
|
66
|
+
SDK de observabilidad de Argus para **aplicaciones**.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import argus
|
|
70
|
+
argus.init()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Eso configura resource, trazas, métricas, logs, propagadores y todas las
|
|
74
|
+
auto-instrumentaciones disponibles, leyendo la configuración del entorno.
|
|
75
|
+
|
|
76
|
+
Para servicios ASGI, una línea más:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
app.add_middleware(argus.middleware(app).__class__) # o ASGIMiddleware directo
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Si escribes una librería, no uses este paquete
|
|
83
|
+
|
|
84
|
+
Usa [`argus-semconv`](../argus-semconv), que depende solo de
|
|
85
|
+
`opentelemetry-api`. Una librería que depende del SDK impone en silencio su
|
|
86
|
+
versión del SDK y sus opiniones sobre exportadores a todo el que dependa de
|
|
87
|
+
ella.
|
|
88
|
+
|
|
89
|
+
## El contrato
|
|
90
|
+
|
|
91
|
+
Verificado por tests en `tests/test_contract.py`, no por buenas intenciones:
|
|
92
|
+
|
|
93
|
+
1. **Nunca tumba la aplicación.** Si el Collector está caído, la app pierde
|
|
94
|
+
telemetría, nunca latencia ni memoria.
|
|
95
|
+
2. **Idempotente.** `init()` dos veces es no-op la segunda.
|
|
96
|
+
3. **No-op sin configurar.** Los decoradores funcionan con coste cero.
|
|
97
|
+
4. **Cero configuración en el caso normal.** Todo viene del entorno.
|
|
98
|
+
5. **Superficie pública mínima**, fijada por test.
|
|
99
|
+
|
|
100
|
+
## Variables de entorno
|
|
101
|
+
|
|
102
|
+
El contrato es de entorno, no de API de Python: un componente Go y uno Python
|
|
103
|
+
se configuran copiando el mismo bloque.
|
|
104
|
+
|
|
105
|
+
| Variable | Significado | Por defecto |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `ARGUS_SERVICE` | El sub-componente (`service.name`) | `unknown-service` |
|
|
108
|
+
| `ARGUS_NAMESPACE` | La aplicación (`service.namespace`) | = servicio |
|
|
109
|
+
| `ARGUS_ROLE` | `api`/`worker`/`scheduler`/`cli`/`model-server`/`frontend` | `api` |
|
|
110
|
+
| `ARGUS_VERSION` | Versión del componente | — |
|
|
111
|
+
| `ARGUS_ENVIRONMENT` | `mac-dev`, `imac`, `server-1`, `ci` | `local` |
|
|
112
|
+
| `ARGUS_ENDPOINT` | Collector **agente local** | `http://localhost:4317` |
|
|
113
|
+
| `ARGUS_PROTOCOL` | `grpc` \| `http/protobuf` | `grpc` |
|
|
114
|
+
| `ARGUS_PROPAGATE` | `never` \| `trusted` \| `always` | `never` |
|
|
115
|
+
| `ARGUS_TRUSTED_CIDRS` | CIDRs de confianza, separados por coma | — |
|
|
116
|
+
| `ARGUS_CAPTURE_CONTENT` | Capturar prompts y respuestas | `false` |
|
|
117
|
+
| `ARGUS_SLO_MS` | Umbral de latencia del componente | `0` (sin umbral) |
|
|
118
|
+
| `ARGUS_DISABLED` | Apagar toda la telemetría | `false` |
|
|
119
|
+
|
|
120
|
+
Las estándar de OpenTelemetry (`OTEL_SERVICE_NAME`,
|
|
121
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_RESOURCE_ATTRIBUTES`) también se respetan,
|
|
122
|
+
para que una app que ya tiene OTel migre sin tocar código.
|
|
123
|
+
|
|
124
|
+
## Por qué `localhost` y no el plano central
|
|
125
|
+
|
|
126
|
+
Las aplicaciones exportan **siempre** al Collector agente de su propia máquina.
|
|
127
|
+
Nunca conocen la dirección del plano central. Eso da tres propiedades:
|
|
128
|
+
|
|
129
|
+
- Mover el plano central de una máquina a otra no toca ni una aplicación.
|
|
130
|
+
- Si el central está suspendido, las apps no se enteran ni se ralentizan: el
|
|
131
|
+
agente local escribe a disco y envía cuando vuelve la conexión.
|
|
132
|
+
- Añadir una máquina es desplegar un agente, no reconfigurar apps.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# argus-sdk
|
|
2
|
+
|
|
3
|
+
SDK de observabilidad de Argus para **aplicaciones**.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import argus
|
|
7
|
+
argus.init()
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Eso configura resource, trazas, métricas, logs, propagadores y todas las
|
|
11
|
+
auto-instrumentaciones disponibles, leyendo la configuración del entorno.
|
|
12
|
+
|
|
13
|
+
Para servicios ASGI, una línea más:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
app.add_middleware(argus.middleware(app).__class__) # o ASGIMiddleware directo
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Si escribes una librería, no uses este paquete
|
|
20
|
+
|
|
21
|
+
Usa [`argus-semconv`](../argus-semconv), que depende solo de
|
|
22
|
+
`opentelemetry-api`. Una librería que depende del SDK impone en silencio su
|
|
23
|
+
versión del SDK y sus opiniones sobre exportadores a todo el que dependa de
|
|
24
|
+
ella.
|
|
25
|
+
|
|
26
|
+
## El contrato
|
|
27
|
+
|
|
28
|
+
Verificado por tests en `tests/test_contract.py`, no por buenas intenciones:
|
|
29
|
+
|
|
30
|
+
1. **Nunca tumba la aplicación.** Si el Collector está caído, la app pierde
|
|
31
|
+
telemetría, nunca latencia ni memoria.
|
|
32
|
+
2. **Idempotente.** `init()` dos veces es no-op la segunda.
|
|
33
|
+
3. **No-op sin configurar.** Los decoradores funcionan con coste cero.
|
|
34
|
+
4. **Cero configuración en el caso normal.** Todo viene del entorno.
|
|
35
|
+
5. **Superficie pública mínima**, fijada por test.
|
|
36
|
+
|
|
37
|
+
## Variables de entorno
|
|
38
|
+
|
|
39
|
+
El contrato es de entorno, no de API de Python: un componente Go y uno Python
|
|
40
|
+
se configuran copiando el mismo bloque.
|
|
41
|
+
|
|
42
|
+
| Variable | Significado | Por defecto |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `ARGUS_SERVICE` | El sub-componente (`service.name`) | `unknown-service` |
|
|
45
|
+
| `ARGUS_NAMESPACE` | La aplicación (`service.namespace`) | = servicio |
|
|
46
|
+
| `ARGUS_ROLE` | `api`/`worker`/`scheduler`/`cli`/`model-server`/`frontend` | `api` |
|
|
47
|
+
| `ARGUS_VERSION` | Versión del componente | — |
|
|
48
|
+
| `ARGUS_ENVIRONMENT` | `mac-dev`, `imac`, `server-1`, `ci` | `local` |
|
|
49
|
+
| `ARGUS_ENDPOINT` | Collector **agente local** | `http://localhost:4317` |
|
|
50
|
+
| `ARGUS_PROTOCOL` | `grpc` \| `http/protobuf` | `grpc` |
|
|
51
|
+
| `ARGUS_PROPAGATE` | `never` \| `trusted` \| `always` | `never` |
|
|
52
|
+
| `ARGUS_TRUSTED_CIDRS` | CIDRs de confianza, separados por coma | — |
|
|
53
|
+
| `ARGUS_CAPTURE_CONTENT` | Capturar prompts y respuestas | `false` |
|
|
54
|
+
| `ARGUS_SLO_MS` | Umbral de latencia del componente | `0` (sin umbral) |
|
|
55
|
+
| `ARGUS_DISABLED` | Apagar toda la telemetría | `false` |
|
|
56
|
+
|
|
57
|
+
Las estándar de OpenTelemetry (`OTEL_SERVICE_NAME`,
|
|
58
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_RESOURCE_ATTRIBUTES`) también se respetan,
|
|
59
|
+
para que una app que ya tiene OTel migre sin tocar código.
|
|
60
|
+
|
|
61
|
+
## Por qué `localhost` y no el plano central
|
|
62
|
+
|
|
63
|
+
Las aplicaciones exportan **siempre** al Collector agente de su propia máquina.
|
|
64
|
+
Nunca conocen la dirección del plano central. Eso da tres propiedades:
|
|
65
|
+
|
|
66
|
+
- Mover el plano central de una máquina a otra no toca ni una aplicación.
|
|
67
|
+
- Si el central está suspendido, las apps no se enteran ni se ralentizan: el
|
|
68
|
+
agente local escribe a disco y envía cuando vuelve la conexión.
|
|
69
|
+
- Añadir una máquina es desplegar un agente, no reconfigurar apps.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "argus-obs-sdk"
|
|
3
|
+
version = "1.0.0a3"
|
|
4
|
+
description = "SDK de observabilidad de Argus. Para APLICACIONES: configura trazas, metricas y logs con una linea."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Emeric Espiritu" }]
|
|
9
|
+
keywords = ["observability", "opentelemetry", "tracing", "metrics", "logs", "genai", "llm"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: System :: Monitoring",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Nucleo deliberadamente pequeño. Un Temporal activity o un CLI no deben
|
|
22
|
+
# arrastrar Starlette, y una app que no habla con Kafka no debe instalar su
|
|
23
|
+
# instrumentacion. Todo lo demas va en extras.
|
|
24
|
+
dependencies = [
|
|
25
|
+
"argus-obs-semconv>=1.0.0a3,<2",
|
|
26
|
+
"opentelemetry-sdk>=1.30,<2",
|
|
27
|
+
"opentelemetry-exporter-otlp-proto-grpc>=1.30,<2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
http = ["opentelemetry-exporter-otlp-proto-http>=1.30,<2"]
|
|
32
|
+
asgi = ["opentelemetry-instrumentation-asgi>=0.51b0", "opentelemetry-instrumentation-fastapi>=0.51b0"]
|
|
33
|
+
client = ["opentelemetry-instrumentation-httpx>=0.51b0", "opentelemetry-instrumentation-requests>=0.51b0"]
|
|
34
|
+
sql = ["opentelemetry-instrumentation-sqlalchemy>=0.51b0", "opentelemetry-instrumentation-asyncpg>=0.51b0"]
|
|
35
|
+
redis = ["opentelemetry-instrumentation-redis>=0.51b0"]
|
|
36
|
+
celery = ["opentelemetry-instrumentation-celery>=0.51b0"]
|
|
37
|
+
kafka = ["opentelemetry-instrumentation-aiokafka>=0.51b0"]
|
|
38
|
+
logging = ["structlog>=25.1"]
|
|
39
|
+
genai = ["openlit>=1.34"]
|
|
40
|
+
all = ["argus-obs-sdk[http,asgi,client,sql,redis,celery,kafka,logging]"]
|
|
41
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "httpx>=0.27", "starlette>=0.37", "pyyaml>=6"]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/argus"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.metadata]
|
|
51
|
+
allow-direct-references = true
|
|
52
|
+
|
|
53
|
+
# Quien encuentre esto en PyPI tiene derecho a saber de donde sale y donde
|
|
54
|
+
# reportar. Un paquete sin origen publico es la misma opacidad de cadena de
|
|
55
|
+
# suministro que nos objeto un equipo consumidor (D-075).
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://github.com/Root1V/argus-obs"
|
|
58
|
+
Repository = "https://github.com/Root1V/argus-obs"
|
|
59
|
+
Issues = "https://github.com/Root1V/argus-obs/issues"
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Argus SDK — observabilidad para APLICACIONES.
|
|
2
|
+
|
|
3
|
+
import argus
|
|
4
|
+
argus.init()
|
|
5
|
+
|
|
6
|
+
Eso configura resource, trazas, metricas, logs, propagadores y todas las
|
|
7
|
+
auto-instrumentaciones disponibles, leyendo la configuracion del entorno.
|
|
8
|
+
|
|
9
|
+
Si escribes una LIBRERIA y no una aplicacion, no uses este paquete: usa
|
|
10
|
+
`argus-semconv`, que depende solo de la API de OpenTelemetry y por tanto no
|
|
11
|
+
impone un SDK ni exportadores a quien te importe.
|
|
12
|
+
|
|
13
|
+
CONTRATO DE DISENO (verificado por tests, no por buenas intenciones):
|
|
14
|
+
|
|
15
|
+
1. Nunca tumba la aplicacion. Si algo falla al inicializar, se degrada a no-op
|
|
16
|
+
con un aviso. Las colas son acotadas y descartan al llenarse: si el Collector
|
|
17
|
+
cae, la app pierde telemetria, nunca latencia ni memoria.
|
|
18
|
+
2. Idempotente. `init()` dos veces es no-op la segunda.
|
|
19
|
+
3. No-op si no esta configurada. Los decoradores funcionan con coste cero.
|
|
20
|
+
4. Cero configuracion en el caso normal. Los argumentos son para sobrescribir.
|
|
21
|
+
5. Superficie publica minima, fijada por test.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import atexit
|
|
27
|
+
import logging
|
|
28
|
+
import warnings
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
# Re-exportamos la superficie de convenciones para que una aplicacion solo
|
|
33
|
+
# tenga que importar `argus`.
|
|
34
|
+
from argus_semconv import (
|
|
35
|
+
GenAISpan,
|
|
36
|
+
Step,
|
|
37
|
+
agent,
|
|
38
|
+
attributes,
|
|
39
|
+
capture_enabled,
|
|
40
|
+
documents,
|
|
41
|
+
genai,
|
|
42
|
+
instrument,
|
|
43
|
+
mask,
|
|
44
|
+
retrieval,
|
|
45
|
+
step,
|
|
46
|
+
tool,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from . import autoinst, propagate
|
|
50
|
+
from ._config import Config, TrustMode
|
|
51
|
+
from ._logging import configure_logging, emit_wide_event
|
|
52
|
+
from ._metrics import GenAIMetrics, configure_metrics
|
|
53
|
+
from ._resource import build_resource
|
|
54
|
+
from ._tracing import configure_tracing
|
|
55
|
+
from .asgi import ASGIMiddleware
|
|
56
|
+
|
|
57
|
+
# De los metadatos del paquete instalado, no de una constante: una constante se
|
|
58
|
+
# queda obsoleta en cuanto se publica una version y nadie se entera.
|
|
59
|
+
try:
|
|
60
|
+
from importlib.metadata import version as _version
|
|
61
|
+
|
|
62
|
+
__version__ = _version("argus-obs-sdk")
|
|
63
|
+
except Exception: # noqa: BLE001 - sin instalar (ejecucion desde el arbol)
|
|
64
|
+
__version__ = "0.0.0.dev0"
|
|
65
|
+
|
|
66
|
+
_HANDLE: Argus | None = None
|
|
67
|
+
_log = logging.getLogger("argus")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class Argus:
|
|
72
|
+
"""Handle devuelto por `init()`.
|
|
73
|
+
|
|
74
|
+
Devolver un handle en vez de `None` no es un capricho: los procesos cortos
|
|
75
|
+
(CLIs, activities, trabajos por lotes) necesitan `force_flush()` antes de
|
|
76
|
+
salir, o la telemetria muere con el proceso sin exportarse.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
config: Config
|
|
80
|
+
tracer_provider: Any = None
|
|
81
|
+
meter_provider: Any = None
|
|
82
|
+
instrumented: tuple[str, ...] = ()
|
|
83
|
+
genai_metrics: GenAIMetrics | None = None
|
|
84
|
+
|
|
85
|
+
def force_flush(self, timeout_millis: int = 5_000) -> None:
|
|
86
|
+
for provider in (self.tracer_provider, self.meter_provider):
|
|
87
|
+
if provider is None:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
provider.force_flush(timeout_millis)
|
|
91
|
+
except Exception: # noqa: BLE001
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def shutdown(self) -> None:
|
|
95
|
+
self.force_flush()
|
|
96
|
+
for provider in (self.tracer_provider, self.meter_provider):
|
|
97
|
+
if provider is None:
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
provider.shutdown()
|
|
101
|
+
except Exception: # noqa: BLE001
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def init(
|
|
106
|
+
service: str | None = None,
|
|
107
|
+
*,
|
|
108
|
+
namespace: str | None = None,
|
|
109
|
+
version: str | None = None,
|
|
110
|
+
role: str | None = None,
|
|
111
|
+
environment: str | None = None,
|
|
112
|
+
endpoint: str | None = None,
|
|
113
|
+
propagate_mode: TrustMode | None = None,
|
|
114
|
+
slo_ms: int | None = None,
|
|
115
|
+
resource_attributes: dict[str, object] | None = None,
|
|
116
|
+
logging_level: int | str | None = None,
|
|
117
|
+
json_logs: bool = True,
|
|
118
|
+
auto_instrument: bool = True,
|
|
119
|
+
skip_instrumentation: frozenset[str] = frozenset(),
|
|
120
|
+
disabled: bool | None = None,
|
|
121
|
+
) -> Argus:
|
|
122
|
+
"""Inicializa la telemetria. Idempotente y a prueba de fallos.
|
|
123
|
+
|
|
124
|
+
Todos los argumentos son opcionales: el caso normal es `argus.init()` y que
|
|
125
|
+
la configuracion venga del entorno, que es lo que permite configurar un
|
|
126
|
+
componente Python y uno Go copiando el mismo bloque de variables.
|
|
127
|
+
"""
|
|
128
|
+
global _HANDLE
|
|
129
|
+
|
|
130
|
+
if _HANDLE is not None:
|
|
131
|
+
warnings.warn(
|
|
132
|
+
"argus.init() ya se habia llamado en este proceso. La segunda "
|
|
133
|
+
"llamada es un no-op.",
|
|
134
|
+
RuntimeWarning,
|
|
135
|
+
stacklevel=2,
|
|
136
|
+
)
|
|
137
|
+
return _HANDLE
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
cfg = Config.from_env(
|
|
141
|
+
service,
|
|
142
|
+
namespace=namespace,
|
|
143
|
+
version=version,
|
|
144
|
+
role=role,
|
|
145
|
+
environment=environment,
|
|
146
|
+
endpoint=endpoint,
|
|
147
|
+
propagate=propagate_mode,
|
|
148
|
+
slo_ms=slo_ms,
|
|
149
|
+
disabled=disabled,
|
|
150
|
+
)
|
|
151
|
+
except Exception as exc: # noqa: BLE001
|
|
152
|
+
warnings.warn(f"Argus: configuracion invalida ({exc}). Telemetria desactivada.", RuntimeWarning, stacklevel=2)
|
|
153
|
+
_HANDLE = Argus(config=Config(service=service or "unknown-service", disabled=True))
|
|
154
|
+
return _HANDLE
|
|
155
|
+
|
|
156
|
+
handle = Argus(config=cfg)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
resource = build_resource(cfg, resource_attributes)
|
|
160
|
+
handle.tracer_provider = configure_tracing(cfg, resource)
|
|
161
|
+
handle.meter_provider = configure_metrics(cfg, resource)
|
|
162
|
+
configure_logging(cfg, level=logging_level, force_json=json_logs)
|
|
163
|
+
|
|
164
|
+
if handle.meter_provider is not None:
|
|
165
|
+
handle.genai_metrics = GenAIMetrics()
|
|
166
|
+
|
|
167
|
+
if auto_instrument:
|
|
168
|
+
activated = autoinst.activate(skip=skip_instrumentation)
|
|
169
|
+
activated += autoinst.activate_genai(skip=skip_instrumentation)
|
|
170
|
+
autoinst.run_hooks()
|
|
171
|
+
handle.instrumented = tuple(activated)
|
|
172
|
+
|
|
173
|
+
except Exception as exc: # noqa: BLE001 - regla 1: nunca tumbar la app
|
|
174
|
+
warnings.warn(
|
|
175
|
+
f"Argus: la inicializacion fallo ({exc}). La aplicacion continua "
|
|
176
|
+
"sin telemetria.",
|
|
177
|
+
RuntimeWarning,
|
|
178
|
+
stacklevel=2,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
atexit.register(handle.shutdown)
|
|
182
|
+
_HANDLE = handle
|
|
183
|
+
|
|
184
|
+
_log.info(
|
|
185
|
+
"argus.init",
|
|
186
|
+
extra={
|
|
187
|
+
"app": cfg.namespace,
|
|
188
|
+
"service": cfg.service,
|
|
189
|
+
"role": cfg.role,
|
|
190
|
+
"environment": cfg.environment,
|
|
191
|
+
"endpoint": cfg.endpoint if not cfg.disabled else "disabled",
|
|
192
|
+
"propagate": cfg.propagate,
|
|
193
|
+
"instrumented": ",".join(handle.instrumented),
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
return handle
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def handle() -> Argus | None:
|
|
200
|
+
"""El handle actual, si `init()` ya se llamo."""
|
|
201
|
+
return _HANDLE
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def middleware(app: Any, **kwargs: Any) -> ASGIMiddleware:
|
|
205
|
+
"""Envuelve una app ASGI heredando la configuracion de `init()`."""
|
|
206
|
+
cfg = _HANDLE.config if _HANDLE else Config.from_env()
|
|
207
|
+
kwargs.setdefault("service", cfg.service)
|
|
208
|
+
kwargs.setdefault("propagate_mode", cfg.propagate)
|
|
209
|
+
kwargs.setdefault("trusted_cidrs", cfg.trusted_cidrs)
|
|
210
|
+
kwargs.setdefault("slo_ms", cfg.slo_ms)
|
|
211
|
+
return ASGIMiddleware(app, **kwargs)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _reset_for_tests() -> None:
|
|
215
|
+
global _HANDLE
|
|
216
|
+
from . import _logging
|
|
217
|
+
|
|
218
|
+
_logging.reset_for_tests()
|
|
219
|
+
_HANDLE = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = [
|
|
223
|
+
# Arranque
|
|
224
|
+
"init",
|
|
225
|
+
"handle",
|
|
226
|
+
"Argus",
|
|
227
|
+
"Config",
|
|
228
|
+
# ASGI
|
|
229
|
+
"ASGIMiddleware",
|
|
230
|
+
"middleware",
|
|
231
|
+
# Convenciones (re-exportadas de argus-semconv)
|
|
232
|
+
"genai",
|
|
233
|
+
"retrieval",
|
|
234
|
+
"tool",
|
|
235
|
+
"agent",
|
|
236
|
+
"documents",
|
|
237
|
+
"step",
|
|
238
|
+
"instrument",
|
|
239
|
+
"GenAISpan",
|
|
240
|
+
"Step",
|
|
241
|
+
"attributes",
|
|
242
|
+
"capture_enabled",
|
|
243
|
+
"mask",
|
|
244
|
+
# Propagacion fuera de HTTP
|
|
245
|
+
"propagate",
|
|
246
|
+
"autoinst",
|
|
247
|
+
# Utilidades
|
|
248
|
+
"emit_wide_event",
|
|
249
|
+
"GenAIMetrics",
|
|
250
|
+
"__version__",
|
|
251
|
+
]
|