feat: 接入 Redis 健康检查
This commit is contained in:
@@ -103,6 +103,7 @@ pnpm prisma:seed
|
|||||||
- `DATABASE_URL`:MySQL 连接地址。
|
- `DATABASE_URL`:MySQL 连接地址。
|
||||||
- `USE_DATABASE_READS`:是否启用 Prisma 数据读写。
|
- `USE_DATABASE_READS`:是否启用 Prisma 数据读写。
|
||||||
- `PRISMA_CONNECT_ON_BOOT`:启动时是否主动连接数据库。
|
- `PRISMA_CONNECT_ON_BOOT`:启动时是否主动连接数据库。
|
||||||
|
- `REDIS_URL`:Redis 连接地址,`/health` 会执行真实 PING;后续 BullMQ worker 复用该连接边界。
|
||||||
- `JENKINS_BASE_URL`、`JENKINS_USERNAME`、`JENKINS_API_TOKEN`:Jenkins 集成配置。
|
- `JENKINS_BASE_URL`、`JENKINS_USERNAME`、`JENKINS_API_TOKEN`:Jenkins 集成配置。
|
||||||
- `GITEA_BASE_URL`、`GITEA_TOKEN`、`GITEA_WEBHOOK_SECRET`、`GITEA_WEBHOOK_RELAY_BASE_URL`:Gitea 集成与 Jenkins relay 诊断配置。
|
- `GITEA_BASE_URL`、`GITEA_TOKEN`、`GITEA_WEBHOOK_SECRET`、`GITEA_WEBHOOK_RELAY_BASE_URL`:Gitea 集成与 Jenkins relay 诊断配置。
|
||||||
- `NOTIFICATION_PROVIDER`、`WECOM_WEBHOOK_URL`、`FEISHU_WEBHOOK_URL`、`NOTIFICATION_WEBHOOK_URL`:通知集成配置。
|
- `NOTIFICATION_PROVIDER`、`WECOM_WEBHOOK_URL`、`FEISHU_WEBHOOK_URL`、`NOTIFICATION_WEBHOOK_URL`:通知集成配置。
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
- `deploy-runs`:发布记录 API、状态模型和 BPMN 步骤摘要。
|
- `deploy-runs`:发布记录 API、状态模型和 BPMN 步骤摘要。
|
||||||
- `integrations/jenkins`:Jenkins API 客户端边界。
|
- `integrations/jenkins`:Jenkins API 客户端边界。
|
||||||
- `integrations/gitea`:Gitea API 客户端边界。
|
- `integrations/gitea`:Gitea API 客户端边界。
|
||||||
|
- `integrations/redis`:Redis PING 健康检查边界,后续 BullMQ worker 复用该模块。
|
||||||
- `notifications/wecom`:企微、飞书和通用 webhook 通知边界。
|
- `notifications/wecom`:企微、飞书和通用 webhook 通知边界。
|
||||||
- `agent-config`:Agent 系统级配置、密钥加密和连接测试。
|
- `agent-config`:Agent 系统级配置、密钥加密和连接测试。
|
||||||
- `agent`:只面向运维工作流的 LLM 代理边界。
|
- `agent`:只面向运维工作流的 LLM 代理边界。
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"@nestjs/platform-express": "^11.1.6",
|
"@nestjs/platform-express": "^11.1.6",
|
||||||
"@nestjs/swagger": "^11.2.0",
|
"@nestjs/swagger": "^11.2.0",
|
||||||
"@prisma/client": "^6.14.0",
|
"@prisma/client": "^6.14.0",
|
||||||
|
"ioredis": "^5.11.1",
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
|
|||||||
Generated
+55
@@ -26,6 +26,9 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^6.14.0
|
specifier: ^6.14.0
|
||||||
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)
|
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)
|
||||||
|
ioredis:
|
||||||
|
specifier: ^5.11.1
|
||||||
|
version: 5.11.1
|
||||||
pino:
|
pino:
|
||||||
specifier: ^10.3.1
|
specifier: ^10.3.1
|
||||||
version: 10.3.1
|
version: 10.3.1
|
||||||
@@ -507,6 +510,9 @@ packages:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@ioredis/commands@1.10.0':
|
||||||
|
resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==}
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -1377,6 +1383,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
cluster-key-slot@1.1.1:
|
||||||
|
resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
co@4.6.0:
|
co@4.6.0:
|
||||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||||
@@ -1493,6 +1503,10 @@ packages:
|
|||||||
defu@6.1.7:
|
defu@6.1.7:
|
||||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||||
|
|
||||||
|
denque@2.1.0:
|
||||||
|
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1900,6 +1914,10 @@ packages:
|
|||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ioredis@5.11.1:
|
||||||
|
resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==}
|
||||||
|
engines: {node: '>=12.22.0'}
|
||||||
|
|
||||||
ipaddr.js@1.9.1:
|
ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -2549,6 +2567,14 @@ packages:
|
|||||||
real-require@1.0.0:
|
real-require@1.0.0:
|
||||||
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
|
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
|
||||||
|
|
||||||
|
redis-errors@1.2.0:
|
||||||
|
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
redis-parser@3.0.0:
|
||||||
|
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
reflect-metadata@0.2.2:
|
reflect-metadata@0.2.2:
|
||||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||||
|
|
||||||
@@ -2687,6 +2713,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
|
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
standard-as-callback@2.1.0:
|
||||||
|
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||||
|
|
||||||
statuses@2.0.2:
|
statuses@2.0.2:
|
||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3544,6 +3573,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.13.2
|
'@types/node': 24.13.2
|
||||||
|
|
||||||
|
'@ioredis/commands@1.10.0': {}
|
||||||
|
|
||||||
'@isaacs/cliui@8.0.2':
|
'@isaacs/cliui@8.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
@@ -4581,6 +4612,8 @@ snapshots:
|
|||||||
|
|
||||||
clone@1.0.4: {}
|
clone@1.0.4: {}
|
||||||
|
|
||||||
|
cluster-key-slot@1.1.1: {}
|
||||||
|
|
||||||
co@4.6.0: {}
|
co@4.6.0: {}
|
||||||
|
|
||||||
collect-v8-coverage@1.0.3: {}
|
collect-v8-coverage@1.0.3: {}
|
||||||
@@ -4665,6 +4698,8 @@ snapshots:
|
|||||||
|
|
||||||
defu@6.1.7: {}
|
defu@6.1.7: {}
|
||||||
|
|
||||||
|
denque@2.1.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
destr@2.0.5: {}
|
destr@2.0.5: {}
|
||||||
@@ -5113,6 +5148,18 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ioredis@5.11.1:
|
||||||
|
dependencies:
|
||||||
|
'@ioredis/commands': 1.10.0
|
||||||
|
cluster-key-slot: 1.1.1
|
||||||
|
debug: 4.4.3
|
||||||
|
denque: 2.1.0
|
||||||
|
redis-errors: 1.2.0
|
||||||
|
redis-parser: 3.0.0
|
||||||
|
standard-as-callback: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
@@ -5885,6 +5932,12 @@ snapshots:
|
|||||||
|
|
||||||
real-require@1.0.0: {}
|
real-require@1.0.0: {}
|
||||||
|
|
||||||
|
redis-errors@1.2.0: {}
|
||||||
|
|
||||||
|
redis-parser@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
redis-errors: 1.2.0
|
||||||
|
|
||||||
reflect-metadata@0.2.2: {}
|
reflect-metadata@0.2.2: {}
|
||||||
|
|
||||||
require-directory@2.1.1: {}
|
require-directory@2.1.1: {}
|
||||||
@@ -6038,6 +6091,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 2.0.0
|
escape-string-regexp: 2.0.0
|
||||||
|
|
||||||
|
standard-as-callback@2.1.0: {}
|
||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
streamsearch@1.1.0: {}
|
streamsearch@1.1.0: {}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AgentModule } from '../agent/agent.module';
|
import { AgentModule } from '../agent/agent.module';
|
||||||
import { GiteaModule } from '../integrations/gitea/gitea.module';
|
import { GiteaModule } from '../integrations/gitea/gitea.module';
|
||||||
import { JenkinsModule } from '../integrations/jenkins/jenkins.module';
|
import { JenkinsModule } from '../integrations/jenkins/jenkins.module';
|
||||||
|
import { RedisModule } from '../integrations/redis/redis.module';
|
||||||
import { WeComModule } from '../notifications/wecom/wecom.module';
|
import { WeComModule } from '../notifications/wecom/wecom.module';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
import { HealthService } from './health.service';
|
import { HealthService } from './health.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AgentModule, GiteaModule, JenkinsModule, WeComModule],
|
imports: [AgentModule, GiteaModule, JenkinsModule, RedisModule, WeComModule],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [HealthService],
|
providers: [HealthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { LlmClient } from '../agent/llm.client';
|
import { LlmClient } from '../agent/llm.client';
|
||||||
import { EnvConfig } from '../config/env.schema';
|
|
||||||
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
||||||
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
||||||
|
import { RedisClient } from '../integrations/redis/redis.client';
|
||||||
import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service';
|
import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { HealthService } from './health.service';
|
import { HealthService } from './health.service';
|
||||||
@@ -12,14 +11,16 @@ import { HealthService } from './health.service';
|
|||||||
*/
|
*/
|
||||||
describe('HealthService', () => {
|
describe('HealthService', () => {
|
||||||
it('returns ok when optional integrations are not configured but available services respond', async () => {
|
it('returns ok when optional integrations are not configured but available services respond', async () => {
|
||||||
const config = {
|
|
||||||
get: jest.fn((key: keyof EnvConfig) => (key === 'REDIS_URL' ? undefined : undefined)),
|
|
||||||
} satisfies Pick<ConfigService<EnvConfig, true>, 'get'>;
|
|
||||||
const prisma = {
|
const prisma = {
|
||||||
databaseHealth: jest.fn(() =>
|
databaseHealth: jest.fn(() =>
|
||||||
Promise.resolve({ status: 'not_configured' as const }),
|
Promise.resolve({ status: 'not_configured' as const }),
|
||||||
),
|
),
|
||||||
} satisfies Pick<PrismaService, 'databaseHealth'>;
|
} satisfies Pick<PrismaService, 'databaseHealth'>;
|
||||||
|
const redis = {
|
||||||
|
healthSummary: jest.fn(() =>
|
||||||
|
Promise.resolve({ status: 'not_configured' as const }),
|
||||||
|
),
|
||||||
|
} satisfies Pick<RedisClient, 'healthSummary'>;
|
||||||
const jenkins = {
|
const jenkins = {
|
||||||
healthSummary: jest.fn(() => ({ status: 'not_configured' as const })),
|
healthSummary: jest.fn(() => ({ status: 'not_configured' as const })),
|
||||||
} satisfies Pick<JenkinsClient, 'healthSummary'>;
|
} satisfies Pick<JenkinsClient, 'healthSummary'>;
|
||||||
@@ -35,8 +36,8 @@ describe('HealthService', () => {
|
|||||||
),
|
),
|
||||||
} satisfies Pick<LlmClient, 'healthSummary'>;
|
} satisfies Pick<LlmClient, 'healthSummary'>;
|
||||||
const service = new HealthService(
|
const service = new HealthService(
|
||||||
config as unknown as ConfigService<EnvConfig, true>,
|
|
||||||
prisma as unknown as PrismaService,
|
prisma as unknown as PrismaService,
|
||||||
|
redis as unknown as RedisClient,
|
||||||
jenkins as unknown as JenkinsClient,
|
jenkins as unknown as JenkinsClient,
|
||||||
gitea as unknown as GiteaClient,
|
gitea as unknown as GiteaClient,
|
||||||
notification as unknown as DeployNotificationService,
|
notification as unknown as DeployNotificationService,
|
||||||
@@ -47,6 +48,7 @@ describe('HealthService', () => {
|
|||||||
|
|
||||||
expect(health.status).toBe('ok');
|
expect(health.status).toBe('ok');
|
||||||
expect(health.dependencies.database.status).toBe('not_configured');
|
expect(health.dependencies.database.status).toBe('not_configured');
|
||||||
|
expect(health.dependencies.redis.status).toBe('not_configured');
|
||||||
expect(health.dependencies.jenkins.status).toBe('not_configured');
|
expect(health.dependencies.jenkins.status).toBe('not_configured');
|
||||||
expect(health.dependencies.notification.status).toBe('not_configured');
|
expect(health.dependencies.notification.status).toBe('not_configured');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { LlmClient } from '../agent/llm.client';
|
import { LlmClient } from '../agent/llm.client';
|
||||||
import { EnvConfig } from '../config/env.schema';
|
|
||||||
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
||||||
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
||||||
|
import { RedisClient } from '../integrations/redis/redis.client';
|
||||||
import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service';
|
import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
@@ -12,8 +11,8 @@ type HealthStatus = 'ok' | 'not_configured' | 'unavailable';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class HealthService {
|
export class HealthService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService<EnvConfig, true>,
|
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly redis: RedisClient,
|
||||||
private readonly jenkins: JenkinsClient,
|
private readonly jenkins: JenkinsClient,
|
||||||
private readonly gitea: GiteaClient,
|
private readonly gitea: GiteaClient,
|
||||||
private readonly notification: DeployNotificationService,
|
private readonly notification: DeployNotificationService,
|
||||||
@@ -27,9 +26,7 @@ export class HealthService {
|
|||||||
}> {
|
}> {
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
database: await this.prisma.databaseHealth(),
|
database: await this.prisma.databaseHealth(),
|
||||||
redis: this.config.get('REDIS_URL', { infer: true })
|
redis: await this.redis.healthSummary(),
|
||||||
? { status: 'ok' as const }
|
|
||||||
: { status: 'not_configured' as const },
|
|
||||||
jenkins: this.jenkins.healthSummary(),
|
jenkins: this.jenkins.healthSummary(),
|
||||||
gitea: this.gitea.healthSummary(),
|
gitea: this.gitea.healthSummary(),
|
||||||
notification: this.notification.healthSummary(),
|
notification: this.notification.healthSummary(),
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Redis 客户端封装真实 PING 健康检查,后续 BullMQ worker 复用同一连接边界。
|
||||||
|
*/
|
||||||
|
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import { redactSensitive } from '../../common/security/redact-sensitive';
|
||||||
|
import { EnvConfig } from '../../config/env.schema';
|
||||||
|
|
||||||
|
export type RedisHealthSummary = {
|
||||||
|
status: 'ok' | 'not_configured' | 'unavailable';
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const redisPingTimeoutMs = 1500;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RedisClient implements OnModuleDestroy {
|
||||||
|
private client?: Redis;
|
||||||
|
private clientUrl?: string;
|
||||||
|
|
||||||
|
constructor(private readonly config: ConfigService<EnvConfig, true>) {}
|
||||||
|
|
||||||
|
async healthSummary(): Promise<RedisHealthSummary> {
|
||||||
|
const redisUrl = this.config.get('REDIS_URL', { infer: true });
|
||||||
|
|
||||||
|
if (!redisUrl) {
|
||||||
|
return { status: 'not_configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pong = String(
|
||||||
|
await this.withTimeout(this.getClient(redisUrl).ping()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return pong === 'PONG'
|
||||||
|
? { status: 'ok' }
|
||||||
|
: { status: 'unavailable', message: `Redis PING 返回异常:${pong}` };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'unavailable',
|
||||||
|
message: this.safeErrorMessage(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleDestroy(): void {
|
||||||
|
if (this.client) {
|
||||||
|
this.client.disconnect();
|
||||||
|
this.client = undefined;
|
||||||
|
this.clientUrl = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getClient(redisUrl: string): Redis {
|
||||||
|
if (this.client && this.clientUrl === redisUrl) {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client?.disconnect();
|
||||||
|
this.clientUrl = redisUrl;
|
||||||
|
this.client = new Redis(redisUrl, {
|
||||||
|
connectTimeout: redisPingTimeoutMs,
|
||||||
|
enableOfflineQueue: false,
|
||||||
|
lazyConnect: false,
|
||||||
|
maxRetriesPerRequest: 1,
|
||||||
|
});
|
||||||
|
this.client.on('error', () => undefined);
|
||||||
|
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private withTimeout<T>(promise: Promise<T>): Promise<T> {
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<T>((_resolve, reject) => {
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error('Redis PING 超时')),
|
||||||
|
redisPingTimeoutMs,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeErrorMessage(error: unknown): string {
|
||||||
|
const message = error instanceof Error ? error.message : 'Redis 连接失败';
|
||||||
|
return redactSensitive({ message }).message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RedisClient } from './redis.client';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [RedisClient],
|
||||||
|
exports: [RedisClient],
|
||||||
|
})
|
||||||
|
export class RedisModule {}
|
||||||
Reference in New Issue
Block a user