Clase 142 — Registro (logging) y observabilidad

Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

El depurador de la clase anterior sirve mientras tienes el programa delante; en producción, con miles de peticiones concurrentes y sin poder pausar nada, tu única ventana al interior del sistema es lo que el propio programa haya dejado escrito. Ese rastro es el registro (logging), y saber leerlo y producirlo es lo que separa "no sé qué pasó" de "aquí está la línea exacta". McConnell, en Code Complete, trata la instrumentación como parte del oficio, no como un añadido: un programa que no cuenta lo que hace es una caja negra imposible de operar.

La unidad básica es un log con dos ingredientes: un nivel que expresa su gravedad —DEBUG, INFO, WARN, ERROR— y unos datos que describen el evento. Nuestro ejercicio produce la línea mínima log=[INFO] procesados=n: un nivel y un dato. Parece poco, pero encierra la decisión clave del logging moderno —el logging estructurado—, donde cada registro es un conjunto de campos legibles por máquina (procesados=5) y no una frase suelta. Sobre esa base se construye la observabilidad: la capacidad de entender el estado interno de un sistema desde sus salidas, articulada en los tres pilares clásicos —logs, métricas y trazas— que permiten operar en producción sin adivinar.

📚 Resultados de aprendizaje

Al finalizar, podrás:

  1. Emitir un registro con su nivel y sus datos en un formato estructurado.
  2. Distinguir los niveles DEBUG, INFO, WARN y ERROR y elegir el correcto para cada evento.
  3. Explicar la observabilidad y sus tres pilares (logs, métricas, trazas).
  4. Identificar la biblioteca de logging idiomática de cada lenguaje del núcleo.

🗺️ Temas

# Tema Por qué importa
1 Logging estructurado Registros con campos legibles por máquina, no frases sueltas, se pueden consultar
2 Niveles DEBUG/INFO/WARN/ERROR permiten filtrar por gravedad y bajar el ruido
3 Observabilidad Los tres pilares (logs, métricas, trazas) hacen operable un sistema en marcha
4 Bibliotecas por lenguaje Cada ecosistema tiene su estándar (logging, SLF4J, slog, tracing…)

📖 Definiciones y características

🧩 Situación

Son las tres de la madrugada y el servicio de pedidos ha empezado a tardar. No puedes conectar un depurador —pausarías a miles de usuarios—, así que abres el panel de logs. Filtras por nivel WARN y aparece una línea repetida: [WARN] cola_reintentos procesados=0. En segundos sabes que los reintentos no avanzan, algo que un mensaje de texto plano habría enterrado en el ruido. Un registro estructurado como [INFO] procesados=5 es exactamente lo que hace posible esa consulta: campos, no prosa.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
5 log=[INFO] procesados=5
0 log=[INFO] procesados=0
3 log=[INFO] procesados=3

📐 Algoritmo (pseudocódigo neutral)

LEER n ; ESCRIBIR log de nivel INFO con procesados=n

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/. Para que la salida sea verificable byte a byte, aquí construimos el log a mano con un print, en vez de invocar la biblioteca de logging real de cada lenguaje; pero la forma del mensaje —nivel entre corchetes y un campo clave=valor— imita deliberadamente lo que esas bibliotecas producen.

Python · python/main.py · python main.py

import sys

