随着 NestJS 项目容器化部署的落地,传统的 docker logs 查看日志方式已无法满足排查需求。今天花时间搭建了一套基于 Elastic Stack (ELK) 的全链路日志监控系统,实现了日志的自动采集、结构化存储和可视化分析。
1. 整体架构
使用 Docker Compose 编排整个服务栈,包含以下组件:
- NestJS App: 业务服务,通过 TCP 直接发送日志。
- Logstash: 接收 TCP 日志流,处理后存入 ES。
- Elasticsearch: 存储和索引日志数据。
- Kibana: 可视化面板,配置了中文界面和子路径访问。
- Fleet Server: 用于管理 Elastic Agent(预留安全监控能力)。
2. 核心配置要点
2.1 Docker Compose 编排
为了在低配服务器上流畅运行,对 ES 和 Logstash 进行了严格的内存限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| services: elasticsearch: image: elasticsearch:7.17.18 environment: - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - xpack.security.enabled=true volumes: - elasticsearch_data:/usr/share/elasticsearch/data
kibana: image: kibana:7.17.18 environment: - SERVER_BASEPATH=/log - SERVER_REWRITEBASEPATH=true - I18N_LOCALE=zh-CN ports: - "5601:5601"
logstash: image: logstash:7.17.18 ports: - "5000:5000" volumes: - ./elk/logstash/pipeline/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
|
2.2 NestJS 自定义 Logstash Transport
为了不依赖文件挂载,直接在 NestJS 中通过 TCP 发送日志。我们需要自定义一个 Winston Transport。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import * as winston from 'winston'; import * as net from 'net';
export class LogstashTransport extends winston.transport { log(info: any, callback: () => void) { if (this.client) { const logEntry = { '@timestamp': new Date().toISOString(), app: this.appName, ...info }; this.client.write(JSON.stringify(logEntry) + '\n'); } callback(); } }
|
2.3 全量请求上下文捕获
这是今天的重头戏。默认的日志往往缺少入参和出参,排查问题非常痛苦。通过自定义中间件,拦截 res.send 方法,我们可以捕获完整的请求体和响应体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Injectable() export class LoggingMiddleware implements NestMiddleware { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
use(req: Request, res: Response, next: NextFunction) { const start = Date.now(); const originalSend = res.send; let responseBody: any; res.send = function (body) { responseBody = body; return originalSend.apply(this, arguments); };
res.on('finish', () => { this.logger.info( `${req.method} ${req.originalUrl} ${res.statusCode}`, { req: { body: req.body, query: req.query, ip: req.ip }, res: { statusCode: res.statusCode, body: tryParseJSON(responseBody) }, context: 'HTTP', } ); });
next(); } }
|
3. 部署与运维
编写了 deploy.sh 脚本,实现本地代码一键推送至服务器并重建容器。
解决的一个关键坑是 Kibana 的子路径代理。在 Nginx 中配置:
1 2 3 4 5 6
| location /log/ { proxy_pass http://localhost:5601/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
|
同时在 Kibana 环境变量中必须配合 SERVER_BASEPATH=/log 和 SERVER_REWRITEBASEPATH=true。
4. 最终效果
配置完成后,在 Kibana 的 Discover 面板中,不仅能看到系统的启动日志,还能展开每一条 HTTP 请求,查看用户到底传了什么参数(req.body),以及系统到底返回了什么(res.body)。
至此,一个轻量级但功能完备的日志监控系统就搭建完成了。