SRE: Observabilidad a Fondo: Trazas, Logs y Métricas
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:
- Ves un pico en el dashboard RED (métricas)
- Hacés clic en un exemplar para abrir una traza de esa ventana de tiempo
- Encontrás el span lento en la traza (ej: una consulta a la base de datos)
- Hacés clic para ir a los logs de ese pod alrededor de ese timestamp
- 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: 0Por favor inicie sesión para poder escribir comentarios.