n = int(sys.stdin.readline())
print(f"log=[INFO] procesados={n}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

La línea f"log=[INFO] procesados={n}" es un log estructurado en miniatura: [INFO] es el nivel y procesados={n} es un campo con clave y valor. En un servicio real no lo escribirías así, sino con el módulo estándar logging: logging.info("procesados=%d", n), que además añadiría el timestamp, el nombre del módulo y respetaría el nivel configurado —si el umbral fuera WARNING, este INFO ni se emitiría—. Esa es la ventaja de una biblioteca frente al print: el mismo código de aplicación produce más o menos detalle según la configuración del entorno, sin tocar la lógica. Ramalho, en Fluent Python, recomienda logging sobre print justo por eso: separa qué quieres registrar de cuánto se registra en cada despliegue.

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const n = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`log=[INFO] procesados=${n}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`log=[INFO] procesados=${n}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        System.out.println("log=[INFO] procesados=" + n);
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

int n = int.Parse(Console.In.ReadToEnd().Trim());
Console.WriteLine($"log=[INFO] procesados={n}");

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    n, _ := strconv.Atoi(strings.TrimSpace(line))
    fmt.Printf("log=[INFO] procesados=%d\n", n)
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let n: i64 = s.trim().parse().unwrap();
    println!("log=[INFO] procesados={n}");
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

int main(void) {
    long n;
    if (scanf("%ld", &n) != 1) return 1;
    printf("log=[INFO] procesados=%ld\n", n);
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL: registro con una tabla/consulta de auditoría.
WITH t(n) AS (VALUES (5))
SELECT printf('log=[INFO] procesados=%d', n) AS resultado FROM t;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
$n = (int) trim(fgets(STDIN));
echo "log=[INFO] procesados=$n\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

En un sistema real, cada uno de estos programas delegaría el registro en la biblioteca canónica de su ecosistema, y el contraste es instructivo. Java rara vez usa System.out.println para logs: emplea la fachada SLF4J con una implementación como Logback por debajo, de modo que el código depende de una interfaz y la configuración decide el destino y el formato —una aplicación directa del principio de programar contra abstracciones que defiende Bloch. Go incorpora desde la versión 1.21 el paquete log/slog en la biblioteca estándar, con logging estructurado nativo (slog.Info("procesado", "n", n)), superando al viejo log de solo texto. Rust favorece la fachada tracing (o log), que unifica logs y trazas de spans en un mismo modelo, muy alineado con la observabilidad moderna. JavaScript va de console.log en desarrollo a bibliotecas como pino o winston en producción, que serializan a JSON de alto rendimiento. Distintas casas, la misma idea: nivel, campos estructurados y un destino configurable.

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔬 Comparación

El concepto de niveles es universal; lo que cambia es la biblioteca estándar de cada ecosistema y si el logging estructurado viene de fábrica.

Lenguaje Biblioteca de referencia ¿Estructurado nativo? Llamada idiomática
Python logging (estándar) Parcial (vía extra/handlers) logging.info("procesados=%d", n)
JavaScript pino / winston (console en dev) Sí (JSON) logger.info({ procesados: n })
TypeScript pino / winston Sí (JSON) logger.info({ procesados: n })
Java SLF4J + Logback / Log4j 2 Sí (con encoders) log.info("procesados={}", n)
C# Microsoft.Extensions.Logging / Serilog logger.LogInformation("procesados={N}", n)
Go log/slog (estándar, 1.21+) slog.Info("proc", "n", n)
Rust tracing / log info!(procesados = n)
C syslog / bibliotecas propias No syslog(LOG_INFO, "...")
SQL tablas de auditoría / logs del motor según motor INSERT INTO auditoria …
PHP Monolog (estándar de facto, PSR-3) $log->info('proc', ['n' => $n])

Dos observaciones útiles. La primera: los lenguajes modernos convergen hacia el logging estructurado de serie —slog en Go, tracing en Rust, la abstracción ILogger en .NET—, reconociendo que un log que una máquina puede consultar vale mucho más que uno que solo un humano puede leer. La segunda: PHP estandarizó su ecosistema con PSR-3, una interfaz común que Monolog y otros implementan, de modo que cambiar de biblioteca no rompe el código de aplicación; es la misma idea de fachada que Java resolvió con SLF4J.

🧬 El concepto en la familia

SLF4J/Logback y Log4j 2 (Java), logging (Python), Serilog y Microsoft.Extensions.Logging (.NET), slog y zap (Go), tracing (Rust), Monolog (PHP), pino y winston (Node): todos son variaciones del mismo modelo —un evento con nivel, mensaje y campos, dirigido a uno o varios destinos configurables—. Por encima de ellos, la observabilidad los integra con métricas y trazas distribuidas mediante estándares como OpenTelemetry, que unifica cómo los servicios exportan estas tres señales.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 142

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 141 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 143 ⏭️