Skip to content

Observability

Anderson Fabian Garcia Nieto edited this page Jul 9, 2026 · 3 revisions

La observabilidad es la capacidad de entender el estado interno de un sistema a través de sus outputs externos. Linker1 implementa los tres pilares de la observabilidad — logs, métricas y traces — utilizando OpenTelemetry como framework de instrumentación y Grafana Cloud como backend de análisis.


Arquitectura de Observabilidad

┌──────────────────────────────────────────────────────────────────────────┐
│                          LINKER1 (JVM)                                   │
│                                                                          │
│  ┌─────────────┐   ┌──────────────┐   ┌──────────────┐                  │
│  │ RequestMetrics│   │  LinkSpans   │   │ SystemMetrics │                  │
│  │  (Counters,  │   │  (Traces +   │   │  (Gauges)    │                  │
│  │  Histograms) │   │  Histograms) │   │              │                  │
│  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘                  │
│         │                  │                   │                          │
│         └──────────┬───────┴───────────────────┘                         │
│                    ▼                                                     │
│           ┌────────────────┐                                             │
│           │   Telemetry    │  ← Composition Root                         │
│           │  (OTel SDK)    │                                             │
│           └────────┬───────┘                                             │
│                    │                                                     │
│  ┌─────────────────┼──────────────────┐                                  │
│  │                 │                  │                                   │
│  ▼                 ▼                  ▼                                   │
│ Tracer           Meter          Logback Appender                         │
│                                  (OTEL bridge)                           │
│                                                                          │
│  ┌─────────────┐   ┌──────────────┐   ┌──────────────┐                  │
│  │HealthCheck  │   │HealthCheck   │   │  SLF4J       │                  │
│  │  Spans      │   │  Metrics     │   │  Loggers     │                  │
│  └─────────────┘   └──────────────┘   └──────────────┘                  │
│                                                                          │
└────────────────────────────┬─────────────────────────────────────────────┘
                             │
                     OTLP (http/protobuf)
                             │
                             ▼
┌────────────────────────────────────────────────────────────────────────┐
│                       GRAFANA CLOUD                                    │
│                                                                        │
│   ┌──────────┐      ┌──────────┐      ┌──────────┐                    │
│   │   Loki   │      │  Mimir   │      │  Tempo   │                    │
│   │  (Logs)  │      │(Metrics) │      │ (Traces) │                    │
│   └──────────┘      └──────────┘      └──────────┘                    │
│                                                                        │
│   ┌─────────────────────────────────────────────┐                      │
│   │         Grafana Dashboards                  │                      │
│   │  Correlación logs ↔ métricas ↔ traces       │                      │
│   └─────────────────────────────────────────────┘                      │
│                                                                        │
│   ┌─────────────────────────────────────────────┐                      │
│   │      Synthetic Monitoring                   │                      │
│   │  GET /healthz cada 5 minutos                │                      │
│   └─────────────────────────────────────────────┘                      │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

¿Por qué instrumentación manual?

OpenTelemetry para Java ofrece dos caminos:

Enfoque Cómo funciona Pros Contras
Java Agent (auto) -javaagent:opentelemetry-javaagent.jar Cero código, instrumenta frameworks conocidos Caja negra, difícil de testear
SDK Manual (nuestro) API directa en el código fuente Visible, testeable, revisable Hay que escribirlo a mano

Linker1 usa instrumentación manual: cada span, métrica y log es una línea explícita de código, no inyección de bytecode. Esto lo hace completamente visible en code review y testeable con opentelemetry-sdk-testing.


Pilar 1: Métricas

Métricas de Requests (RED Method)

La clase RequestMetrics implementa el método RED (Rate, Errors, Duration):

requestMetrics.recordRequest(route, statusCode, durationMillis);
Métrica Tipo Unidad Significado
linker.http.requests Counter {request} Total de HTTP requests atendidos
linker.http.errors Counter {request} Total de requests con status ≥ 400
linker.http.request.duration Histogram ms Duración de HTTP requests

Atributos (tags):

  • route: ruta del endpoint (/{id}, /link)
  • status_code: código HTTP de respuesta

Métricas de Base de Datos

Métrica Tipo Unidad Significado
linker.db.operation.duration Histogram ms Duración de operaciones de repositorio

Atributos: operation (create / resolve)

Métricas de Sistema (USE Method)

