# 日志系统架构设计

**版本**: v1.1.0  
**状态**: ✅ 基础功能已实现 | 📋 增强功能规划中  
**最后更新**: 2025-12-12

---

## 系统架构

### 整体架构图

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                              应用层                                          │
│  ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐   │
│  │   IAM 服务    │ │  Form 服务    │ │ Approval 服务 │ │ Inventory 服务 │   │
│  │     (CN)      │ │     (CN)      │ │     (US)      │ │     (UAE)      │   │
│  └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘   │
└──────────┼─────────────────┼─────────────────┼─────────────────┼───────────┘
           │                 │                 │                 │
           ▼                 ▼                 ▼                 ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           日志系统核心层                                      │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                       LoggingInterceptor                               │ │
│  │                自动捕获 HTTP 请求/响应（NestJS 拦截器）                    │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                     │                                       │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │
│  │ TraceContext │  │ LoggerService│  │ SamplingService│ │ AlertService │    │
│  │  追踪上下文   │  │   日志服务   │  │   采样服务     │  │   告警服务   │    │
│  │    📋        │  │      ✅      │  │      📋       │  │      📋      │    │
│  └──────────────┘  └──────────────┘  └──────────────┘  └──────────────┘    │
│                                     │                                       │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                         Winston Logger                                 │ │
│  │              多输出方式：Console + File + JSON（ELK 兼容）               │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────┬───────────────────────────────────────┘
                                      │
┌─────────────────────────────────────▼───────────────────────────────────────┐
│                              存储层                                          │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                         多区域日志存储                                   │ │
│  │  开发: backend/logs/                                                   │ │
│  │  生产: /var/log/ffoa/{REGION}/{SERVICE}/{TYPE}-YYYY-MM-DD.log         │ │
│  │                                                                        │ │
│  │  ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐         │ │
│  │  │   CN    │     │   US    │     │   UAE   │     │ Temporal │         │ │
│  │  │ Region  │     │ Region  │     │ Region  │     │ Workflow │         │ │
│  │  └─────────┘     └─────────┘     └─────────┘     └─────────┘         │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```

### 模块划分

| 模块 | 职责 | 状态 | 依赖 |
|------|------|------|------|
| **LoggingInterceptor** | HTTP 请求自动捕获 | ✅ 已实现 | LoggerService |
| **LoggerService** | 日志核心服务 | ✅ 已实现 | Winston |
| **TraceContext** | 分布式追踪上下文 | 📋 规划中 | nanoid |
| **SamplingService** | 日志采样策略 | 📋 规划中 | - |
| **AlertService** | 慢请求/异常告警 | 📋 规划中 | NotificationService |
| **TemporalLogger** | Temporal 工作流日志 | 📋 规划中 | LoggerService |
| **CleanupService** | 日志清理与容错 | 📋 规划中 | node-cron |

### 与其他系统的集成

| 系统 | 集成方式 | 说明 |
|------|----------|------|
| **所有 HTTP 服务** | NestJS 全局拦截器 | 自动记录请求/响应 |
| **审计系统** | 相互独立 | 日志系统负责技术运维，审计系统负责业务合规 |
| **通知系统** | AlertService 集成 | 告警通知推送（Slack/飞书） |
| **Temporal 工作流** | TemporalLogger | Activity/Workflow 日志 |
| **ELK Stack** | JSON 结构化日志 | 日志收集和分析 |
| **OpenTelemetry** | 预留兼容 | traceId/spanId 设计兼容 OTel |

---

## 数据模型

### 日志类型层次

```
┌─────────────────────────────────────────────────────────────────┐
│                        日志类型层次                               │
│                                                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│  │ Application │  │    HTTP     │  │    Error    │             │
│  │    Log      │  │    Log      │  │    Log      │             │
│  │   (全量)    │  │  (可采样)   │  │  (永远100%) │             │
│  └─────────────┘  └─────────────┘  └─────────────┘             │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                   Temporal Log                               ││
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         ││
│  │  │  Workflow   │  │  Activity   │  │   Retry     │         ││
│  │  │    Log      │  │    Log      │  │    Log      │         ││
│  │  └─────────────┘  └─────────────┘  └─────────────┘         ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```

### 日志文件结构

> **环境差异说明**:
> - **开发环境**: 默认使用 `backend/logs/` 本地目录
> - **生产环境**: 使用 `/var/log/ffoa/{REGION}/{SERVICE}/` 结构

```
# 开发环境
backend/logs/
├── application-YYYY-MM-DD.log    # 所有日志
├── error-YYYY-MM-DD.log          # 仅错误
└── http/
    └── http-YYYY-MM-DD.log       # HTTP 请求/响应

