This commit is contained in:
@@ -0,0 +1,233 @@
|
|||||||
|
from typing import Dict, Iterable, Optional
|
||||||
|
|
||||||
|
from prometheus_client import (
|
||||||
|
CollectorRegistry,
|
||||||
|
Counter,
|
||||||
|
Gauge,
|
||||||
|
Histogram,
|
||||||
|
Summary,
|
||||||
|
start_http_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusMetric:
|
||||||
|
"""
|
||||||
|
Helper ligero para exponer métricas de Prometheus con prefijo y etiquetas comunes.
|
||||||
|
Inicia un endpoint HTTP para que Prometheus pueda scrapear las métricas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix: str = "suite_logs",
|
||||||
|
default_labels: Optional[Dict[str, str]] = None,
|
||||||
|
port: int = 8000,
|
||||||
|
start_server: bool = True,
|
||||||
|
registry: Optional[CollectorRegistry] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param prefix: prefijo que se agregará a todas las métricas (ej: suite_logs_)
|
||||||
|
:param default_labels: etiquetas que se agregan a todas las series (ej: {"service": "api"})
|
||||||
|
:param port: puerto HTTP donde se expondrán las métricas
|
||||||
|
:param start_server: inicia el servidor HTTP automáticamente
|
||||||
|
:param registry: permite inyectar un CollectorRegistry (útil para tests)
|
||||||
|
"""
|
||||||
|
self.prefix = prefix.rstrip("_")
|
||||||
|
self.default_labels = dict(default_labels or {})
|
||||||
|
self.registry = registry or CollectorRegistry()
|
||||||
|
self.port = port
|
||||||
|
self._metric_cache: Dict[str, object] = {}
|
||||||
|
|
||||||
|
# Se evita iniciar múltiples servidores si se crean varias instancias por error.
|
||||||
|
if start_server:
|
||||||
|
start_http_server(self.port, registry=self.registry)
|
||||||
|
|
||||||
|
def counter(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
doc: str,
|
||||||
|
labels: Optional[Iterable[str]] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
full_name = self._metric_name(name, prefix)
|
||||||
|
metric = Counter(
|
||||||
|
full_name,
|
||||||
|
doc,
|
||||||
|
labelnames=self._label_names(labels),
|
||||||
|
registry=self.registry,
|
||||||
|
)
|
||||||
|
return _CounterHandle(metric, self.default_labels)
|
||||||
|
|
||||||
|
def gauge(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
doc: str,
|
||||||
|
labels: Optional[Iterable[str]] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
full_name = self._metric_name(name, prefix)
|
||||||
|
metric = Gauge(
|
||||||
|
full_name,
|
||||||
|
doc,
|
||||||
|
labelnames=self._label_names(labels),
|
||||||
|
registry=self.registry,
|
||||||
|
)
|
||||||
|
return _GaugeHandle(metric, self.default_labels)
|
||||||
|
|
||||||
|
def histogram(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
doc: str,
|
||||||
|
labels: Optional[Iterable[str]] = None,
|
||||||
|
buckets: Optional[Iterable[float]] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
full_name = self._metric_name(name, prefix)
|
||||||
|
metric = Histogram(
|
||||||
|
full_name,
|
||||||
|
doc,
|
||||||
|
labelnames=self._label_names(labels),
|
||||||
|
registry=self.registry,
|
||||||
|
buckets=buckets,
|
||||||
|
)
|
||||||
|
return _ObserveHandle(metric, self.default_labels)
|
||||||
|
|
||||||
|
def summary(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
doc: str,
|
||||||
|
labels: Optional[Iterable[str]] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
full_name = self._metric_name(name, prefix)
|
||||||
|
metric = Summary(
|
||||||
|
full_name,
|
||||||
|
doc,
|
||||||
|
labelnames=self._label_names(labels),
|
||||||
|
registry=self.registry,
|
||||||
|
)
|
||||||
|
return _ObserveHandle(metric, self.default_labels)
|
||||||
|
|
||||||
|
# Métodos rápidos para emitir métricas sin guardar el handle
|
||||||
|
def counter_value(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
amount: float = 1.0,
|
||||||
|
labels: Optional[Dict[str, str]] = None,
|
||||||
|
doc: str = "",
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
metric = self._get_or_create(Counter, name, doc, labels, prefix)
|
||||||
|
_CounterHandle(metric, self.default_labels).inc(amount, **(labels or {}))
|
||||||
|
|
||||||
|
def gauge_value(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
value: float,
|
||||||
|
labels: Optional[Dict[str, str]] = None,
|
||||||
|
doc: str = "",
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
metric = self._get_or_create(Gauge, name, doc, labels, prefix)
|
||||||
|
_GaugeHandle(metric, self.default_labels).set(value, **(labels or {}))
|
||||||
|
|
||||||
|
def histogram_observe(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
value: float,
|
||||||
|
labels: Optional[Dict[str, str]] = None,
|
||||||
|
doc: str = "",
|
||||||
|
buckets: Optional[Iterable[float]] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
metric = self._get_or_create(
|
||||||
|
Histogram, name, doc, labels, prefix, buckets=buckets
|
||||||
|
)
|
||||||
|
_ObserveHandle(metric, self.default_labels).observe(value, **(labels or {}))
|
||||||
|
|
||||||
|
def summary_observe(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
value: float,
|
||||||
|
labels: Optional[Dict[str, str]] = None,
|
||||||
|
doc: str = "",
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
metric = self._get_or_create(Summary, name, doc, labels, prefix)
|
||||||
|
_ObserveHandle(metric, self.default_labels).observe(value, **(labels or {}))
|
||||||
|
|
||||||
|
def _metric_name(self, name: str, prefix: Optional[str]) -> str:
|
||||||
|
base = (prefix or self.prefix).rstrip("_")
|
||||||
|
return f"{base}_{name}" if base else name
|
||||||
|
|
||||||
|
def _label_names(self, labels: Optional[Iterable[str] | Dict[str, str]]) -> Iterable[str]:
|
||||||
|
names = list(self.default_labels.keys())
|
||||||
|
if labels:
|
||||||
|
source = labels.keys() if isinstance(labels, dict) else labels
|
||||||
|
for label in source:
|
||||||
|
if label not in names:
|
||||||
|
names.append(label)
|
||||||
|
return names
|
||||||
|
|
||||||
|
def _get_or_create(
|
||||||
|
self,
|
||||||
|
metric_cls,
|
||||||
|
name: str,
|
||||||
|
doc: str,
|
||||||
|
labels: Optional[Dict[str, str]],
|
||||||
|
prefix: Optional[str],
|
||||||
|
buckets: Optional[Iterable[float]] = None,
|
||||||
|
):
|
||||||
|
full_name = self._metric_name(name, prefix)
|
||||||
|
labelnames = self._label_names(labels)
|
||||||
|
cache_key = (metric_cls.__name__, full_name, tuple(labelnames))
|
||||||
|
|
||||||
|
if cache_key in self._metric_cache:
|
||||||
|
return self._metric_cache[cache_key]
|
||||||
|
|
||||||
|
kwargs: Dict[str, object] = {
|
||||||
|
"labelnames": labelnames,
|
||||||
|
"registry": self.registry,
|
||||||
|
}
|
||||||
|
if metric_cls is Histogram and buckets is not None:
|
||||||
|
kwargs["buckets"] = buckets
|
||||||
|
|
||||||
|
metric = metric_cls(full_name, doc or full_name, **kwargs)
|
||||||
|
self._metric_cache[cache_key] = metric
|
||||||
|
return metric
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseHandle:
|
||||||
|
def __init__(self, metric, default_labels: Dict[str, str]):
|
||||||
|
self.metric = metric
|
||||||
|
self.default_labels = default_labels
|
||||||
|
|
||||||
|
def _child(self, labels: Optional[Dict[str, str]]):
|
||||||
|
if not self.metric._labelnames:
|
||||||
|
return self.metric
|
||||||
|
|
||||||
|
merged = {**self.default_labels, **(labels or {})}
|
||||||
|
missing = [l for l in self.metric._labelnames if l not in merged]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Faltan labels obligatorios: {missing}")
|
||||||
|
return self.metric.labels(**merged)
|
||||||
|
|
||||||
|
|
||||||
|
class _CounterHandle(_BaseHandle):
|
||||||
|
def inc(self, amount: float = 1.0, **labels):
|
||||||
|
self._child(labels).inc(amount)
|
||||||
|
|
||||||
|
|
||||||
|
class _GaugeHandle(_BaseHandle):
|
||||||
|
def set(self, value: float, **labels):
|
||||||
|
self._child(labels).set(value)
|
||||||
|
|
||||||
|
def inc(self, amount: float = 1.0, **labels):
|
||||||
|
self._child(labels).inc(amount)
|
||||||
|
|
||||||
|
def dec(self, amount: float = 1.0, **labels):
|
||||||
|
self._child(labels).dec(amount)
|
||||||
|
|
||||||
|
|
||||||
|
class _ObserveHandle(_BaseHandle):
|
||||||
|
def observe(self, value: float, **labels):
|
||||||
|
self._child(labels).observe(value)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .LokiLogger import LokiLogger
|
||||||
|
from .PrometheusMetric import PrometheusMetric
|
||||||
|
|
||||||
|
__all__ = ["LokiLogger", "PrometheusMetric"]
|
||||||
|
|||||||
@@ -8,6 +8,50 @@ Esta configuraci�n incluye un stack completo de monitoreo con:
|
|||||||
## Estructura de Archivos
|
## Estructura de Archivos
|
||||||
|
|
||||||
|
|
||||||
|
## Uso rápido en Python (logs y métricas)
|
||||||
|
|
||||||
|
- Logs a Loki / Alloy:
|
||||||
|
```python
|
||||||
|
from Logger import LokiLogger
|
||||||
|
|
||||||
|
logger = LokiLogger(service_name="mi_servicio", min_level="INFO")
|
||||||
|
logger.info("Aplicación iniciada")
|
||||||
|
logger.error("Algo falló", add_fields={"detalle": "stacktrace"})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Métricas con prefijo listo para scrapeo de Prometheus:
|
||||||
|
```python
|
||||||
|
from Logger import PrometheusMetric
|
||||||
|
|
||||||
|
metrics = PrometheusMetric(
|
||||||
|
prefix="suite_logs",
|
||||||
|
default_labels={"service_name": "mi_servicio", "env": "dev"},
|
||||||
|
port=9102, # inicia un servidor HTTP en este puerto
|
||||||
|
)
|
||||||
|
|
||||||
|
requests_total = metrics.counter(
|
||||||
|
"requests_total", "Solicitudes procesadas", labels=["endpoint"]
|
||||||
|
)
|
||||||
|
latency_seconds = metrics.histogram(
|
||||||
|
"latency_seconds",
|
||||||
|
"Latencia de peticiones",
|
||||||
|
labels=["endpoint"],
|
||||||
|
buckets=[0.1, 0.5, 1, 2, 5],
|
||||||
|
)
|
||||||
|
|
||||||
|
requests_total.inc(endpoint="/health")
|
||||||
|
latency_seconds.observe(0.35, endpoint="/health")
|
||||||
|
```
|
||||||
|
Agrega el puerto (`9102` en el ejemplo) como target de scrape en Prometheus/Alloy para ver las series con el prefijo definido.
|
||||||
|
|
||||||
|
Atajo: si quieres emitir sin guardar handles, usa `counter_value`, `gauge_value`, `histogram_observe` y `summary_observe`, pudiendo sobreescribir el prefijo por métrica:
|
||||||
|
```python
|
||||||
|
metrics.gauge_value("workers_active", 3, prefix="backend", labels={"queue": "ingest"})
|
||||||
|
metrics.counter_value("processed_total", 1, prefix="backend", labels={"queue": "ingest"})
|
||||||
|
```
|
||||||
|
|
||||||
|
En este repo, Alloy ya está configurado para scrapear `host.docker.internal:9102` con el `job_name="app_metrics"`. Si tu script expone métricas en ese puerto (con `PrometheusMetric(port=9102)`), se almacenarán sin cambios adicionales.
|
||||||
|
|
||||||
## Configuración Inicial
|
## Configuración Inicial
|
||||||
|
|
||||||
### 1. Configurar variables de entorno
|
### 1. Configurar variables de entorno
|
||||||
@@ -157,4 +201,4 @@ docker exec prometheus promtool check config /etc/prometheus/prometheus.yml
|
|||||||
```bash
|
```bash
|
||||||
# Ver configuraci�n activa
|
# Ver configuraci�n activa
|
||||||
curl http://localhost:9009/config
|
curl http://localhost:9009/config
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ prometheus.scrape "cadvisor" {
|
|||||||
job_name = "cadvisor"
|
job_name = "cadvisor"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scraping fijo para métricas expuestas desde el host (scripts Python)
|
||||||
|
// Corre por defecto en host.docker.internal:9102 para PrometheusMetric
|
||||||
|
prometheus.scrape "app_metrics" {
|
||||||
|
targets = [{"__address__" = "host.docker.internal:9102"}]
|
||||||
|
forward_to = [prometheus.remote_write.prometheus.receiver]
|
||||||
|
scrape_interval = "15s"
|
||||||
|
metrics_path = "/metrics"
|
||||||
|
job_name = "app_metrics"
|
||||||
|
}
|
||||||
|
|
||||||
// Receptor para métricas externas
|
// Receptor para métricas externas
|
||||||
prometheus.receive_http "external_metrics" {
|
prometheus.receive_http "external_metrics" {
|
||||||
http {
|
http {
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- monitoring
|
- monitoring
|
||||||
restart: always
|
restart: always
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
- prometheus
|
- prometheus
|
||||||
- loki
|
- loki
|
||||||
|
|||||||
Reference in New Issue
Block a user