La clase SystemMetrics implementa gauges que se muestrean cada 30 segundos:

Métrica Tipo Unidad Significado
linker.links.count Gauge {link} Número actual de links almacenados
linker.jvm.heap.used Gauge By Heap JVM en uso
linker.jvm.heap.max Gauge By Heap JVM máximo disponible
linker.jvm.threads Gauge {thread} Threads JVM activos
linker.process.uptime Gauge s Tiempo desde el inicio del proceso

Métricas de Health Check

Métrica Tipo Unidad Significado
linker.healthcheck.checks Counter {check} Total de health checks realizados
linker.healthcheck.failures Counter {check} Total de health checks fallidos
linker.healthcheck.duration Histogram ms Duración del check (conexión + SELECT 1)
linker.healthcheck.up Gauge {status} 1 = healthy, 0 = unhealthy

Atributos: outcome (healthy / unhealthy)


Pilar 2: Traces (Distributed Tracing)

Traces implementados

Linker1 genera 3 tipos de traces, cada uno con spans anidados:

1. Trace link.create (POST /link)

link.create                          ← Parent span
 └── link.create.persist             ← Child span (repo call)
     Attributes: link.url
     Status: OK / ERROR
     + linker.db.operation.duration histogram (operation=create)

2. Trace link.resolve (GET /{id})

link.resolve                         ← Parent span
 └── link.resolve.lookup             ← Child span (repo lookup)
     Attributes: link.id
     Status: OK / ERROR
     + linker.db.operation.duration histogram (operation=resolve)

3. Trace mysql.healthcheck (GET /healthz)

mysql.healthcheck                    ← Single span (SpanKind.SERVER)
    Attributes: db.system=mysql, db.statement=SELECT 1
    Status: OK / ERROR

Propagación de errores

Cuando una operación falla:

  1. La excepción se registra en el child span (span.recordException(e))
  2. El child span se marca como StatusCode.ERROR
  3. El error se propaga al parent span (parent.setStatus(StatusCode.ERROR))
  4. La excepción se re-lanza para que el handler HTTP genere el código de error correcto

Esto garantiza que en Grafana Tempo se puede ver el trace completo con el error exacto y dónde ocurrió.


Pilar 3: Logs

Framework

  • SLF4J como API de logging
  • Logback como implementación
  • OpenTelemetry Logback Appender como bridge hacia OTLP

Configuración (logback.xml)

<!-- Console output -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- formato estándar -->
</appender>

<!-- OpenTelemetry bridge -->
<appender name="OTEL" 
    class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
    <captureExperimentalAttributes>true</captureExperimentalAttributes>
    <captureCodeAttributes>true</captureCodeAttributes>
    <captureMdcAttributes>*</captureMdcAttributes>
</appender>

Niveles de Log

Nivel Uso Ejemplo
TRACE Detalle más fino del ciclo de vida de un request Received GET request on path=/{id}
DEBUG Pasos intermedios útiles para troubleshooting Resolving link id={id}, Database ready at path={}
INFO Eventos significativos esperados Created link id={id} url={url}, Server started on port={}
WARN Problemas recuperables (errores de cliente) Link id={id} not found, Alias conflict for alias={}
ERROR Fallos inesperados que necesitan investigación Failed to resolve link id={} (con stack trace)

Control de Verbosidad

La variable LOG_LEVEL controla la verbosidad sin rebuild:

# Default (INFO)
java -jar linker1.jar

# Para troubleshooting
LOG_LEVEL=DEBUG java -jar linker1.jar

# Máxima verbosidad
LOG_LEVEL=TRACE java -jar linker1.jar

Scope controlado: LOG_LEVEL solo afecta al paquete linker y a Main. Las librerías third-party (Jetty, SQLite, LaunchDarkly) permanecen en INFO para evitar ruido.


Configuración OTLP

Variables de Entorno

Variable Propósito Default
OTEL_SERVICE_NAME Nombre del servicio en telemetría linker1
OTEL_EXPORTER_OTLP_ENDPOINT URL del collector OTLP (SDK default, no-op si ausente)
OTEL_EXPORTER_OTLP_PROTOCOL Protocolo de exportación http/protobuf (hardcodeado)
OTEL_EXPORTER_OTLP_HEADERS Headers de autenticación (ninguno)

El problema del protocolo: grpc vs http/protobuf

Este fue uno de nuestros mayores aprendizajes operativos.

