argus-obs-semconv 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.
@@ -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,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: argus-obs-semconv
3
+ Version: 1.0.0a3
4
+ Summary: Convenciones semanticas de Argus. Para LIBRERIAS: depende solo de la API de OpenTelemetry.
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,observability,opentelemetry,semantic-conventions
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: opentelemetry-api<2,>=1.30
21
+ Provides-Extra: dev
22
+ Requires-Dist: opentelemetry-sdk<2,>=1.30; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: pyyaml>=6; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # argus-semconv
29
+
30
+ Convenciones semánticas de Argus para **librerías**.
31
+
32
+ ## Cuándo usar este paquete y cuándo no
33
+
34
+ | Escribes… | Importa | Por qué |
35
+ |---|---|---|
36
+ | Una **librería** (Axonium, synaptum, un SDK tuyo) | `argus-semconv` | Solo depende de `opentelemetry-api`. No impone SDK ni exportadores a quien te use |
37
+ | Una **aplicación** (servicio, worker, CLI) | `argus-sdk` | Es quien configura e inicializa la telemetría |
38
+
39
+ ## La garantía
40
+
41
+ Si la aplicación que importa tu librería **no** ha inicializado un SDK de
42
+ OpenTelemetry, toda la instrumentación de este paquete es **no-op con coste
43
+ cero**. Si **sí** lo ha inicializado, se enciende sola usando *su*
44
+ configuración, *su* endpoint y *su* muestreo.
45
+
46
+ Eso significa que puedes instrumentar tus librerías sin imponer nada a nadie.
47
+
48
+ ```python
49
+ from argus_semconv import genai
50
+
51
+ # En Axonium, dentro del cliente de inferencia:
52
+ with genai("chat", provider="ollama", request_model=model) as g:
53
+ resp = await self._call_backend(...)
54
+ g.usage(input_tokens=resp.prompt_tokens, output_tokens=resp.completion_tokens)
55
+ g.backend(backend_id=backend.id, ttft_ms=resp.ttft_ms)
56
+ ```
57
+
58
+ ## Las constantes están generadas
59
+
60
+ `attributes.py` se genera desde `libs/semconv-model/argus.yaml`. No lo edites a
61
+ mano: ejecuta `python tools/gen_semconv.py`. Un test de CI verifica que no hay
62
+ deriva entre el modelo y lo commiteado.
@@ -0,0 +1,35 @@
1
+ # argus-semconv
2
+
3
+ Convenciones semánticas de Argus para **librerías**.
4
+
5
+ ## Cuándo usar este paquete y cuándo no
6
+
7
+ | Escribes… | Importa | Por qué |
8
+ |---|---|---|
9
+ | Una **librería** (Axonium, synaptum, un SDK tuyo) | `argus-semconv` | Solo depende de `opentelemetry-api`. No impone SDK ni exportadores a quien te use |
10
+ | Una **aplicación** (servicio, worker, CLI) | `argus-sdk` | Es quien configura e inicializa la telemetría |
11
+
12
+ ## La garantía
13
+
14
+ Si la aplicación que importa tu librería **no** ha inicializado un SDK de
15
+ OpenTelemetry, toda la instrumentación de este paquete es **no-op con coste
16
+ cero**. Si **sí** lo ha inicializado, se enciende sola usando *su*
17
+ configuración, *su* endpoint y *su* muestreo.
18
+
19
+ Eso significa que puedes instrumentar tus librerías sin imponer nada a nadie.
20
+
21
+ ```python
22
+ from argus_semconv import genai
23
+
24
+ # En Axonium, dentro del cliente de inferencia:
25
+ with genai("chat", provider="ollama", request_model=model) as g:
26
+ resp = await self._call_backend(...)
27
+ g.usage(input_tokens=resp.prompt_tokens, output_tokens=resp.completion_tokens)
28
+ g.backend(backend_id=backend.id, ttft_ms=resp.ttft_ms)
29
+ ```
30
+
31
+ ## Las constantes están generadas
32
+
33
+ `attributes.py` se genera desde `libs/semconv-model/argus.yaml`. No lo edites a
34
+ mano: ejecuta `python tools/gen_semconv.py`. Un test de CI verifica que no hay
35
+ deriva entre el modelo y lo commiteado.
@@ -0,0 +1,119 @@
1
+ // GENERADO AUTOMATICAMENTE. NO EDITAR A MANO.
2
+ //
3
+ // Fuente: libs/semconv-model/argus.yaml
4
+ // Regenerar: python tools/gen_semconv.py
5
+
6
+ package semconv
7
+
8
+ const (
9
+ SemconvVersion = "1.0.0"
10
+ GenAISemconvVersion = "1.39.0"
11
+ )
12
+
13
+ // identity: Identidad de aplicacion y componente
14
+ const (
15
+ ServiceNamespace = "service.namespace"
16
+ ServiceName = "service.name"
17
+ ServiceVersion = "service.version"
18
+ ServiceInstanceID = "service.instance.id"
19
+ DeploymentEnvironmentName = "deployment.environment.name"
20
+ ArgusComponentRole = "argus.component.role"
21
+ )
22
+
23
+ // langfuse_bridge: Atributos que Langfuse interpreta de forma especial
24
+ const (
25
+ LangfuseEnvironment = "langfuse.environment"
26
+ LangfuseRelease = "langfuse.release"
27
+ LangfuseObservationType = "langfuse.observation.type"
28
+ LangfuseTraceName = "langfuse.trace.name"
29
+ LangfuseTraceTags = "langfuse.trace.tags"
30
+ )
31
+
32
+ // attribution: Etiquetas de atribucion para FinOps y analisis
33
+ const (
34
+ ArgusApp = "argus.app"
35
+ ArgusFeature = "argus.feature"
36
+ ArgusUseCase = "argus.use_case"
37
+ ArgusRunID = "argus.run.id"
38
+ ArgusTenant = "argus.tenant"
39
+ )
40
+
41
+ // wide_event: Campos del evento ancho canonico
42
+ const (
43
+ ArgusEvent = "argus.event"
44
+ ArgusOutcome = "argus.outcome"
45
+ ArgusDurationMs = "argus.duration_ms"
46
+ ArgusStepDepth = "argus.step.depth"
47
+ )
48
+
49
+ // hotpath: Marcas que enrutan al camino caliente de deteccion
50
+ const (
51
+ ArgusHot = "argus.hot"
52
+ ArgusSloBreached = "argus.slo.breached"
53
+ ArgusSloThresholdMs = "argus.slo.threshold_ms"
54
+ ArgusGuardrail = "argus.guardrail"
55
+ )
56
+
57
+ // errors: Clasificacion de errores
58
+ const (
59
+ ErrorType = "error.type"
60
+ ArgusErrorRetryable = "argus.error.retryable"
61
+ ArgusErrorRetryPolicy = "argus.error.retry_policy"
62
+ )
63
+
64
+ // genai: Convenciones GenAI de OpenTelemetry
65
+ const (
66
+ GenAIOperationName = "gen_ai.operation.name"
67
+ GenAIProviderName = "gen_ai.provider.name"
68
+ GenAIRequestModel = "gen_ai.request.model"
69
+ GenAIResponseModel = "gen_ai.response.model"
70
+ GenAIResponseID = "gen_ai.response.id"
71
+ GenAIResponseFinishReasons = "gen_ai.response.finish_reasons"
72
+ GenAIUsageInputTokens = "gen_ai.usage.input_tokens"
73
+ GenAIUsageOutputTokens = "gen_ai.usage.output_tokens"
74
+ GenAIUsageCacheReadInputTokens = "gen_ai.usage.cache_read.input_tokens"
75
+ GenAIRequestTemperature = "gen_ai.request.temperature"
76
+ GenAIRequestMaxTokens = "gen_ai.request.max_tokens"
77
+ GenAIRequestTopP = "gen_ai.request.top_p"
78
+ GenAIConversationID = "gen_ai.conversation.id"
79
+ GenAIAgentName = "gen_ai.agent.name"
80
+ GenAIAgentID = "gen_ai.agent.id"
81
+ GenAIToolName = "gen_ai.tool.name"
82
+ GenAIToolType = "gen_ai.tool.type"
83
+ GenAIToolCallID = "gen_ai.tool.call.id"
84
+ GenAIInputMessages = "gen_ai.input.messages"
85
+ GenAIOutputMessages = "gen_ai.output.messages"
86
+ GenAISystemInstructions = "gen_ai.system_instructions"
87
+ GenAIToolCallArguments = "gen_ai.tool.call.arguments"
88
+ GenAIToolCallResult = "gen_ai.tool.call.result"
89
+ GenAIContentTruncated = "gen_ai.content.truncated"
90
+ )
91
+
92
+ // inference: Detalles de inferencia local que solo conoce el SDK cliente
93
+ const (
94
+ ArgusBackendID = "argus.backend.id"
95
+ ArgusBackendCircuitState = "argus.backend.circuit_state"
96
+ ArgusBackendFallback = "argus.backend.fallback"
97
+ ArgusCostUSD = "argus.cost_usd"
98
+ ArgusTtftMs = "argus.ttft_ms"
99
+ ArgusFirstTokenMs = "argus.first_token_ms"
100
+ ArgusTokensPerSecond = "argus.tokens_per_second"
101
+ )
102
+
103
+ // retrieval: Recuperacion de documentos
104
+ const (
105
+ ArgusRetrievalStore = "argus.retrieval.store"
106
+ ArgusRetrievalTopK = "argus.retrieval.top_k"
107
+ ArgusRetrievalDocCount = "argus.retrieval.doc_count"
108
+ ArgusRetrievalScoreMin = "argus.retrieval.score_min"
109
+ ArgusRetrievalScoreMax = "argus.retrieval.score_max"
110
+ )
111
+
112
+ // Metricas
113
+ const (
114
+ MetricGenAIClientOperationDuration = "gen_ai.client.operation.duration"
115
+ MetricGenAIClientTokenUsage = "gen_ai.client.token.usage"
116
+ MetricGenAIServerTimeToFirstToken = "gen_ai.server.time_to_first_token"
117
+ MetricArgusStepDuration = "argus.step.duration"
118
+ MetricArgusCostUSD = "argus.cost.usd"
119
+ )
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "argus-obs-semconv"
3
+ version = "1.0.0a3"
4
+ description = "Convenciones semanticas de Argus. Para LIBRERIAS: depende solo de la API de OpenTelemetry."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Emeric Espiritu" }]
9
+ keywords = ["observability", "opentelemetry", "semantic-conventions", "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
+ # ATENCION: esta lista de dependencias es un contrato, no un detalle.
22
+ #
23
+ # Este paquete lo importan LIBRERIAS (Axonium, synaptum, ...), no aplicaciones.
24
+ # Una libreria que depende del SDK impone en silencio su version del SDK y sus
25
+ # opiniones sobre exportadores a TODO el que dependa de ella. Con quince o mas
26
+ # aplicaciones importando Axonium, eso es un infierno de dependencias.
27
+ #
28
+ # Por eso: SOLO la API, y con rango abierto dentro de la mayor. NUNCA una
29
+ # version exacta, NUNCA el SDK, NUNCA un exportador.
30
+ dependencies = [
31
+ "opentelemetry-api>=1.30,<2",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "opentelemetry-sdk>=1.30,<2", "pyyaml>=6"]
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/argus_semconv"]
43
+
44
+ # Quien encuentre esto en PyPI tiene derecho a saber de donde sale y donde
45
+ # reportar. Un paquete sin origen publico es la misma opacidad de cadena de
46
+ # suministro que nos objeto un equipo consumidor (D-075).
47
+ [project.urls]
48
+ Homepage = "https://github.com/Root1V/argus-obs"
49
+ Repository = "https://github.com/Root1V/argus-obs"
50
+ Issues = "https://github.com/Root1V/argus-obs/issues"
@@ -0,0 +1,62 @@
1
+ """Convenciones semanticas de Argus, para LIBRERIAS.
2
+
3
+ Este paquete depende SOLO de `opentelemetry-api`. Nunca del SDK, nunca de un
4
+ exportador. Esa restriccion es el contrato:
5
+
6
+ - Si la aplicacion que te importa no inicializo un SDK, todo esto es no-op con
7
+ coste cero, y tu libreria funciona exactamente igual que sin instrumentar.
8
+ - Si lo inicializo, tu instrumentacion se enciende sola usando SU
9
+ configuracion, SU endpoint y SU muestreo.
10
+
11
+ Una libreria que dependiera del SDK impondria en silencio su version y sus
12
+ opiniones sobre exportadores a todo el que dependa de ella.
13
+
14
+ Las aplicaciones no usan este paquete directamente: usan `argus-sdk`, que lo
15
+ re-exporta ademas de configurar la telemetria.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from . import attributes
21
+ from ._content import capture_enabled, mask
22
+ from .genai import GenAISpan, agent, documents, genai, retrieval, tool
23
+ from .guardrails import AgentRun, Budget, GuardrailBreach, current_run
24
+ from .steps import Step, instrument, step
25
+
26
+ # La version del PAQUETE, distinta de la de las convenciones: el paquete puede
27
+ # publicarse varias veces sin que cambien las convenciones.
28
+ try:
29
+ from importlib.metadata import version as _version
30
+
31
+ __version__ = _version("argus-obs-semconv")
32
+ except Exception: # noqa: BLE001
33
+ __version__ = "0.0.0.dev0"
34
+
35
+ #: Version del MODELO de convenciones (libs/semconv-model/argus.yaml).
36
+ SEMCONV_VERSION = attributes.SEMCONV_VERSION
37
+
38
+ __all__ = [
39
+ # Convenciones generadas
40
+ "attributes",
41
+ # GenAI
42
+ "genai",
43
+ "retrieval",
44
+ "tool",
45
+ "agent",
46
+ "documents",
47
+ "GenAISpan",
48
+ # Guardarrailes de agentes
49
+ "Budget",
50
+ "GuardrailBreach",
51
+ "AgentRun",
52
+ "current_run",
53
+ # Unidades de trabajo
54
+ "step",
55
+ "instrument",
56
+ "Step",
57
+ # Contenido
58
+ "capture_enabled",
59
+ "mask",
60
+ "SEMCONV_VERSION",
61
+ "__version__",
62
+ ]
@@ -0,0 +1,108 @@
1
+ """Captura y enmascarado de contenido de prompts.
2
+
3
+ Dos decisiones que conviene entender antes de tocar este fichero.
4
+
5
+ 1. El contenido va en ATRIBUTOS de span, no en eventos.
6
+
7
+ La guia canonica de las semconv GenAI dice que el contenido debe ir en
8
+ eventos de span, para poder descartarlo en el Collector sin tocar codigo.
9
+ El razonamiento es bueno. Pero Langfuse lee `gen_ai.input.messages` y
10
+ `gen_ai.output.messages` de los ATRIBUTOS del span. Si seguimos la guia al
11
+ pie de la letra, Langfuse ingiere el span y lo muestra sin input ni output,
12
+ que es justo lo que lo hace util.
13
+
14
+ Resolucion: emitir en atributos, y borrarlos en la rama del Collector que
15
+ va a ClickHouse (donde son decenas de KB sin ninguna consulta que los use).
16
+
17
+ 2. Enmascarar aqui es la PRIMERA capa, no la unica.
18
+
19
+ El Collector tiene un `redaction` processor como red de seguridad. Esa
20
+ segunda capa es la que importa de verdad: significa que un fallo de
21
+ instrumentacion en una app no se convierte en una fuga en el almacen.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import re
29
+ from collections.abc import Callable
30
+ from typing import Any, Final
31
+
32
+ _TRUE: Final = frozenset({"1", "true", "yes", "on"})
33
+
34
+ # Umbral por defecto. Un prompt entero puede ser de decenas de KB; un atributo
35
+ # de span no es sitio para eso.
36
+ _DEFAULT_MAX_BYTES: Final = 8192
37
+
38
+ _PATTERNS: Final[tuple[tuple[re.Pattern[str], str], ...]] = (
39
+ (re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"), "[EMAIL]"),
40
+ (re.compile(r"\b(?:\d[ -]*?){13,19}\b"), "[CARD]"),
41
+ # Telefono: EXIGE separadores o prefijo internacional. Una tirada suelta de
42
+ # diez digitos es casi siempre un identificador, un timestamp o un contador,
43
+ # no un telefono. Redactarla destruye datos utiles y no protege nada.
44
+ # Mismo criterio que el `redaction` processor del Collector: las dos capas
45
+ # tienen que coincidir o los datos salen distintos segun por donde pasen.
46
+ (re.compile(r"\+\d{1,3}[-.\s]\d{2,4}[-.\s]\d{2,4}[-.\s]?\d{0,4}"), "[PHONE]"),
47
+ (re.compile(r"\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b"), "[PHONE]"),
48
+ (re.compile(r"\b\d{8}[A-HJ-NP-TV-Z]\b"), "[DNI]"),
49
+ (re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b"), "[IBAN]"),
50
+ (re.compile(r"(?i)\b(?:sk|pk)-[A-Za-z0-9_\-]{16,}"), "[KEY]"),
51
+ (re.compile(r"(?i)\bBearer\s+[A-Za-z0-9_\-.]{16,}"), "[TOKEN]"),
52
+ )
53
+
54
+
55
+ def capture_enabled() -> bool:
56
+ """Si se captura el contenido de prompts y respuestas.
57
+
58
+ Por defecto NO. Se activa en desarrollo y de forma selectiva por aplicacion
59
+ en produccion, donde la evaluacion aporta.
60
+ """
61
+ val = os.getenv("ARGUS_CAPTURE_CONTENT", "")
62
+ if val:
63
+ return val.strip().lower() in _TRUE
64
+ # Compatibilidad con la variable estandar de OpenTelemetry.
65
+ return os.getenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "").strip().lower() in _TRUE
66
+
67
+
68
+ def max_bytes() -> int:
69
+ try:
70
+ return max(0, int(os.getenv("ARGUS_CONTENT_MAX_BYTES", str(_DEFAULT_MAX_BYTES))))
71
+ except ValueError:
72
+ return _DEFAULT_MAX_BYTES
73
+
74
+
75
+ def mask(text: str) -> str:
76
+ """Redacta PII conocida. Primera capa; el Collector es la segunda."""
77
+ for pattern, replacement in _PATTERNS:
78
+ text = pattern.sub(replacement, text)
79
+ return text
80
+
81
+
82
+ def serialize(
83
+ payload: Any,
84
+ *,
85
+ masker: Callable[[str], str] | None = None,
86
+ limit: int | None = None,
87
+ ) -> tuple[str, bool]:
88
+ """Serializa a JSON, enmascara y trunca.
89
+
90
+ Devuelve `(texto, truncado)`. Nunca lanza: si el payload no es
91
+ serializable cae a `repr`, porque perder telemetria es aceptable y tumbar
92
+ la aplicacion del usuario no lo es.
93
+ """
94
+ try:
95
+ raw = json.dumps(payload, ensure_ascii=False, default=str)
96
+ except (TypeError, ValueError):
97
+ raw = repr(payload)
98
+
99
+ raw = (masker or mask)(raw)
100
+
101
+ cap = max_bytes() if limit is None else limit
102
+ if cap and len(raw.encode("utf-8")) > cap:
103
+ # Cortamos por bytes y descartamos el ultimo caracter posiblemente
104
+ # partido a mitad de secuencia UTF-8.
105
+ raw = raw.encode("utf-8")[:cap].decode("utf-8", errors="ignore")
106
+ return raw, True
107
+
108
+ return raw, False
@@ -0,0 +1,124 @@
1
+ """Instrumentos de metricas GenAI, emitidos desde la API de OpenTelemetry.
2
+
3
+ Por que vive aqui y no en el SDK de aplicacion: si las metricas hubiera que
4
+ emitirlas a mano, nadie las emitiria. Las convenciones dicen que
5
+ `gen_ai.client.operation.duration` y `gen_ai.client.token.usage` van desde el
6
+ dia uno, y eso solo se cumple si salen SOLAS del mismo context manager que ya
7
+ se usa para las trazas.
8
+
9
+ La API de metricas de OpenTelemetry tiene la misma propiedad que la de trazas:
10
+ sin SDK configurado devuelve instrumentos no operativos con coste cero. Asi que
11
+ una libreria puede emitir metricas sin imponer nada a quien la importe, igual
12
+ que con los spans.
13
+
14
+ Los BUCKETS no se fijan aqui —eso es configuracion del SDK— sino en las vistas
15
+ de `argus-obs-sdk`, que los aplica por nombre de instrumento cuando esta
16
+ presente.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ from opentelemetry import metrics
24
+
25
+ from . import attributes as A
26
+
27
+ _meter = metrics.get_meter("argus-semconv", A.SEMCONV_VERSION)
28
+
29
+ # Creados una vez al importar. Sin SDK son no-ops.
30
+ _duracion = _meter.create_histogram(
31
+ A.M_GEN_AI_CLIENT_OPERATION_DURATION,
32
+ unit=A.M_GEN_AI_CLIENT_OPERATION_DURATION_UNIT,
33
+ description="Duracion de la operacion GenAI",
34
+ )
35
+ _tokens = _meter.create_histogram(
36
+ A.M_GEN_AI_CLIENT_TOKEN_USAGE,
37
+ unit=A.M_GEN_AI_CLIENT_TOKEN_USAGE_UNIT,
38
+ description="Tokens consumidos, con el tipo como dimension",
39
+ )
40
+ _ttft = _meter.create_histogram(
41
+ A.M_GEN_AI_SERVER_TIME_TO_FIRST_TOKEN,
42
+ unit=A.M_GEN_AI_SERVER_TIME_TO_FIRST_TOKEN_UNIT,
43
+ description="Tiempo hasta el primer token",
44
+ )
45
+ _coste = _meter.create_counter(
46
+ A.M_ARGUS_COST_USD,
47
+ unit=A.M_ARGUS_COST_USD_UNIT,
48
+ description="Coste atribuido",
49
+ )
50
+
51
+
52
+ def _base(operation: str, provider: str, model: str | None) -> dict[str, Any]:
53
+ attrs: dict[str, Any] = {
54
+ A.GEN_AI_OPERATION_NAME: operation,
55
+ A.GEN_AI_PROVIDER_NAME: provider,
56
+ }
57
+ if model:
58
+ attrs[A.GEN_AI_REQUEST_MODEL] = model
59
+ return attrs
60
+
61
+
62
+ def record_duration(
63
+ *, operation: str, provider: str, model: str | None,
64
+ seconds: float, error_type: str | None = None,
65
+ ) -> None:
66
+ attrs = _base(operation, provider, model)
67
+ if error_type:
68
+ attrs[A.ERROR_TYPE] = error_type
69
+ try:
70
+ _duracion.record(seconds, attrs)
71
+ except Exception: # noqa: BLE001 - la telemetria nunca tumba la app
72
+ pass
73
+
74
+
75
+ def record_tokens(
76
+ *, operation: str, provider: str, model: str | None,
77
+ input_tokens: int | None = None, output_tokens: int | None = None,
78
+ ) -> None:
79
+ """`gen_ai.token.type` va como DIMENSION, no como dos instrumentos.
80
+
81
+ Es lo que dice la especificacion y lo que permite agregar por modelo sin
82
+ unir series a mano.
83
+ """
84
+ attrs = _base(operation, provider, model)
85
+ try:
86
+ if input_tokens:
87
+ _tokens.record(input_tokens, {**attrs, "gen_ai.token.type": "input"})
88
+ if output_tokens:
89
+ _tokens.record(output_tokens, {**attrs, "gen_ai.token.type": "output"})
90
+ except Exception: # noqa: BLE001
91
+ pass
92
+
93
+
94
+ def record_ttft(*, provider: str, model: str | None, seconds: float, backend_id: str | None = None) -> None:
95
+ attrs = _base("chat", provider, model)
96
+ attrs.pop(A.GEN_AI_OPERATION_NAME, None)
97
+ if backend_id:
98
+ attrs[A.ARGUS_BACKEND_ID] = backend_id
99
+ try:
100
+ _ttft.record(seconds, attrs)
101
+ except Exception: # noqa: BLE001
102
+ pass
103
+
104
+
105
+ def record_cost(
106
+ *, cost_usd: float, model: str | None,
107
+ app: str | None = None, feature: str | None = None, use_case: str | None = None,
108
+ ) -> None:
109
+ """Dimensiones pensadas para poder ATRIBUIR el coste.
110
+
111
+ Sin app, funcionalidad y caso de uso, el coste llega como un numero opaco a
112
+ fin de mes y no se puede optimizar nada.
113
+ """
114
+ attrs = {
115
+ A.ARGUS_APP: app or "unknown",
116
+ A.ARGUS_FEATURE: feature or "unknown",
117
+ A.ARGUS_USE_CASE: use_case or "unknown",
118
+ }
119
+ if model:
120
+ attrs[A.GEN_AI_REQUEST_MODEL] = model
121
+ try:
122
+ _coste.add(cost_usd, attrs)
123
+ except Exception: # noqa: BLE001
124
+ pass