diff --git a/Fabric.Backend.Center/package-lock.json b/Fabric.Backend.Center/package-lock.json index 53631b6..d08a7a6 100644 --- a/Fabric.Backend.Center/package-lock.json +++ b/Fabric.Backend.Center/package-lock.json @@ -27,6 +27,7 @@ "@eslint/js": "^10.0.1", "@nestjs/testing": "^10.4.22", "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.10.1", "@types/supertest": "^7.2.0", @@ -1036,6 +1037,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1047,6 +1059,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -1075,6 +1097,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1116,6 +1170,41 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/superagent": { "version": "8.1.9", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", diff --git a/Fabric.Backend.Center/package.json b/Fabric.Backend.Center/package.json index b40c54a..162bac5 100644 --- a/Fabric.Backend.Center/package.json +++ b/Fabric.Backend.Center/package.json @@ -36,6 +36,7 @@ "@eslint/js": "^10.0.1", "@nestjs/testing": "^10.4.22", "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.10.1", "@types/supertest": "^7.2.0", diff --git a/Fabric.Backend.Center/src/common/request-context.middleware.ts b/Fabric.Backend.Center/src/common/request-context.middleware.ts new file mode 100644 index 0000000..89ffaba --- /dev/null +++ b/Fabric.Backend.Center/src/common/request-context.middleware.ts @@ -0,0 +1,30 @@ +import { randomUUID } from 'crypto'; +import { NextFunction, Request, Response } from 'express'; + +type ReqWithId = Request & { requestId?: string }; + +export function requestContextMiddleware(req: ReqWithId, res: Response, next: NextFunction): void { + const headerId = req.headers['x-request-id']; + const requestId = + (Array.isArray(headerId) ? headerId[0] : headerId) || randomUUID(); + + req.requestId = requestId; + res.setHeader('x-request-id', requestId); + + const startedAt = Date.now(); + res.on('finish', () => { + const log = { + level: 'info', + service: 'center', + requestId, + method: req.method, + path: req.originalUrl, + statusCode: res.statusCode, + durationMs: Date.now() - startedAt, + timestamp: new Date().toISOString(), + }; + console.log(JSON.stringify(log)); + }); + + next(); +} diff --git a/Fabric.Backend.Center/src/main.ts b/Fabric.Backend.Center/src/main.ts index b7b6bc6..60271c1 100644 --- a/Fabric.Backend.Center/src/main.ts +++ b/Fabric.Backend.Center/src/main.ts @@ -3,6 +3,7 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; +import { requestContextMiddleware } from './common/request-context.middleware'; function requireEnv(name: string): string { const value = process.env[name]; @@ -28,6 +29,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); + app.use(requestContextMiddleware); app.useGlobalPipes( new ValidationPipe({ whitelist: true, diff --git a/Fabric.Backend.Guild/package-lock.json b/Fabric.Backend.Guild/package-lock.json index 549cb9b..2a3049e 100644 --- a/Fabric.Backend.Guild/package-lock.json +++ b/Fabric.Backend.Guild/package-lock.json @@ -27,6 +27,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@nestjs/testing": "^10.4.22", + "@types/express": "^5.0.6", "@types/node": "^22.10.1", "@types/supertest": "^7.2.0", "@typescript-eslint/eslint-plugin": "^8.59.3", @@ -1117,6 +1118,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1128,6 +1140,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -1165,6 +1187,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1188,6 +1242,41 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/superagent": { "version": "8.1.9", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", diff --git a/Fabric.Backend.Guild/package.json b/Fabric.Backend.Guild/package.json index e393a85..d993f66 100644 --- a/Fabric.Backend.Guild/package.json +++ b/Fabric.Backend.Guild/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@nestjs/testing": "^10.4.22", + "@types/express": "^5.0.6", "@types/node": "^22.10.1", "@types/supertest": "^7.2.0", "@typescript-eslint/eslint-plugin": "^8.59.3", diff --git a/Fabric.Backend.Guild/src/common/request-context.middleware.ts b/Fabric.Backend.Guild/src/common/request-context.middleware.ts new file mode 100644 index 0000000..6a2646b --- /dev/null +++ b/Fabric.Backend.Guild/src/common/request-context.middleware.ts @@ -0,0 +1,30 @@ +import { randomUUID } from 'crypto'; +import { NextFunction, Request, Response } from 'express'; + +type ReqWithId = Request & { requestId?: string }; + +export function requestContextMiddleware(req: ReqWithId, res: Response, next: NextFunction): void { + const headerId = req.headers['x-request-id']; + const requestId = + (Array.isArray(headerId) ? headerId[0] : headerId) || randomUUID(); + + req.requestId = requestId; + res.setHeader('x-request-id', requestId); + + const startedAt = Date.now(); + res.on('finish', () => { + const log = { + level: 'info', + service: 'guild', + requestId, + method: req.method, + path: req.originalUrl, + statusCode: res.statusCode, + durationMs: Date.now() - startedAt, + timestamp: new Date().toISOString(), + }; + console.log(JSON.stringify(log)); + }); + + next(); +} diff --git a/Fabric.Backend.Guild/src/main.ts b/Fabric.Backend.Guild/src/main.ts index c5f280d..7164670 100644 --- a/Fabric.Backend.Guild/src/main.ts +++ b/Fabric.Backend.Guild/src/main.ts @@ -3,10 +3,12 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; +import { requestContextMiddleware } from './common/request-context.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); + app.use(requestContextMiddleware); app.useGlobalPipes( new ValidationPipe({ whitelist: true, diff --git a/docs/TODO-backend-center-guild.md b/docs/TODO-backend-center-guild.md index 8be1c39..6361425 100644 --- a/docs/TODO-backend-center-guild.md +++ b/docs/TODO-backend-center-guild.md @@ -95,7 +95,7 @@ ## 6. 部署与运维 - [x] `docker-compose.prod.yml`(去掉 `DB_SYNC=true`) - [x] DB migration 机制(TypeORM migration) -- [ ] 结构化日志 + request id +- [x] 结构化日志 + request id - [ ] 基础监控指标(QPS、延迟、错误率) - [ ] 备份与恢复流程文档