Files
wwjcloud-nest-v1/wwjcloud-nest/src/common/libraries/sentry/sentry.service.ts
wanwu 1ed0085d15 feat: WWJCloud 企业级全栈框架 v0.3.5 完整更新
🚀 核心更新:
-  完善 NestJS 企业级架构设计
-  优化配置中心和基础设施层
-  增强第三方服务集成能力
-  完善多租户架构支持
- 🎯 对标 Java Spring Boot 和 PHP ThinkPHP

📦 新增文件:
- wwjcloud-nest 完整框架结构
- Docker 容器化配置
- 管理后台界面
- 数据库迁移脚本

🔑 Key: ebb38b43ec39f355f071294fd1cf9c42
2025-10-13 01:27:37 +08:00

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);
}
}
}