DevOps

Monitoreo y observabilidad para servicios Go: métricas, trazado y logs en un ecosistema unificado

Ruslan Ismailov Publicado 14 min de lectura
M

Introducción: los tres pilares de la observabilidad

La observabilidad es la capacidad de comprender el estado interno de un sistema a partir de sus salidas externas. Para los servicios Go en producción, esto implica tres componentes obligatorios:

  • Métricas — indicadores numéricos del estado del sistema a lo largo del tiempo (CPU, RPS, latencia, tasa de errores).
  • Trazado distribuido — seguimiento del recorrido de una solicitud a través de todos los servicios y componentes.
  • Logs — registros estructurados de eventos con contexto.

Sin cada uno de estos pilares, trabajas a ciegas. Las métricas dicen "algo se rompió", los logs explican "por qué", y el trazado revela "dónde exactamente". En 2026, OpenTelemetry se ha convertido en el estándar de facto, y la combinación Prometheus + Grafana + Loki + Tempo cubre las tres áreas en un ecosistema unificado. En este artículo recorreremos todo el camino desde la instrumentación del código hasta el despliegue en Kubernetes.

Instrumentación de una aplicación Go: cliente Prometheus y métricas personalizadas

El primer paso es añadir métricas directamente en el código del servicio. El cliente oficial de Prometheus para Go permite exportar contadores, histogramas, gauges y summaries.

// go get github.com/prometheus/client_golang/prometheus\n// go get github.com/prometheus/client_golang/prometheus/promhttp\n\npackage main\n\nimport (\n    \"net/http\"\n    \"time\"\n\n    \"github.com/prometheus/client_golang/prometheus\"\n    \"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\nvar (\n    httpRequestsTotal = prometheus.NewCounterVec(\n        prometheus.CounterOpts{\n            Name: \"http_requests_total\",\n            Help: \"Total number of HTTP requests\",\n        },\n        []string{\"method\", \"path\", \"status\"},\n    )\n\n    httpRequestDuration = prometheus.NewHistogramVec(\n        prometheus.HistogramOpts{\n            Name:    \"http_request_duration_seconds\",\n            Help:    \"HTTP request duration in seconds\",\n            Buckets: prometheus.DefBuckets,\n        },\n        []string{\"method\", \"path\"},\n    )\n)\n\nfunc init() {\n    prometheus.MustRegister(httpRequestsTotal)\n    prometheus.MustRegister(httpRequestDuration)\n}\n\nfunc metricsMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        start := time.Now()\n        rw := &responseWriter{w, http.StatusOK}\n        next.ServeHTTP(rw, r)\n        duration := time.Since(start).Seconds()\n\n        statusStr := http.StatusText(rw.status)\n        httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, statusStr).Inc()\n        httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)\n    })\n}\n\nfunc main() {\n    mux := http.NewServeMux()\n    mux.Handle(\"/metrics\", promhttp.Handler())\n    mux.Handle(\"/api/v1/orders\", metricsMiddleware(http.HandlerFunc(ordersHandler)))\n    http.ListenAndServe(\":8080\", mux)\n}

El endpoint /metrics es el punto estándar de recolección de métricas para Prometheus. Usa las etiquetas con cuidado: una alta cardinalidad (por ejemplo, user_id como label) degrada seriamente el rendimiento de Prometheus.

Trazado distribuido con OpenTelemetry en Go — práctica en 2026

OpenTelemetry (OTel) se ha convertido en el estándar unificado para trazado y métricas. En 2026, el SDK se estabilizó y la integración en servicios Go es directa y sencilla.

// go get go.opentelemetry.io/otel\n// go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\n// go get go.opentelemetry.io/otel/sdk/trace\n\npackage telemetry\n\nimport (\n    \"context\"\n\n    \"go.opentelemetry.io/otel\"\n    \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n    \"go.opentelemetry.io/otel/sdk/resource\"\n    sdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n    semconv \"go.opentelemetry.io/otel/semconv/v1.21.0\"\n)\n\nfunc InitTracer(serviceName, otlpEndpoint string) (*sdktrace.TracerProvider, error) {\n    exporter, err := otlptracehttp.New(\n        context.Background(),\n        otlptracehttp.WithEndpoint(otlpEndpoint),\n        otlptracehttp.WithInsecure(),\n    )\n    if err != nil {\n        return nil, err\n    }\n\n    res := resource.NewWithAttributes(\n        semconv.SchemaURL,\n        semconv.ServiceName(serviceName),\n        semconv.ServiceVersion(\"1.0.0\"),\n    )\n\n    tp := sdktrace.NewTracerProvider(\n        sdktrace.WithBatcher(exporter),\n        sdktrace.WithResource(res),\n        sdktrace.WithSampler(sdktrace.AlwaysSample()),\n    )\n    otel.SetTracerProvider(tp)\n    return tp, nil\n}\n\n// Uso en un handler:\nfunc ordersHandler(w http.ResponseWriter, r *http.Request) {\n    tracer := otel.Tracer(\"orders-service\")\n    ctx, span := tracer.Start(r.Context(), \"GetOrders\")\n    defer span.End()\n\n    orders, err := db.GetOrders(ctx)\n    if err != nil {\n        span.RecordError(err)\n        http.Error(w, \"internal error\", http.StatusInternalServerError)\n        return\n    }\n    // ...\n}