# 生产环境
/var/log/ffoa/
├── CN/                               # 中国区域
│   ├── iam/                          # IAM 服务
│   │   ├── application-2025-12-07.log
│   │   ├── error-2025-12-07.log
│   │   └── http/
│   │       └── http-2025-12-07.log
│   ├── form/                         # Form 服务
│   ├── approval/                     # Approval 服务
│   └── temporal/                     # Temporal 工作流
│       ├── workflow-2025-12-07.log
│       └── activity-2025-12-07.log
├── US/                               # 美国区域
└── UAE/                              # 阿联酋区域
```

### ID 命名约定

> 参见 [PRD - ID 命名约定](PRD.md#id-命名约定)

| ID 类型 | 格式 | 作用域 | 使用场景 |
|---------|------|--------|----------|
| **RequestID** | `{timestamp}-{random6}` | 单服务 | 服务内部请求追踪 |
| **traceId** | `{region}-{timestamp}-{random8}` | 跨服务 | 跨区域、跨服务请求关联 |
| **spanId** | `{service}-{timestamp}-{random6}` | 单操作 | 调用链中的单个操作标识 |

```typescript
// 生成规则示例
const requestId = `${Date.now()}-${nanoid(6)}`;     // 1733580000123-abc
const traceId = `${region}-${Date.now()}-${nanoid(8)}`;  // CN-1733580000000-a1b2c3d4
const spanId = `${service}-${Date.now()}-${nanoid(6)}`;  // FORM-1733580000123-x1y2z3
```

### 核心日志结构（ELK 兼容）

```typescript
// 结构化日志接口（完整定义见 PRD）
interface StructuredLog {
  "@timestamp": string;        // ISO 8601 格式
  level: string;               // ERROR | WARN | INFO | DEBUG
  message: string;             // 日志消息
  
  trace: {
    id: string;                // traceId
    span_id: string;           // spanId
    parent_span_id?: string;   // parentSpanId
  };
  
  http: {
    request_id: string;        // RequestID
    method: string;            // HTTP 方法
    url: string;               // 请求 URL
    status_code: number;       // 响应状态码
    duration_ms: number;       // 响应时间
  };
  
  user: {
    id: string;                // userId
    name: string;              // username
  };
  
  client: {
    ip: string;                // IP 地址
    user_agent: string;        // User-Agent
    geo?: { country: string; city: string; };
  };
  
  service: {
    name: string;              // IAM | Form | Approval
    version: string;           // 服务版本
    instance: string;          // 实例 ID
    region: string;            // CN | US | UAE
  };
  
  temporal?: {                 // Temporal 相关（可选）
    workflow_id: string;
    workflow_type: string;
    activity_id?: string;
    activity_type?: string;
    attempt?: number;
  };
  
  error?: {                    // 错误信息（可选）
    type: string;
    message: string;
    code?: string;
    stack?: string;            // 仅开发环境
    details?: object;
  };
}
```

---

## 核心设计决策

### 决策 1: 分布式追踪标识符

**问题**: 多区域、多服务环境下如何追踪完整请求链路？

**方案**: 引入 traceId/spanId/parentSpanId 三级追踪标识 + OpenTelemetry 兼容设计

**理由**:
1. **traceId**: 跨服务请求的全局唯一标识，从请求入口生成，传递到所有下游服务
2. **spanId**: 当前操作的唯一标识，每个服务调用生成新的 spanId
3. **parentSpanId**: 父操作标识，形成调用链
4. **OTel 兼容**: 字段设计与 OpenTelemetry 概念一致，便于后续接入 Jaeger/Tempo

```
[用户请求] → [IAM 服务] → [Form 服务] → [Approval 服务]
     │            │            │              │
     └─ traceId ──┴─ traceId ──┴── traceId ───┘
                  │            │              │
              spanId-1    spanId-2       spanId-3
                  │            │              │
                  └─ parent ───┴── parent ────┘
```

**Header 传递**:

| Header | 方向 | 说明 |
|--------|------|------|
| `X-Request-Id` | 响应 | 返回给客户端 |
| `X-Trace-Id` | 请求/响应 | 跨服务传递 |
| `X-Span-Id` | 请求 | 下游作为 parentSpanId |

### 决策 2: 日志采样策略

**问题**: 大规模用户（10K+）时日志量过大，磁盘压力大

**方案**: 分级采样 + 动态调整

**配置归属**: 由**平台运维团队**统一配置，业务服务不应修改

| 日志类型 | 采样条件 | 采样率 |
|----------|----------|--------|
| 错误日志 | 永远 | 100% |
| 慢请求 (>1s) | 永远 | 100% |
| 认证失败 | 永远 | 100% |
| DEBUG 日志 | QPS > 1000 | 10% |
| HTTP 成功日志 | QPS > 5000 | 20% |
| 健康检查 | 永远 | 0% |

### 决策 3: 写入失败容错

**问题**: 磁盘满、权限错误时日志丢失

**方案**: 多级 Fallback + 自动恢复 + 紧急清理

```
文件写入
    │
    ├─ 成功 → 正常记录
    │
    └─ 失败 ┬─ Fallback 到 Console
            │
            ├─ 连续失败 3 次 → 发送告警（Slack/飞书）
            │
            ├─ 每 5 分钟重试恢复文件写入
            │
            └─ 磁盘 > 90% → 紧急删除最旧日志
```

### 决策 4: JSON 结构化 vs 文本日志

**问题**: 日志格式如何选择？

**方案**: 双格式输出

| 环境 | Console 格式 | File 格式 |
|------|-------------|-----------|
| 开发 | 彩色文本（带 emoji） | 文本 |
| 生产 | 简化文本 | JSON（ELK 兼容） |

```typescript
// 文本格式（Console 开发环境）
📥 [1763256789-abc123] POST /api/v1/users | User: admin(001) | IP: 192.168.1.1

// JSON 格式（File 生产环境）
{"@timestamp":"2025-12-07T10:30:00.123Z","level":"INFO","http":{"method":"POST",...}}
```

### 决策 5: 告警配置管理

**问题**: 告警规则由谁管理？存放在哪里？

**方案**: 集中配置 + 平台运维管理

- **维护者**: 平台运维团队
- **存储位置**: `config/logging.alerts.yaml` 或 Apollo/Nacos 配置中心
- **加载时机**: 服务启动时加载，支持热更新

---

## 技术选型

### 技术栈

| 层次 | 技术 | 选型理由 |
|------|------|----------|
| 日志库 | Winston | Node.js 生态最成熟，多 Transport 支持 |
| 日志轮转 | winston-daily-rotate-file | 自动轮转、压缩、清理 |
| 追踪 ID | nanoid | 高性能、无冲突、短 ID |
| 定时任务 | node-cron | 日志清理、定期检查 |
| 告警推送 | Slack/飞书 Webhook | 即时通知 |
| 未来扩展 | OpenTelemetry SDK | 链路追踪系统集成 |

### 目录结构

```
backend/src/modules/logging/
├── docs/                              # 模块文档
│   ├── README.md
│   ├── PRD.md
│   ├── ARCHITECTURE.md
│   ├── API.md
│   └── TODO.md
├── config/
│   └── winston.config.ts              # Winston 配置
├── services/
│   ├── index.ts
│   ├── logger.service.ts              # 框架内核心实现（Winston 封装）
│   ├── app-logger.service.ts          # 业务日志服务（对外暴露，含 withContext）✅
│   ├── trace.context.ts               # 追踪上下文（📋 规划）
│   ├── sampling.service.ts            # 采样服务（📋 规划）
│   ├── cleanup.service.ts             # 清理服务（📋 规划）
│   └── temporal-logger.service.ts     # Temporal 日志（📋 规划）
├── interceptors/
│   ├── index.ts
│   └── logging.interceptor.ts         # HTTP 日志拦截器
├── alerts/
│   ├── alert.service.ts               # 告警服务（📋 规划）
│   └── alert.config.ts                # 告警配置（📋 规划）
├── logging.module.ts                  # 日志模块
└── index.ts                           # 模块导出
```

### Logger 命名规范

> **重要**: 为避免命名混淆，请遵循以下规范：

| 类名 | 职责 | 使用场景 |
|------|------|----------|
| `LoggerService` | 框架内核心实现，封装 Winston | 仅在 `LoggingInterceptor`、`AlertService` 等框架服务中使用 |
| `AppLogger` | 对业务暴露的日志服务 | 业务 Controller / Service 中注入使用 |
| `Logger`（NestJS 原生） | 框架自身日志 | `main.ts` bootstrap、模块初始化、框架级错误 |

```typescript
// ✅ 业务代码中使用 AppLogger
import { AppLogger } from '@/modules/logging';

@Injectable()
export class FormService {
  constructor(private readonly logger: AppLogger) {}
}

// ✅ 框架代码中使用 LoggerService
import { LoggerService } from '@/modules/logging';

@Injectable()
export class LoggingInterceptor {
  constructor(private readonly loggerService: LoggerService) {}
}

// ✅ main.ts 中使用 NestJS 原生 Logger
import { Logger } from '@nestjs/common';
const logger = new Logger('Bootstrap');
logger.log('Application started');
```

---

## 核心服务实现

### LoggingInterceptor（✅ 已实现）

> **实现注意事项**:
> 1. `tap()` 回调中无法直接获取 `statusCode`，需从 `context.switchToHttp().getResponse()` 获取
> 2. RxJS 7+ 中 `catchError` 应使用 `throwError(() => error)` 而非直接 `throw`
> 3. 使用 `finalize()` 可以统一处理成功/失败的日志记录

```typescript
import { throwError } from 'rxjs';
import { catchError, tap, finalize } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly loggerService: LoggerService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();
    const { method, url, ip, user } = request;
    const requestId = this.generateRequestId();
    const startTime = Date.now();

    // 注入 RequestID 到请求和响应头
    request.headers['x-request-id'] = requestId;
    response.setHeader('X-Request-Id', requestId);

    // 记录请求入站
    this.loggerService.logRequest({ requestId, method, url, ip, user });

    // 用于记录是否发生错误
    let error: Error | null = null;

    return next.handle().pipe(
      tap(() => {
        // 成功响应：从 response 对象获取真实 statusCode
        const statusCode = response.statusCode;
        const duration = Date.now() - startTime;
        this.loggerService.logResponse({ 
          requestId, method, url, statusCode, duration, user 
        });
      }),
      catchError((err) => {
        // 记录错误
        error = err;
        const duration = Date.now() - startTime;
        const statusCode = err.status || err.statusCode || 500;
        this.loggerService.logError({ 
          requestId, method, url, statusCode, error: err, duration, user 
        });
        // RxJS 7+: 使用 throwError(() => error) 而非 throw
        return throwError(() => err);
      }),
      // 可选：使用 finalize 统一处理（无论成功/失败）
      // finalize(() => {
      //   const duration = Date.now() - startTime;
      //   this.loggerService.logComplete({ requestId, duration, hasError: !!error });
      // }),
    );
  }

  private generateRequestId(): string {
    return `${Date.now()}-${nanoid(6)}`;
  }
}
```

### TraceContext（📋 规划中）

```typescript
@Injectable({ scope: Scope.REQUEST })
export class TraceContext {
  private traceId: string;
  private spanId: string;
  private parentSpanId?: string;
  private region: string;
  private service: string;

