feat: 增加日志打印和异常处理

This commit is contained in:
mazxd
2025-05-11 17:55:59 +08:00
parent e43ffe011f
commit fd8b4ec01b
21 changed files with 939 additions and 119 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
"redis": "^5.0.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0"
"winston": "^3.17.0"
},
"devDependencies": {
"@types/cors": "^2.8.0",
+1 -1
View File
@@ -28,7 +28,7 @@
"redis": "^5.0.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"winston": "^3.11.0"
"winston": "^3.17.0"
},
"devDependencies": {
"@types/cors": "^2.8.0",
@@ -9,6 +9,7 @@ export class BaiduDriveAdapter implements DriveAdapter {
account: Account
): Promise<TransferResult> {
try {
logger.info('Baidu drive transferAndShare', { sourceLink, sourceLinkType, accountId: account.id });
// TODO: 实现百度网盘的具体转存和分享逻辑
// 1. 解析账号凭证
const credentials = JSON.parse(account.credentials);
+3 -1
View File
@@ -86,7 +86,7 @@ export async function getSearchResults(req: Request, res: Response) {
// 转换参数类型
const searchParams = {
keyword: keyword as string,
types: Array.isArray(types) ? types : types ? [types as string] : [],
types: Array.isArray(types) ? types.map(t => String(t)) : types ? [String(types)] : [],
page: Number(page),
fileType: fileType as string,
fileSize: fileSize as string,
@@ -94,6 +94,7 @@ export async function getSearchResults(req: Request, res: Response) {
mode: mode as 'standard' | 'fuzzy'
};
const clientIp = req.ip || req.socket.remoteAddress;
const result = await search(searchParams);
res.json(result);
} catch (error) {
@@ -158,6 +159,7 @@ export async function getLinkForResource(req: Request, res: Response) {
});
}
const clientIp = req.ip || req.socket.remoteAddress;
const result = await generateLink(resourceId);
res.json(result);
} catch (error) {
+49
View File
@@ -0,0 +1,49 @@
import { AppError } from '../utils/AppError';
import logger from '../lib/logger';
import { AxiosError } from 'axios';
export function handleError(errorMessage?: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error: unknown) {
// 记录错误日志
logger.error(`${errorMessage || '操作执行失败'}:`, {
method: propertyKey,
args,
error: error instanceof Error ? error.message : '未知错误',
stack: error instanceof Error ? error.stack : undefined
});
// 如果是 AppError,直接抛出
if (error instanceof AppError) {
throw error;
}
// 如果是 Axios 错误,转换为 AppError
if (error instanceof AxiosError) {
const statusCode = error.response?.status || 500;
throw new AppError(
error.response?.data?.message || '请求外部服务失败',
statusCode
);
}
// 其他错误转换为 AppError
throw new AppError(
error instanceof Error ? error.message : '操作执行失败',
500
);
}
};
return descriptor;
};
}
+67
View File
@@ -0,0 +1,67 @@
import winston from 'winston';
import path from 'path';
// 定义日志级别
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
// 根据环境选择日志级别
const level = () => {
const env = process.env.NODE_ENV || 'development';
const isDevelopment = env === 'development';
return isDevelopment ? 'debug' : 'warn';
};
// 定义日志颜色
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
// 添加颜色
winston.addColors(colors);
// 定义日志格式
const format = winston.format.combine(
// 添加时间戳
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
// 添加颜色
winston.format.colorize({ all: true }),
// 定义日志打印格式
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
);
// 定义日志输出目标
const transports = [
// 控制台输出
new winston.transports.Console(),
// 错误日志文件
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
}),
// 所有日志文件
new winston.transports.File({
filename: path.join('logs', 'all.log')
}),
];
// 创建日志记录器
const logger = winston.createLogger({
level: level(),
levels,
format,
transports,
});
export default logger;
+55
View File
@@ -0,0 +1,55 @@
import { PrismaClient, Prisma } from '@prisma/client';
import logger from './logger';
declare global {
var prisma: PrismaClient | undefined;
}
export const prisma = global.prisma || new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'event',
level: 'error',
},
{
emit: 'event',
level: 'info',
},
{
emit: 'event',
level: 'warn',
},
],
});
// 监听 SQL 查询
(prisma as any).$on('query', (e: Prisma.QueryEvent) => {
logger.debug('SQL Query', {
query: e.query,
params: e.params,
duration: e.duration,
});
});
// 监听错误
(prisma as any).$on('error', (e: Prisma.LogEvent) => {
logger.error('Prisma Error', { error: e });
});
// 监听信息
(prisma as any).$on('info', (e: Prisma.LogEvent) => {
logger.info('Prisma Info', { message: e });
});
// 监听警告
(prisma as any).$on('warn', (e: Prisma.LogEvent) => {
logger.warn('Prisma Warning', { message: e });
});
if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma;
}
+48
View File
@@ -0,0 +1,48 @@
import { Request, Response, NextFunction } from 'express';
import { ActivityLogService } from '../services/activityLogService';
export interface LoggedRequest extends Request {
startTime?: number;
}
export function activityLogger(operationType: string) {
return async (req: LoggedRequest, res: Response, next: NextFunction) => {
// 记录开始时间
req.startTime = Date.now();
// 保存原始的 res.json 方法
const originalJson = res.json;
// 重写 res.json 方法
res.json = function (body: any) {
// 计算请求持续时间
const durationMs = req.startTime ? Date.now() - req.startTime : undefined;
// 异步记录日志,不等待完成
ActivityLogService.logActivity({
operationType,
clientIp: req.ip || req.socket.remoteAddress,
keyword: req.query.keyword as string,
itemId: req.params.resourceId,
details: JSON.stringify({
method: req.method,
path: req.path,
query: req.query,
params: req.params,
statusCode: res.statusCode,
responseTime: durationMs
}),
durationMs,
errorCode: res.statusCode >= 400 ? `HTTP_${res.statusCode}` : undefined,
errorMessage: res.statusCode >= 400 ? body.message : undefined
}).catch(error => {
console.error('Failed to log activity:', error);
});
// 调用原始的 json 方法
return originalJson.call(this, body);
};
next();
};
}
+38 -25
View File
@@ -1,40 +1,53 @@
import { Router } from 'express';
import { SearchService } from '../services/searchService';
import { activityLogger } from '../middleware/activityLogger';
const router = Router();
const searchService = new SearchService();
router.get('/search', async (req, res) => {
try {
const { keyword } = req.query;
router.get('/search',
activityLogger('SEARCH'),
async (req, res) => {
try {
const { keyword } = req.query;
if (!keyword || typeof keyword !== 'string') {
return res.status(400).json({
success: false,
code: 400,
message: 'Keyword is required and must be a string',
});
}
if (!keyword || typeof keyword !== 'string') {
return res.status(400).json({
success: false,
code: 400,
message: 'Keyword is required and must be a string',
});
}
const result = await searchService.search(keyword);
if (!result) {
return res.status(500).json({
const result = await searchService.search(keyword);
res.json(result);
} catch (error) {
console.error('Search route error:', error);
res.status(500).json({
success: false,
code: 500,
message: 'Search service error',
message: 'Internal server error',
});
}
return res.json(result);
} catch (error) {
console.error('Search route error:', error);
return res.status(500).json({
success: false,
code: 500,
message: 'Internal server error',
});
}
});
);
router.get('/link/:resourceId',
activityLogger('GENERATE_LINK'),
async (req, res) => {
try {
const { resourceId } = req.params;
const result = await searchService.generateLink(resourceId);
res.json(result);
} catch (error) {
console.error('Generate link route error:', error);
res.status(500).json({
success: false,
code: 500,
message: 'Failed to generate link',
});
}
}
);
export default router;
@@ -0,0 +1,66 @@
import { prisma } from '../lib/prisma';
import { Prisma } from '@prisma/client';
// 监听 SQL 查询
(prisma as any).$on('query', (e: Prisma.QueryEvent) => {
console.log('\n=== SQL Query ===');
console.log('Query:', e.query);
console.log('Params:', e.params);
console.log('Duration:', e.duration, 'ms');
console.log('================\n');
});
// 监听错误
(prisma as any).$on('error', (e: Error) => {
console.error('\n=== Prisma Error ===');
console.error(e);
console.error('===================\n');
});
export class ActivityLogService {
static async logActivity({
operationType,
clientIp,
keyword,
itemId,
driveType,
managedAccountId,
generatedLink,
errorCode,
errorMessage,
durationMs,
details
}: {
operationType: string;
clientIp?: string;
keyword?: string;
itemId?: string;
driveType?: string;
managedAccountId?: number;
generatedLink?: string;
errorCode?: string;
errorMessage?: string;
durationMs?: number;
details?: string;
}) {
try {
await prisma.activityLog.create({
data: {
operationType,
clientIp,
keyword,
itemId,
driveType,
managedAccountId,
generatedLink,
errorCode,
errorMessage,
durationMs,
details
}
});
} catch (error) {
console.error('Failed to log activity:', error);
}
}
}
+99 -88
View File
@@ -1,21 +1,24 @@
import axios from 'axios';
import { config } from '../config/config';
import { createClient } from 'redis';
import logger from '../lib/logger';
import { handleError } from '../decorators/errorHandler';
import { AppError } from '../utils/AppError';
const redis = createClient({
url: `redis://${config.redis.host}:${config.redis.port}`,
password: config.redis.password
});
// Redis error handling and connection monitoring
redis.on('error', err => console.error('Redis Client Error:', err));
redis.on('connect', () => console.log('Redis Client Connected'));
redis.on('ready', () => console.log('Redis Client Ready'));
redis.on('reconnecting', () => console.log('Redis Client Reconnecting'));
// Redis 错误处理和连接监控
redis.on('error', err => logger.error('Redis 客户端错误:', { error: err }));
redis.on('connect', () => logger.info('Redis 客户端已连接'));
redis.on('ready', () => logger.info('Redis 客户端就绪'));
redis.on('reconnecting', () => logger.warn('Redis 客户端正在重连'));
// Initialize Redis connection
// 初始化 Redis 连接
redis.connect().catch(err => {
console.error('Redis Connection Error:', err);
logger.error('Redis 连接错误:', { error: err });
});
const TOKEN_KEY = 'search_service_token';
@@ -89,98 +92,106 @@ export class SearchService {
constructor() {
this.baseUrl = config.searchService.baseUrl;
logger.info('搜索服务已初始化', { baseUrl: this.baseUrl });
}
private async getToken(): Promise<string | null> {
try {
// Try to get token from Redis
const cachedToken = await redis.get(TOKEN_KEY);
if (cachedToken) {
return cachedToken;
}
// Login to get new token
const response = await axios.post<LoginResponse>(
`${this.baseUrl}${config.searchService.endpoints.login}`,
{
username: process.env.SEARCH_SERVICE_USERNAME,
password: process.env.SEARCH_SERVICE_PASSWORD,
}
);
if (response.data.success && response.data.data.token) {
// Cache token in Redis
await redis.set(TOKEN_KEY, response.data.data.token, {
EX: 21600, // 6 hours
});
return response.data.data.token;
}
} catch (error) {
console.error('Error getting token:', error);
@handleError('获取认证 token 失败')
private async getToken(): Promise<string> {
// 尝试从 Redis 获取 token
const cachedToken = await redis.get(TOKEN_KEY);
if (cachedToken) {
logger.debug('从缓存中获取到 token');
return cachedToken;
}
return null;
}
public async search(keyword: string): Promise<SearchResponse | null> {
try {
const token = await this.getToken();
if (!token) {
throw new Error('Failed to get authentication token');
logger.info('缓存中未找到 token,尝试登录获取');
// 登录获取新 token
const response = await axios.post<LoginResponse>(
`${this.baseUrl}${config.searchService.endpoints.login}`,
{
username: process.env.SEARCH_SERVICE_USERNAME,
password: process.env.SEARCH_SERVICE_PASSWORD,
}
);
const response = await axios.get<SearchResponse>(
`${this.baseUrl}${config.searchService.endpoints.search}`,
{
params: { keyword },
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
} catch (error) {
console.error('Error performing search:', error);
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Token expired, clear it from Redis
await redis.del(TOKEN_KEY);
}
return null;
if (!response.data.success || !response.data.data.token) {
throw new AppError('登录失败', 401);
}
logger.info('成功获取新 token');
// 将 token 缓存到 Redis
await redis.set(TOKEN_KEY, response.data.data.token, {
EX: 21600, // 6 小时
});
return response.data.data.token;
}
public async searchByParams(params: SearchParams): Promise<SearchResult> {
try {
// TODO: 实现实际的搜索逻辑
return {
success: true,
code: 0,
data: [],
message: '搜索成功'
};
} catch (error) {
console.error('Search service error:', error);
throw error;
}
}
@handleError('搜索执行失败')
public async search(keyword: string): Promise<SearchResponse> {
logger.info('开始执行搜索', { keyword });
const token = await this.getToken();
public async generateLink(resourceId: string): Promise<LinkResult> {
try {
// TODO: 实现实际的链接生成逻辑
return {
success: true,
code: 0,
data: {
url: `https://example.com/download/${resourceId}`,
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
const response = await axios.get<SearchResponse>(
`${this.baseUrl}${config.searchService.endpoints.search}`,
{
params: { keyword },
headers: {
Authorization: `Bearer ${token}`,
},
message: '链接生成成功'
};
} catch (error) {
console.error('Generate link service error:', error);
throw error;
}
}
);
logger.info('搜索完成', {
keyword,
resultCount: response.data.data?.length || 0
});
return response.data;
}
@handleError('参数化搜索失败')
public async searchByParams(params: SearchParams): Promise<SearchResult> {
logger.info('开始执行参数化搜索', {
keyword: params.keyword,
types: params.types,
page: params.page,
mode: params.mode
});
// TODO: 实现实际的搜索逻辑
const result = {
success: true,
code: 0,
data: [],
message: '搜索成功'
};
logger.info('参数化搜索完成', {
keyword: params.keyword,
resultCount: result.data.length
});
return result;
}
@handleError('生成资源链接失败')
public async generateLink(resourceId: string): Promise<LinkResult> {
logger.info('开始生成资源链接', { resourceId });
// TODO: 实现实际的链接生成逻辑
const result = {
success: true,
code: 0,
data: {
url: `https://example.com/download/${resourceId}`,
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
},
message: '链接生成成功'
};
logger.info('资源链接生成成功', {
resourceId,
expireTime: result.data.expireTime
});
return result;
}
}
+7
View File
@@ -0,0 +1,7 @@
export class AppError extends Error {
statusCode: number;
status: string;
isOperational: boolean;
constructor(message: string, statusCode: number);
}
+14
View File
@@ -0,0 +1,14 @@
export class AppError extends Error {
statusCode: number;
status: string;
isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
+3 -1
View File
@@ -6,7 +6,9 @@
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]