Las trazas se envían a Grafana Tempo a través del colector OTLP. Es fundamental propagar el context.Context a través de todas las llamadas — esta es la base del trazado distribuido en Go. Para la instrumentación automática de clientes HTTP y bases de datos, utiliza los paquetes contrib oficiales de OTel.

Logging estructurado: slog, zerolog e integración con Loki

Desde Go 1.21, la biblioteca estándar incluye log/slog, un logger estructurado. Para servicios de alta carga, zerolog es preferible por su asignación de memoria cero.

// zerolog\npackage main\n\nimport (\n    \"os\"\n    \"github.com/rs/zerolog\"\n    \"github.com/rs/zerolog/log\"\n)\n\nfunc main() {\n    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix\n    log.Logger = zerolog.New(os.Stdout).With().\n        Timestamp().\n        Str(\"service\", \"orders-service\").\n        Str(\"env\", \"production\").\n        Logger()\n\n    log.Info().\n        Str(\"method\", \"GET\").\n        Str(\"path\", \"/api/v1/orders\").\n        Int(\"status\", 200).\n        Dur(\"duration\", duration).\n        Msg(\"request completed\")\n}

Los logs en formato JSON desde stdout son recolectados por Promtail (o Grafana Alloy en 2026) y enviados a Grafana Loki. En Loki, los logs se indexan por etiquetas (service, env, level), mientras que la búsqueda de texto completo opera sobre el contenido. Importante: no crees etiquetas innecesarias en Loki — el mismo problema de alta cardinalidad que en Prometheus.

Para correlacionar logs con trazas, añade trace_id y span_id en cada entrada de log:

import \"go.opentelemetry.io/otel/trace\"\n\nfunc logWithTrace(ctx context.Context, msg string) {\n    span := trace.SpanFromContext(ctx)\n    sc := span.SpanContext()\n    log.Info().\n        Str(\"trace_id\", sc.TraceID().String()).\n        Str(\"span_id\", sc.SpanID().String()).\n        Msg(msg)\n}

Almacenamiento de métricas con Redis para agregación en tiempo real

Prometheus gestiona bien el almacenamiento a largo plazo, pero para la agregación en tiempo real — como rate limiting, leaderboards o contadores de ventana deslizante — Redis resulta muy conveniente. La combinación Go + Redis permite calcular métricas directamente en memoria y volcarlas periódicamente en Prometheus mediante un colector personalizado.

// go get github.com/redis/go-redis/v9\n\npackage metrics\n\nimport (\n    \"context\"\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/prometheus/client_golang/prometheus\"\n)\n\ntype RedisMetricsCollector struct {\n    rdb     *redis.Client\n    counter *prometheus.Desc\n}\n\nfunc NewRedisMetricsCollector(rdb *redis.Client) *RedisMetricsCollector {\n    return &RedisMetricsCollector{\n        rdb: rdb,\n        counter: prometheus.NewDesc(\n            \"realtime_orders_processed_total\",\n            \"Orders processed (from Redis counter)\",\n            []string{\"region\"}, nil,\n        ),\n    }\n}\n\nfunc (c *RedisMetricsCollector) Describe(ch chan<- *prometheus.Desc) {\n    ch <- c.counter\n}\n\nfunc (c *RedisMetricsCollector) Collect(ch chan<- prometheus.Metric) {\n    ctx := context.Background()\n    val, _ := c.rdb.Get(ctx, \"orders:processed:eu\").Float64()\n    ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, val, \"eu\")\n}

Redis también se utiliza para cachear los resultados de consultas pesadas de Prometheus en dashboards y para almacenar agregados temporales entre reinicios del servicio.

Despliegue del stack de monitoreo en Kubernetes: Prometheus Operator, Grafana, Tempo

En entornos de producción con Kubernetes, se recomienda utilizar kube-prometheus-stack (chart de Helm), que despliega Prometheus Operator, Grafana y un conjunto de alertas predefinidas.

# values.yaml para kube-prometheus-stack\nprometheus:\n  prometheusSpec:\n    retention: 30d\n    storageSpec:\n      volumeClaimTemplate:\n        spec:\n          storageClassName: fast-ssd\n          resources:\n            requests:\n              storage: 100Gi\n\ngrafana:\n  enabled: true\n  adminPassword: \"changeme\"\n  additionalDataSources:\n    - name: Loki\n      type: loki\n      url: http://loki:3100\n    - name: Tempo\n      type: tempo\n      url: http://tempo:3200\n\nalertmanager:\n  enabled: true

Para el descubrimiento automático de servicios, utiliza ServiceMonitor — el CRD de Prometheus Operator:

apiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n  name: orders-service\n  namespace: production\nspec:\n  selector:\n    matchLabels:\n      app: orders-service\n  endpoints:\n    - port: http\n      path: /metrics\n      interval: 15s

