随着 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 进行了严格的内存限制。
yaml
# docker-compose.prod.yml
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" # 暴露 TCP 端口接收日志
volumes:
- ./elk/logstash/pipeline/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
2.2 NestJS 自定义 Logstash Transport
为了不依赖文件挂载,直接在 NestJS 中通过 TCP 发送日志。我们需要自定义一个 Winston Transport。
typescript
// src/common/logger/logstash.transport.ts
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 // 包含 message, level, req, res 等所有元数据
};
this.client.write(JSON.stringify(logEntry) + '\n');
}
callback();
}
}
2.3 全量请求上下文捕获
这是今天的重头戏。默认的日志往往缺少入参和出参,排查问题非常痛苦。通过自定义中间件,拦截 res.send 方法,我们可以捕获完整的请求体和响应体。
typescript
// src/common/middleware/logging.middleware.ts
@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();
// 劫持 res.send 以获取响应体
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 中配置:
nginx
location /log/ {
proxy_pass http://localhost:5601/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 关键:Kibana 需要知道它运行在代理后面
}
同时在 Kibana 环境变量中必须配合 SERVER_BASEPATH=/log 和 SERVER_REWRITEBASEPATH=true。
4. 最终效果
配置完成后,在 Kibana 的 Discover 面板中,不仅能看到系统的启动日志,还能展开每一条 HTTP 请求,查看用户到底传了什么参数(req.body),以及系统到底返回了什么(res.body)。
至此,一个轻量级但功能完备的日志监控系统就搭建完成了。