Files
BYW-VoiceHub/VoiceHub-main/server/services/apiLogService.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

411 lines
11 KiB
TypeScript

import { db } from '~/drizzle/db'
import { apiKeys, apiLogs } from '~/drizzle/schema'
import { and, count, desc, eq, gte, like, lte, or, sql } from 'drizzle-orm'
/**
* API访问日志服务
* 提供日志记录、查询和统计功能
*/
export class ApiLogService {
/**
* 记录API访问日志
*/
static async logAccess(logData: {
apiKeyId: string | null
endpoint: string
method: string
ipAddress: string
userAgent: string
statusCode: number
responseTimeMs: number
requestBody?: string | null
responseBody?: string | null
errorMessage?: string
}) {
try {
console.log(`[ApiLogService] 记录API访问日志:`, {
apiKeyId: logData.apiKeyId,
endpoint: logData.endpoint,
method: logData.method,
ipAddress: logData.ipAddress,
statusCode: logData.statusCode,
responseTimeMs: logData.responseTimeMs,
userAgent: logData.userAgent?.substring(0, 50) + '...',
hasError: !!logData.errorMessage
})
await db.insert(apiLogs).values({
apiKeyId: logData.apiKeyId,
endpoint: logData.endpoint,
method: logData.method,
ipAddress: logData.ipAddress,
userAgent: logData.userAgent,
statusCode: logData.statusCode,
responseTimeMs: logData.responseTimeMs,
requestBody: logData.requestBody,
responseBody: logData.responseBody,
errorMessage: logData.errorMessage
})
console.log(`[ApiLogService] API访问日志记录成功`)
} catch (error) {
// 静默处理日志记录错误,避免影响主要业务流程
console.error('[ApiLogService] API访问日志记录失败:', error)
}
}
/**
* 查询API访问日志
*/
static async getLogs(options: {
page?: number
limit?: number
apiKeyId?: string
endpoint?: string
method?: string
statusCode?: number
ipAddress?: string
startDate?: string
endDate?: string
search?: string
}) {
const {
page = 1,
limit = 20,
apiKeyId,
endpoint,
method,
statusCode,
ipAddress,
startDate,
endDate,
search
} = options
const offset = (page - 1) * limit
const whereConditions = []
// 构建查询条件
if (apiKeyId) {
whereConditions.push(eq(apiLogs.apiKeyId, apiKeyId))
}
if (endpoint) {
whereConditions.push(like(apiLogs.endpoint, `%${endpoint}%`))
}
if (method) {
whereConditions.push(eq(apiLogs.method, method.toUpperCase()))
}
if (statusCode) {
whereConditions.push(eq(apiLogs.statusCode, statusCode))
}
if (ipAddress) {
whereConditions.push(like(apiLogs.ipAddress, `%${ipAddress}%`))
}
if (startDate) {
whereConditions.push(gte(apiLogs.createdAt, new Date(startDate)))
}
if (endDate) {
const endDateTime = new Date(endDate)
endDateTime.setHours(23, 59, 59, 999)
whereConditions.push(lte(apiLogs.createdAt, endDateTime))
}
if (search) {
whereConditions.push(
or(
like(apiLogs.endpoint, `%${search}%`),
like(apiLogs.ipAddress, `%${search}%`),
like(apiLogs.userAgent, `%${search}%`),
like(apiLogs.errorMessage, `%${search}%`)
)
)
}
const whereClause =
whereConditions.length > 0
? whereConditions.length === 1
? whereConditions[0]
: and(...whereConditions)
: undefined
// 查询日志数据
const logsData = await db
.select({
id: apiLogs.id,
apiKeyId: apiLogs.apiKeyId,
apiKeyName: apiKeys.name,
endpoint: apiLogs.endpoint,
method: apiLogs.method,
ipAddress: apiLogs.ipAddress,
userAgent: apiLogs.userAgent,
statusCode: apiLogs.statusCode,
responseTimeMs: apiLogs.responseTimeMs,
requestBody: apiLogs.requestBody,
responseBody: apiLogs.responseBody,
errorMessage: apiLogs.errorMessage,
createdAt: apiLogs.createdAt
})
.from(apiLogs)
.leftJoin(apiKeys, eq(apiLogs.apiKeyId, apiKeys.id))
.where(whereClause)
.orderBy(desc(apiLogs.createdAt))
.limit(limit)
.offset(offset)
// 查询总数
const totalResult = await db.select({ count: count() }).from(apiLogs).where(whereClause)
const total = totalResult[0]?.count || 0
return {
logs: logsData,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
}
/**
* 获取API访问统计信息
*/
static async getStats(
options: {
apiKeyId?: string
startDate?: string
endDate?: string
groupBy?: 'hour' | 'day' | 'month'
} = {}
) {
const { apiKeyId, startDate, endDate, groupBy = 'day' } = options
const whereConditions = []
if (apiKeyId) {
whereConditions.push(eq(apiLogs.apiKeyId, apiKeyId))
}
if (startDate) {
whereConditions.push(gte(apiLogs.createdAt, new Date(startDate)))
}
if (endDate) {
const endDateTime = new Date(endDate)
endDateTime.setHours(23, 59, 59, 999)
whereConditions.push(lte(apiLogs.createdAt, endDateTime))
}
const whereClause =
whereConditions.length > 0
? whereConditions.length === 1
? whereConditions[0]
: and(...whereConditions)
: undefined
// 基础统计
const basicStats = await db
.select({
totalRequests: count(),
successRequests: count(
sql`CASE WHEN ${apiLogs.statusCode} >= 200 AND ${apiLogs.statusCode} < 300 THEN 1 END`
),
errorRequests: count(sql`CASE WHEN ${apiLogs.statusCode} >= 400 THEN 1 END`),
avgResponseTime: sql<number>`ROUND(AVG(${apiLogs.responseTimeMs}), 2)`,
maxResponseTime: sql<number>`MAX(${apiLogs.responseTimeMs})`,
minResponseTime: sql<number>`MIN(${apiLogs.responseTimeMs})`
})
.from(apiLogs)
.where(whereClause)
// 按状态码统计
const statusCodeStats = await db
.select({
statusCode: apiLogs.statusCode,
count: count()
})
.from(apiLogs)
.where(whereClause)
.groupBy(apiLogs.statusCode)
.orderBy(desc(count()))
// 按端点统计
const endpointStats = await db
.select({
endpoint: apiLogs.endpoint,
count: count(),
avgResponseTime: sql<number>`ROUND(AVG(${apiLogs.responseTimeMs}), 2)`
})
.from(apiLogs)
.where(whereClause)
.groupBy(apiLogs.endpoint)
.orderBy(desc(count()))
.limit(10)
// 按API Key统计
const apiKeyStats = await db
.select({
apiKeyId: apiLogs.apiKeyId,
apiKeyName: apiKeys.name,
count: count(),
avgResponseTime: sql<number>`ROUND(AVG(${apiLogs.responseTimeMs}), 2)`
})
.from(apiLogs)
.leftJoin(apiKeys, eq(apiLogs.apiKeyId, apiKeys.id))
.where(whereClause)
.groupBy(apiLogs.apiKeyId, apiKeys.name)
.orderBy(desc(count()))
.limit(10)
// 按时间分组统计
let timeGroupFormat: string
switch (groupBy) {
case 'hour':
timeGroupFormat = 'YYYY-MM-DD HH24:00:00'
break
case 'month':
timeGroupFormat = 'YYYY-MM-01'
break
default:
timeGroupFormat = 'YYYY-MM-DD'
}
const timeStats = await db
.select({
time: sql<string>`TO_CHAR(${apiLogs.createdAt}, ${sql.raw(`'${timeGroupFormat}'`)})`
})
.from(apiLogs)
.where(whereClause)
.groupBy(sql`TO_CHAR(${apiLogs.createdAt}, ${sql.raw(`'${timeGroupFormat}'`)})`)
.orderBy(sql`TO_CHAR(${apiLogs.createdAt}, ${sql.raw(`'${timeGroupFormat}'`)})`)
return {
basic: basicStats[0] || {
totalRequests: 0,
successRequests: 0,
errorRequests: 0,
avgResponseTime: 0,
maxResponseTime: 0,
minResponseTime: 0
},
statusCodes: statusCodeStats,
endpoints: endpointStats,
apiKeys: apiKeyStats,
timeline: timeStats
}
}
/**
* 获取热门端点统计
*/
static async getPopularEndpoints(
options: {
limit?: number
startDate?: string
endDate?: string
} = {}
) {
const { limit = 10, startDate, endDate } = options
const whereConditions = []
if (startDate) {
whereConditions.push(gte(apiLogs.createdAt, new Date(startDate)))
}
if (endDate) {
const endDateTime = new Date(endDate)
endDateTime.setHours(23, 59, 59, 999)
whereConditions.push(lte(apiLogs.createdAt, endDateTime))
}
const whereClause =
whereConditions.length > 0
? whereConditions.length === 1
? whereConditions[0]
: and(...whereConditions)
: undefined
return await db
.select({
endpoint: apiLogs.endpoint,
method: apiLogs.method,
totalRequests: count(),
successRequests: count(
sql`CASE WHEN ${apiLogs.statusCode} >= 200 AND ${apiLogs.statusCode} < 300 THEN 1 END`
),
errorRequests: count(sql`CASE WHEN ${apiLogs.statusCode} >= 400 THEN 1 END`),
avgResponseTime: sql<number>`ROUND(AVG(${apiLogs.responseTimeMs}), 2)`,
lastAccessed: sql<Date>`MAX(${apiLogs.createdAt})`
})
.from(apiLogs)
.where(whereClause)
.groupBy(apiLogs.endpoint, apiLogs.method)
.orderBy(desc(count()))
.limit(limit)
}
/**
* 获取错误日志统计
*/
static async getErrorStats(
options: {
limit?: number
startDate?: string
endDate?: string
} = {}
) {
const { limit = 10, startDate, endDate } = options
const whereConditions = [sql`${apiLogs.statusCode} >= 400`]
if (startDate) {
whereConditions.push(gte(apiLogs.createdAt, new Date(startDate)))
}
if (endDate) {
const endDateTime = new Date(endDate)
endDateTime.setHours(23, 59, 59, 999)
whereConditions.push(lte(apiLogs.createdAt, endDateTime))
}
const whereClause = and(...whereConditions)
return await db
.select({
errorMessage: apiLogs.errorMessage,
statusCode: apiLogs.statusCode,
endpoint: apiLogs.endpoint,
count: count(),
lastOccurred: sql<Date>`MAX(${apiLogs.createdAt})`
})
.from(apiLogs)
.where(whereClause)
.groupBy(apiLogs.errorMessage, apiLogs.statusCode, apiLogs.endpoint)
.orderBy(desc(count()))
.limit(limit)
}
/**
* 清理过期日志
*/
static async cleanupOldLogs(daysToKeep: number = 90) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep)
try {
const result = await db.delete(apiLogs).where(lte(apiLogs.createdAt, cutoffDate))
console.log(`Cleaned up API logs older than ${daysToKeep} days`)
return result
} catch (error) {
console.error('Failed to cleanup old API logs:', error)
throw error
}
}
}