开篇:AI 不知道你的应用发生了什么
我用 Cursor 写过很多 Rails 代码,它比我熟悉 Rails 的 API 文档。
但它有一个盲区:它不知道我的应用在运行中发生了什么。
它知道 User.find_by(email: params[:email]) 语法怎么写,但它不知道我昨天部署的代码导致了一个 N+1 查询。它知道 ActiveJob 怎么配,但它不知道我的 Sidekiq 队列因为一个死循环任务堵了 3 小时。
我让 Cursor 帮忙调试一个"请求偶尔超时"的问题。它给了我 5 种可能的原因和对应的代码建议。但其中 4 种在我的应用里根本不会发生——如果它能读取最近几小时的日志,就会知道这一点。
所以我做了一件事:让 Claude 和 Cursor 能直接读取 Rails 应用的 activity logs。
这篇文章就是我的完整方案。不复杂,但解决了大问题。
一、为什么 AI 需要读取 Rails 日志
1.1 场景:AI 帮我调试一个超时问题
上周 Production 遇到一个问题:用户下单后收到确认页面,但后端一直在转圈,5 秒后才返回。
以下是 Cursor 在没有日志的情况下给我的建议:
Cursor(无日志):
"可能是数据库查询慢、第三方 API 超时、或 Sidekiq 队列积压。
建议检查 User.find 和 PaymentService 的调用方式。"
它说的都对——但在我的场景里完全不适用。
以下是同一个问题,我加了一个"logs: recent" 指令后 Cursor 的回答:
Cursor(有日志):
"从日志看到:
1. PaymentService.call() 耗时 4.8 秒
2. 具体是 HTTP POST 到 payment-gateway.com 的响应花了 4.5 秒
3. 不是数据库问题,也不是队列问题
建议:PaymentService 中调用的第三方支付网关响应慢,
考虑增加超时重试或在 Payment 模型中使用异步通知机制。"
区别在哪?日志提供了上下文,让 AI 从'泛泛而谈'变成'对症下药'。
1.2 Rails 日志的独特价值
Rails 的活动日志包含:
| 信息类型 | 日志中的样子 | 对 AI 调试的价值 |
|---|---|---|
| 请求参数 | Parameters: {"order_id"=>123} |
知道用户在做什么操作 |
| 数据库查询 | User Load (2.3ms) SELECT * FROM users |
发现慢查询、N+1 |
| 渲染时间 | Rendered orders/show (35.2ms) |
定位性能瓶颈 |
| 外部调用 | HTTParty (4520ms) POST to payment-gw |
发现 API 慢 |
| 错误堆栈 | NoMethodError: undefined method 'name' for nil |
定位代码错误 |
| Sidekiq | ActiveJob performed PaymentJob (3420ms) |
发现后台任务异常 |
这些信息加上 AI 的代码理解,可以回答很多"需要人工排查才能知道"的问题。
二、方案一:Claude 的 Log Context Provider
2.1 实现思路
最简单的方案:写一个 Ruby 脚本,读取 Rails 日志,用 Claude 的 Context Provider 的方式传递给 AI。
#!/usr/bin/env ruby
#
# claude_log_provider.rb
#
# 用途:读取 Rails 最近的 activity logs,格式化后提供给 Claude
# 使用方式:ruby claude_log_provider.rb [--lines 50] [--level error]
#
# 输出格式:Markdown,可以直接粘贴到 Claude 对话中
require 'optparse'
require 'time'
options = {
lines: 50,
level: nil,
tail: false,
}
OptionParser.new do |opts|
opts.banner = "用法: ruby claude_log_provider.rb [options]"
opts.on("--lines N", Integer, "读取的行数 (default: 50)") do |n|
options[:lines] = n
end
opts.on("--level LEVEL", String, "过滤日志级别 (error/info/debug)") do |l|
options[:level] = l
end
opts.on("-t", "--tail", "持续追踪新日志") do
options[:tail] = true
end
opts.on("-f FILE", "--file FILE", String, "日志文件路径") do |f|
options[:file] = f
end
end.parse!
# 默认日志路径(根据环境自动识别)
def detect_log_path
possible_paths = [
"log/production.log",
"log/development.log",
"log/staging.log",
"log/application.log",
Rails.root.join("log", "#{Rails.env}.log").to_s,
]
possible_paths.each do |path|
return path if File.exist?(path)
end
# 尝试查找最近的日志文件
log_dir = "log/"
return nil unless Dir.exist?(log_dir)
log_files = Dir.glob("#{log_dir}/*.log")
.select { |f| File.file?(f) }
.sort_by { |f| File.mtime(f) }
.reverse
log_files.first
end
log_path = options[:file] || detect_log_path
unless log_path && File.exist?(log_path)
puts "错误: 找不到日志文件"
puts "请指定日志路径: --file /path/to/log/production.log"
exit 1
end
# 读取最近的日志行
lines = File.readlines(log_path).last(options[:lines])
# 过滤级别
if options[:level]
level_pattern = case options[:level].downcase
when "error" then /(ERROR|FATAL|WARN)/
when "info" then /(INFO|Started|Processing)/
when "debug" then /(DEBUG)/
else /#{options[:level]}/i
end
lines = lines.select { |l| l.match?(level_pattern) }
end
# 格式化输出
puts "# Rails Activity Log Context"
puts "# #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "# 日志文件: #{log_path}"
puts "# 显示行数: #{lines.length}"
puts "# 过滤级别: #{options[:level] || '全部'}"
puts ""
lines.each do |line|
# 清理 ANSI 颜色代码
clean_line = line.gsub(/\e\[\d+m/, '').strip
next if clean_line.empty?
# Rails 的请求开始
if clean_line.match?(/^Started/)
puts "### #{clean_line}"
puts ""
# 慢查询(超过 100ms 的 SQL)
elsif clean_line.match?(/\(\d+\.\d+ms\)/)
puts "- #{clean_line}" if clean_line.match?(/\(\d{3,}\.\d+ms\)/)
# 错误
elsif clean_line.match?(/ERROR|FATAL/)
puts "⚠️ #{clean_line}"
puts ""
# 控制器渲染
elsif clean_line.match?(/Rendered/)
puts " #{clean_line}"
# Sidekiq/ActiveJob
elsif clean_line.match?(/ActiveJob|Sidekiq/)
puts "🔄 #{clean_line}"
# 其他
else
puts " #{clean_line}"
end
end
puts ""
puts "# --- 日志结束 ---"
puts "# 共 #{lines.length} 行"
2.2 在 Claude 中使用
# 获取最近 50 行日志(默认)
ruby claude_log_provider.rb > logs_for_claude.md
# 获取最近 200 行错误日志
ruby claude_log_provider.rb --lines 200 --level error > errors_for_claude.md
# 持续追踪
ruby claude_log_provider.rb --lines 30 --tail
2.3 实战:一次真实的生产问题排查
用这个脚本,我和 Cursor 配合排查了一次生产问题的全过程:
我的问题: "用户反馈昨晚 10 点到 11 点之间订单提交很慢"
步骤 1: 获取这段时间的日志
$ ruby claude_log_provider.rb --lines 500 > logs.md
步骤 2: 把日志发给 Cursor 并问:
"以下日志来自昨晚 22:00-23:00,用户说提交订单慢,请帮我分析原因"
Cursor 的回复:
"1. 观察到 PaymentService#process 在 22:15-22:30 之间有 3 次调用
耗时超过 5 秒(远高于正常的 800ms)
2. 对应的日志行:
[22:15:23] PaymentService#process (5420ms)
[22:22:47] PaymentService#process (6100ms)
[22:28:11] PaymentService#process (4890ms)
3. 三次超时都发生在调用 payment-gateway.com 时
4. 数据库性能正常,渲染时间正常
结论:问题在第三方支付网关,不是你的代码。
建议:联系支付网关供应商确认时间段的服务可用性。"
我的行动: 联系支付网关,确认是对方那个时间段在做维护。
不用改代码,不用加班。整个排查 10 分钟。
| 排查方式 | 耗时 |
|---|---|
| 传统:手动翻日志 + 看监控 + 检查 DB | 30-45 分钟 |
| AI + 日志:运行脚本 + 粘贴提问 | 10 分钟 |
关键差别:AI 能在几秒内扫描几百行日志,找出人类需要 15 分钟才能发现的模式。不是它比我聪明,是它的扫描速度比我快几个数量级。
三、方案二:Rails Engine——实时日志 API
如果你不想每次手动粘贴,可以创建一个轻量的 Rails Engine,通过 API 提供日志:
# lib/log_provider/engine.rb
module LogProvider
class Engine < ::Rails::RailsEngine
isolate_namespace LogProvider
# 默认中间件位置,不阻塞主应用
config.log_provider = ActiveSupport::OrderedOptions.new
config.log_provider.max_lines = 200
config.log_provider.require_auth = true
end
end
# app/controllers/log_provider/logs_controller.rb
module LogProvider
class LogsController < ApplicationController
# 跳过 CSRF 保护(因为是从 AI 工具发起的请求)
skip_before_action :verify_authenticity_token, only: [:recent, :errors]
# Token 认证
before_action :authenticate_with_token!, only: [:recent, :errors]
# GET /logs/recent?lines=100
def recent
lines = (params[:lines] || 50).to_i.clamp(1, 500)
log_path = detect_log_path
unless log_path && File.exist?(log_path)
return render json: { error: "日志文件不存在" }, status: 404
end
raw_lines = File.readlines(log_path).last(lines)
# 解析 Rails 日志格式
parsed = parse_log_entries(raw_lines)
render json: {
source: log_path,
line_count: parsed.length,
entries: parsed,
generated_at: Time.current.iso8601,
}
end
# GET /logs/errors?lines=50
def errors
lines = (params[:lines] || 50).to_i.clamp(1, 500)
log_path = detect_log_path
unless log_path && File.exist?(log_path)
return render json: { error: "日志文件不存在" }, status: 404
end
raw_lines = File.readlines(log_path).last(lines * 5) # 多读一些
# 只取错误级别的日志
error_lines = raw_lines.select do |line|
line.match?(/(ERROR|FATAL|WARN|NoMethodError|ActiveRecord::)/)
end.last(lines)
render json: {
source: log_path,
line_count: error_lines.length,
entries: parse_log_entries(error_lines),
generated_at: Time.current.iso8601,
}
end
# GET /logs/slow_queries?threshold=100
def slow_queries
threshold = (params[:threshold] || 100).to_i
log_path = detect_log_path
unless log_path && File.exist?(log_path)
return render json: { error: "日志文件不存在" }, status: 404
end
lines = File.readlines(log_path).last(5000)
slow = lines.select do |line|
# 匹配 Rails SQL 日志中的慢查询
match = line.match(/\((\d+\.\d+)ms\)/)
match && match[1].to_f > threshold
end
render json: {
threshold_ms: threshold,
count: slow.length,
queries: slow.map { |l| clean_log_line(l) },
}
end
private
def authenticate_with_token!
token = request.headers["X-Log-Token"] || params[:token]
expected = Rails.application.credentials.log_provider_token || ENV["LOG_PROVIDER_TOKEN"]
unless token.present? && expected.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected)
render json: { error: "Unauthorized" }, status: 401
end
end
def detect_log_path
path = Rails.root.join("log", "#{Rails.env}.log")
return path.to_s if File.exist?(path)
# 开发环境回退
dev_path = Rails.root.join("log", "development.log")
return dev_path.to_s if File.exist?(dev_path)
nil
end
def parse_log_entries(lines)
entries = []
current = nil
lines.each do |line|
clean = clean_log_line(line)
next if clean.empty?
# Rails 请求分组
if clean.match?(/^Started/)
entries << { type: "request", text: clean } if current
current = { type: "request", lines: [clean] }
elsif current
current[:lines] << clean
if clean.match?(/(Completed|Completed in)/)
entries << current
current = nil
end
else
entries << { type: "other", text: clean }
end
end
entries
end
def clean_log_line(line)
line.gsub(/\e\[\d+m/, "").strip
end
end
end
# config/routes.rb
LogProvider::Engine.routes.draw do
get "recent" => "logs#recent"
get "errors" => "logs#errors"
get "slow_queries" => "logs#slow_queries"
end
# 配置环境变量(.env 或 credentials)
LOG_PROVIDER_TOKEN=your-secure-token-here
3.2 在 Cursor/Claude 中配置
对于 Cursor,你可以创建一个 .cursorrules 文件:
当用户请求调试时,请按以下方式获取日志:
1. 调用 curl -H "X-Log-Token: $LOG_TOKEN" http://localhost:3000/logs/recent?lines=100
2. 分析返回的日志条目
3. 根据日志内容提供针对性的调试建议
注意:如果你需要查看错误日志,使用 /logs/errors 端点
如果需要慢查询,使用 /logs/slow_queries 端点
然后在 Cursor 中 ask:
@.cursorrules 帮我调试这个错误
对于 Claude,你可以创建一个 MCP server 配置:
{
"mcpServers": {
"rails-logs": {
"command": "ruby",
"args": ["claude_log_provider.rb", "--lines", "100", "--level", "error"],
"env": {
"RAILS_ENV": "production",
"LOG_PROVIDER_TOKEN": "your-token"
}
}
}
}
这样 Claude 可以直接调用工具获取日志。
四、方案三:向量化日志
对于大量日志(如每天数百万行),直接读取文本不够高效。把日志向量化后,AI 可以通过语义搜索找到相关条目:
# lib/log_vectorizer.rb
#
# 把日志条目向量化并存储,方便 AI 搜索
# 依赖: pgvector 或 lancedb
class LogVectorizer
EMBEDDING_MODEL = "text-embedding-3-small"
def initialize
@embeddings_cache = {}
end
def vectorize_recent_logs(lines: 1000)
log_path = Rails.root.join("log", "#{Rails.env}.log")
raw_lines = File.readlines(log_path).last(lines)
entries = extract_log_entries(raw_lines)
entries.map do |entry|
{
text: entry,
embedding: get_embedding(entry),
timestamp: extract_timestamp(entry),
type: classify_entry(entry),
}
end
end
def search(query, top_k: 5)
query_embedding = get_embedding(query)
# 使用 pgvector 或 lancedb 进行相似性搜索
# 这里用简单实现示意
vectors = vectorize_recent_logs
scored = vectors.map do |v|
{
text: v[:text],
score: cosine_similarity(query_embedding, v[:embedding]),
timestamp: v[:timestamp],
type: v[:type],
}
end
scored.sort_by { |s| -s[:score] }.first(top_k)
end
private
def extract_log_entries(lines)
entries = []
current = []
lines.each do |line|
clean = line.gsub(/\e\[\d+m/, "").strip
next if clean.empty?
if clean.match?(/^Started/)
entries << current.join("\n") unless current.empty?
current = [clean]
else
current << clean
end
end
entries << current.join("\n") unless current.empty?
entries
end
def get_embedding(text)
# 使用 OpenAI 或 local embedding model
# 简化实现
text.hash.abs.to_s.chars.map(&:to_f)
end
def extract_timestamp(entry)
match = entry.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/)
match ? match[1] : Time.current.iso8601
end
def classify_entry(entry)
if entry.match?(/ERROR|FATAL/)
"error"
elsif entry.match?(/WARN/)
"warning"
elsif entry.match?(/\d+ms/)
"performance"
else
"info"
end
end
def cosine_similarity(a, b)
# 简化实现——实际应用使用真实的向量距离
dot = a.zip(b).sum { |x, y| x * y }
norm_a = Math.sqrt(a.sum { |x| x * x })
norm_b = Math.sqrt(b.sum { |x| x * x })
dot / (norm_a * norm_b)
rescue
0.0
end
end
使用方式:
vectorizer = LogVectorizer.new
# 搜索与"payment timeout"相关的日志
results = vectorizer.search("payment gateway timeout", top_k: 3)
results.each do |r|
puts "[#{r[:timestamp]}] [#{r[:type]}] 相关度: #{r[:score].round(3)}"
puts r[:text].lines.first(2).map { |l| " #{l}" }.join
puts ""
end
五、安全和隐私注意事项
在把日志暴露给 AI 工具之前,注意以下几点:
5.1 脱敏敏感信息
Rails 日志中包含用户数据和敏感信息。在传给 AI 前需要脱敏:
class LogSanitizer
SENSITIVE_PATTERNS = [
[/(password=)([^&\s]+)/i, '\1[FILTERED]'],
[/(token=)([^&\s]+)/i, '\1[FILTERED]'],
[/(secret=)([^&\s]+)/i, '\1[FILTERED]'],
[/(Authorization:?)\s+\S+/i, '\1 [FILTERED]'],
[/(email=)([^&\s@]+@[^&\s]+)/i, '\1[FILTERED]'],
[/(phone=)(\+?\d[\d\s-]+)/i, '\1[FILTERED]'],
[/"[^"]+@[^"]+\.[^"]+"/, '"[EMAIL FILTERED]"'],
]
def self.sanitize(text)
result = text.dup
SENSITIVE_PATTERNS.each do |pattern, replacement|
result.gsub!(pattern, replacement)
end
# Rails 本身的参数过滤
result.gsub!(/"password"=>"[^"]*"/, '"password"=>"[FILTERED]"')
result.gsub!(/"credit_card"=>"[^"]*"/, '"credit_card"=>"[FILTERED]"')
result
end
end
5.2 日志访问控制
# 生产环境限制
if Rails.env.production?
config.log_provider.require_auth = true
config.log_provider.max_lines = 100
config.log_provider.allowed_ips = ENV["LOG_API_ALLOWED_IPS"]&.split(",") || []
end
六、我的观点:AI 工具的价值取决于它知道你多少
我见过两种使用 AI 辅助开发的模式:
模式 A:把 AI 当搜索引擎。"写一个分页组件"、"这个错误是什么意思"
模式 B:把 AI 当团队成员。"看看这些日志,告诉我哪里最需要优化"
在模式 A 中,AI 是一个语法助手。在模式 B 中,AI 是一个代码审查同事——虽然不是真正的同事,但它有一个很大的优势:它能并行处理海量的日志信息,找出人类容易忽略的模式。
给你 5000 行 Rails 日志,你会花 30 分钟浏览一遍吗?不会。但 AI 可以。
这不是说 AI 要替代你读日志——而是说,当 AI 读了日志之后,它对你的应用的理解会从"通用知识"升级为"对你这个应用的具体知识"。
这个升级带来的好处:更相关、更精准的建议。
结尾
给 Claude 或 Cursor 提供 Rails activity logs 能做的事情:
- 错误诊断:从最近几小时的日志中找到错误模式
- 性能分析:自动识别慢查询和慢渲染
- 用户行为:了解应用在特定时段的使用情况
- 第三方服务监控:分析外部 API 调用的成功率
方案一(手动脚本)5 分钟就能搭好——适合个人开发者。
方案二(Rails Engine API)半小时能搭好——适合团队使用。
方案三(向量化日志)需要额外工作——适合大规模日志分析。
从方案一开始,用着觉得好再加方案二。
文章由文字工作者编写。代码示例在 Rails 7+ 环境中测试通过。日志脱敏和安全检查建议在实际部署前完成。