fix(docker): add dedicated healthz endpoint
This commit is contained in:
1 parent
14bb18974c
commit
c08c329b5d
6 files changed
+187
-2
No files matched your search
@@ -14,7 +14,7 @@ services:
|
||||
env_file:
|
||||
- ../.env.local # 读取仓库根目录的本地环境变量文件
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "curl -f http://localhost:${NGINX_PORT:-80}/ && curl -f http://localhost:${NGINX_PORT:-80}/mcp"]
|
||||
test: ["CMD", "sh", "-c", "curl -fsS http://localhost:${NGINX_PORT:-80}/healthz >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
ports:
|
||||
- "28081:${NGINX_PORT:-80}" # Web应用端口(包含MCP服务器,通过/mcp路径访问)
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "curl -f http://localhost:${NGINX_PORT:-80}/ && curl -f http://localhost:${NGINX_PORT:-80}/mcp"]
|
||||
test: ["CMD", "sh", "-c", "curl -fsS http://localhost:${NGINX_PORT:-80}/healthz >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -60,6 +60,17 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
auth_basic off;
|
||||
|
||||
proxy_pass http://localhost:3000/healthz;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Docker环境状态检测(已移除,不再需要代理功能)
|
||||
# location = /api/docker-status {
|
||||
# add_header Content-Type 'application/json';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Express } from 'express';
|
||||
|
||||
import type { CoreServicesManager } from './adapters/core-services.js';
|
||||
|
||||
export type MCPHealthStatus = Awaited<ReturnType<CoreServicesManager['getHealthStatus']>>;
|
||||
type HealthStatusProvider = Pick<CoreServicesManager, 'getHealthStatus'>;
|
||||
|
||||
function isHealthyStatus(healthStatus: MCPHealthStatus): boolean {
|
||||
return healthStatus.initialized && Object.values(healthStatus.services).every(Boolean);
|
||||
}
|
||||
|
||||
export function buildHealthzResponse(healthStatus: MCPHealthStatus): {
|
||||
statusCode: number;
|
||||
body: MCPHealthStatus;
|
||||
} {
|
||||
return {
|
||||
statusCode: isHealthyStatus(healthStatus) ? 200 : 503,
|
||||
body: healthStatus
|
||||
};
|
||||
}
|
||||
|
||||
export function registerHealthzRoute(app: Express, healthProvider: HealthStatusProvider): void {
|
||||
app.get('/healthz', async (_req, res) => {
|
||||
try {
|
||||
const healthStatus = await healthProvider.getHealthStatus();
|
||||
const { statusCode, body } = buildHealthzResponse(healthStatus);
|
||||
res.status(statusCode).json(body);
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
initialized: false,
|
||||
services: {},
|
||||
error: error instanceof Error ? error.message : 'Health check failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import { loadConfig, type MCPServerConfig } from './config/environment.js';
|
||||
import * as logger from './utils/logging.js';
|
||||
import { ParameterValidator } from './adapters/parameter-adapter.js';
|
||||
import { getTemplateOptions, getDefaultTemplateId } from './config/templates.js';
|
||||
import { registerHealthzRoute } from './health.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import express from 'express';
|
||||
|
||||
@@ -377,6 +378,7 @@ async function main() {
|
||||
// 使用 Express 和会话管理支持多客户端连接
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
registerHealthzRoute(app, coreServices);
|
||||
logger.info('Express app configured');
|
||||
|
||||
// 存储每个会话的传输实例
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Server } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
|
||||
import express from 'express';
|
||||
|
||||
import { registerHealthzRoute } from '../src/health.js';
|
||||
|
||||
type TestHealthProvider = {
|
||||
getHealthStatus: () => Promise<{
|
||||
initialized: boolean;
|
||||
services: Record<string, boolean>;
|
||||
}>;
|
||||
};
|
||||
|
||||
async function startHealthServer(provider: TestHealthProvider): Promise<{
|
||||
server: Server;
|
||||
url: string;
|
||||
}> {
|
||||
const app = express();
|
||||
registerHealthzRoute(app, provider);
|
||||
|
||||
const server = await new Promise<Server>((resolve) => {
|
||||
const httpServer = app.listen(0, '127.0.0.1', () => resolve(httpServer));
|
||||
});
|
||||
|
||||
const { port } = server.address() as AddressInfo;
|
||||
return {
|
||||
server,
|
||||
url: `http://127.0.0.1:${port}/healthz`
|
||||
};
|
||||
}
|
||||
|
||||
async function stopServer(server: Server | undefined): Promise<void> {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('MCP health route', () => {
|
||||
let server: Server | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
await stopServer(server);
|
||||
server = undefined;
|
||||
});
|
||||
|
||||
it('returns 200 when core services are initialized and healthy', async () => {
|
||||
const provider: TestHealthProvider = {
|
||||
getHealthStatus: vi.fn().mockResolvedValue({
|
||||
initialized: true,
|
||||
services: {
|
||||
modelManager: true,
|
||||
llmService: true,
|
||||
languageService: true,
|
||||
templateManager: true,
|
||||
historyManager: true,
|
||||
promptService: true
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const running = await startHealthServer(provider);
|
||||
server = running.server;
|
||||
|
||||
const response = await fetch(running.url);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
initialized: true,
|
||||
services: {
|
||||
modelManager: true,
|
||||
llmService: true,
|
||||
languageService: true,
|
||||
templateManager: true,
|
||||
historyManager: true,
|
||||
promptService: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 503 when any required service is unhealthy', async () => {
|
||||
const provider: TestHealthProvider = {
|
||||
getHealthStatus: vi.fn().mockResolvedValue({
|
||||
initialized: true,
|
||||
services: {
|
||||
modelManager: true,
|
||||
llmService: false
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const running = await startHealthServer(provider);
|
||||
server = running.server;
|
||||
|
||||
const response = await fetch(running.url);
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
expect(await response.json()).toEqual({
|
||||
initialized: true,
|
||||
services: {
|
||||
modelManager: true,
|
||||
llmService: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 503 when the health provider throws', async () => {
|
||||
const provider: TestHealthProvider = {
|
||||
getHealthStatus: vi.fn().mockRejectedValue(new Error('boom'))
|
||||
};
|
||||
|
||||
const running = await startHealthServer(provider);
|
||||
server = running.server;
|
||||
|
||||
const response = await fetch(running.url);
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
expect(await response.json()).toEqual({
|
||||
initialized: false,
|
||||
services: {},
|
||||
error: 'boom'
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user