Grafana Tempo se despliega por separado y recibe trazas via OTLP. En Grafana se configura la correlación: desde el dashboard de métricas se puede navegar a los logs en Loki, y desde los logs al trazado en Tempo mediante el trace_id. Esto es precisamente el ecosistema unificado de observabilidad.

Alertas: configuración de reglas e integración con mensajería

Alertmanager enruta las alertas a Slack, Telegram, PagerDuty y otros sistemas. Ejemplo de PrometheusRule:

apiVersion: monitoring.coreos.com/v1\nkind: PrometheusRule\nmetadata:\n  name: orders-service-alerts\nspec:\n  groups:\n    - name: orders.rules\n      rules:\n        - alert: HighErrorRate\n          expr: |\n            rate(http_requests_total{status=\"Internal Server Error\"}[5m])\n            / rate(http_requests_total[5m]) > 0.05\n          for: 2m\n          labels:\n            severity: critical\n          annotations:\n            summary: \"Alta tasa de errores en {{ $labels.path }}\"\n            description: \"La tasa de errores es {{ $value | humanizePercentage }}\"\n\n        - alert: SlowResponseTime\n          expr: |\n            histogram_quantile(0.99,\n              rate(http_request_duration_seconds_bucket[5m])\n            ) > 1.0\n          for: 5m\n          labels:\n            severity: warning\n          annotations:\n            summary: \"Latencia P99 por encima de 1s\"

Configuración de Alertmanager para Telegram:

route:\n  group_by: ['alertname', 'severity']\n  receiver: 'telegram'\n\nreceivers:\n  - name: 'telegram'\n    telegram_configs:\n      - bot_token: '${TELEGRAM_BOT_TOKEN}'\n        chat_id: -1001234567890\n        message: |\n          🚨 *{{ .GroupLabels.alertname }}*\n          {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}

Ejemplo práctico: stack completo de observabilidad para un microservicio Go

A continuación, un docker-compose para desarrollo local que levanta todo el stack con un solo comando.

version: '3.8'\n\nservices:\n  orders-service:\n    build: ./services/orders\n    ports:\n      - \"8080:8080\"\n    environment:\n      - OTLP_ENDPOINT=otel-collector:4318\n      - REDIS_URL=redis:6379\n    depends_on:\n      - redis\n      - otel-collector\n\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"6379:6379\"\n\n  otel-collector:\n    image: otel/opentelemetry-collector-contrib:latest\n    volumes:\n      - ./otel-config.yaml:/etc/otel/config.yaml\n    command: [\"--config=/etc/otel/config.yaml\"]\n    ports:\n      - \"4318:4318\"\n\n  prometheus:\n    image: prom/prometheus:latest\n    volumes:\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml\n    ports:\n      - \"9090:9090\"\n\n  grafana:\n    image: grafana/grafana:latest\n    ports:\n      - \"3000:3000\"\n    environment:\n      - GF_AUTH_ANONYMOUS_ENABLED=true\n    volumes:\n      - ./grafana/provisioning:/etc/grafana/provisioning\n\n  loki:\n    image: grafana/loki:latest\n    ports:\n      - \"3100:3100\"\n\n  tempo:\n    image: grafana/tempo:latest\n    command: [\"-config.file=/etc/tempo.yaml\"]\n    volumes:\n      - ./tempo.yaml:/etc/tempo.yaml\n    ports:\n      - \"3200:3200\"\n\n  promtail:\n    image: grafana/promtail:latest\n    volumes:\n      - /var/lib/docker/containers:/var/lib/docker/containers:ro\n      - ./promtail-config.yaml:/etc/promtail/config.yaml\n    command: -config.file=/etc/promtail/config.yaml

Configuración del OTel Collector (otel-config.yaml):

receivers:\n  otlp:\n    protocols:\n      http:\n        endpoint: 0.0.0.0:4318\n\nexporters:\n  otlp/tempo:\n    endpoint: tempo:4317\n    tls:\n      insecure: true\n  prometheus:\n    endpoint: 0.0.0.0:8889\n\nservice:\n  pipelines:\n    traces:\n      receivers: [otlp]\n      exporters: [otlp/tempo]\n    metrics:\n      receivers: [otlp]\n      exporters: [prometheus]

Tras ejecutar docker compose up -d, abre Grafana en localhost:3000. Conecta las fuentes de datos Prometheus, Loki y Tempo, configura la correlación — y tendrás un stack de observabilidad completo listo para desarrollo.

Conclusión

Construir observabilidad para microservicios Go hoy es más sencillo de lo que parece: OpenTelemetry unifica el trazado y las métricas, zerolog/slog cubren el logging estructurado, y la combinación Prometheus + Grafana + Loki + Tempo ofrece una ventana única para el análisis. Redis complementa orgánicamente el stack para la agregación en tiempo real. En Kubernetes, Prometheus Operator y ServiceMonitor automatizan el descubrimiento de servicios y la recolección de métricas. Comienza con docker-compose local, desarrolla buenos hábitos de instrumentación y luego migra el stack a producción — tus servicios Go se volverán verdaderamente transparentes.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →