🚀 核心更新: - ✅ 完善 NestJS 企业级架构设计 - ✅ 优化配置中心和基础设施层 - ✅ 增强第三方服务集成能力 - ✅ 完善多租户架构支持 - 🎯 对标 Java Spring Boot 和 PHP ThinkPHP 📦 新增文件: - wwjcloud-nest 完整框架结构 - Docker 容器化配置 - 管理后台界面 - 数据库迁移脚本 🔑 Key: ebb38b43ec39f355f071294fd1cf9c42
688 lines
16 KiB
TypeScript
688 lines
16 KiB
TypeScript
import {
|
|
Injectable,
|
|
Logger,
|
|
OnModuleInit,
|
|
OnModuleDestroy,
|
|
} from '@nestjs/common';
|
|
import * as Sentry from '@sentry/node';
|
|
import {
|
|
ExceptionReporterInterface,
|
|
ExceptionStatsInterface,
|
|
ExceptionType,
|
|
ExceptionSeverity,
|
|
} from '../../exception/exception.interface';
|
|
|
|
/**
|
|
* Sentry 服务
|
|
* 基于 NestJS 官方示例实现
|
|
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
|
* 对应 Java: 错误追踪服务
|
|
*/
|
|
@Injectable()
|
|
export class SentryService
|
|
implements
|
|
ExceptionReporterInterface,
|
|
ExceptionStatsInterface,
|
|
OnModuleInit,
|
|
OnModuleDestroy
|
|
{
|
|
private readonly logger = new Logger(SentryService.name);
|
|
private initialized = false;
|
|
private stats = new Map<string, number>();
|
|
|
|
constructor(private readonly config: any) {
|
|
this.initializeSentry();
|
|
}
|
|
|
|
async onModuleInit() {
|
|
if (this.config?.enabled && !this.initialized) {
|
|
try {
|
|
Sentry.init({
|
|
dsn: this.config.dsn,
|
|
environment: this.config.environment || process.env.NODE_ENV,
|
|
release: this.config.release || process.env.npm_package_version,
|
|
tracesSampleRate: this.config.tracesSampleRate || 0.1,
|
|
profilesSampleRate: this.config.profilesSampleRate || 0.1,
|
|
beforeSend: this.beforeSend.bind(this),
|
|
beforeBreadcrumb: this.beforeBreadcrumb.bind(this),
|
|
integrations: [
|
|
// 使用默认集成
|
|
],
|
|
});
|
|
|
|
this.initialized = true;
|
|
this.logger.log('Sentry initialized successfully');
|
|
} catch (error) {
|
|
this.logger.error('Failed to initialize Sentry', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
async onModuleDestroy() {
|
|
if (this.initialized) {
|
|
try {
|
|
await Sentry.close(2000);
|
|
this.initialized = false;
|
|
this.logger.log('Sentry closed successfully');
|
|
} catch (error) {
|
|
this.logger.error('Failed to close Sentry', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private initializeSentry() {
|
|
// Sentry 初始化在 onModuleInit 中进行
|
|
}
|
|
|
|
// ==================== 异常上报接口 ====================
|
|
|
|
/**
|
|
* 上报异常
|
|
*/
|
|
async report(exception: any, context?: any): Promise<boolean> {
|
|
if (!this.initialized || !this.shouldReport(exception)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
Sentry.withScope((scope) => {
|
|
// 设置标签
|
|
this.setTags(scope, exception, context);
|
|
|
|
// 设置上下文
|
|
this.setContext(scope, exception, context);
|
|
|
|
// 设置用户信息
|
|
this.setUser(scope, context);
|
|
|
|
// 设置额外信息
|
|
this.setExtra(scope, exception, context);
|
|
|
|
// 设置级别
|
|
this.setLevel(scope, exception);
|
|
|
|
// 捕获异常
|
|
Sentry.captureException(exception);
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
this.logger.error('Failed to report exception to Sentry', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 批量上报异常
|
|
*/
|
|
async reportBatch(
|
|
exceptions: Array<{ exception: any; context?: any }>,
|
|
): Promise<boolean[]> {
|
|
const results: boolean[] = [];
|
|
|
|
for (const { exception, context } of exceptions) {
|
|
const result = await this.report(exception, context);
|
|
results.push(result);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* 检查是否应该上报
|
|
*/
|
|
shouldReport(exception: any): boolean {
|
|
if (!this.config?.enabled) {
|
|
return false;
|
|
}
|
|
|
|
// 根据异常类型和严重程度判断
|
|
const type = this.getExceptionType(exception);
|
|
const severity = this.getExceptionSeverity(exception);
|
|
|
|
// 只上报高严重程度的异常
|
|
if (
|
|
severity === ExceptionSeverity.HIGH ||
|
|
severity === ExceptionSeverity.CRITICAL
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// 某些类型的异常总是上报
|
|
if (
|
|
type === ExceptionType.SYSTEM ||
|
|
type === ExceptionType.INTERNAL_SERVER_ERROR
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ==================== 异常统计接口 ====================
|
|
|
|
/**
|
|
* 记录异常统计
|
|
*/
|
|
record(exception: any, context?: any): void {
|
|
const type = this.getExceptionType(exception);
|
|
const severity = this.getExceptionSeverity(exception);
|
|
|
|
const key = `${type}:${severity}`;
|
|
const count = this.stats.get(key) || 0;
|
|
this.stats.set(key, count + 1);
|
|
}
|
|
|
|
/**
|
|
* 获取异常统计
|
|
*/
|
|
getStats(timeRange?: { start: Date; end: Date }): any {
|
|
const stats: any = {
|
|
total: 0,
|
|
byType: {},
|
|
bySeverity: {},
|
|
byTime: [],
|
|
topExceptions: [],
|
|
rate: 0,
|
|
trend: 'stable',
|
|
};
|
|
|
|
// 计算统计信息
|
|
for (const [key, count] of this.stats) {
|
|
const [type, severity] = key.split(':');
|
|
stats.total += count;
|
|
|
|
stats.byType[type] = (stats.byType[type] || 0) + count;
|
|
stats.bySeverity[severity] = (stats.bySeverity[severity] || 0) + count;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* 重置统计
|
|
*/
|
|
reset(): void {
|
|
this.stats.clear();
|
|
}
|
|
|
|
// ==================== 工具方法 ====================
|
|
|
|
/**
|
|
* 设置标签
|
|
*/
|
|
private setTags(scope: Sentry.Scope, exception: any, context?: any): void {
|
|
const type = this.getExceptionType(exception);
|
|
const severity = this.getExceptionSeverity(exception);
|
|
|
|
scope.setTag('exception.type', type);
|
|
scope.setTag('exception.severity', severity);
|
|
|
|
if (context?.request) {
|
|
scope.setTag('http.method', context.request.method);
|
|
scope.setTag('http.url', context.request.url);
|
|
scope.setTag('http.status_code', context.response?.statusCode);
|
|
}
|
|
|
|
if (context?.user) {
|
|
scope.setTag('user.id', context.user.id);
|
|
scope.setTag('user.role', context.user.role);
|
|
}
|
|
|
|
if (context?.environment) {
|
|
scope.setTag('environment', context.environment.nodeEnv);
|
|
scope.setTag('version', context.environment.version);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置上下文
|
|
*/
|
|
private setContext(scope: Sentry.Scope, exception: any, context?: any): void {
|
|
if (context?.request) {
|
|
scope.setContext('request', {
|
|
method: context.request.method,
|
|
url: context.request.url,
|
|
headers: this.sanitizeHeaders(context.request.headers),
|
|
body: this.sanitizeBody(context.request.body),
|
|
query: context.request.query,
|
|
params: context.request.params,
|
|
ip: context.request.ip,
|
|
userAgent: context.request.userAgent,
|
|
});
|
|
}
|
|
|
|
if (context?.response) {
|
|
scope.setContext('response', {
|
|
statusCode: context.response.statusCode,
|
|
headers: this.sanitizeHeaders(context.response.headers),
|
|
body: this.sanitizeBody(context.response.body),
|
|
size: context.response.size,
|
|
});
|
|
}
|
|
|
|
if (context?.environment) {
|
|
scope.setContext('environment', {
|
|
nodeEnv: context.environment.nodeEnv,
|
|
version: context.environment.version,
|
|
hostname: context.environment.hostname,
|
|
pid: context.environment.pid,
|
|
});
|
|
}
|
|
|
|
if (context?.trace) {
|
|
scope.setContext('trace', {
|
|
traceId: context.trace.traceId,
|
|
spanId: context.trace.spanId,
|
|
correlationId: context.trace.correlationId,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置用户信息
|
|
*/
|
|
private setUser(scope: Sentry.Scope, context?: any): void {
|
|
if (context?.user) {
|
|
scope.setUser({
|
|
id: context.user.id,
|
|
username: context.user.username,
|
|
email: context.user.email,
|
|
role: context.user.role,
|
|
permissions: context.user.permissions,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置额外信息
|
|
*/
|
|
private setExtra(scope: Sentry.Scope, exception: any, context?: any): void {
|
|
if (exception.code) {
|
|
scope.setExtra('error.code', exception.code);
|
|
}
|
|
|
|
if (exception.details) {
|
|
scope.setExtra('error.details', exception.details);
|
|
}
|
|
|
|
if (exception.cause) {
|
|
scope.setExtra('error.cause', exception.cause);
|
|
}
|
|
|
|
if (context?.meta) {
|
|
scope.setExtra('meta', context.meta);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置级别
|
|
*/
|
|
private setLevel(scope: Sentry.Scope, exception: any): void {
|
|
const severity = this.getExceptionSeverity(exception);
|
|
|
|
switch (severity) {
|
|
case ExceptionSeverity.CRITICAL:
|
|
scope.setLevel('fatal');
|
|
break;
|
|
case ExceptionSeverity.HIGH:
|
|
scope.setLevel('error');
|
|
break;
|
|
case ExceptionSeverity.MEDIUM:
|
|
scope.setLevel('warning');
|
|
break;
|
|
case ExceptionSeverity.LOW:
|
|
scope.setLevel('info');
|
|
break;
|
|
default:
|
|
scope.setLevel('error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取异常类型
|
|
*/
|
|
private getExceptionType(exception: any): ExceptionType {
|
|
if (exception.name) {
|
|
const name = exception.name.toLowerCase();
|
|
|
|
if (name.includes('validation') || name.includes('badrequest')) {
|
|
return ExceptionType.VALIDATION;
|
|
}
|
|
if (name.includes('unauthorized') || name.includes('authentication')) {
|
|
return ExceptionType.AUTHENTICATION;
|
|
}
|
|
if (name.includes('forbidden') || name.includes('authorization')) {
|
|
return ExceptionType.AUTHORIZATION;
|
|
}
|
|
if (name.includes('notfound')) {
|
|
return ExceptionType.NOT_FOUND;
|
|
}
|
|
if (name.includes('conflict')) {
|
|
return ExceptionType.CONFLICT;
|
|
}
|
|
if (name.includes('timeout')) {
|
|
return ExceptionType.TIMEOUT;
|
|
}
|
|
if (name.includes('ratelimit')) {
|
|
return ExceptionType.RATE_LIMIT;
|
|
}
|
|
if (name.includes('database') || name.includes('db')) {
|
|
return ExceptionType.DATABASE;
|
|
}
|
|
if (name.includes('cache')) {
|
|
return ExceptionType.CACHE;
|
|
}
|
|
if (name.includes('network') || name.includes('connection')) {
|
|
return ExceptionType.NETWORK;
|
|
}
|
|
if (name.includes('external') || name.includes('api')) {
|
|
return ExceptionType.EXTERNAL_API;
|
|
}
|
|
if (name.includes('business')) {
|
|
return ExceptionType.BUSINESS;
|
|
}
|
|
if (name.includes('system')) {
|
|
return ExceptionType.SYSTEM;
|
|
}
|
|
}
|
|
|
|
if (exception.statusCode) {
|
|
switch (exception.statusCode) {
|
|
case 400:
|
|
return ExceptionType.BAD_REQUEST;
|
|
case 401:
|
|
return ExceptionType.AUTHENTICATION;
|
|
case 403:
|
|
return ExceptionType.AUTHORIZATION;
|
|
case 404:
|
|
return ExceptionType.NOT_FOUND;
|
|
case 409:
|
|
return ExceptionType.CONFLICT;
|
|
case 429:
|
|
return ExceptionType.RATE_LIMIT;
|
|
case 500:
|
|
return ExceptionType.INTERNAL_SERVER_ERROR;
|
|
case 503:
|
|
return ExceptionType.SERVICE_UNAVAILABLE;
|
|
default:
|
|
return ExceptionType.UNKNOWN;
|
|
}
|
|
}
|
|
|
|
return ExceptionType.UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* 获取异常严重程度
|
|
*/
|
|
private getExceptionSeverity(exception: any): ExceptionSeverity {
|
|
const type = this.getExceptionType(exception);
|
|
|
|
switch (type) {
|
|
case ExceptionType.INTERNAL_SERVER_ERROR:
|
|
case ExceptionType.SYSTEM:
|
|
return ExceptionSeverity.CRITICAL;
|
|
case ExceptionType.AUTHENTICATION:
|
|
case ExceptionType.AUTHORIZATION:
|
|
case ExceptionType.DATABASE:
|
|
case ExceptionType.NETWORK:
|
|
return ExceptionSeverity.HIGH;
|
|
case ExceptionType.VALIDATION:
|
|
case ExceptionType.BAD_REQUEST:
|
|
case ExceptionType.CONFLICT:
|
|
case ExceptionType.CACHE:
|
|
case ExceptionType.EXTERNAL_API:
|
|
return ExceptionSeverity.MEDIUM;
|
|
case ExceptionType.NOT_FOUND:
|
|
case ExceptionType.TIMEOUT:
|
|
case ExceptionType.RATE_LIMIT:
|
|
case ExceptionType.BUSINESS:
|
|
return ExceptionSeverity.LOW;
|
|
default:
|
|
return ExceptionSeverity.MEDIUM;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清理请求头
|
|
*/
|
|
private sanitizeHeaders(
|
|
headers: Record<string, string>,
|
|
): Record<string, string> {
|
|
const sanitized: Record<string, string> = {};
|
|
const sensitiveHeaders = [
|
|
'authorization',
|
|
'cookie',
|
|
'x-api-key',
|
|
'x-auth-token',
|
|
];
|
|
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
if (sensitiveHeaders.includes(key.toLowerCase())) {
|
|
sanitized[key] = '[REDACTED]';
|
|
} else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* 清理请求体
|
|
*/
|
|
private sanitizeBody(body: any): any {
|
|
if (!body) return body;
|
|
|
|
if (typeof body === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(body);
|
|
return this.sanitizeObject(parsed);
|
|
} catch {
|
|
return body;
|
|
}
|
|
}
|
|
|
|
if (typeof body === 'object') {
|
|
return this.sanitizeObject(body);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* 清理对象
|
|
*/
|
|
private sanitizeObject(obj: any): any {
|
|
if (!obj || typeof obj !== 'object') return obj;
|
|
|
|
const sanitized = { ...obj };
|
|
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
|
|
|
for (const field of sensitiveFields) {
|
|
if (sanitized[field]) {
|
|
sanitized[field] = '[REDACTED]';
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* 发送前处理
|
|
*/
|
|
private beforeSend(event: Sentry.Event): Sentry.Event | null {
|
|
// 过滤敏感信息
|
|
if (event.extra) {
|
|
event.extra = this.sanitizeObject(event.extra);
|
|
}
|
|
|
|
if (event.contexts) {
|
|
event.contexts = this.sanitizeObject(event.contexts);
|
|
}
|
|
|
|
if (event.tags) {
|
|
event.tags = this.sanitizeObject(event.tags);
|
|
}
|
|
|
|
return event;
|
|
}
|
|
|
|
/**
|
|
* 面包屑前处理
|
|
*/
|
|
private beforeBreadcrumb(
|
|
breadcrumb: Sentry.Breadcrumb,
|
|
): Sentry.Breadcrumb | null {
|
|
// 过滤敏感信息
|
|
if (breadcrumb.data) {
|
|
breadcrumb.data = this.sanitizeObject(breadcrumb.data);
|
|
}
|
|
|
|
return breadcrumb;
|
|
}
|
|
|
|
// ==================== 公共方法 ====================
|
|
|
|
/**
|
|
* 手动捕获异常
|
|
*/
|
|
captureException(exception: any, context?: any): void {
|
|
if (this.initialized) {
|
|
Sentry.withScope((scope) => {
|
|
this.setTags(scope, exception, context);
|
|
this.setContext(scope, exception, context);
|
|
this.setUser(scope, context);
|
|
this.setExtra(scope, exception, context);
|
|
this.setLevel(scope, exception);
|
|
|
|
Sentry.captureException(exception);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 手动捕获消息
|
|
*/
|
|
captureMessage(
|
|
message: string,
|
|
level: Sentry.SeverityLevel = 'info',
|
|
context?: any,
|
|
): void {
|
|
if (this.initialized) {
|
|
Sentry.withScope((scope) => {
|
|
if (context) {
|
|
this.setContext(scope, null, context);
|
|
this.setUser(scope, context);
|
|
this.setExtra(scope, null, context);
|
|
}
|
|
|
|
scope.setLevel(level);
|
|
Sentry.captureMessage(message);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 开始事务
|
|
*/
|
|
startTransaction(name: string, op: string): any {
|
|
if (this.initialized) {
|
|
// 新版本 Sentry API 已变更,暂时返回 null
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 获取当前事务
|
|
*/
|
|
getCurrentTransaction(): any {
|
|
if (this.initialized) {
|
|
// 新版本 Sentry API 已变更,暂时返回 undefined
|
|
return undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* 设置用户上下文
|
|
*/
|
|
setUserContext(user: {
|
|
id: string;
|
|
username?: string;
|
|
email?: string;
|
|
role?: string;
|
|
}): void {
|
|
if (this.initialized) {
|
|
Sentry.setUser(user);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置标签
|
|
*/
|
|
setTag(key: string, value: string): void {
|
|
if (this.initialized) {
|
|
Sentry.setTag(key, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置上下文(公共方法)
|
|
*/
|
|
setContextPublic(key: string, context: any): void {
|
|
if (this.initialized) {
|
|
Sentry.setContext(key, context);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置额外信息(公共方法)
|
|
*/
|
|
setExtraPublic(key: string, value: any): void {
|
|
if (this.initialized) {
|
|
Sentry.setExtra(key, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置级别(公共方法)
|
|
*/
|
|
setLevelPublic(level: Sentry.SeverityLevel): void {
|
|
if (this.initialized) {
|
|
// 新版本 Sentry API 已变更,暂时不执行
|
|
// Sentry.setLevel(level);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 添加面包屑
|
|
*/
|
|
addBreadcrumb(breadcrumb: Sentry.Breadcrumb): void {
|
|
if (this.initialized) {
|
|
Sentry.addBreadcrumb(breadcrumb);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 配置作用域
|
|
*/
|
|
configureScope(callback: (scope: Sentry.Scope) => void): void {
|
|
if (this.initialized) {
|
|
// 新版本 Sentry API 已变更,暂时不执行
|
|
// Sentry.configureScope(callback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 使用作用域
|
|
*/
|
|
withScope(callback: (scope: Sentry.Scope) => void): void {
|
|
if (this.initialized) {
|
|
Sentry.withScope(callback);
|
|
}
|
|
}
|
|
}
|