- Add prom-client 15; shared registry in src/metrics/registry.ts (7 metrics) - HTTP request counter + duration histogram via metricsMiddleware - DB query duration histogram wrapping pg Pool.query - Redis command duration histogram via typed instrumentRedisMethod wrapper - agentidp_tokens_issued_total in OAuth2Service - agentidp_agents_registered_total in AgentService - GET /metrics unauthenticated endpoint (Prometheus text format) - docker-compose.monitoring.yml overlay (Prometheus + Grafana) - Grafana auto-provisioned datasource + pre-built AgentIdP dashboard - docs/devops/operations.md monitoring section added - 36/36 unit tests passing, 100% coverage on new metrics code - Fix pre-existing unused import in tests/integration/agents.test.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
/**
|
|
* Unit tests for src/routes/metrics.ts
|
|
*
|
|
* Verifies that GET /metrics returns 200 with Prometheus exposition format
|
|
* and does NOT require authentication.
|
|
*/
|
|
|
|
import express, { Application } from 'express';
|
|
import request from 'supertest';
|
|
import { createMetricsRouter } from '../../../src/routes/metrics';
|
|
import { metricsRegistry } from '../../../src/metrics/registry';
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Helpers
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/** Build a minimal Express app that mounts only the metrics router. */
|
|
function buildTestApp(): Application {
|
|
const app = express();
|
|
app.use('/metrics', createMetricsRouter());
|
|
return app;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Tests
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('GET /metrics', () => {
|
|
let app: Application;
|
|
|
|
beforeEach(() => {
|
|
metricsRegistry.resetMetrics();
|
|
app = buildTestApp();
|
|
});
|
|
|
|
it('returns HTTP 200', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('returns Content-Type containing text/plain', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.headers['content-type']).toMatch(/text\/plain/);
|
|
});
|
|
|
|
it('does NOT require an Authorization header', async () => {
|
|
// Call without any auth header — must still succeed
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.status).toBe(200);
|
|
expect(res.status).not.toBe(401);
|
|
expect(res.status).not.toBe(403);
|
|
});
|
|
|
|
it('response body contains agentidp_tokens_issued_total', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.text).toContain('agentidp_tokens_issued_total');
|
|
});
|
|
|
|
it('response body contains agentidp_agents_registered_total', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.text).toContain('agentidp_agents_registered_total');
|
|
});
|
|
|
|
it('response body contains agentidp_http_requests_total', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.text).toContain('agentidp_http_requests_total');
|
|
});
|
|
|
|
it('response body contains agentidp_http_request_duration_seconds', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.text).toContain('agentidp_http_request_duration_seconds');
|
|
});
|
|
|
|
it('response body contains agentidp_db_query_duration_seconds', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.text).toContain('agentidp_db_query_duration_seconds');
|
|
});
|
|
|
|
it('response body contains agentidp_redis_command_duration_seconds', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
expect(res.text).toContain('agentidp_redis_command_duration_seconds');
|
|
});
|
|
|
|
it('response body is valid Prometheus text exposition format (starts with # HELP or TYPE)', async () => {
|
|
const res = await request(app).get('/metrics');
|
|
// Prometheus text format always begins with comment lines starting with '# '
|
|
expect(res.text).toMatch(/^# (HELP|TYPE)/m);
|
|
});
|
|
});
|