Docker+NestJS+ELK:从零搭建全链路日志监控系统

随着 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=/logSERVER_REWRITEBASEPATH=true

4. 最终效果

配置完成后,在 Kibana 的 Discover 面板中,不仅能看到系统的启动日志,还能展开每一条 HTTP 请求,查看用户到底传了什么参数(req.body),以及系统到底返回了什么(res.body)。

至此,一个轻量级但功能完备的日志监控系统就搭建完成了。

相关推荐
踏浪无痕11 分钟前
Prometheus 动态指标可视化的深度优化:Counter 与 Gauge 的差异化处理
后端·架构·监控
小鸡脚来咯24 分钟前
前端传输的数据格式的选择
java·开发语言·前端·后端
IT_陈寒1 小时前
Vite 5.0 性能优化实战:从3秒到300ms的构建提速秘籍
前端·人工智能·后端
jyd01241 小时前
Spring Boot 文件上传大小限制问题:413 与 MaxUploadSizeExceededException 解决方案
java·spring boot·后端
虾说羊1 小时前
Spring Boot前后端分离项目部署
java·spring boot·后端
小王不爱笑1322 小时前
SpringBoot 项目新建的五种方式详细笔记
spring boot·笔记·后端
superman超哥2 小时前
Rust 内存泄漏检测与防范:超越所有权的内存管理挑战
开发语言·后端·rust·内存管理·rust内存泄漏
悟空码字2 小时前
SpringBoot整合FFmpeg,打造你的专属视频处理工厂
java·spring boot·后端
独自归家的兔2 小时前
Spring Boot 版本怎么选?2/3/4 深度对比 + 迁移避坑指南(含 Java 8→21 适配要点)
java·spring boot·后端
superman超哥2 小时前
Rust 移动语义(Move Semantics)的工作原理:零成本所有权转移的深度解析
开发语言·后端·rust·工作原理·深度解析·rust移动语义·move semantics