Files
sentryagent-idp/tests/unit/routes/metrics.test.ts
SentryAgent.ai Developer a504964e5f feat(phase-2): workstream 7 — Prometheus + Grafana Monitoring
- 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>
2026-03-29 06:13:41 +00:00

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);
});
});