diff --git a/README.md b/README.md index ddd1fc3..8236cd8 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ containerización, CI/CD y observabilidad. `docker.io/adrianberriel/curso-devops-api` y actualizar el secret `RENDER_DEPLOY_HOOK_URL`. ### Fase 5 — Observabilidad y Monitoreo -- [ ] Logs estructurados en JSON (timestamp, level, path, status_code) +- [x] Logs estructurados en JSON (timestamp, level, path, status_code) — `ConsoleLogger` de NestJS en modo `json`, más un + middleware global (`LoggerMiddleware`) que emite un evento por request con `method`, `path` y `status_code` - [ ] Conexión a plataforma de monitoreo (Grafana Cloud / Datadog / New Relic / Sentry) - [ ] Dashboard propio (sin plantillas) - [ ] Golden Signals: tráfico, latencia, errores @@ -250,6 +251,34 @@ comprimida en Docker Hub. Dockerfile y expone el puerto 3000. Permite levantar el entorno completo con `docker compose up --build`. +#### Logs Estructurados + +Los logs se emiten como eventos JSON de una línea, en vez de texto plano legible por humanos: es el formato que +consumen los agregadores de logs (CloudWatch, Grafana Loki, Datadog), donde cada campo queda indexado y se puede +filtrar o graficar — por ejemplo, contar 5xx por minuto para las Golden Signals. + +Se usó el `ConsoleLogger` que ya trae NestJS con `json: true` +([documentación oficial](https://docs.nestjs.com/techniques/logger#json-logging)), en lugar de sumar una dependencia +como `pino` o `winston`: el requerimiento se cubre con el logger nativo, y evitar la dependencia extra mantiene la +imagen final más liviana. + +- **`src/main.ts`:** el `ConsoleLogger` se configura con `json: true` y `flattenParams: true` (los campos propios + quedan en la raíz del objeto en vez de anidados bajo `params`, más cómodo de consultar en el agregador). +- **`src/common/middleware/logger.middleware.ts`:** middleware global (patrón `NestMiddleware`, registrado en + `AppModule` vía `configure()`) que se suscribe al evento `finish` de la response. Se eligió `finish` en lugar de + loguear en el camino de ida porque es el único punto donde el `status_code` ya es el definitivo: así quedan + registrados también los 404 y los 400 que resuelven el `ValidationPipe` o los filtros de excepción, sin llegar al + controller. +- **Colores según entorno:** `colors` queda activo solo fuera de producción. Con colores, la salida lleva códigos ANSI + embebidos y deja de ser JSON parseable, así que en producción se apaga. No hace falta configurarlo en `release.yml` + porque el `Dockerfile` ya fija `ENV NODE_ENV=production` en el stage `runner`. + +Ejemplo de un evento emitido en producción: + +```json +{"level":"log","pid":75890,"timestamp":1790541207212,"message":"GET /products/999 404","context":"HTTP","method":"GET","path":"/products/999","status_code":404} +``` + #### Estrategia de Versionado Se adoptó **Semantic Versioning (SemVer)** (`MAJOR.MINOR.PATCH`, ej. `v1.0.0`). @@ -312,8 +341,9 @@ _Borrador. Los apartados marcados como Pendiente dependen de la Fase 5 o del exp **Velocidad del feedback:** en la primera release, `Lint` tardó 23 s, `Unit tests` 13 s y el build y push de la imagen 1 m 7 s. -**Visibilidad de fallos (errores 5xx o latencia alta):** _Pendiente — depende de la Fase 5 (logs JSON, plataforma de -monitoreo y alertas)._ +**Visibilidad de fallos (errores 5xx o latencia alta):** primer paso cubierto — cada request emite un evento JSON con +`status_code`, de modo que los errores son filtrables por campo en lugar de tener que leerse a ojo. _Pendiente: enviar +esos logs a una plataforma de monitoreo y definir alertas sobre ellos._ ![Dashboard con las Golden Signals](docs/images/monitoreo-dashboard.png) diff --git a/src/app.module.ts b/src/app.module.ts index 54eff51..85495ae 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,16 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AppController } from './app.controller.js'; import { AppService } from './app.service.js'; import { ProductsModule } from './products/products.module.js'; +import { LoggerMiddleware } from './common/middleware/logger.middleware.js'; @Module({ imports: [ProductsModule], controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggerMiddleware).forRoutes('*'); + } +} diff --git a/src/common/middleware/logger.middleware.ts b/src/common/middleware/logger.middleware.ts new file mode 100644 index 0000000..e87cf7f --- /dev/null +++ b/src/common/middleware/logger.middleware.ts @@ -0,0 +1,18 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { NextFunction, Request, Response } from 'express'; + +@Injectable() +export class LoggerMiddleware implements NestMiddleware { + private readonly logger = new Logger('HTTP'); + + use(req: Request, res: Response, next: NextFunction) { + const { method, originalUrl: path } = req; + + res.on('finish', () => { + const { statusCode: status_code } = res; + this.logger.log(`${method} ${path} ${status_code}`, { method, path, status_code }); + }); + + next(); + } +} diff --git a/src/main.ts b/src/main.ts index 310c3f2..f4fc4a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,16 @@ import { NestFactory } from '@nestjs/core'; -import { ValidationPipe } from '@nestjs/common'; +import { ConsoleLogger, ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module.js'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + logger: new ConsoleLogger({ + json: true, + flattenParams: true, + colors: process.env.NODE_ENV !== 'production', + }), + }); app.useGlobalPipes(new ValidationPipe()); const config = new DocumentBuilder()