广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何用Ruby记录API请求审计日志并追踪请求者IP、时间与操作详情?

排查一次API数据异常时,如果没有完整记录请求者IP、请求时间和操作详情,往往需要在应用日志、代理日志和数据库日志之间来回比对。Ruby生态提供了轻量且灵活的方案,可以通过Rack中间件或框架回调把每次API调用固化成结构化审计日志。

如何用Ruby记录API请求审计日志并追踪请求者IP、时间与操作详情?

审计日志不只是帮助事后追溯,还能用于频率限制、风控分析和接口调用统计。本文围绕Ruby服务端如何采集request.ip、时间戳、HTTP方法与路径、请求参数和操作结果,给出可落地的中间件与Rails实现。

一、审计日志的字段设计与脱敏策略

审计日志是否可用,取决于字段是否足够还原现场。除了请求者IP、时间和操作详情这三个核心要素,还应当补充HTTP方法、完整路径、响应状态码和耗时。如果应用存在登录态,还需要记录用户ID或调用方AppID,这样可以把一次API调用关联到具体主体。时间戳建议统一使用UTC的ISO 8601格式,避免服务器时区混乱造成跨系统比对困难。

操作详情并不等同于原始请求参数。直接把整个params写入日志虽然简单,但用户密码、token、支付卡号等敏感字段会随之进入日志系统,扩大泄露面。记录前应做脱敏,至少覆盖password、token、authorization、credit_card等常见键;对嵌套参数也需要递归处理。下面的方法生成一条结构化审计条目,并用sanitize_params过滤敏感值。

AUDIT_FIELDS = %w[timestamp ip method path params user_id action status duration_ms].freeze
def build_audit_entry(request:, user_id:, action:, status:, duration_ms:)
  {
    timestamp: Time.now.utc.iso8601(3),
    ip: request.ip,
    method: request.request_method,
    path: request.fullpath,
    params: sanitize_params(request.params),
    user_id: user_id,
    action: action,
    status: status,
    duration_ms: duration_ms
  }
end
def sanitize_params(params)
  sensitive = %w[password token authorization credit_card]
  params.each_with_object({}) do |(k, v), acc|
    acc[k] = sensitive.include?(k.to_s.downcase) ? "[FILTERED]" : v
  end
end

上述示例只处理了顶层键,生产环境建议使用ActionController::Parameters的filtered_parameters,或者自行实现递归脱敏。字段集合可以根据团队约定调整,例如增加User-Agent、Referer、请求体大小等,但不要无限扩展,否则日志体积和写入开销会快速上升。

二、用Rack中间件统一采集请求信息

Rack中间件位于Web服务器与Ruby应用之间,所有符合Rack规范的框架都能直接使用,包括Rails、Sinatra和Grape。中间件最适合记录通用访问信息:请求者IP、请求时间、方法、路径、状态码和响应耗时。它不关心具体业务,只要请求进入就会执行。

下面是一个完整中间件实现。通过Process.clock_gettime获取单调时钟,避免系统时间调整影响耗时计算。请求结束后用Logger输出一行JSON,方便日志采集器解析。异常分支单独记录错误信息,并重新抛出异常,保证应用原有错误处理不被吞掉。

class ApiAuditMiddleware
  def initialize(app, logger: Logger.new($stdout))
    @app = app
    @logger = logger
  end
  def call(env)
    started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    status, headers, body = @app.call(env)
    duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(2)
    request = Rack::Request.new(env)
    entry = {
      time: Time.now.utc.iso8601(3),
      ip: request.ip,
      method: request.request_method,
      path: request.fullpath,
      referer: request.referer,
      user_agent: request.user_agent,
      status: status,
      duration_ms: duration_ms
    }
    @logger.info(entry.to_json)
    [status, headers, body]
  rescue StandardError => e
    @logger.error({
      time: Time.now.utc.iso8601(3),
      ip: begin
        Rack::Request.new(env).ip
      rescue StandardError
        "unknown"
      end,
      error: e.class.name,
      message: e.message,
      path: env["PATH_INFO"]
    }.to_json)
    raise
  end