  constructor() {
    this.spanId = this.generateSpanId();
    this.region = process.env.LOG_REGION || 'LOCAL';
    this.service = process.env.LOG_SERVICE || 'APP';
  }

  setFromHeaders(headers: IncomingHttpHeaders) {
    this.traceId = headers['x-trace-id'] as string || this.generateTraceId();
    this.parentSpanId = headers['x-span-id'] as string;
  }

  private generateTraceId(): string {
    return `${this.region}-${Date.now()}-${nanoid(8)}`;
  }

  private generateSpanId(): string {
    return `${this.service}-${Date.now()}-${nanoid(6)}`;
  }

  // 获取要传递给下游的 Headers
  getOutgoingHeaders(): Record<string, string> {
    return {
      'X-Trace-Id': this.traceId,
      'X-Span-Id': this.spanId,
    };
  }

  // 获取日志上下文
  getLogContext(): object {
    return {
      trace: {
        id: this.traceId,
        span_id: this.spanId,
        parent_span_id: this.parentSpanId,
      },
    };
  }
}
```

### SamplingService（📋 规划中）

```typescript
@Injectable()
export class SamplingService {
  private requestCounter = 0;
  private lastResetTime = Date.now();

  shouldLog(context: LogContext): boolean {
    // 错误日志永远记录
    if (context.level === 'ERROR') return true;
    
    // 慢请求永远记录
    if (context.duration && context.duration > 1000) return true;
    
    // 健康检查永不记录
    if (context.url?.includes('/health') || context.url?.includes('/ready')) {
      return false;
    }
    
    // 认证相关永远记录
    if (context.url?.startsWith('/api/v1/auth')) return true;
    
    // 动态采样（基于 QPS）
    const qps = this.getCurrentQps();
    if (qps > 5000) return Math.random() < 0.2; // 20%
    if (qps > 1000) return Math.random() < 0.5; // 50%
    
    return true; // 100%
  }

  private getCurrentQps(): number {
    const now = Date.now();
    if (now - this.lastResetTime > 1000) {
      const qps = this.requestCounter;
      this.requestCounter = 0;
      this.lastResetTime = now;
      return qps;
    }
    this.requestCounter++;
    return this.requestCounter;
  }
}
```

### AlertService（📋 规划中）

```typescript
@Injectable()
export class AlertService {
  constructor(
    private readonly httpService: HttpService,
    private readonly configService: ConfigService,
  ) {}

  async checkAndAlert(context: LogContext) {
    // 慢请求告警
    if (context.duration > this.configService.get('LOG_ALERT_SLOW_THRESHOLD_MS', 2000)) {
      await this.sendSlowRequestAlert(context);
    }
  }

  async sendSlowRequestAlert(context: SlowRequestContext) {
    const message = this.formatSlowRequestMessage(context);
    await this.sendToSlack(message);
  }

  async sendHighErrorRateAlert(context: ErrorRateContext) {
    const message = this.formatErrorRateMessage(context);
    await this.sendToSlack(message);
    await this.sendToFeishu(message);
  }

  async sendDiskSpaceAlert(usedPercent: number) {
    const message = `⚠️ [DISK_WARNING] 日志磁盘使用率: ${usedPercent}%`;
    await this.sendToSlack(message);
  }

  private formatSlowRequestMessage(ctx: SlowRequestContext): string {
    return `🚨 [SLOW_REQUEST] ${ctx.region} Region
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Path: ${ctx.method} ${ctx.url}
Duration: ${ctx.duration}ms (threshold: 2000ms)
User: ${ctx.user} (${ctx.userId})
TraceId: ${ctx.traceId}
Time: ${new Date().toISOString()}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
  }

  private async sendToSlack(message: string) {
    const webhookUrl = this.configService.get('SLACK_WEBHOOK_URL');
    if (!webhookUrl) return;
    
    await this.httpService.post(webhookUrl, { text: message }).toPromise();
  }
}
```

### CleanupService（📋 规划中）

```typescript
@Injectable()
export class CleanupService {
  private readonly logger = new Logger(CleanupService.name);

  // 定时清理任务（每日凌晨 3 点）
  @Cron('0 3 * * *')
  async scheduledCleanup() {
    const retentionDays = {
      http: 14,
      application: 30,
      error: 30,
    };
    
    for (const [type, days] of Object.entries(retentionDays)) {
      await this.deleteOlderThan(type, days);
    }
    
    this.logger.log('Scheduled cleanup completed');
  }

  // 紧急清理（磁盘 > 90%）
  async emergencyCleanup() {
    const diskUsage = await this.getDiskUsage();
    
    if (diskUsage > 90) {
      this.logger.warn(`Disk usage ${diskUsage}%, starting emergency cleanup`);
      
      while (await this.getDiskUsage() > 70) {
        await this.deleteOldestLog();
      }
      
      this.logger.log('Emergency cleanup completed');
    }
  }

  private async deleteOlderThan(type: string, days: number) {
    // 删除超期日志文件
  }

  private async deleteOldestLog() {
    // 删除最旧的日志文件
  }

  private async getDiskUsage(): Promise<number> {
    // 获取磁盘使用率
    return 0;
  }
}
```

### TemporalLogger（📋 规划中）

> Temporal 日志字段为推荐标准，具体实现可由「流程引擎 / 审批团队」按实际情况扩展，
> 但需保留核心字段：`workflow_id`、`activity_type`、`status`

> **实现说明**：
> - 推荐使用 `LoggerService`（而非 NestJS 原生 Logger）以统一走 Winston + traceId + JSON 格式
> - 这样 Temporal 日志也能被 ELK 收集和结构化查询
> - 下方示例为简化版，实际实现时注入 `LoggerService`

```typescript
import { LoggerService } from '@/common/logger/logger.service';

@Injectable()
export class TemporalLoggerService {
  constructor(private readonly loggerService: LoggerService) {}

  logActivityStart(context: ActivityContext) {
    this.loggerService.log('info', {
      message: `Activity started: ${context.activityType}`,
      temporal: {
        workflow_id: context.workflowId,
        workflow_type: context.workflowType,
        workflow_run_id: context.workflowRunId,
        activity_id: context.activityId,
        activity_type: context.activityType,
        attempt: context.attempt,
      },
      execution: {
        status: 'STARTED',
      },
      context: {
        business_key: context.businessKey,
        initiator_id: context.initiatorId,
      },
    });
  }

  logActivityComplete(context: ActivityContext, durationMs: number) {
    this.loggerService.log('info', {
      message: `Activity completed: ${context.activityType}`,
      temporal: { ...context },
      execution: {
        status: 'COMPLETED',
        duration_ms: durationMs,
      },
    });
  }

  logActivityFailed(context: ActivityContext, error: Error) {
    this.loggerService.log('error', {
      message: `Activity failed: ${context.activityType}`,
      temporal: { ...context },
      execution: {
        status: 'FAILED',
        error: {
          type: error.name,
          message: error.message,
        },
      },
    });
  }

  logWorkflowStateChange(context: WorkflowContext, previousState: string, currentState: string, trigger: string) {
    this.loggerService.log('info', {
      message: `Workflow state changed: ${previousState} → ${currentState}`,
      temporal: {
        workflow_id: context.workflowId,
        workflow_type: context.workflowType,
        workflow_run_id: context.workflowRunId,
      },
      execution: {
        previous_state: previousState,
        current_state: currentState,
        trigger: trigger,
      },
    });
  }
}
```

---

## 配置管理

### 环境变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `NODE_ENV` | `development` | 环境：development/production |
| `LOG_LEVEL` | `info` | 日志级别：error/warn/info/debug |
| `LOG_REGION` | `LOCAL` | 区域标识：CN/US/UAE |
| `LOG_SERVICE` | `APP` | 服务标识：IAM/Form/Approval |
| `LOG_INSTANCE` | `pod-0` | 实例标识 |
| `LOG_BASE_DIR` | `./logs` | 日志根目录 |
| `LOG_SAMPLING_ENABLED` | `false` | 是否启用采样 |
| `LOG_ALERT_ENABLED` | `false` | 是否启用告警 |
| `LOG_ALERT_SLOW_THRESHOLD_MS` | `2000` | 慢请求阈值（ms） |
| `SLACK_WEBHOOK_URL` | - | Slack 告警 Webhook |
| `FEISHU_WEBHOOK_URL` | - | 飞书告警 Webhook |

### Winston 配置

```typescript
// winston.config.ts
import { WinstonModuleOptions } from 'nest-winston';
import { transports, format } from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';

const { combine, timestamp, json, printf, colorize } = format;

// 从环境变量读取配置（与上方环境变量表对应）
const LOG_BASE_DIR = process.env.LOG_BASE_DIR || './logs';
const LOG_REGION = process.env.LOG_REGION || 'LOCAL';
const LOG_SERVICE = process.env.LOG_SERVICE || 'APP';

export const winstonConfig: WinstonModuleOptions = {
  level: process.env.LOG_LEVEL || 'info',
  format: combine(
    timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
    process.env.NODE_ENV === 'production' ? json() : printf(devFormat),
  ),
  transports: [
    // 控制台输出（彩色）
    new transports.Console({
      format: combine(colorize(), printf(consoleFormat)),
    }),
    // 应用日志
    new DailyRotateFile({
      dirname: `${LOG_BASE_DIR}/${LOG_REGION}/${LOG_SERVICE}`,
      filename: 'application-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '30d',
      format: json(),
    }),
    // 错误日志
    new DailyRotateFile({
      dirname: `${LOG_BASE_DIR}/${LOG_REGION}/${LOG_SERVICE}`,
      filename: 'error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      level: 'error',
      maxSize: '20m',
      maxFiles: '30d',
      format: json(),
    }),
    // HTTP 日志
    new DailyRotateFile({
      dirname: `${LOG_BASE_DIR}/${LOG_REGION}/${LOG_SERVICE}/http`,
      filename: 'http-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '50m',
      maxFiles: '14d',
      format: json(),
    }),
  ],
};
```

### 告警配置文件示例

```yaml
# config/logging.alerts.yaml
alerts:
  slowRequest:
    enabled: true
    thresholdMs: 2000
    excludePaths:
      - /api/v1/files/upload
      - /api/v1/reports/generate
  
  errorRate:
    enabled: true
    thresholdPercent: 5
    windowMinutes: 5
    minRequests: 100
  
  ipAnomaly:
    enabled: true
    maxRequestsPerMinute: 100
    blockDurationMinutes: 10
  
  diskSpace:
    enabled: true
    warningPercent: 80
    criticalPercent: 90

notifications:
  slack:
    webhookUrl: ${SLACK_WEBHOOK_URL}
  feishu:
    webhookUrl: ${FEISHU_WEBHOOK_URL}
  email:
    enabled: false
```

---

## 安全设计

### 敏感数据脱敏

```typescript
const SENSITIVE_FIELDS = [
  'password',
  'passwordHash',
  'token',
  'accessToken',
  'refreshToken',
  'secret',
  'apiKey',
  'authorization',
  'creditCard',
  'idNumber',
];

/**
 * 脱敏函数
 * 
 * ⚠️ 实现注意事项：
 * 1. 仅对已知结构（请求体、响应体、error.details）调用此函数
 * 2. 避免对可能存在循环引用的对象（如 Request/Response 对象）调用
 * 3. 生产实现时可考虑添加 visited Set 防止循环引用导致栈溢出
 * 4. 建议设置最大递归深度（如 10 层）
 */
function sanitize(obj: any, visited = new WeakSet(), depth = 0): any {
  // 边界检查：非对象、null、已访问、深度过大
  if (!obj || typeof obj !== 'object') return obj;
  if (visited.has(obj)) return '[Circular Reference]';
  if (depth > 10) return '[Max Depth Exceeded]';
  
  visited.add(obj);
  
  const sanitized = Array.isArray(obj) ? [...obj] : { ...obj };
  
  for (const field of SENSITIVE_FIELDS) {
    if (sanitized[field]) {
      sanitized[field] = '***REDACTED***';
    }
  }
  
  // 递归处理嵌套对象
  for (const key of Object.keys(sanitized)) {
    if (typeof sanitized[key] === 'object' && sanitized[key] !== null) {
      sanitized[key] = sanitize(sanitized[key], visited, depth + 1);
    }
  }
  
  return sanitized;
}
```

**使用建议**:

```typescript
// ✅ 正确：仅对已知安全结构调用
const sanitizedBody = sanitize(request.body);
const sanitizedError = sanitize(error.details);

// ❌ 避免：可能包含循环引用的对象
const sanitizedRequest = sanitize(request);  // Request 对象可能有循环引用
```

### 日志文件权限

```bash
# 日志目录权限
chmod 755 /var/log/ffoa
chmod 755 /var/log/ffoa/{REGION}
chmod 755 /var/log/ffoa/{REGION}/{SERVICE}

# 日志文件权限（仅应用进程可写）
chmod 644 /var/log/ffoa/{REGION}/{SERVICE}/*.log
```

---

## 性能优化

### 异步写入

- 所有日志写入使用异步方式，不阻塞主请求
- Winston 使用 Stream 模式写入文件
- 采样机制减少高 QPS 场景下的写入量

### 缓冲区

- 日志写入使用 Buffer，批量刷盘
- 默认 Buffer 大小：16KB

### 性能指标

| 指标 | 目标值 |
|------|--------|
| 日志记录开销（info 模式） | < 2ms/请求 |
| 日志记录开销（debug 模式） | < 5ms/请求 |
| 文件写入延迟 | < 10ms |
| 内存占用 | < 50MB |

---

## 监控与运维

### 健康检查

```typescript
@Injectable()
export class LoggingHealthIndicator extends HealthIndicator {
  async isHealthy(): Promise<HealthIndicatorResult> {
    const isWritable = await this.checkWritable();
    const diskUsage = await this.checkDiskUsage();
    
    const isHealthy = isWritable && diskUsage < 90;
    
    return this.getStatus('logging', isHealthy, {
      writable: isWritable,
      diskUsage: `${diskUsage}%`,
    });
  }

  private async checkWritable(): Promise<boolean> {
    // 尝试写入测试文件
  }

  private async checkDiskUsage(): Promise<number> {
    // 获取磁盘使用率
  }
}
```

### 常用运维命令

```bash
# 实时查看 HTTP 日志
tail -f logs/http/http-$(date +%Y-%m-%d).log

# 通过 traceId 追踪（跨服务）
grep "CN-1733580000000-a1b2c3d4" /var/log/ffoa/CN/*/http/*.log

# 查找慢请求（>2秒）
grep "ms" logs/http/http-$(date +%Y-%m-%d).log | \
  awk -F'|' '$3 ~ /[0-9]+ms/ {split($3, a, "ms"); if(a[1] > 2000) print}'

# 统计各区域请求数
for region in CN US UAE; do
  echo "$region: $(grep -c '📥' /var/log/ffoa/$region/*/http/*.log 2>/dev/null || echo 0)"
done

# Temporal Activity 失败统计
grep '"status":"FAILED"' /var/log/ffoa/CN/temporal/activity-*.log | wc -l

# 查看磁盘使用
df -h /var/log/ffoa
```

---

## 相关文档

- [日志系统 PRD](PRD.md) - 产品需求文档
- [日志系统 API](API.md) - API 接口文档
- [日志系统 TODO](TODO.md) - 开发待办
- [审计系统](../../../audit/docs/README.md) - 业务合规审计

---

**文档维护**: FFOA 开发团队  
**更新频率**: 架构变更时更新
