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
38
backend/package-lock.json
generated
38
backend/package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
|||
"express-rate-limit": "^8.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"nats": "^2.28.2",
|
||||
"prom-client": "^15.1.3",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -44,6 +45,15 @@
|
|||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
||||
"integrity": "sha512-5gSP1liv10Gjp8cMEnFd6shzkL/D6W1uhXSFNCxDC+YI8+L8wkCYCbJ7n77Ezb4wE/xzMogecE+DtamEe9PZjg=="
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
|
|
@ -133,6 +143,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
|
|
@ -1191,6 +1207,19 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/prom-client": {
|
||||
"version": "15.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
|
||||
"integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"tdigest": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
@ -1560,6 +1589,15 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"express-rate-limit": "^8.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"nats": "^2.28.2",
|
||||
"prom-client": "^15.1.3",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
110
backend/src/lib/metrics.js
Normal file
110
backend/src/lib/metrics.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import client from 'prom-client';
|
||||
|
||||
export const register = client.register;
|
||||
|
||||
// Collect default Node.js metrics (CPU, memory, event loop)
|
||||
client.collectDefaultMetrics();
|
||||
|
||||
// --- Gauges ---
|
||||
|
||||
export const websocketConnections = new client.Gauge({
|
||||
name: 'pokerface_websocket_connections',
|
||||
help: 'Current active Socket.IO connections'
|
||||
});
|
||||
|
||||
export const roomsActive = new client.Gauge({
|
||||
name: 'pokerface_rooms_active',
|
||||
help: 'Current active rooms',
|
||||
labelNames: ['tenant']
|
||||
});
|
||||
|
||||
export const uniqueUsers = new client.Gauge({
|
||||
name: 'pokerface_unique_users',
|
||||
help: 'Unique jiraAccountIds seen (resets hourly)'
|
||||
});
|
||||
|
||||
export const uniqueTenants = new client.Gauge({
|
||||
name: 'pokerface_unique_tenants',
|
||||
help: 'Unique jiraCloudIds seen (resets hourly)'
|
||||
});
|
||||
|
||||
// --- Counters ---
|
||||
|
||||
export const oauthLoginsTotal = new client.Counter({
|
||||
name: 'pokerface_oauth_logins_total',
|
||||
help: 'OAuth login completions',
|
||||
labelNames: ['status']
|
||||
});
|
||||
|
||||
export const sessionsCreatedTotal = new client.Counter({
|
||||
name: 'pokerface_sessions_created_total',
|
||||
help: 'Poker sessions started'
|
||||
});
|
||||
|
||||
export const sessionsSavedTotal = new client.Counter({
|
||||
name: 'pokerface_sessions_saved_total',
|
||||
help: 'Sessions completed with saved estimate'
|
||||
});
|
||||
|
||||
export const votesTotal = new client.Counter({
|
||||
name: 'pokerface_votes_total',
|
||||
help: 'Votes cast'
|
||||
});
|
||||
|
||||
export const httpRequestsTotal = new client.Counter({
|
||||
name: 'pokerface_http_requests_total',
|
||||
help: 'API HTTP requests',
|
||||
labelNames: ['method', 'route', 'status']
|
||||
});
|
||||
|
||||
export const jiraRequestsTotal = new client.Counter({
|
||||
name: 'pokerface_jira_requests_total',
|
||||
help: 'Jira API calls',
|
||||
labelNames: ['endpoint']
|
||||
});
|
||||
|
||||
export const jiraErrorsTotal = new client.Counter({
|
||||
name: 'pokerface_jira_errors_total',
|
||||
help: 'Failed Jira API calls'
|
||||
});
|
||||
|
||||
// --- Histograms ---
|
||||
|
||||
export const httpRequestDuration = new client.Histogram({
|
||||
name: 'pokerface_http_request_duration_seconds',
|
||||
help: 'API request latency in seconds',
|
||||
labelNames: ['method', 'route'],
|
||||
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
|
||||
});
|
||||
|
||||
export const jiraRequestDuration = new client.Histogram({
|
||||
name: 'pokerface_jira_request_duration_seconds',
|
||||
help: 'Jira API request latency in seconds',
|
||||
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
||||
});
|
||||
|
||||
// --- Unique tracking sets (reset hourly) ---
|
||||
|
||||
const uniqueUserIds = new Set();
|
||||
const uniqueTenantIds = new Set();
|
||||
|
||||
export function trackUniqueUser(jiraAccountId) {
|
||||
if (jiraAccountId) {
|
||||
uniqueUserIds.add(jiraAccountId);
|
||||
uniqueUsers.set(uniqueUserIds.size);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackUniqueTenant(jiraCloudId) {
|
||||
if (jiraCloudId) {
|
||||
uniqueTenantIds.add(jiraCloudId);
|
||||
uniqueTenants.set(uniqueTenantIds.size);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
uniqueUserIds.clear();
|
||||
uniqueTenantIds.clear();
|
||||
uniqueUsers.set(0);
|
||||
uniqueTenants.set(0);
|
||||
}, 3600_000);
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '../middleware/auth.js';
|
||||
import { kvOAuthState } from '../lib/nats.js';
|
||||
import { safeError } from '../lib/errors.js';
|
||||
import { oauthLoginsTotal } from '../lib/metrics.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -63,9 +64,11 @@ router.get('/oauth/callback', async (req, res) => {
|
|||
avatarUrl: profile.avatarUrl
|
||||
});
|
||||
setSessionCookie(res, jwt);
|
||||
oauthLoginsTotal.inc({ status: 'success' });
|
||||
|
||||
res.redirect(`${frontendUrl}?auth=success`);
|
||||
} catch (error) {
|
||||
oauthLoginsTotal.inc({ status: 'failure' });
|
||||
console.error('[oauth] Callback failed:', error.message);
|
||||
const safeMessage = process.env.NODE_ENV === 'production' ? 'Jira authentication failed.' : error.message;
|
||||
res.redirect(`${frontendUrl}?auth=error&message=${encodeURIComponent(safeMessage)}`);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { kvOAuth } from '../lib/nats.js';
|
||||
import { jiraRequestsTotal, jiraErrorsTotal, jiraRequestDuration } from '../lib/metrics.js';
|
||||
|
||||
const ATLASSIAN_AUTH_URL = 'https://auth.atlassian.com/authorize';
|
||||
const ATLASSIAN_TOKEN_URL = 'https://auth.atlassian.com/oauth/token';
|
||||
|
|
@ -172,19 +173,33 @@ async function getValidConnection(jiraAccountId) {
|
|||
}
|
||||
|
||||
async function jiraFetch(jiraAccountId, path, options = {}) {
|
||||
const endpoint = path.split('?')[0];
|
||||
jiraRequestsTotal.inc({ endpoint });
|
||||
const end = jiraRequestDuration.startTimer();
|
||||
|
||||
const connection = await getValidConnection(jiraAccountId);
|
||||
const url = `https://api.atlassian.com/ex/jira/${connection.cloudId}${path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
end();
|
||||
jiraErrorsTotal.inc();
|
||||
throw err;
|
||||
}
|
||||
|
||||
end();
|
||||
|
||||
if (!response.ok) {
|
||||
jiraErrorsTotal.inc();
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Jira API failed: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import crypto from 'crypto';
|
||||
import { kvSessions, kvSessionIndex, withCasRetry } from '../lib/nats.js';
|
||||
import { sessionsCreatedTotal } from '../lib/metrics.js';
|
||||
|
||||
const FIBONACCI = [0, 0.5, 1, 2, 3, 5, 8, 13, 20, 40, 100];
|
||||
|
||||
|
|
@ -134,6 +135,7 @@ export async function createScopedSession({ issueKey, issueId, issueTitle, roomI
|
|||
};
|
||||
await putSession(session);
|
||||
await kvSessionIndex.put(issueIndexKey(tenantCloudId, issueKey), id);
|
||||
sessionsCreatedTotal.inc();
|
||||
return getSnapshot(session);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import crypto from 'crypto';
|
||||
import { kvRooms, kvSprintIndex, kvActiveRooms, withCasRetry } from '../lib/nats.js';
|
||||
import { roomsActive } from '../lib/metrics.js';
|
||||
|
||||
function assertCloudId(cloudId) {
|
||||
if (!cloudId) {
|
||||
|
|
@ -52,6 +53,7 @@ export async function createRoom(payload) {
|
|||
|
||||
await kvRooms.put(roomKey(cloudId, id), JSON.stringify(room));
|
||||
await kvSprintIndex.put(sprintKey(cloudId, sprintId), id);
|
||||
roomsActive.inc({ tenant: cloudId });
|
||||
|
||||
// Update active rooms set with CAS retry
|
||||
await withCasRetry(kvActiveRooms, cloudId, (activeIds) => {
|
||||
|
|
@ -113,6 +115,7 @@ export async function deleteRoom(roomId, cloudId) {
|
|||
const room = entry.json();
|
||||
await kvRooms.delete(roomKey(cloudId, roomId));
|
||||
await kvSprintIndex.delete(sprintKey(cloudId, room.sprintId));
|
||||
roomsActive.dec({ tenant: cloudId });
|
||||
|
||||
// Remove from active rooms set with CAS retry
|
||||
await withCasRetry(kvActiveRooms, cloudId, (activeIds) => {
|
||||
|
|
|
|||
223
grafana/pokerface-dashboard.json
Normal file
223
grafana/pokerface-dashboard.json
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
{
|
||||
"annotations": { "list": [] },
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
|
||||
"id": 100,
|
||||
"title": "Overview",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": { "steps": [{ "color": "green", "value": null }] },
|
||||
"color": { "mode": "thresholds" }
|
||||
}
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
|
||||
"id": 1,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } },
|
||||
"title": "Unique Users",
|
||||
"type": "stat",
|
||||
"targets": [{ "expr": "pokerface_unique_users", "legendFormat": "", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": { "steps": [{ "color": "green", "value": null }] },
|
||||
"color": { "mode": "thresholds" }
|
||||
}
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
|
||||
"id": 2,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } },
|
||||
"title": "Unique Tenants",
|
||||
"type": "stat",
|
||||
"targets": [{ "expr": "pokerface_unique_tenants", "legendFormat": "", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": { "steps": [{ "color": "blue", "value": null }] },
|
||||
"color": { "mode": "thresholds" }
|
||||
}
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
|
||||
"id": 3,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } },
|
||||
"title": "Active Connections",
|
||||
"type": "stat",
|
||||
"targets": [{ "expr": "pokerface_websocket_connections", "legendFormat": "", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": { "steps": [{ "color": "purple", "value": null }] },
|
||||
"color": { "mode": "thresholds" }
|
||||
}
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
|
||||
"id": 4,
|
||||
"options": { "colorMode": "value", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } },
|
||||
"title": "Active Rooms",
|
||||
"type": "stat",
|
||||
"targets": [{ "expr": "sum(pokerface_rooms_active)", "legendFormat": "", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
|
||||
"id": 101,
|
||||
"title": "Activity",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 6 },
|
||||
"id": 5,
|
||||
"options": { "tooltip": { "mode": "single" } },
|
||||
"title": "Sessions Created / hr",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "rate(pokerface_sessions_created_total[1h]) * 3600", "legendFormat": "sessions/hr", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 6 },
|
||||
"id": 6,
|
||||
"options": { "tooltip": { "mode": "single" } },
|
||||
"title": "Votes / hr",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "rate(pokerface_votes_total[1h]) * 3600", "legendFormat": "votes/hr", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 6 },
|
||||
"id": 7,
|
||||
"options": { "tooltip": { "mode": "multi" } },
|
||||
"title": "OAuth Logins / hr",
|
||||
"type": "timeseries",
|
||||
"targets": [
|
||||
{ "expr": "rate(pokerface_oauth_logins_total{status=\"success\"}[1h]) * 3600", "legendFormat": "success", "refId": "A" },
|
||||
{ "expr": "rate(pokerface_oauth_logins_total{status=\"failure\"}[1h]) * 3600", "legendFormat": "failure", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
|
||||
"id": 102,
|
||||
"title": "Performance",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 15 },
|
||||
"id": 8,
|
||||
"options": { "tooltip": { "mode": "multi" } },
|
||||
"title": "HTTP Request Rate by Route",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "sum by (route) (rate(pokerface_http_requests_total[5m]))", "legendFormat": "{{route}}", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "unit": "s", "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 6, "x": 6, "y": 15 },
|
||||
"id": 9,
|
||||
"options": { "tooltip": { "mode": "multi" } },
|
||||
"title": "HTTP Latency p95",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "histogram_quantile(0.95, sum by (le, route) (rate(pokerface_http_request_duration_seconds_bucket[5m])))", "legendFormat": "{{route}}", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "unit": "s", "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 6, "x": 12, "y": 15 },
|
||||
"id": 10,
|
||||
"options": { "tooltip": { "mode": "single" } },
|
||||
"title": "Jira API Latency p95",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "histogram_quantile(0.95, sum by (le) (rate(pokerface_jira_request_duration_seconds_bucket[5m])))", "legendFormat": "p95", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 6, "x": 18, "y": 15 },
|
||||
"id": 11,
|
||||
"options": { "tooltip": { "mode": "single" } },
|
||||
"title": "Jira Error Rate",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "rate(pokerface_jira_errors_total[5m])", "legendFormat": "errors/s", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
|
||||
"id": 103,
|
||||
"title": "System",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "unit": "bytes", "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 24 },
|
||||
"id": 12,
|
||||
"options": { "tooltip": { "mode": "multi" } },
|
||||
"title": "Node.js Memory Usage",
|
||||
"type": "timeseries",
|
||||
"targets": [
|
||||
{ "expr": "process_resident_memory_bytes", "legendFormat": "RSS", "refId": "A" },
|
||||
{ "expr": "nodejs_heap_size_used_bytes", "legendFormat": "Heap Used", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "unit": "s", "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 24 },
|
||||
"id": 13,
|
||||
"options": { "tooltip": { "mode": "single" } },
|
||||
"title": "Event Loop Lag",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "nodejs_eventloop_lag_seconds", "legendFormat": "lag", "refId": "A" }]
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"fieldConfig": { "defaults": { "unit": "percentunit", "custom": { "drawStyle": "line", "fillOpacity": 10 } } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 24 },
|
||||
"id": 14,
|
||||
"options": { "tooltip": { "mode": "single" } },
|
||||
"title": "CPU Usage",
|
||||
"type": "timeseries",
|
||||
"targets": [{ "expr": "rate(process_cpu_seconds_total[5m])", "legendFormat": "CPU", "refId": "A" }]
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"tags": ["pokerface"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {},
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"name": "datasource",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"type": "datasource"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": { "from": "now-3h", "to": "now" },
|
||||
"title": "Pokerface",
|
||||
"uid": "pokerface-overview"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue