-
Notifications
You must be signed in to change notification settings - Fork 1
Observability
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.
┌──────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
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.
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étrica | Tipo | Unidad | Significado |
|---|---|---|---|
linker.db.operation.duration |
Histogram | ms |
Duración de operaciones de repositorio |
Atributos: operation (create / resolve)
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é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)
Linker1 genera 3 tipos de traces, cada uno con spans anidados:
link.create ← Parent span
└── link.create.persist ← Child span (repo call)
Attributes: link.url
Status: OK / ERROR
+ linker.db.operation.duration histogram (operation=create)
link.resolve ← Parent span
└── link.resolve.lookup ← Child span (repo lookup)
Attributes: link.id
Status: OK / ERROR
+ linker.db.operation.duration histogram (operation=resolve)
mysql.healthcheck ← Single span (SpanKind.SERVER)
Attributes: db.system=mysql, db.statement=SELECT 1
Status: OK / ERROR
Cuando una operación falla:
- La excepción se registra en el child span (
span.recordException(e)) - El child span se marca como
StatusCode.ERROR - El error se propaga al parent span (
parent.setStatus(StatusCode.ERROR)) - 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ó.
- SLF4J como API de logging
- Logback como implementación
- OpenTelemetry Logback Appender como bridge hacia OTLP
<!-- 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>| 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) |
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.jarScope 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.
| 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) |
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.
✅ 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.
| Estado de la variable | Comportamiento del SDK |
|---|---|
| Ausente | SDK usa defaults internos (no-op export, silencioso) ✅ |
Vacía ("") |
ConfigurationException → app 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| 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 |
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.
# 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)>"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.
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
ERRORal span padre
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.