You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于NodeJS Express+Postgres+Sequelize的API请求响应存储与调试需求

实现基于Express+Sequelize的API请求调试沙箱(迷你Wireshark)

我来给你一步步实现这个迷你调试沙箱,正好适配你现有的Express+Sequelize+Postgres栈,完全满足请求响应存储、网页表格展示和JSON导出的需求:

1. 创建请求日志的Sequelize模型

首先定义一个RequestLog模型,用来存储每一条请求的完整信息。在你的Sequelize模型目录下新建requestLog.js:

// models/requestLog.js
module.exports = (sequelize, DataTypes) => {
  const RequestLog = sequelize.define('RequestLog', {
    method: {
      type: DataTypes.STRING(10), // GET/POST/PUT等,长度足够
      allowNull: false
    },
    path: {
      type: DataTypes.STRING(255),
      allowNull: false
    },
    requestBody: {
      type: DataTypes.TEXT, // 支持大体积请求体
      allowNull: true
    },
    responseBody: {
      type: DataTypes.TEXT, // 支持大体积响应体
      allowNull: true
    },
    statusCode: {
      type: DataTypes.INTEGER,
      allowNull: false
    }
  }, {
    timestamps: true, // 自动生成createdAt和updatedAt字段
    tableName: 'request_logs'
  });

  return RequestLog;
};

然后在Sequelize初始化文件中导入这个模型,执行sequelize.sync()或者用迁移脚本完成数据库表同步。

2. 编写Express中间件捕获请求与响应

接下来写一个全局中间件,用来捕获所有经过API的请求和响应。注意要把这个中间件放在express.json()和express.urlencoded()之后,确保能正确解析请求体:

// middleware/requestLogger.js
const { RequestLog } = require('../models');

const requestLogger = async (req, res, next) => {
  // 捕获请求基本信息
  const requestInfo = {
    method: req.method,
    path: req.path,
    requestBody: JSON.stringify(req.body) // 转成字符串方便存储
  };

  // 重写res.json和ressend方法,捕获响应内容
  const originalJson = res.json;
  const originalSend = res.send;

  let responseBody = '';

  res.json = function(data) {
    responseBody = JSON.stringify(data);
    return originalJson.call(this, data);
  };

  res.send = function(data) {
    if (typeof data === 'object') {
      responseBody = JSON.stringify(data);
    } else {
      responseBody = data.toString();
    }
    return originalSend.call(this, data);
  };

  // 响应结束后异步存储日志到数据库
  res.on('finish', async () => {
    try {
      await RequestLog.create({
        ...requestInfo,
        responseBody,
        statusCode: res.statusCode
      });
    } catch (err) {
      // 日志存储失败不影响主业务流程
      console.error('Failed to save request log:', err);
    }
  });

  next();
};

module.exports = requestLogger;

在Express主文件中注册这个中间件:

// app.js
const express = require('express');
const requestLogger = require('./middleware/requestLogger');

const app = express();

// 先解析请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 注册日志中间件(放在所有路由之前)
app.use(requestLogger);

// 你的业务路由
app.use('/api', require('./routes/api'));

// ...其他服务配置

3. 后端接口:日志查询与JSON导出

写两个接口,分别用来分页获取日志列表和导出所有日志为JSON文件:

// routes/debug.js
const express = require('express');
const { RequestLog } = require('../models');
const router = express.Router();

// 分页获取日志列表
router.get('/logs', async (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  const offset = (page - 1) * limit;

  try {
    const { count, rows } = await RequestLog.findAndCountAll({
      order: [['createdAt', 'DESC']], // 最新日志优先
      limit: parseInt(limit),
      offset: parseInt(offset)
    });

    res.json({
      total: count,
      page: parseInt(page),
      limit: parseInt(limit),
      logs: rows
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch logs' });
  }
});

// 导出所有日志为JSON
router.get('/logs/export', async (req, res) => {
  try {
    const logs = await RequestLog.findAll({
      order: [['createdAt', 'DESC']]
    });

    // 设置响应头触发下载
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Content-Disposition', 'attachment; filename=api-logs.json');

    // 格式化JSON后发送
    res.send(JSON.stringify(logs, null, 2));
  } catch (err) {
    res.status(500).json({ error: 'Failed to export logs' });
  }
});

module.exports = router;

在主文件中注册调试路由:

// app.js
app.use('/debug', require('./routes/debug'));
// 静态文件服务(用于加载前端页面)
app.use(express.static('public'));

4. 前端日志表格页面

新建public/debug.html,实现日志表格展示和导出功能:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>API Debug Sandbox</title>
  <style>
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
    th { background-color: #f2f2f2; }
    .log-details { display: none; margin-top: 10px; padding: 10px; background-color: #f9f9f9; border-radius: 4px; }
    .expand-btn { cursor: pointer; color: #007bff; }
    .export-btn { padding: 10px 20px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; margin: 20px 0; }
    pre { white-space: pre-wrap; word-wrap: break-word; }
  </style>
</head>
<body>
  <h1>API Request/Response Logs</h1>
  <button class="export-btn" onclick="exportLogs()">Export as JSON</button>
  <table id="logs-table">
    <thead>
      <tr>
        <th>Timestamp</th>
        <th>Method</th>
        <th>Path</th>
        <th>Status Code</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody id="logs-body"></tbody>
  </table>

  <script>
    // 获取并渲染日志列表
    async function fetchLogs() {
      try {
        const response = await fetch('/debug/logs?limit=100');
        const data = await response.json();
        
        const tbody = document.getElementById('logs-body');
        tbody.innerHTML = '';

        data.logs.forEach(log => {
          const row = document.createElement('tr');
          row.innerHTML = `
            <td>${new Date(log.createdAt).toLocaleString()}</td>
            <td>${log.method}</td>
            <td>${log.path}</td>
            <td>${log.statusCode}</td>
            <td><span class="expand-btn" onclick="toggleDetails(${log.id})">View Details</span></td>
          `;

          // 详情行
          const detailsRow = document.createElement('tr');
          detailsRow.id = `details-${log.id}`;
          detailsRow.innerHTML = `
            <td colspan="5">
              <div class="log-details">
                <h3>Request Body:</h3>
                <pre>${formatJson(log.requestBody)}</pre>
                <h3>Response Body:</h3>
                <pre>${formatJson(log.responseBody)}</pre>
              </div>
            </td>
          `;

          tbody.appendChild(row);
          tbody.appendChild(detailsRow);
        });
      } catch (err) {
        alert('Failed to fetch logs');
        console.error(err);
      }
    }

    // 切换详情显示/隐藏
    function toggleDetails(logId) {
      const details = document.getElementById(`details-${logId}`).querySelector('.log-details');
      details.style.display = details.style.display === 'block' ? 'none' : 'block';
    }

    // 格式化JSON字符串
    function formatJson(jsonStr) {
      try {
        return JSON.stringify(JSON.parse(jsonStr), null, 2);
      } catch (err) {
        return jsonStr; // 非JSON格式直接显示原字符串
      }
    }

    // 导出日志
    function exportLogs() {
      window.open('/debug/logs/export', '_blank');
    }

    // 页面加载时初始化,每5秒自动刷新
    window.onload = fetchLogs;
    setInterval(fetchLogs, 5000);
  </script>
</body>
</html>

启动服务后访问http://localhost:3000/debug.html,就能看到实时更新的请求日志表格,点击"View Details"可展开查看请求/响应体,点击导出按钮即可下载JSON格式的日志文件。

5. 关键优化与注意事项

  • 敏感数据过滤:如果请求/响应包含密码、token等敏感信息,需在中间件中提前过滤或加密,比如将requestBody中的password字段替换为***。
  • 日志清理:添加定时任务(如node-schedule)定期删除N天前的日志,避免数据库表膨胀。
  • 性能优化:给method、path、createdAt字段添加数据库索引,提升查询速度;日志量极大时建议实现前端滚动加载。
  • 错误隔离:日志存储逻辑需用try-catch包裹,确保日志存储失败不会影响主业务流程。

内容的提问来源于stack exchange,提问作者code-8

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 04:09:33