end

中间件注册也很简单。Rails应用可以在config/application.rb中写入config.middleware.insert_before 0, ApiAuditMiddleware;Sinatra项目则在入口文件中使用use ApiAuditMiddleware。需要注意,request.ip会根据Rack的受信代理列表处理X-Forwarded-For头,如果应用前面有Nginx或负载均衡器,应确认代理IP在信任范围内。不要直接读取X-Forwarded-For的第一个值,该头可以被客户端伪造。

三、在Rails控制器中记录操作详情

Rack中间件能记录方法、路径和状态,但无法自然知晓动作含义,例如是创建订单还是导出数据。操作详情更适合放在控制器层,利用框架路由已经解析出的控制器名和动作名,生成带有业务语义的审计条目。

Rails中可以通过around_action在动作执行前后计时。请求结束后,控制器上下文里已经能拿到controller_name、action_name、request.ip和response.status。下面的代码放在ApplicationController中,对所有API控制器生效。当前用户通过current_user辅助方法获取,未登录时为nil。

class ApplicationController < ActionController::API
  around_action :audit_api_request
  private
  def audit_api_request
    started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    yield
    duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(2)
    AuditLogger.log(
      controller: controller_name,
      action: action_name,
      ip: request.ip,
      user_id: current_user&.id,
      params: request.filtered_parameters,
      status: response.status,
      duration_ms: duration_ms
    )
  end
end

如果不想侵入控制器,还可以订阅ActiveSupport::Notifications的process_action.action_controller事件。Rails在处理每个控制器动作时会发布该事件,payload中包含控制器、动作、路径、请求对象和状态码。订阅器可以放在初始化文件中,适合需要向现有项目无侵入加入审计日志的场景。

ActiveSupport::Notifications.subscribe("process_action.action_controller") do |_name, started, finished, _unique_id, payload|
  duration_ms = ((finished - started) * 1000).round(2)
  AuditLogger.log(
    time: Time.now.utc.iso8601(3),
    ip: payload[:request].ip,
    controller: payload[:controller],
    action: payload[:action],
    path: payload[:path],
    status: payload[:status],
    duration_ms: duration_ms
  )
end

无论选择哪种方式,都要确保异常请求也能被记录。使用around_action时,如果动作抛异常,yield后的记录代码不会执行,因此建议配合ensure或全局异常处理中间件,为失败请求保留审计。也可以在Rack中间件中捕获状态码500的访问,但业务操作详情仍以控制器层记录为主。

四、审计日志的存储与检索建议

审计日志最终要能被快速检索,否则问题发生时捞不出来。单机开发阶段可以直接写入按天滚动的文件,使用Ruby标准库Logger配合JSON行格式。下面的配置会按大小滚动,并统一输出UTC时间,避免多人协作时时间口径不一致。

require "logger"
file_logger = Logger.new("log/api_audit.log", 10, 10 * 1024 * 1024)
file_logger.formatter = proc do |severity, datetime, _progname, msg|
  "#{datetime.utc.iso8601(3)} #{severity} #{msg}\n"
end
def write_audit_log(logger, entry)
  logger.info(entry.to_json)
end

多实例部署时,文件日志需要由Filebeat、Fluentd等采集器统一汇总到Elasticsearch或ClickHouse中。结构化JSON字段天然适合建立索引,通常会对请求者IP、时间、动作和状态码建立联合查询。保留周期可以根据业务要求设定,例如高风险接口保留180天,普通接口保留30天。日志内容应只增不改,至少做到应用层不可修改。

对于写入量大、查询要求高的系统,也可以将审计日志异步批量写入数据库表。表结构至少包含id、request_ip、requested_at、action、params_json、status_code、duration_ms。但需要权衡写入延迟与批量大小,避免审计逻辑拖慢主业务。关键原则是:采集尽力而为,不能因为审计日志写入失败就让API请求失败。

广告位 · 文章底部(后台-模块管理替换此区域)