Logging efectivo en el aplicaciones de software

Logging efectivo en el aplicaciones de software

Recuerdo muchos años atrás cuando veía esas aplicaciones de Java loggeando datos en la consola porque diferentes procesos se habían o estaban ejecutando, seguramente era Log4j y de esa librería salieron muchos ports hacia otros lenguajes; pero ahora que pienso con calma todo esos recuerdos, pues era tedioso cuando se trataba de encontrar algun problema y seguir el trazo, quizas similar a lo que hoy hacen algunos haciendo console.log() de javascript.

Actualmente las herramientas han evolucionado, pero algo que no ha cambiado es que es lo que nosotros como desarrolladores enviamos a nuestros logs para después analizarlo. Pues bien, en este artículo, vamos a sumergirnos en el fascinante mundo de los logs y descubrir por qué son tan importantes, cómo pueden salvarnos de innumerables dolores de cabeza, y cómo podemos hacerlos realmente efectivos.

La importancia de los logs: más allá de un simple "print"

Los logs son mucho más que simples mensajes impresos en la consola. Son el diario de vida de nuestra aplicación, un registro detallado de cada evento importante, cada error y cada acción significativa. Cuando están bien implementados, los logs se convierten en nuestros ojos y oídos dentro del sistema, permitiéndonos:

  1. Entender el flujo de la aplicación
  2. Identificar y diagnosticar problemas rápidamente
  3. Monitorear el rendimiento y la salud del sistema
  4. Cumplir con requisitos de auditoría y seguridad

El desafío: logging efectivo vs. ruido innecesario

Uno de los mayores desafíos en el logging es encontrar el equilibrio adecuado. Por un lado, queremos capturar suficiente información para que sea útil tras necesitar un análisis. Por otro, no queremos inundar nuestros sistemas con datos irrelevantes que dificulten encontrar la información importante cuando la necesitamos.

He visto proyectos donde los logs eran tan escasos que, cuando ocurría un problema, era prácticamente imposible entender qué había sucedido. También he visto lo contrario: sistemas que generaban gigabytes de logs por hora, haciendo que encontrar un error específico fuera como buscar una aguja en un pajar.

La clave está en ser estratégicos sobre qué registramos y cómo lo hacemos.

El poder del contexto: transformando datos en información

Algo que aprendí por las malas, porque hubo momentos en que usaba logs en aplicaciones productivas y tratar de encontrar respuesta a problemas, pues era difícil porque no tenía la suficiente informacion, perdía tiempo analizando, determinando que hacía falta y nuevamente poniendo el nuevo código en producción, y es que un log sin contexto es casi tan inútil como no tener log en absoluto. El contexto en los logs es crucial, especialmente en situaciones complejas donde múltiples sistemas interactúan entre sí.

Veamos un ejemplo:

console.log('Error en la suscripción del usuario');

Este log nos dice que algo salió mal, pero no nos da ninguna pista sobre qué usuario, qué tipo de suscripción, o qué causó el error. En un sistema complejo, este tipo de log podría llevarnos a una investigación prolongada, revisando múltiples servicios y bases de datos para entender qué sucedió.

Incluso muchos harían algo como esto (imagina que usamos Rollbar):

Rollbar.error(e, 'Error en la suscripción del usuario');

En este caso, aunque tenemos más información, aún es insuficiente, y necesitaríamos recordar el archivo, el método o incluso la lógica interna.

Ahora comparémoslo con un log más informativo:

logger.error('Fallo en la suscripción del usuario', {
  userId: 'abc123',
  subscriptionPlan: 'premium',
  error: 'Tarjeta rechazada',
  transactionId: 'tx-789',
  timestamp: '2023-09-06T14:32:10Z'
});

La diferencia es como el día y la noche. Con este segundo enfoque, tenemos toda la información que necesitamos para comenzar a investigar y resolver el problema rápidamente. Podemos identificar al usuario afectado, el plan al que intentaba suscribirse, la razón del fallo, y incluso rastrear la transacción a través de diferentes servicios si es necesario.

El contexto en los logs nos permite:

  1. Reducir drásticamente el tiempo de resolución de problemas
  2. Identificar patrones y tendencias en los errores
  3. Reconstruir la secuencia de eventos que llevaron a un problema
  4. Proporcionar mejor soporte a los usuarios afectados

Estructurando nuestros logs: el ejemplo de Winston y Loki

Una de las mejores decisiones que tomé en mis proyectos recientes fue adoptar una estructura consistente para mis logs y utilizar herramientas que facilitaran esta tarea. En particular, la combinación de Winston (una biblioteca de logging para Node.js) y Loki (un sistema de agregación de logs) ha sido un cambio radical en cómo abordo el logging.

Winston es una biblioteca de logging versátil y fácil de usar que permite estructurar nuestros logs de manera consistente. Por otro lado, Loki es una excelente opción para almacenar y consultar logs, especialmente en sistemas distribuidos, debido a su eficiencia y escalabilidad. Ya he hablado de ambas herramientas anteriormente en este blog e incluso me atrevería a decir que Loki es mejor para logs que ElasticSearch por su versatilidad, aunque no tan bueno en términos de búsqueda como este último.

Veamos un ejemplo de cómo podríamos estructurar nuestros logs usando Winston y enviándolos a Loki:

const winston = require('winston');
const LokiTransport = require('winston-loki');

const logger = winston.createLogger({
  transports: [
    new LokiTransport({
      host: "http://loki:3100",
      labels: { job: 'node_app' },
      json: true,
      format: winston.format.json()
    })
  ]
});

// Ejemplo de uso
logger.log({
  level: 'error',
  message: 'Usuario falló al suscribirse al tema',
  timestamp: new Date().toISOString(),
  context: {
    service: 'servicio-suscripcion-usuario',
    version: '1.2.3',
    environment: process.env.NODE_ENV
  },
  transactionId: 'abc-123-def-456',
  payload: {
    error: {
      message: 'Saldo insuficiente',
      code: 'INSUFFICIENT_FUNDS'
    },
    data: {
      userId: 'user123',
      topicId: 'topic456'
    },
    params: {
      planType: 'premium',
      duration: '1 year'
    }
  },
  class: 'SubscriptionService',
  method: 'subscribeTopic'
});

Este enfoque nos proporciona varios beneficios:

  1. Estructuración consistente: Todos nuestros logs siguen el mismo formato, lo que facilita su análisis.
  2. Rico en contexto: Incluimos información sobre el servicio, la versión, el entorno y detalles específicos de la transacción.
  3. Fácil de buscar: Con Loki, podemos realizar consultas complejas sobre nuestros logs estructurados.
  4. Separación de concerns: El código de negocio se mantiene limpio, mientras que la lógica de logging se maneja de manera centralizada.

Es importante mencionar que, aunque este ejemplo usa Winston y Loki, existen alternativas para otros lenguajes de programación. Por ejemplo, en Java podrías usar Log4j con Elasticsearch, o en Python podrías usar la biblioteca logging con Graylog.

Seguridad en el logging: protegiendo la información sensible

Un aspecto crítico del logging que a menudo se pasa por alto es la seguridad. Es crucial asegurarse de que nuestros logs no expongan información sensible que pueda comprometer la privacidad de nuestros usuarios o la seguridad de nuestro sistema.

Algunas prácticas recomendadas incluyen:

  1. Enmascaramiento de datos sensibles: Nunca loguees contraseñas, tokens de autenticación o números de tarjeta de crédito completos. En su lugar, enmascara estos datos. Por ejemplo:
   logger.info('Usuario autenticado', {
     userId: 'user123',
     email: 'u***@example.com',
     lastFourDigits: '1234'
   });
  1. Uso de identificadores en lugar de datos personales: En lugar de registrar nombres completos o direcciones de correo electrónico, usa identificadores únicos.

  2. Cifrado de logs: Si tus logs contienen información potencialmente sensible, considera cifrarlos en reposo y en tránsito.

  3. Control de acceso: Implementa un sistema de control de acceso robusto para tus logs. No todos en tu organización deberían tener acceso a todos los logs.

El valor del contexto: cuando más es más

Un aspecto que a menudo genera dudas entre los desarrolladores es la cantidad de código adicional que el logging efectivo puede introducir en nuestras aplicaciones. Es cierto que al implementar un sistema de logging robusto y rico en contexto, nuestro código puede crecer significativamente. Sin embargo, es crucial entender que este "overhead" es una inversión que vale la pena hacer.

Te cuento una anécdota personal: hace unos años, estaba trabajando en un proyecto de comercio electrónico. Teníamos un sistema de logging básico que registraba principalmente errores y algunas operaciones críticas. Un día, empezamos a recibir quejas de usuarios que no podían completar sus compras. Nuestros logs mostraban un simple mensaje de error: "Fallo en el proceso de pago".

Pasamos horas investigando, revisando código, intentando reproducir el error, sin éxito. Finalmente, decidimos implementar un logging más detallado y con más contexto. Sí, nuestro código creció. Lo que antes era:

try {
  processPayment(order);
} catch (error) {
  logger.error('Fallo en el proceso de pago', error);
}

Se convirtió en (no recuerdo la implementación pero era algo similar a esto):

try {
  processPayment(order);
} catch (error) {
  logger.error('Fallo en el proceso de pago', {
    orderId: order.id,
    userId: order.userId,
    amount: order.total,
    paymentMethod: order.paymentMethod,
    errorCode: error.code,
    errorMessage: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString()
  });
}

A primera vista, puede parecer excesivo. Sin embargo, la próxima vez que ocurrió el error, pudimos identificar el problema en cuestión de minutos. Resultó que el error solo ocurría con un proveedor de pagos específico, un tipo de tarjeta específica que solo usaba un solo cliente, y ocurrría con órdenes sobre cierto monto, lo cual solo afectaba a usuarios de una región particular ¿Bastante detalladado cierto?

Esta experiencia me enseñó que, aunque el logging detallado puede aumentar la cantidad de código, el valor que proporciona en términos de capacidad de depuración, tiempo de resolución de problemas y comprensión del sistema es invaluable.

Algunos consejos para manejar este "overhead" de código:

  1. Usa helpers de logging: Crea funciones auxiliares que encapsulen la lógica de logging común para reducir la duplicación de código.

  2. Aprovecha el contexto global: Configura información de contexto global (como ID de sesión o información del usuario) una vez y déjala disponible para todos los logs subsiguientes.

  3. Utiliza aspectos o decoradores: En lenguajes que lo soporten, puedes usar programación orientada a aspectos o decoradores para añadir logging sin modificar el código de negocio directamente.

  4. Balancea detalle y rendimiento: No necesitas loguear absolutamente todo. Enfócate en la información que será más útil para el debugging y el análisis.

Recuerda, el objetivo del logging no es solo registrar que algo sucedió, sino proporcionar toda la información necesaria para entender por qué sucedió y cómo solucionarlo. Si bien puede parecer que estás escribiendo más código de logging que de lógica de negocio, este código adicional puede ahorrarte horas, días o incluso semanas de debugging en el futuro.

Mejores prácticas para un logging efectivo

Después de años de prueba y error, he compilado algunas mejores prácticas que me han sido de gran utilidad:

  1. Usa niveles de log apropiadamente: No todo es un error. Utiliza los niveles de log de manera consistente y significativa. Aquí hay una guía rápida:

    Nivel Cuándo usarlo
    ERROR Errores críticos que requieren intervención inmediata
    WARN Situaciones anómalas o inesperadas que no interrumpen el flujo principal
    INFO Eventos importantes del ciclo de vida de la aplicación (inicio, parada, etc.)
    DEBUG Información detallada útil durante el desarrollo y depuración
    TRACE Información muy detallada, generalmente solo en desarrollo
  2. Incluye identificadores únicos: Un ID de transacción puede ayudarte a rastrear una solicitud a través de múltiples servicios.

  3. Registra el contexto: Incluye información sobre el estado de la aplicación, los parámetros de entrada y cualquier dato relevante para entender el evento.

  4. Evita información sensible: Como mencionamos en la sección de seguridad, nunca registres contraseñas, tokens de autenticación u otra información confidencial.

  5. Usa formato estructurado: JSON u otros formatos estructurados facilitan el análisis y la búsqueda.

  6. Centraliza tus logs: Utiliza herramientas como Loki o ELK Stack para agregar logs de múltiples servicios en un solo lugar.

  7. Monitorea y alerta: Configura alertas basadas en patrones de log para ser notificado proactivamente de problemas.

Conclusión

En mi experiencia, invertir tiempo en una estrategia de logging sólida es una de las mejores decisiones que puedes tomar como desarrollador. Los logs bien estructurados y ricos en contexto se convierten en tu mejor aliado cuando las cosas van mal (y créeme, en algún momento irán mal).

Recuerda, el objetivo final no es solo registrar eventos, sino contar la historia de tu aplicación de una manera que te permita entenderla, mantenerla y mejorarla con el tiempo. Con las herramientas y prácticas adecuadas, los logs pasan de ser una tarea tediosa a ser una parte fundamental de tu arsenal de desarrollo.

Así que la próxima vez que estés tentado a agregar un simple console.log(), detente un momento y piensa: ¿estoy registrando la información correcta de la manera correcta? Tu yo futuro (y el de tu equipo) te lo agradecerán.

Happy coding! :D


Photo by [Kutan Ural](https://unsplaPhoto by Annie Spratt on Unsplash

Written with StackEdit.

Jack Fiallos

Jack Fiallos

Te gustó este artículo?