SRE: Observabilidad a Fondo: Trazas, Logs y Métricas

2026-02-28 | Gabriel Garrido | 9 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introducción

En los artículos anteriores cubrimos SLIs, SLOs y automatizaciones y gestión de incidentes. Ambos asumen que realmente podés ver lo que está pasando en tus sistemas. De eso se trata la observabilidad.


El monitoreo te dice cuándo algo está roto. La observabilidad te dice por qué. La diferencia importa cuando estás a las 3am tratando de entender por qué la latencia subió: necesitás pasar de “algo está lento” a “esta consulta específica a la base de datos en este servicio específico es lenta porque está haciendo un full table scan” en minutos, no en horas.


En este artículo vamos a cubrir los tres pilares de la observabilidad (métricas, logs, trazas), cómo instrumentar tus aplicaciones con OpenTelemetry, cómo construir dashboards de Grafana que sean realmente útiles durante incidentes, y cómo configurar agregación de logs con Loki. Todo con ejemplos prácticos que podés aplicar a tus cargas de trabajo en Kubernetes.


Vamos al tema.


Los tres pilares

La observabilidad se construye sobre tres tipos de datos de telemetría, cada uno con un propósito diferente:


  • Métricas: Datos numéricos agregados a lo largo del tiempo. “¿Cuántas requests por segundo?” “¿Cuál es la latencia p99?” Rápidas de consultar, baratas de almacenar, geniales para alertas y dashboards.
  • Logs: Eventos discretos con contexto. “La request X falló con error Y en el momento Z.” Ricos en detalle pero caros de almacenar y lentos de consultar a escala.
  • Trazas: El recorrido de una request a través de múltiples servicios. “Esta request pasó por el servicio A, después B, después C, y la parte lenta fue la llamada de B a la base de datos.” Esenciales para debuggear sistemas distribuidos.

La idea clave es que estos tres son complementarios. Las métricas te dicen que algo anda mal. Los logs te dicen qué salió mal. Las trazas te dicen en qué parte del sistema salió mal. Necesitás los tres.


# El flujo de observabilidad durante un incidente:
#
# 1. MÉTRICAS: Alerta salta: "latencia p99 > 300ms por 5 minutos"
#    └─ Sabés QUÉ anda mal
#
# 2. TRAZAS: Encontrás trazas lentas: "90% de las requests lentas pasan por payment-service → db"
#    └─ Sabés DÓNDE anda mal
#
# 3. LOGS: Revisás logs de payment-service: "ERROR: pool de conexiones agotado, esperó 5s por conexión"
#    └─ Sabés POR QUÉ anda mal

OpenTelemetry: un SDK para instrumentar todo

OpenTelemetry (OTel) es el estándar para instrumentar aplicaciones. Provee un único SDK que puede emitir métricas, logs y trazas desde tu código. Lo lindo es que instrumentás una vez y podés enviar los datos a cualquier backend (Prometheus, Jaeger, Grafana, Datadog, etc.).


Para nuestra aplicación Elixir/Phoenix, el setup se ve así:


# mix.exs - agregar dependencias de OpenTelemetry
defp deps do
  [
    # OpenTelemetry core
    {:opentelemetry, "~> 1.4"},
    {:opentelemetry_api, "~> 1.3"},
    {:opentelemetry_exporter, "~> 1.7"},

    # Librerías de auto-instrumentación
    {:opentelemetry_phoenix, "~> 2.0"},
    {:opentelemetry_ecto, "~> 1.2"},
    {:opentelemetry_finch, "~> 0.2"},

    # ... tus deps existentes
  ]
end

Después configurás el SDK de OpenTelemetry:


# config/runtime.exs
if config_env() == :prod do
  config :opentelemetry,
    span_processor: :batch,
    traces_exporter: :otlp

  config :opentelemetry_exporter,
    otlp_protocol: :grpc,
    otlp_endpoint: System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT") || "http://otel-collector:4317"
end

Y configurás la auto-instrumentación en el arranque de tu aplicación:


# lib/tr/application.ex
def start(_type, _args) do
  # Configurar instrumentación de OpenTelemetry
  OpentelemetryPhoenix.setup(adapter: :bandit)
  OpentelemetryEcto.setup([:tr, :repo])

  children = [
    # ... tus children existentes
  ]

  opts = [strategy: :one_for_one, name: Tr.Supervisor]
  Supervisor.start_link(children, opts)
end

Con solo este setup, cada request HTTP a tu app Phoenix automáticamente tiene una traza con spans para la acción del controller, las consultas de Ecto, y las llamadas HTTP salientes. Sin instrumentación manual para lo básico.


Para spans customizados cuando necesitás más detalle:


# lib/tr/search.ex
require OpenTelemetry.Tracer, as: Tracer

def search(term) do
  Tracer.with_span "search.execute" do
    Tracer.set_attribute("search.term", term)

    results =
      Tracer.with_span "search.query_index" do
        Haystack.index(index_name())
        |> Haystack.query(term)
      end

    Tracer.set_attribute("search.results_count", length(results))
    results
  end
end

El Collector de OpenTelemetry

