21.06-用户行为日志
要点
- 用户行为日志记录用户在系统中的操作:登录、创建 API key、发送消息等
- 用于安全审计、用户画像和产品分析
- 区分关键行为(登录、付费)和普通行为(浏览、点击)
- 注意隐私合规,不要记录敏感操作细节
内容
1. 用户行为日志的用途
用户行为日志(也叫审计日志或操作日志)有三个主要用途:
| 用途 | 示例 | 需要的字段 |
|---|---|---|
| 安全审计 | 谁在什么时候登录了系统?谁删除了 API key? | userId, action, timestamp, ip, userAgent |
| 用户画像 | 用户 A 每天调用 100 次 API,主要在早上 9-11 点 | userId, action, timestamp, metadata |
| 产品分析 | 80% 的用户在注册后 7 天内创建了 API key | userId, action, timestamp |
不同用途对日志的详细程度要求不同:
- 安全审计:需要完整信息(IP、userAgent、操作对象)
- 用户画像:只需要 userId + action + timestamp
- 产品分析:可以采样,不需要全量记录
2. 哪些行为需要记录
AI 网关的典型用户行为:
账户相关:
user.login- 用户登录user.logout- 用户登出user.register- 用户注册user.password_change- 修改密码user.api_key.create- 创建 API keyuser.api_key.delete- 删除 API key
AI 调用相关:
ai.call.success- AI 调用成功(已经在前几节记录过)ai.call.failed- AI 调用失败ai.conversation.create- 创建新对话ai.message.send- 发送消息
付费相关:
payment.subscription.create- 创建订阅payment.subscription.cancel- 取消订阅payment.invoice.paid- 支付成功
管理操作:
admin.user.disable- 禁用用户admin.api_key.revoke- 撤销 API key
3. 基础实现
// src/lib/audit-log.ts
export interface AuditEvent {
userId: string
action: string
timestamp: number
ip?: string
userAgent?: string
metadata?: Record<string, unknown>
}
export async function recordAuditEvent(
env: Env,
event: AuditEvent
) {
// 1. 记录到 Analytics Engine(用于分析)
env.ANALYTICS.writeDataPoint({
blobs: [event.userId, event.action, JSON.stringify(event.metadata)],
doubles: [event.timestamp],
indexes: [event.userId],
})
// 2. 记录到数据库(用于审计查询)
await env.DB.prepare(`
INSERT INTO audit_logs (
user_id, action, timestamp, ip, user_agent, metadata
) VALUES (?, ?, ?, ?, ?, ?)
`).bind(
event.userId,
event.action,
event.timestamp,
event.ip,
event.userAgent,
JSON.stringify(event.metadata)
).run()
}// src/routes/auth.ts
import { recordAuditEvent } from '../lib/audit-log'
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
const ip = c.req.header('CF-Connecting-IP')
const userAgent = c.req.header('User-Agent')
const user = await verifyPassword(email, password)
if (!user) {
// 登录失败也记录
await recordAuditEvent(c.env, {
userId: email, // 登录失败时还没有 userId
action: 'user.login.failed',
timestamp: Date.now(),
ip,
userAgent,
metadata: { reason: 'invalid_password' },
})
return c.json({ error: 'Invalid credentials' }, 401)
}
// 登录成功
await recordAuditEvent(c.env, {
userId: user.id,
action: 'user.login',
timestamp: Date.now(),
ip,
userAgent,
})
const token = generateJWT(user)
return c.json({ token })
})4. 中间件统一记录
对于所有需要鉴权的接口,可以在中间件里统一记录用户行为:
// src/middleware/audit-logger.ts
import { Context, Next } from 'hono'
import { recordAuditEvent } from '../lib/audit-log'
export async function auditLogger(c: Context, next: Next) {
const userId = c.get('userId')
const startedAt = Date.now()
await next()
// 只对需要鉴权的接口记录
if (!userId) return
const ip = c.req.header('CF-Connecting-IP')
const userAgent = c.req.header('User-Agent')
// 根据请求生成 action 名称
const action = generateActionName(c.req.method, c.req.path)
await recordAuditEvent(c.env, {
userId,
action,
timestamp: startedAt,
ip,
userAgent,
metadata: {
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration: Date.now() - startedAt,
},
})
}
function generateActionName(method: string, path: string): string {
// POST /v1/chat/completions → ai.call
// POST /api/keys → user.api_key.create
// DELETE /api/keys/:id → user.api_key.delete
if (path.includes('/chat/completions') && method === 'POST') {
return 'ai.call'
}
if (path.includes('/api/keys')) {
if (method === 'POST') return 'user.api_key.create'
if (method === 'DELETE') return 'user.api_key.delete'
}
return `${method.toLowerCase()}.${path.replace(/\//g, '.')}`
}// src/index.ts
app.use('/v1/*', auditLogger)
app.use('/api/*', auditLogger)5. 安全审计查询
审计日志的主要场景是安全审计,提供查询接口:
// src/routes/admin/audit.ts
app.get('/admin/audit-logs', async (c) => {
const userId = c.req.query('userId')
const action = c.req.query('action')
const startTime = c.req.query('startTime')
const endTime = c.req.query('endTime')
const limit = parseInt(c.req.query('limit') || '100')
let query = `
SELECT * FROM audit_logs
WHERE 1=1
`
const params: any[] = []
if (userId) {
query += ' AND user_id = ?'
params.push(userId)
}
if (action) {
query += ' AND action = ?'
params.push(action)
}
if (startTime) {
query += ' AND timestamp >= ?'
params.push(parseInt(startTime))
}
if (endTime) {
query += ' AND timestamp <= ?'
params.push(parseInt(endTime))
}
query += ' ORDER BY timestamp DESC LIMIT ?'
params.push(limit)
const logs = await c.env.DB.prepare(query)
.bind(...params)
.all()
return c.json(logs)
})管理员可以查询:
- 某个用户在最近 7 天的所有操作
- 某个 API key 的所有使用记录
- 所有登录失败的记录
6. 敏感操作的特殊处理
某些敏感操作需要额外记录:
// 删除 API key
app.delete('/api/keys/:id', async (c) => {
const userId = c.get('userId')
const keyId = c.req.param('id')
// 1. 记录操作前的状态
const key = await c.env.DB.prepare(`
SELECT * FROM api_keys WHERE id = ? AND user_id = ?
`).bind(keyId, userId).first()
if (!key) {
return c.json({ error: 'Not found' }, 404)
}
// 2. 执行删除
await c.env.DB.prepare(`
DELETE FROM api_keys WHERE id = ?
`).bind(keyId).run()
// 3. 记录审计日志(包含被删除对象的快照)
await recordAuditEvent(c.env, {
userId,
action: 'user.api_key.delete',
timestamp: Date.now(),
ip: c.req.header('CF-Connecting-IP'),
userAgent: c.req.header('User-Agent'),
metadata: {
deletedKeyId: keyId,
deletedKeyName: key.name,
deletedKeyPrefix: key.key_prefix, // 只记录前缀,不记录完整 key
lastUsedAt: key.last_used_at,
},
})
return c.json({ success: true })
})注意:
- API key 本身不要记录到审计日志(只记录前缀或 ID)
- 密码、token 等敏感信息不要记录
- 如果需要记录请求体,先做脱敏(参考 21.04 节)
7. 用户行为分析
审计日志不仅用于安全审计,还可以用于产品分析:
// 查询用户活跃度
export async function getUserActivity(env: Env, userId: string) {
const activity = await env.DB.prepare(`
SELECT
action,
COUNT(*) as count,
MIN(timestamp) as first_seen,
MAX(timestamp) as last_seen
FROM audit_logs
WHERE user_id = ?
AND timestamp > ?
GROUP BY action
ORDER BY count DESC
`).bind(
userId,
Date.now() - 30 * 24 * 60 * 60 * 1000 // 最近 30 天
).all()
return activity.results
}// 查询新用户转化率
export async function getNewUserConversion(env: Env) {
// 统计注册后 7 天内创建 API key 的用户比例
const result = await env.DB.prepare(`
WITH new_users AS (
SELECT user_id, timestamp as registered_at
FROM audit_logs
WHERE action = 'user.register'
AND timestamp > ?
),
with_api_key AS (
SELECT DISTINCT n.user_id
FROM new_users n
JOIN audit_logs a ON n.user_id = a.user_id
WHERE a.action = 'user.api_key.create'
AND a.timestamp < n.registered_at + 7 * 24 * 60 * 60 * 1000
)
SELECT
COUNT(DISTINCT n.user_id) as total_new_users,
COUNT(DISTINCT w.user_id) as converted_users,
COUNT(DISTINCT w.user_id) * 100.0 / COUNT(DISTINCT n.user_id) as conversion_rate
FROM new_users n
LEFT JOIN with_api_key w ON n.user_id = w.user_id
`).bind(Date.now() - 90 * 24 * 60 * 60 * 1000).first()
return result
}8. 日志保留策略
审计日志会快速增长,需要设置保留策略:
// src/cron/cleanup-audit-logs.ts
export default {
async scheduled(event, env, ctx) {
// 保留策略:
// - 普通操作日志:保留 90 天
// - 安全相关日志(登录、API key):保留 1 年
// - 付费相关日志:永久保留
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000
const oneYearAgo = Date.now() - 365 * 24 * 60 * 60 * 1000
// 清理普通操作日志(90 天)
await env.DB.prepare(`
DELETE FROM audit_logs
WHERE action NOT IN (
'user.login', 'user.login.failed', 'user.logout',
'user.api_key.create', 'user.api_key.delete',
'payment.*'
)
AND timestamp < ?
`).bind(ninetyDaysAgo).run()
// 清理安全相关日志(1 年)
await env.DB.prepare(`
DELETE FROM audit_logs
WHERE action IN (
'user.login', 'user.login.failed', 'user.logout',
'user.api_key.create', 'user.api_key.delete'
)
AND timestamp < ?
`).bind(oneYearAgo).run()
// 付费相关日志永久保留,不清理
},
}9. 小结
用户行为日志的关键点:
- 记录什么:登录、API key 管理、AI 调用、付费操作等关键行为
- 安全审计:记录 IP、userAgent、操作对象,支持追溯
- 敏感脱敏:API key、密码等敏感信息不要完整记录
- 保留策略:不同类型的日志设置不同的保留期限
- 产品分析:审计日志可以用于用户活跃度、转化率分析
用户行为日志是安全合规的基础,也是产品改进的重要依据。下一节讲 Trace 追踪,看看怎么追踪跨服务的请求链路。