Skip to content

Commit 61dcc24

Browse files
Merge pull request #17 from nextinterfaces/promql-html
Adds Prometheus Dashboard
2 parents 4881f96 + 4c070a8 commit 61dcc24

File tree

2 files changed

+237
-1
lines changed

2 files changed

+237
-1
lines changed

apps/items-service/src/html.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function getRootPageHtml(): string {
4848
<p><a href="https://app.roussev.com/grafana/d/items-service-metrics/items-service-metrics" target="_blank">Grafana</a> - Metrics dashboard</p>
4949
<p><a href="/jaeger" target="_blank">Jaeger</a> - Distributed tracing</p>
5050
<p><a href="/items/metrics" target="_blank">Metrics Endpoint</a> - Raw Prometheus metrics</p>
51+
<p><a href="/items/prometheus-queries" target="_blank">Prometheus Queries</a> - Preconfigured metric queries</p>
5152
<p><a href="/prometheus" target="_blank">Prometheus</a> - Metrics collection</p>
5253
5354
<p><strong>Documentation:</strong></p>
@@ -62,3 +63,231 @@ export function getRootPageHtml(): string {
6263
</html>`;
6364
}
6465

66+
export function getPrometheusQueriesHtml(): string {
67+
return `<!DOCTYPE html>
68+
<html lang="en">
69+
<head>
70+
<meta charset="UTF-8">
71+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
72+
<title>Items Service - Prometheus Metrics</title>
73+
<style>
74+
body {
75+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
76+
margin: 0;
77+
padding: 20px;
78+
background: #f5f5f5;
79+
}
80+
.container {
81+
max-width: 1400px;
82+
margin: 0 auto;
83+
}
84+
h1 {
85+
color: #333;
86+
margin-bottom: 10px;
87+
}
88+
.subtitle {
89+
color: #666;
90+
margin-bottom: 30px;
91+
}
92+
.metrics-grid {
93+
display: grid;
94+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
95+
gap: 20px;
96+
margin-bottom: 30px;
97+
}
98+
.metric-card {
99+
background: white;
100+
border-radius: 8px;
101+
padding: 20px;
102+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
103+
}
104+
.metric-card h3 {
105+
margin: 0 0 10px 0;
106+
color: #333;
107+
font-size: 16px;
108+
}
109+
.metric-card p {
110+
margin: 0 0 15px 0;
111+
color: #666;
112+
font-size: 14px;
113+
}
114+
.query-box {
115+
background: #f8f9fa;
116+
border: 1px solid #e1e4e8;
117+
border-radius: 4px;
118+
padding: 10px;
119+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
120+
font-size: 13px;
121+
margin-bottom: 10px;
122+
word-break: break-all;
123+
}
124+
.btn {
125+
display: inline-block;
126+
padding: 8px 16px;
127+
background: #e85d04;
128+
color: white;
129+
text-decoration: none;
130+
border-radius: 4px;
131+
font-size: 14px;
132+
transition: background 0.2s;
133+
}
134+
.btn:hover {
135+
background: #dc2f02;
136+
}
137+
.section {
138+
background: white;
139+
border-radius: 8px;
140+
padding: 20px;
141+
margin-bottom: 20px;
142+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
143+
}
144+
.section h2 {
145+
margin-top: 0;
146+
color: #333;
147+
border-bottom: 2px solid #e85d04;
148+
padding-bottom: 10px;
149+
}
150+
code {
151+
background: #f8f9fa;
152+
padding: 2px 6px;
153+
border-radius: 3px;
154+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
155+
font-size: 13px;
156+
}
157+
</style>
158+
</head>
159+
<body>
160+
<div class="container">
161+
<h1>Items Service - Prometheus Metrics Dashboard</h1>
162+
163+
<h2 style="margin-top: 30px; color: #333;">Basic Metrics</h2>
164+
<div class="metrics-grid">
165+
<div class="metric-card">
166+
<h3>Service Health</h3>
167+
<p>Check if items-service is up and being scraped</p>
168+
<div class="query-box">up{job="items-service"}</div>
169+
<a href="/prometheus/graph?g0.expr=up%7Bjob%3D%22items-service%22%7D&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
170+
class="btn" target="_blank">Open</a>
171+
</div>
172+
173+
<div class="metric-card">
174+
<h3>Total HTTP Requests</h3>
175+
<p>All HTTP requests received by the service</p>
176+
<div class="query-box">http_server_requests_total</div>
177+
<a href="/prometheus/graph?g0.expr=http_server_requests_total&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
178+
class="btn" target="_blank">Open</a>
179+
</div>
180+
181+
<div class="metric-card">
182+
<h3>Requests by Endpoint</h3>
183+
<p>HTTP requests grouped by route</p>
184+
<div class="query-box">http_server_requests_total{route="/v1/items"}</div>
185+
<a href="/prometheus/graph?g0.expr=http_server_requests_total%7Broute%3D%22%2Fv1%2Fitems%22%7D&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
186+
class="btn" target="_blank">Open</a>
187+
</div>
188+
</div>
189+
190+
<h2 style="margin-top: 30px; color: #333;">📈 Request Rate (RPS)</h2>
191+
<div class="metrics-grid">
192+
<div class="metric-card">
193+
<h3>Overall Request Rate</h3>
194+
<p>Requests per second over the last 5 minutes</p>
195+
<div class="query-box">rate(http_server_requests_total[5m])</div>
196+
<a href="/prometheus/graph?g0.expr=rate(http_server_requests_total%5B5m%5D)&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
197+
class="btn" target="_blank">Open</a>
198+
</div>
199+
200+
<div class="metric-card">
201+
<h3>Request Rate by Endpoint</h3>
202+
<p>RPS grouped by route and method</p>
203+
<div class="query-box">sum by(route, method) (rate(http_server_requests_total[5m]))</div>
204+
<a href="/prometheus/graph?g0.expr=sum%20by(route%2C%20method)%20(rate(http_server_requests_total%5B5m%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
205+
class="btn" target="_blank">Open</a>
206+
</div>
207+
208+
<div class="metric-card">
209+
<h3>Request Rate by Status Code</h3>
210+
<p>RPS grouped by HTTP status code</p>
211+
<div class="query-box">sum by(status_code) (rate(http_server_requests_total[5m]))</div>
212+
<a href="/prometheus/graph?g0.expr=sum%20by(status_code)%20(rate(http_server_requests_total%5B5m%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
213+
class="btn" target="_blank">Open</a>
214+
</div>
215+
</div>
216+
217+
<h2 style="margin-top: 30px; color: #333;">⏱️ Latency Metrics</h2>
218+
<div class="metrics-grid">
219+
<div class="metric-card">
220+
<h3>Average Response Time</h3>
221+
<p>Mean request duration in milliseconds</p>
222+
<div class="query-box">rate(http_server_duration_sum[5m]) / rate(http_server_duration_count[5m])</div>
223+
<a href="/prometheus/graph?g0.expr=rate(http_server_duration_sum%5B5m%5D)%20%2F%20rate(http_server_duration_count%5B5m%5D)&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
224+
class="btn" target="_blank">Open</a>
225+
</div>
226+
227+
<div class="metric-card">
228+
<h3>P95 Latency</h3>
229+
<p>95th percentile response time</p>
230+
<div class="query-box">histogram_quantile(0.95, rate(http_server_duration_bucket[5m]))</div>
231+
<a href="/prometheus/graph?g0.expr=histogram_quantile(0.95%2C%20rate(http_server_duration_bucket%5B5m%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
232+
class="btn" target="_blank">Open</a>
233+
</div>
234+
235+
<div class="metric-card">
236+
<h3>P99 Latency</h3>
237+
<p>99th percentile response time</p>
238+
<div class="query-box">histogram_quantile(0.99, rate(http_server_duration_bucket[5m]))</div>
239+
<a href="/prometheus/graph?g0.expr=histogram_quantile(0.99%2C%20rate(http_server_duration_bucket%5B5m%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
240+
class="btn" target="_blank">Open</a>
241+
</div>
242+
243+
<div class="metric-card">
244+
<h3>P50 Latency (Median)</h3>
245+
<p>50th percentile response time</p>
246+
<div class="query-box">histogram_quantile(0.50, rate(http_server_duration_bucket[5m]))</div>
247+
<a href="/prometheus/graph?g0.expr=histogram_quantile(0.50%2C%20rate(http_server_duration_bucket%5B5m%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
248+
class="btn" target="_blank">Open</a>
249+
</div>
250+
</div>
251+
252+
<h2 style="margin-top: 30px; color: #333;">Error Monitoring</h2>
253+
<div class="metrics-grid">
254+
<div class="metric-card">
255+
<h3>Error Rate (5xx)</h3>
256+
<p>Rate of server errors</p>
257+
<div class="query-box">rate(http_server_requests_total{status_code=~"5.."}[5m])</div>
258+
<a href="/prometheus/graph?g0.expr=rate(http_server_requests_total%7Bstatus_code%3D~%225..%22%7D%5B5m%5D)&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
259+
class="btn" target="_blank">Open</a>
260+
</div>
261+
262+
<div class="metric-card">
263+
<h3>Client Error Rate (4xx)</h3>
264+
<p>Rate of client errors</p>
265+
<div class="query-box">rate(http_server_requests_total{status_code=~"4.."}[5m])</div>
266+
<a href="/prometheus/graph?g0.expr=rate(http_server_requests_total%7Bstatus_code%3D~%224..%22%7D%5B5m%5D)&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
267+
class="btn" target="_blank">Open</a>
268+
</div>
269+
270+
<div class="metric-card">
271+
<h3>Error Percentage</h3>
272+
<p>Percentage of requests that failed</p>
273+
<div class="query-box">sum(rate(http_server_requests_total{status_code=~"5.."}[5m])) / sum(rate(http_server_requests_total[5m])) * 100</div>
274+
<a href="/prometheus/graph?g0.expr=sum(rate(http_server_requests_total%7Bstatus_code%3D~%225..%22%7D%5B5m%5D))%20%2F%20sum(rate(http_server_requests_total%5B5m%5D))%20*%20100&g0.tab=0&g0.stacked=0&g0.show_exemplars=0&g0.range_input=1h"
275+
class="btn" target="_blank">Open</a>
276+
</div>
277+
</div>
278+
279+
<div class="section" style="margin-top: 30px;">
280+
<h2>🧪 Generate Test Traffic</h2>
281+
<p>Before viewing metrics, generate some traffic:</p>
282+
<pre style="background: #f8f9fa; padding: 15px; border-radius: 4px; overflow-x: auto;"><code>for i in {1..50}; do
283+
curl -s https://app.roussev.com/items/v1/health > /dev/null
284+
curl -s https://app.roussev.com/items/v1/items > /dev/null
285+
sleep 0.5
286+
done</code></pre>
287+
</div>
288+
289+
</div>
290+
</body>
291+
</html>`;
292+
}
293+

apps/items-service/src/router.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { trace, SpanStatusCode } from "@opentelemetry/api";
77
import { HealthController, ItemsController } from "./controllers.js";
8-
import { getSwaggerHtml, getRootPageHtml } from "./html.js";
8+
import { getSwaggerHtml, getRootPageHtml, getPrometheusQueriesHtml } from "./html.js";
99
import { openapi } from "./openapi.js";
1010
import { json, notFound, html } from "./http-utils.js";
1111
import type { ServerConfig } from "./config.js";
@@ -46,6 +46,13 @@ export class Router {
4646
handler: () => html(getSwaggerHtml(this.config.appPrefix)),
4747
});
4848

49+
// Prometheus queries dashboard
50+
this.routes.push({
51+
method: "GET",
52+
path: "/prometheus-queries",
53+
handler: () => html(getPrometheusQueriesHtml()),
54+
});
55+
4956
// OpenAPI spec
5057
this.routes.push({
5158
method: "GET",

0 commit comments

Comments
 (0)