fastify-request-logs
v2.0.1
Published
Una librería de logging versátil que funciona tanto con **Fastify** como de forma **standalone** (sin frameworks). Registra automáticamente todas las peticiones HTTP de manera estructurada, con soporte opcional para Google Cloud Logging.
Readme
fastify-request-logs
Una librería de logging versátil que funciona tanto con Fastify como de forma standalone (sin frameworks). Registra automáticamente todas las peticiones HTTP de manera estructurada, con soporte opcional para Google Cloud Logging.
✨ Características principales
- 🚀 Dual Mode: Funciona con Fastify y también de forma independiente
- 📊 Logs estructurados: JSON automático con toda la información de la petición
- 🌐 Google Cloud Logging: Integración nativa opcional
- 🔄 AsyncLocalStorage: Logging sin parámetros desde cualquier función
- 🎯 PubSub Ready: Perfecto para colas de mensajes, scripts, trabajos por lotes
- 🔧 TypeScript: Soporte completo con tipos
- ⚡ Alta performance: Optimizado para aplicaciones de alto tráfico
Instalación
npm install fastify-request-logs📋 Tabla de contenidos
- Uso con Fastify
- Uso Standalone (sin frameworks)
- Google Cloud Logging
- AsyncLocalStorage: Logging sin parámetros
- API Reference
- Configuración
🌐 Uso con Fastify
import fastify from 'fastify';
import { logger } from 'fastify-request-logs';
const app = fastify();
// Implementar el logger directamente
logger(app, {
only_errors: false,
domain: 'mi-api',
service: 'usuarios',
colors: true
});
app.get('/test', async (request, reply) => {
// El logger está disponible automáticamente en cada request
request.logger.add('custom-log', 'Este es un log personalizado');
return { message: 'Hello World' };
});
// Tipado recomendado en TypeScript
// import type { FastifyRequestWithLogger } from 'fastify-request-logs';
// app.get('/typed', async (request: FastifyRequestWithLogger) => {
// request.logger.add('typed-example', true);
// return { ok: true };
// });🔧 Uso Standalone (sin frameworks)
Nuevo en v2.0.0: Ahora puedes usar la librería sin Fastify para casos como:
- Procesamiento de mensajes de colas (PubSub, SQS, etc.)
- Scripts de procesamiento por lotes
- Trabajos cron
- Cualquier aplicación que no sea un servidor HTTP
Uso básico standalone
El constructor de
LoggerInstancesolo necesita saberurl,method,bodyyparams. El campologgeres opcional, así que no tienes que inyectar una instancia manualmente.
import {
LoggerInstance,
LoggerOptions,
printError,
printLog,
runWithLoggerAsync
} from 'fastify-request-logs';
type PubSubMessage = {
id: string;
data: Buffer;
ack: () => void;
nack: () => void;
};
type Etiqueta = {
id: number;
nombre: string;
};
const loggerOptions: LoggerOptions = {
only_errors: false,
domain: 'procesador-mensajes',
service: 'etiquetas',
module: 'carga-inicial',
colors: true
};
export const procesarMensaje = async (
mensaje: PubSubMessage
): Promise<void> => {
// 1. Crear el logger standalone usando un payload que parece un request
const logger = new LoggerInstance(
{
url: `/pubsub/carga-inicial/${mensaje.id}`,
method: 'MENSAJE',
body: mensaje.data.toString(),
params: { idMensaje: mensaje.id }
},
loggerOptions
);
try {
// 2. Ejecutar la lógica dentro del contexto AsyncLocalStorage
await runWithLoggerAsync(logger, async () => {
printLog('mensaje-recibido', mensaje.id);
const datos = JSON.parse(mensaje.data.toString()) as Etiqueta[];
printLog('datos-parseados', datos.length);
const servicio = new MiServicio();
await servicio.procesar(datos);
printLog('procesamiento-exitoso', true);
});
// 3. Confirmar el mensaje y cerrar el log
mensaje.ack();
logger.finish({ exitoso: true }, false, 200);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
printError('error-procesamiento', message);
if (esErrorRecuperable(error)) {
mensaje.nack();
} else {
mensaje.ack();
}
logger.finish({ exitoso: false, error: message }, true, 500);
}
};
class MiServicio {
public async procesar(etiquetas: Etiqueta[]): Promise<void> {
printLog('servicio-iniciado', etiquetas.length);
for (const etiqueta of etiquetas) {
printLog('procesando-etiqueta', etiqueta.id);
await this.validar(etiqueta);
await this.guardar(etiqueta);
printLog('etiqueta-guardada', etiqueta.id);
}
printLog('servicio-completado', true);
}
private async validar(etiqueta: Etiqueta): Promise<void> {
if (!etiqueta.nombre) {
throw new Error('Etiqueta sin nombre');
}
}
// Simula un guardado con un pequeño delay
private async guardar(etiqueta: Etiqueta): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 100));
if (etiqueta.id === 999) {
throw new Error('Error de base de datos');
}
}
}
const esErrorRecuperable = (error: unknown): boolean => {
const message = error instanceof Error ? error.message : String(error);
const normalized = message.toLowerCase();
if (
normalized.includes('json') ||
normalized.includes('invalid') ||
normalized.includes('parsing')
) {
return false;
}
return true;
};Casos de uso standalone
1. Procesamiento de mensajes de cola (PubSub, SQS, etc.)
import type { Message } from '@google-cloud/pubsub';
import {
LoggerInstance,
LoggerOptions,
printError,
printLog,
runWithLoggerAsync
} from 'fastify-request-logs';
const baseOptions: LoggerOptions = {
only_errors: false,
domain: 'procesador-mensajes',
service: 'etiquetas',
module: 'carga-inicial',
colors: true
};
export const cargaInicialListener = async (mensaje: Message): Promise<void> => {
const logger = new LoggerInstance(
{
url: `/pubsub/carga-inicial/${mensaje.id}`,
method: 'MENSAJE',
body: mensaje.data?.toString(),
params: { idMensaje: mensaje.id }
},
{
...baseOptions,
useGCloudLogging: true,
gcloudProjectId: process.env.GOOGLE_CLOUD_PROJECT_ID
}
);
try {
await runWithLoggerAsync(logger, async () => {
printLog('mensaje-recibido', mensaje.id);
const validatedData = validateData<EtiquetaCargaInicialModel[]>(
guardarEtiquetaSchema,
JSON.parse(mensaje.data.toString())
);
const service = DEPENDENCY_CONTAINER.get(GuardarEtiquetaService);
await service.guardarEtiqueta(validatedData);
printLog('procesamiento-exitoso', true);
});
mensaje.ack();
logger.finish({ exitoso: true }, false, 200);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
printError('error-procesamiento', message);
if (message.includes('validation') || message.includes('schema')) {
mensaje.ack();
} else {
mensaje.nack();
}
logger.finish({ exitoso: false, error: message }, true, 500);
}
};2. Scripts de procesamiento por lotes
import { promises as fs } from 'node:fs';
import {
LoggerInstance,
LoggerOptions,
printLog,
runWithLoggerAsync
} from 'fastify-request-logs';
const batchLoggerOptions: LoggerOptions = {
only_errors: false,
domain: 'batch-processor',
service: 'file-handler',
colors: true
};
export const procesarArchivos = async (archivos: string[]): Promise<void> => {
for (const archivo of archivos) {
const logger = new LoggerInstance(
{
url: `/batch/archivo/${archivo}`,
method: 'BATCH_PROCESS',
body: { archivo },
params: { archivoId: archivo }
},
batchLoggerOptions
);
await runWithLoggerAsync(logger, async () => {
printLog('archivo-iniciado', archivo);
const contenido = await fs.readFile(archivo);
printLog('archivo-leido', contenido.length);
await procesarContenido(contenido); // Tu lógica de negocio aquí
printLog('archivo-completado', true);
logger.finish({ archivo, exitoso: true }, false, 200);
});
}
};API Standalone
// Nuevas exportaciones disponibles
import {
LoggerInstance, // Clase principal del logger
printLog, // Agregar log sin parámetros
printError, // Agregar error sin parámetros
hasRequestLogger, // Verificar si hay contexto
runWithLogger, // Ejecutar en contexto (sync)
runWithLoggerAsync, // Ejecutar en contexto (async)
RequestContext // Acceso directo al contexto
} from 'fastify-request-logs';Configuración
Opciones disponibles
interface LoggerOptions {
only_errors: boolean; // Solo registrar errores
domain: string; // Dominio de la aplicación
service: string; // Nombre del servicio
module?: string; // Módulo específico (opcional)
colors: boolean; // Habilitar colores en consola
useGCloudLogging?: boolean; // Usar Google Cloud Logging
gcloudProjectId?: string; // ID del proyecto de Google Cloud
}Google Cloud Logging
Configuración básica
Para usar Google Cloud Logging, configura las opciones así:
logger(app, {
only_errors: false,
domain: 'mi-api',
service: 'usuarios',
colors: true,
useGCloudLogging: true,
gcloudProjectId: 'mi-proyecto-gcp'
});Requisitos previos
Instalar las dependencias de Google Cloud:
npm install @google-cloud/loggingConfigurar las credenciales de Google Cloud:
Opción A: Variable de entorno (Recomendado para producción)
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-key.json"Opción B: SDK de Google Cloud (Desarrollo local)
gcloud auth application-default loginOpción C: Entornos de Google Cloud (Automático)
- Google Kubernetes Engine (GKE)
- Cloud Run
- Compute Engine
- App Engine
Configurar el proyecto de Google Cloud:
gcloud config set project YOUR_PROJECT_ID
Verificar configuración
Para verificar que todo está configurado correctamente:
# Verificar credenciales
gcloud auth application-default print-access-token
# Verificar proyecto activo
gcloud config get-value projectCaracterísticas de Google Cloud Logging
Cuando useGCloudLogging está habilitado:
- Logs estructurados: Los logs se envían con metadatos estructurados
- Severidad automática: Se asigna automáticamente
INFOoERRORsegún el tipo de respuesta - Etiquetas: Se incluyen automáticamente las etiquetas de domain, service y module
- HTTP Request metadata: Se incluye información de la petición HTTP (URL, método, status code)
- Fallback seguro: Si hay errores con Google Cloud Logging, automáticamente regresa a console.log
Troubleshooting
❌ Error: "gcloudProjectId is required"
// ❌ Incorrecto
logger(app, {
useGCloudLogging: true // Falta gcloudProjectId
});
// ✅ Correcto
logger(app, {
useGCloudLogging: true,
gcloudProjectId: 'mi-proyecto-gcp' // Requerido
});❌ Error: "Error initializing Google Cloud Logging"
Causa común: Credenciales no configuradas
Solución:
# Para desarrollo local
gcloud auth application-default login
# Para producción
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"❌ Error: "Failed to write to Google Cloud Logging"
Causas posibles:
- Permisos insuficientes: El service account necesita el rol
roles/logging.logWriter - Proyecto incorrecto: Verificar que el
gcloudProjectIdsea correcto - API no habilitada: Habilitar Cloud Logging API
Soluciones:
# Habilitar la API de Cloud Logging
gcloud services enable logging.googleapis.com
# Verificar permisos del service account
gcloud projects get-iam-policy YOUR_PROJECT_ID
# Otorgar permisos de logging
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:[email protected]" \
--role="roles/logging.logWriter"⚠️ Warning: "Google Cloud Logging is enabled but not properly initialized"
Esto indica que useGCloudLogging: true pero la inicialización falló. Revisa los mensajes de error anteriores para identificar la causa.
🔍 Verificar que los logs lleguen a Google Cloud
- Ir a Google Cloud Console → Logging → Logs Explorer
- Filtrar por:
resource.type="global" logName="projects/YOUR_PROJECT_ID/logs/DOMAIN-SERVICE-logs" - Buscar logs recientes de tu aplicación
Ejemplo de log en Google Cloud
{
"severity": "INFO",
"httpRequest": {
"status": 200,
"requestUrl": "/api/users",
"requestMethod": "GET"
},
"labels": {
"domain": "mi-api",
"service": "usuarios",
"module": "default"
},
"jsonPayload": {
"INFO": {
"__url": "/api/users",
"__method": "GET",
"__params": {},
"__body": {},
"domain": "mi-api",
"service": "usuarios"
},
"LOGS": {
"custom-log": "Este es un log personalizado"
},
"RESPONSE": {
"message": "Hello World",
"statusCode": 200
}
}
}📚 API Reference
Logging personalizado
Método tradicional (sigue funcionando)
app.get('/ejemplo', async (request, reply) => {
// Agregar logs personalizados
request.logger.add('usuario-id', request.user?.id);
request.logger.add('accion', 'consulta-datos');
// Registrar errores específicos
try {
// ... código que puede fallar
} catch (error) {
request.logger.error('database-error', error.message, 'DB_001');
}
return { success: true };
});Nuevo método con AsyncLocalStorage (✨ Recomendado)
import { printLog, printError, getRequestLogger } from 'fastify-request-logs';
// Funciones auxiliares que pueden hacer logging sin recibir parámetros
async function getUserData(userId) {
printLog('function-called', 'getUserData');
printLog('user-id', userId);
if (!userId) {
printError('validation-error', 'User ID is required', 'USER_001');
throw new Error('User ID is required');
}
return { id: userId, name: 'John Doe' };
}
app.get('/ejemplo', async (request, reply) => {
// ✅ Ahora puedes hacer logging desde cualquier función
printLog('route-accessed', '/ejemplo');
try {
const userData = await getUserData(request.params.id);
printLog('request-success', true);
return userData;
} catch (error) {
// Los errores ya se registraron automáticamente
reply.code(400);
return { error: error.message, isError: true };
}
});Funciones de conveniencia disponibles
import {
printLog,
printError,
getRequestLogger,
hasRequestLogger,
runWithLogger,
runWithLoggerAsync
} from 'fastify-request-logs';
// ✅ Funciones que funcionan tanto en Fastify como standalone
printLog('key', 'value'); // Retorna boolean
printError('error-type', 'Error message'); // Retorna boolean
hasRequestLogger(); // Verifica si hay contexto
getRequestLogger(); // Obtiene logger actual
// ✅ Solo para uso standalone
runWithLogger(logger, () => {
// Función síncrona
printLog('dentro-contexto', 'funciona');
});
await runWithLoggerAsync(logger, async () => {
// Función asíncrona
printLog('dentro-contexto-async', 'funciona');
await algunaOperacion();
});Cambios en v2.0.0
printLog()ahora retornaboolean(antesvoid)printError()ahora retornaboolean(antesvoid)- Nuevas funciones:
hasRequestLogger(),runWithLogger(),runWithLoggerAsync() - Nueva exportación:
RequestContext,LoggerInstance
Soporte para Pub/Sub
La librería detecta automáticamente payloads de Google Cloud Pub/Sub y los decodifica:
// El body se decodifica automáticamente si es un mensaje de Pub/Sub
app.post('/webhook', async (request, reply) => {
// request.logger capturará automáticamente el mensaje decodificado
return { received: true };
});Configuración de desarrollo vs producción
Desarrollo
logger(app, {
only_errors: false,
domain: 'dev-api',
service: 'usuarios',
colors: true,
useGCloudLogging: false // Usar console.log en desarrollo
});Producción
logger(app, {
only_errors: false,
domain: 'prod-api',
service: 'usuarios',
colors: false,
useGCloudLogging: true,
gcloudProjectId: process.env.GOOGLE_CLOUD_PROJECT_ID
});AsyncLocalStorage: Logging sin parámetros
Ventajas
✅ Sin pasar parámetros: Puedes hacer logging desde cualquier función sin pasar request.logger
✅ Agrupación automática: Todos los logs de una petición se agrupan automáticamente
✅ Seguro para concurrencia: Compatible con múltiples réplicas y alta carga
✅ Compatibilidad hacia atrás: El método tradicional sigue funcionando
✅ Funciona con async/await: Mantiene el contexto a través de operaciones asíncronas
Cómo funciona
// Antes (incómodo)
async function processData(data, logger) {
logger.add('processing-data', data);
const result = await database.query(data);
logger.add('query-result', result);
return result;
}
// Después (elegante)
async function processData(data) {
printLog('processing-data', data);
const result = await database.query(data);
printLog('query-result', result);
return result;
}Requisitos
- Node.js 12.17.0+ (para AsyncLocalStorage)
- Funciona en cualquier entorno: On-premise, Google Cloud, AWS, etc.
- Compatible con clustering: Funciona con múltiples workers
Estructura de logs
Cada log incluye las siguientes secciones:
- INFO: Información básica de la petición (URL, método, parámetros, body)
- LOGS: Logs personalizados agregados con
request.logger.add()oprintLog() - RESPONSE: Respuesta exitosa del endpoint
- ERROR_RESPONSE: Respuesta de error (si aplica)
- ERRORS: Errores específicos registrados con
request.logger.error()oprintError()
Licencia
MIT