No querés que tu aplicación envíe telemetría directamente a los backends. El Collector de OpenTelemetry se sienta entre tus apps y tus backends, manejando batching, reintentos, filtrado y ruteo.


Deployalo como un DaemonSet en Kubernetes para que cada nodo tenga un collector local:


# otel-collector/daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
        - name: collector
          image: otel/opentelemetry-collector-contrib:0.96.0
          ports:
            - containerPort: 4317   # receptor OTLP gRPC
              protocol: TCP
            - containerPort: 4318   # receptor OTLP HTTP
              protocol: TCP
          volumeMounts:
            - name: config
              mountPath: /etc/otelcol-contrib
      volumes:
        - name: config
          configMap:
            name: otel-collector-config

La configuración del collector rutea datos a los backends correctos:


# otel-collector/config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-config
  namespace: monitoring
data:
  config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318

    processors:
      batch:
        timeout: 5s
        send_batch_size: 1000

      # Agregar atributos de recurso
      resource:
        attributes:
          - key: k8s.cluster.name
            value: "production"
            action: upsert

      # Filtrar trazas de health check (ruido)
      filter:
        traces:
          span:
            - 'attributes["http.target"] == "/health"'
            - 'attributes["http.target"] == "/ready"'

    exporters:
      # Trazas a Tempo
      otlp/tempo:
        endpoint: tempo:4317
        tls:
          insecure: true

      # Métricas a Prometheus
      prometheus:
        endpoint: 0.0.0.0:8889

      # Logs a Loki
      loki:
        endpoint: http://loki:3100/loki/api/v1/push

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [batch, resource, filter]
          exporters: [otlp/tempo]
        metrics:
          receivers: [otlp]
          processors: [batch, resource]
          exporters: [prometheus]
        logs:
          receivers: [otlp]
          processors: [batch, resource]
          exporters: [loki]

Esto te da una separación limpia: tus apps envían todo al collector local, y el collector maneja el ruteo a Tempo (trazas), Prometheus (métricas) y Loki (logs).


Trazabilidad distribuida con Tempo

Grafana Tempo es un excelente backend de trazas. Es fácil de deployar, escala bien, y se integra nativamente con Grafana para visualización.


Las consultas más útiles durante un incidente:


# Encontrar trazas lentas (latencia > 1s) para un servicio específico
{ resource.service.name = "tr-web" } && duration > 1s

# Encontrar trazas con error
{ resource.service.name = "tr-web" } && status = error

# Encontrar trazas para un endpoint específico
{ resource.service.name = "tr-web" && span.http.target = "/blog" }

# Encontrar trazas con consultas a DB de más de 500ms
{ span.db.system = "postgresql" } && duration > 500ms

El verdadero poder del tracing se muestra en sistemas distribuidos. Cuando una request fluye por servicio A → B → C, y es lenta, la traza inmediatamente te muestra qué hop introdujo la latencia. Sin trazas, estarías greppeando logs en tres servicios diferentes tratando de correlacionar timestamps.


Agregación de logs con Loki

Grafana Loki es como Prometheus pero para logs. Indexa metadatos (labels) en lugar del contenido completo del log, haciéndolo mucho más barato de operar que soluciones basadas en Elasticsearch.


Para Kubernetes, el setup más simple usa Promtail como DaemonSet para enviar logs de contenedores a Loki. Promtail descubre pods automáticamente y enriquece los logs con labels de Kubernetes (namespace, nombre del pod, nombre del contenedor). Después podés consultar logs en Grafana usando LogQL:


# Todos los logs del deployment tr-web
{namespace="default", app="tr-web"}

# Solo logs de error
{namespace="default", app="tr-web"} |= "error" != "404"

# Parseo de logs estructurados (si usás logs JSON)
{namespace="default", app="tr-web"} | json | level="error"

# Contando errores por minuto
count_over_time({namespace="default", app="tr-web"} |= "error" [1m])

# Encontrar consultas lentas a la base de datos
{namespace="default", app="tr-web"} |= "query" | json | duration > 500

Para nuestra app Elixir, el logging estructurado hace a Loki mucho más poderoso:


# config/prod.exs
config :logger, :console,
  format: {LogfmtEx, :format},
  metadata: [:request_id, :trace_id, :span_id, :user_id]

El trace_id en tus logs es la clave. Te permite saltar de una línea de log a la traza distribuida completa en Tempo, conectando los tres pilares sin problemas.


Dashboards de Grafana que realmente ayudan

La mayoría de los dashboards son inútiles durante incidentes porque muestran demasiada información y nada de ella es accionable. Acá cómo construir dashboards que ayuden:


1. El dashboard RED (Rate, Errors, Duration)

Este es el dashboard más útil para cualquier servicio. Tres paneles:


# Tasa: requests por segundo
sum(rate(http_requests_total{service="tr-web"}[5m]))

# Errores: porcentaje de tasa de error
sum(rate(http_requests_total{service="tr-web", status=~"5.."}[5m]))
/
sum(rate(http_requests_total{service="tr-web"}[5m])) * 100

