# Observabilidad y Alertas — Instrumentación y Alertas Completas

Referencia extendida de la skill `monitoring-alertas`. Contiene ejemplos completos
de instrumentación Prometheus, OpenTelemetry, logging estructurado, reglas de
alerting y runbooks.

---

## Prometheus — Instrumentación en Python

```python
# instrumentacion.py
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time

# Contadores — valores que solo suben
PETICIONES_TOTALES = Counter(
    "http_requests_total",
    "Total de peticiones HTTP",
    labelnames=["metodo", "endpoint", "codigo_estado"],
)

ERRORES_TOTALES = Counter(
    "errores_aplicacion_total",
    "Total de errores de la aplicacion",
    labelnames=["tipo", "servicio"],
)

# Histograma — distribucion de latencia
DURACION_PETICION = Histogram(
    "http_request_duration_seconds",
    "Duracion de peticiones HTTP en segundos",
    labelnames=["metodo", "endpoint"],
    buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
)

# Gauge — valores que suben y bajan
TAMANIO_COLA = Gauge(
    "cola_tareas_actual",
    "Numero de tareas pendientes en la cola",
    labelnames=["tipo_cola"],
)

# Middleware FastAPI para instrumentacion automatica
from fastapi import Request, Response
import asyncio


async def middleware_metricas(request: Request, call_next):
    inicio = time.perf_counter()
    respuesta = None

    try:
        respuesta = await call_next(request)
        return respuesta
    finally:
        duracion = time.perf_counter() - inicio
        codigo = str(respuesta.status_code) if respuesta else "500"

        PETICIONES_TOTALES.labels(
            metodo=request.method,
            endpoint=request.url.path,
            codigo_estado=codigo,
        ).inc()

        DURACION_PETICION.labels(
            metodo=request.method,
            endpoint=request.url.path,
        ).observe(duracion)
```

---

## OpenTelemetry — Trazas Distribuidas en Python

```python
# telemetria.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor

def configurar_telemetria(nombre_servicio: str, endpoint_otlp: str) -> None:
    proveedor = TracerProvider()
    exportador = OTLPSpanExporter(endpoint=endpoint_otlp)
    proveedor.add_span_processor(BatchSpanProcessor(exportador))
    trace.set_tracer_provider(proveedor)

    FastAPIInstrumentor.instrument()
    SQLAlchemyInstrumentor().instrument()
    HTTPXClientInstrumentor().instrument()


tracer = trace.get_tracer(__name__)


async def procesar_pago(pedido_id: str, monto: float) -> dict:
    with tracer.start_as_current_span("procesar_pago") as span:
        span.set_attribute("pedido.id", pedido_id)
        span.set_attribute("pedido.monto", monto)
        span.set_attribute("servicio.nombre", "pagos")

        try:
            with tracer.start_as_current_span("validar_fondos"):
                fondos_ok = await verificar_fondos(pedido_id, monto)
                span.set_attribute("fondos.suficientes", fondos_ok)

            if not fondos_ok:
                span.set_status(trace.StatusCode.ERROR, "Fondos insuficientes")
                raise ErrorValidacion("Fondos insuficientes")

            with tracer.start_as_current_span("gateway_pago"):
                resultado = await gateway.cobrar(monto)
                span.add_event("pago_completado", {"transaccion_id": resultado["id"]})
                return resultado

        except Exception as exc:
            span.record_exception(exc)
            span.set_status(trace.StatusCode.ERROR)
            raise
```

---

## Logging Estructurado en Producción