El SDK de OpenTelemetry Java usa grpc por defecto. Grafana Cloud solo acepta http/protobuf. El desajuste producía este error silencioso:

Failed to export ... Server responded with UNIMPLEMENTED

La aplicación seguía respondiendo 200 en todas las rutas. Los health checks pasaban. Los smoke tests pasaban. Pero no se exportaba absolutamente nada a Grafana.

Solución: hardcodear OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf en la unidad systemd, no como variable configurable sino como decisión de integración fija.

El endpoint debe incluir /otlp

✅ https://otlp-gateway-prod-us-east-3.grafana.net/otlp
❌ https://otlp-gateway-prod-us-east-3.grafana.net

Con http/protobuf, el SDK appenda /v1/traces, /v1/metrics, /v1/logs al endpoint. Sin /otlp, las requests van al path incorrecto y fallan silenciosamente.

Endpoint vacío vs ausente

Estado de la variable Comportamiento del SDK
Ausente SDK usa defaults internos (no-op export, silencioso) ✅
Vacía ("") ConfigurationExceptionapp no arranca
Con valor Exporta a ese endpoint ✅

Por esto, deploy.sh y pipeline.yml solo escriben la línea Environment= cuando hay un valor real:

if [ -n "$OTEL_EXPORTER_OTLP_ENDPOINT" ]; then
  echo "Environment=\"OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT\"" | \
    sudo tee -a /etc/systemd/system/linker1.service > /dev/null
fi

Grafana Cloud

Servicios utilizados

Servicio Pilar Función
Loki Logs Almacenamiento, búsqueda y correlación de logs
Mimir Métricas Almacenamiento de métricas en formato Prometheus
Tempo Traces Almacenamiento y visualización de traces distribuidos
Synthetic Monitoring Uptime Polling automático a /healthz cada 5 minutos

Synthetic Monitoring

Configurado manualmente en Grafana Cloud:

  • Tipo: HTTP check
  • Target: https://1.n-la-c.app/healthz
  • Frecuencia: cada 5 minutos
  • Reporta: uptime, latencia, respuesta HTTP

Esto genera tráfico sintético que alimenta las métricas de health check incluso cuando no hay tráfico real de usuarios.

Configuración para Grafana Cloud

# En la VM o como variables en el pipeline:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-gateway-<region>.grafana.net/otlp"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64(instance-id:api-token)>"

Degradación Graciosa

La observabilidad está diseñada para degradar sin romper la aplicación:

Escenario Comportamiento
OTEL_EXPORTER_OTLP_ENDPOINT ausente App funciona normalmente, telemetría no se exporta
Grafana Cloud caído El SDK descarta batches silenciosamente, app no se ve afectada
MYSQL_* no configurado /healthz retorna 503, app funciona normalmente
LaunchDarkly caído FeatureFlags retorna defaults seguros (false)

Principio: la observabilidad es una mejora, no un requisito. La aplicación debe funcionar correctamente incluso sin telemetría.


Testabilidad

Toda la instrumentación es testeable con opentelemetry-sdk-testing:

// En tests: SDK in-memory, sin collector real
InMemoryMetricReader metricReader = InMemoryMetricReader.create();
SdkMeterProvider meterProvider = SdkMeterProvider.builder()
    .registerMetricReader(metricReader)
    .build();

Telemetry telemetry = new Telemetry(
    OpenTelemetrySdk.builder()
        .setMeterProvider(meterProvider)
        .setTracerProvider(tracerProvider)
        .build()
);

Los tests verifican:

  • Que los spans se crean con los nombres y atributos correctos
  • Que los contadores se incrementan correctamente
  • Que los histogramas registran duraciones
  • Que los errores propagan el status ERROR al span padre

Correlación de Señales

Uno de los mayores beneficios de usar OpenTelemetry es la correlación entre los tres pilares:

Un usuario reporta que la app es lenta
    │
    ▼
Grafana Mimir: linker.http.request.duration tiene un spike en /{id}
    │
    ▼
Grafana Tempo: trace link.resolve muestra que link.resolve.lookup toma 2s
    │
    ▼
Grafana Loki: log "Failed to resolve link id=abc" con stack trace
    │
    ▼
Causa raíz: SQLite lock por concurrent writes

Todas las señales están etiquetadas con el mismo service.name=linker1, permitiendo navegar entre ellas en un solo dashboard.

Clone this wiki locally