# Duración: percentiles de latencia
histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{service="tr-web"}[5m])) by (le))
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service="tr-web"}[5m])) by (le))
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{service="tr-web"}[5m])) by (le))

2. El dashboard de SLO

Mostrá la tasa de quemado del presupuesto de error junto con los valores reales del SLI:


# Valor actual del SLI (disponibilidad en 30 días)
sli:availability:ratio_rate30d{service="tr-web"}

# Presupuesto de error restante
1 - (
  (1 - sli:availability:ratio_rate30d{service="tr-web"})
  /
  (1 - 0.999)
)

Principios de diseño de dashboards:


  • Poné los paneles más importantes arriba. Durante un incidente nadie scrollea.
  • Usá rangos de tiempo consistentes en todos los paneles.
  • Agregá anotaciones para deploys. Una línea vertical mostrando “deploy v1.2.3” ayuda a correlacionar cambios con problemas.
  • Usá umbrales para colorear paneles en rojo/amarillo/verde basándose en objetivos de SLO.
  • Incluí links a runbooks en las descripciones de los paneles.

Correlacionando los tres pilares

El verdadero poder de la observabilidad viene cuando podés moverte sin problemas entre métricas, trazas y logs. Grafana hace esto posible a través de exemplars y linking entre data sources.


Los exemplars son trace IDs adjuntados a puntos de datos de métricas. Cuando ves un pico en tu gráfico de latencia, podés hacer clic y saltar directamente a una traza que fue parte de ese pico.


El linking de traza a logs te permite hacer clic desde un span de traza a los logs correspondientes en Loki.


Con este setup, tu flujo de debugging durante un incidente se convierte en:


  1. Ves un pico en el dashboard RED (métricas)
  2. Hacés clic en un exemplar para abrir una traza de esa ventana de tiempo
  3. Encontrás el span lento en la traza (ej: una consulta a la base de datos)
  4. Hacés clic para ir a los logs de ese pod alrededor de ese timestamp
  5. Ves el mensaje de error real en los logs

Todo este flujo toma segundos, no los minutos u horas de greppear logs manualmente.


Instrumentando Elixir con OpenTelemetry: patrones prácticos

Acá van algunos patrones prácticos para instrumentar aplicaciones Elixir más allá de la auto-instrumentación básica:


Jobs en background (como nuestras tareas de Quantum)


# lib/tr/sponsors.ex
require OpenTelemetry.Tracer, as: Tracer

def start do
  Tracer.with_span "sponsors.sync", %{kind: :internal} do
    start_app()

    sponsors = get_sponsors(100)
    nodes = get_in(sponsors, ["data", "user", "sponsors", "nodes"]) || []

    Tracer.set_attribute("sponsors.count", length(nodes))

    Enum.each(nodes, fn sponsor ->
      Tr.SponsorsCache.add_or_update(sponsor)
    end)

    :ok
  end
end

Operaciones de GenServer


# lib/tr/sponsors_cache.ex
def handle_call({:get, login}, _from, state) do
  Tracer.with_span "sponsors_cache.get" do
    Tracer.set_attribute("sponsor.login", login)
    result = Map.get(state, login)
    {:reply, result, state}
  end
end

Interacciones de LiveView


# lib/tr_web/live/search_live.ex
def handle_event("search", %{"q" => query}, socket) do
  Tracer.with_span "live.search", %{attributes: %{"search.query" => query}} do
    results = Tr.Search.search(query)
    Tracer.set_attribute("search.results_count", length(results))
    {:noreply, assign(socket, results: results, query: query)}
  end
end

Qué evitar

Algunos anti-patrones comunes de observabilidad:


  • No loguees todo. Logs de alta cardinalidad son caros. Logueá eventos, no cada llamada a función.
  • No traces todo. Filtrá health checks y readiness probes. Son ruido.
  • No crees un dashboard para cada métrica. Empezá con RED y USE, agregá más solo cuando sea necesario.
  • No te olvides del sampling. En producción a escala, traceá 10-20% de las requests. Sampleá 100% de los errores.
  • No te saltees el logging estructurado. Logs no estructurados (“algo salió mal”) son casi inútiles en Loki.

Notas finales

La observabilidad no se trata de tener más datos. Se trata de tener los datos correctos conectados de la manera correcta para que puedas pasar de “algo anda mal” a “sé exactamente qué anda mal y por qué” en minutos.


El stack que cubrimos: OpenTelemetry, Prometheus, Tempo, Loki, Grafana, es todo open source y corre hermosamente en Kubernetes. Empezá con auto-instrumentación (se configura en 10 minutos), construí un dashboard RED, y agregá más instrumentación a medida que la necesites.


En el próximo artículo vamos a explorar chaos engineering: cómo romper proactivamente tus sistemas para construir confianza en tu observabilidad y procesos de respuesta a incidentes.


¡Espero que te haya resultado útil y lo hayas disfrutado! ¡Hasta la próxima!


Errata

Si encontrás algún error o tenés alguna sugerencia, por favor mandame un mensaje para que se corrija.

También podés revisar el código fuente y los cambios en las fuentes acá



$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-02-28 | Gabriel Garrido