```python
# logging_config.py
import structlog
import logging
import sys


def configurar_logging(nivel: str = "INFO", formato: str = "json") -> None:
    procesadores_compartidos = [
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso", utc=True),
        structlog.stdlib.add_logger_name,
    ]

    if formato == "json":
        procesadores_compartidos.append(structlog.processors.JSONRenderer())
    else:
        procesadores_compartidos.append(structlog.dev.ConsoleRenderer())

    structlog.configure(
        processors=procesadores_compartidos,
        wrapper_class=structlog.make_filtering_bound_logger(
            logging.getLevelName(nivel)
        ),
        logger_factory=structlog.PrintLoggerFactory(sys.stdout),
        cache_logger_on_first_use=True,
    )


import structlog

logger = structlog.get_logger()


async def middleware_logging(request: Request, call_next):
    structlog.contextvars.clear_contextvars()
    structlog.contextvars.bind_contextvars(
        request_id=request.headers.get("X-Request-ID", str(uuid.uuid4())),
        metodo=request.method,
        ruta=request.url.path,
        ip_cliente=request.client.host if request.client else None,
    )

    logger.info("peticion_recibida")

    inicio = time.perf_counter()
    respuesta = await call_next(request)
    duracion_ms = (time.perf_counter() - inicio) * 1000

    logger.info(
        "peticion_completada",
        codigo_estado=respuesta.status_code,
        duracion_ms=round(duracion_ms, 2),
    )

    return respuesta
```

---

## Reglas de Alerting — Prometheus Alertmanager

```yaml
# alerts/api.yml
groups:
  - name: api_slos
    rules:
      - alert: DisponibilidadBajaSLO
        expr: |
          (
            sum(rate(http_requests_total{status!~"5.."}[5m]))
            /
            sum(rate(http_requests_total[5m]))
          ) < 0.999
        for: 5m
        labels:
          severidad: critica
          equipo: backend
        annotations:
          resumen: "Disponibilidad por debajo del SLO"
          descripcion: >
            La disponibilidad actual es {{ $value | humanizePercentage }},
            por debajo del SLO de 99.9%.
          runbook: "https://wiki.empresa.com/runbooks/disponibilidad-baja"

      - alert: LatenciaAltaP99
        expr: |
          histogram_quantile(0.99,
            sum(rate(http_request_duration_seconds_bucket[5m])) by (le, endpoint)
          ) > 1.0
        for: 3m
        labels:
          severidad: advertencia
          equipo: backend
        annotations:
          resumen: "P99 de latencia supera 1 segundo"
          runbook: "https://wiki.empresa.com/runbooks/latencia-alta"

      - alert: TasaErroresAlta
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m])) by (endpoint)
          /
          sum(rate(http_requests_total[5m])) by (endpoint)
          > 0.05
        for: 2m
        labels:
          severidad: critica
          equipo: backend
        annotations:
          resumen: "Tasa de errores 5xx supera 5%"
          runbook: "https://wiki.empresa.com/runbooks/errores-5xx"

  - name: infraestructura
    rules:
      - alert: UsoMemoriaAlto
        expr: |
          (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)
          / node_memory_MemTotal_bytes > 0.90
        for: 10m
        labels:
          severidad: advertencia
        annotations:
          resumen: "Uso de memoria superior al 90%"
          runbook: "https://wiki.empresa.com/runbooks/memoria-alta"

      - alert: UsoCPUAlto
        expr: |
          100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance) * 100 > 85
        for: 15m
        labels:
          severidad: advertencia
```

---

## Runbook — Plantilla Mínima

```markdown
# Runbook: TasaErroresAlta

## Sintoma
Alerta `TasaErroresAlta` disparada. Tasa de errores 5xx > 5% en endpoint X.

## Impacto
- Usuarios afectados: ~N% de trafico
- Funcionalidad degradada: [describir]

## Diagnostico (en orden)

### 1. Verificar despliegues recientes
kubectl rollout history deployment/api-backend

### 2. Ver logs de errores
kubectl logs -l app=api-backend --since=10m | grep "ERROR"

### 3. Revisar dependencias externas
- Base de datos: verificar conexiones
- Cache Redis: redis-cli ping

## Acciones de remediacion

### Si fue un deploy reciente:
kubectl rollout undo deployment/api-backend

### Si la BD esta saturada:
- Activar modo de solo lectura (feature flag DB_READ_ONLY=true)

## Escalar a
- Si no resuelve en 15 minutos: notificar a #oncall-senior
- Si hay perdida de datos: escalar a CTO inmediatamente
```
