Add Prometheus metrics and Grafana dashboard
All checks were successful
Build & Push Container Image / build (push) Successful in 9s
All checks were successful
Build & Push Container Image / build (push) Successful in 9s
Instrument backend with prom-client: HTTP request count/latency, WebSocket connections, Jira API health, session/vote/room counters, and unique user/tenant tracking. Expose unauthenticated /metrics endpoint. Include pre-built Grafana dashboard JSON. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
99cdd5b102
commit
c31161af19
9 changed files with 437 additions and 8 deletions
|
|
@ -26,6 +26,16 @@ import {
|
|||
import { updateIssueEstimate } from './services/jiraService.js';
|
||||
import { getSocketUser } from './middleware/auth.js';
|
||||
import { safeError } from './lib/errors.js';
|
||||
import {
|
||||
register,
|
||||
websocketConnections,
|
||||
votesTotal,
|
||||
sessionsSavedTotal,
|
||||
httpRequestsTotal,
|
||||
httpRequestDuration,
|
||||
trackUniqueUser,
|
||||
trackUniqueTenant
|
||||
} from './lib/metrics.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
|
@ -77,10 +87,28 @@ app.use(cookieParser());
|
|||
app.use(express.json({ limit: '1mb' }));
|
||||
app.use('/api/', rateLimit({ windowMs: 60_000, max: 100, standardHeaders: true, legacyHeaders: false }));
|
||||
|
||||
// Prometheus metrics endpoint (no auth, before rate limiter)
|
||||
app.get('/metrics', async (_req, res) => {
|
||||
res.set('Content-Type', register.contentType);
|
||||
res.end(await register.metrics());
|
||||
});
|
||||
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// HTTP metrics middleware
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === '/metrics' || req.path === '/api/health') return next();
|
||||
const end = httpRequestDuration.startTimer();
|
||||
res.on('finish', () => {
|
||||
const route = req.route?.path || req.path;
|
||||
httpRequestsTotal.inc({ method: req.method, route, status: res.statusCode });
|
||||
end({ method: req.method, route });
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/api/poker', pokerRoutes);
|
||||
app.use('/api/jira', jiraRoutes);
|
||||
app.use('/api/poker/rooms', roomRoutes);
|
||||
|
|
@ -163,6 +191,10 @@ io.on('connection', (socket) => {
|
|||
return;
|
||||
}
|
||||
socket.user = user;
|
||||
websocketConnections.inc();
|
||||
trackUniqueUser(user.jiraAccountId);
|
||||
trackUniqueTenant(user.jiraCloudId);
|
||||
socket.on('disconnect', () => { websocketConnections.dec(); });
|
||||
const throttled = socketThrottle(socket);
|
||||
|
||||
throttled('poker:join', async ({ sessionId }) => {
|
||||
|
|
@ -222,6 +254,7 @@ io.on('connection', (socket) => {
|
|||
socket.emit('poker:error', { error: 'Unable to submit vote for this session.' });
|
||||
return;
|
||||
}
|
||||
votesTotal.inc();
|
||||
const reveal = await revealIfComplete(sessionId, socket.user.jiraCloudId);
|
||||
|
||||
await emitSessionState(sessionId, socket.user.jiraCloudId);
|
||||
|
|
@ -272,6 +305,7 @@ io.on('connection', (socket) => {
|
|||
socket.emit('poker:error', { error: 'Unable to save estimate for this session.' });
|
||||
return;
|
||||
}
|
||||
sessionsSavedTotal.inc();
|
||||
|
||||
const issueRef = saved.session.issueId || saved.session.issueKey;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue