Skip to content

Commit 7bebdc6

Browse files
authored
Feat/export as html table (#22)
* feat(generate): Extract Expr and For fields from prometheus alert rule * Add backticks for inline code * feat: Add option to export as HTML
1 parent 565abeb commit 7bebdc6

File tree

10 files changed

+477
-1
lines changed

10 files changed

+477
-1
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
promdoc*
1+
promdoc*
2+
!promdoc.css
3+
!promdoc.png

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ release:
1313
GOOS=darwin GOARCH=arm64 go build -o promdoc-darwin-arm64 -ldflags="-X 'github.com/plexsystems/promdoc/internal/commands.version=$(version)'"
1414
GOOS=windows GOARCH=amd64 go build -o promdoc-windows-amd64 -ldflags="-X 'github.com/plexsystems/promdoc/internal/commands.version=$(version)'"
1515
GOOS=linux GOARCH=amd64 go build -o promdoc-linux-amd64 -ldflags="-X 'github.com/plexsystems/promdoc/internal/commands.version=$(version)'"
16+
17+
.PHONY: minify-css
18+
minify-css:
19+
@cat promdoc.css | esbuild --loader=css --minify

examples/csv/expected.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ Name,RuleGroup,Summary,Description,Severity,Expr,For,Runbook
22
DescriptionAlert,Description,TestSummary,TestDescription,TestSeverity,up == 0,1w,TestRunbookURL
33
MessageAlert,Message,TestSummary,TestMessage,TestSeverity,api_http_request_latencies_second{quantile="0.5"} > 1,15m,TestRunbookURL
44
Alert1,MultiAlert1,TestSummary1,TestAlert1,TestSeverity1,job:request_latency_seconds:mean5m{job="myjob"} > 0.5,2d,TestRunbookURL1
5+
Alert1,MultiAlert2,TestSummary1,TestAlert1,TestSeverity1,job:request_latency_seconds:mean5m{job="myjob"} > 0.5,,TestRunbookURL1
56
Alert2,MultiAlert2,TestSummary2,TestAlert2,TestSeverity2,( predict_linear(prometheus_notifications_queue_length{job="prometheus"}[5m], 60 * 30) > min_over_time(prometheus_notifications_queue_capacity{job="prometheus"}[5m]) ),,TestRunbookURL2

examples/html/expected.html

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<html>
2+
<head>
3+
<title>Prometheus Alerts</title>
4+
<meta charset="utf-8" />
5+
<style>
6+
body{font-family:Microsoft Yahei,Helvetica,arial,sans-serif;font-size:14px;line-height:1.6;background-color:#fff;padding:30px;color:#516272;min-width:95%;margin:0 auto}body>*:first-child{margin-top:0!important}body>*:last-child{margin-bottom:0!important}a{color:#4183c4}a.absent{color:#c00}a.anchor{display:block;padding-left:30px;margin-left:-30px;cursor:pointer;position:absolute;top:0;left:0;bottom:0}h1,h2,h3,h4,h5,h6{margin:20px 0 10px;padding:0;font-weight:700;-webkit-font-smoothing:antialiased;cursor:text;position:relative}h1{font-size:28px;color:#2b3f52}h2{font-size:24px;border-bottom:1px solid #dde4e9;color:#2b3f52}h3{font-size:18px;color:#2b3f52}h4{font-size:16px;color:#2b3f52}h5{font-size:14px;color:#2b3f52}h6{color:#2b3f52;font-size:14px}p,blockquote,ul,ol,dl,li,table{margin:15px 0;color:#516272}body>h2:first-child{margin-top:0;padding-top:0}body>h1:first-child{margin-top:0;padding-top:0}body>h1:first-child+h2{margin-top:0;padding-top:0}body>h3:first-child,body>h4:first-child,body>h5:first-child,body>h6:first-child{margin-top:0;padding-top:0}a:first-child h1,a:first-child h2,a:first-child h3,a:first-child h4,a:first-child h5,a:first-child h6{margin-top:0;padding-top:0}h1 p,h2 p,h3 p,h4 p,h5 p,h6 p{margin-top:0}li p.first{display:inline-block}li{margin:0}ul,ol{padding-left:30px}ul :first-child,ol :first-child{margin-top:0}table{padding:0;border-collapse:collapse}table tr{border-top:1px solid #cccccc;background-color:#fff;margin:0;padding:0}table tr:nth-child(2n){background-color:#f8f8f8}table tr th{font-weight:700;border:1px solid #cccccc;margin:0;padding:6px 13px}table tr td{border:1px solid #cccccc;margin:0;padding:6px 13px}table tr th :first-child,table tr td :first-child{margin-top:0}table tr th :last-child,table tr td :last-child{margin-bottom:0}code{margin:0 2px;padding:0 5px;white-space:pre-wrap;word-break:break-all;display:block;border:1px solid #eaeaea;background-color:#f8f8f8;border-radius:3px}td code{margin:0;padding:0;white-space:pre}*{-webkit-print-color-adjust:exact}@media screen and (min-width: 914px){body{width:960px;margin:0 auto}}@media print{table,td{page-break-inside:avoid}td{word-wrap:break-word}}
7+
</style>
8+
</head>
9+
10+
<body>
11+
<nav>
12+
<ul>
13+
<li><a href="#Description">Description</a></li>
14+
<li><a href="#Message">Message</a></li>
15+
<li><a href="#MultiAlert1">MultiAlert1</a></li>
16+
<li><a href="#MultiAlert2">MultiAlert2</a></li>
17+
</ul>
18+
</nav>
19+
20+
<h2 id="Description">Description</h2>
21+
<table>
22+
<thead>
23+
<tr>
24+
<th>Name</th>
25+
<th>Summary</th>
26+
<th>Description</th>
27+
<th>Severity</th>
28+
<th>Expr</th>
29+
<th>For</th>
30+
<th>Runbook</th>
31+
</tr>
32+
</thead>
33+
<tbody>
34+
<tr>
35+
<td>DescriptionAlert</td>
36+
<td>TestSummary</td>
37+
<td>TestDescription</td>
38+
<td>TestSeverity</td>
39+
<td><code>up == 0</code></td>
40+
<td>1w</td>
41+
<td><a href="TestRunbookURL" target="blank">TestRunbookURL</td>
42+
</tr>
43+
</tbody>
44+
</table>
45+
<h2 id="Message">Message</h2>
46+
<table>
47+
<thead>
48+
<tr>
49+
<th>Name</th>
50+
<th>Summary</th>
51+
<th>Description</th>
52+
<th>Severity</th>
53+
<th>Expr</th>
54+
<th>For</th>
55+
<th>Runbook</th>
56+
</tr>
57+
</thead>
58+
<tbody>
59+
<tr>
60+
<td>MessageAlert</td>
61+
<td>TestSummary</td>
62+
<td>TestMessage</td>
63+
<td>TestSeverity</td>
64+
<td><code>api_http_request_latencies_second{quantile=&#34;0.5&#34;} &gt; 1</code></td>
65+
<td>15m</td>
66+
<td><a href="TestRunbookURL" target="blank">TestRunbookURL</td>
67+
</tr>
68+
</tbody>
69+
</table>
70+
<h2 id="MultiAlert1">MultiAlert1</h2>
71+
<table>
72+
<thead>
73+
<tr>
74+
<th>Name</th>
75+
<th>Summary</th>
76+
<th>Description</th>
77+
<th>Severity</th>
78+
<th>Expr</th>
79+
<th>For</th>
80+
<th>Runbook</th>
81+
</tr>
82+
</thead>
83+
<tbody>
84+
<tr>
85+
<td>Alert1</td>
86+
<td>TestSummary1</td>
87+
<td>TestAlert1</td>
88+
<td>TestSeverity1</td>
89+
<td><code>job:request_latency_seconds:mean5m{job=&#34;myjob&#34;} &gt; 0.5</code></td>
90+
<td>2d</td>
91+
<td><a href="TestRunbookURL1" target="blank">TestRunbookURL1</td>
92+
</tr>
93+
</tbody>
94+
</table>
95+
<h2 id="MultiAlert2">MultiAlert2</h2>
96+
<table>
97+
<thead>
98+
<tr>
99+
<th>Name</th>
100+
<th>Summary</th>
101+
<th>Description</th>
102+
<th>Severity</th>
103+
<th>Expr</th>
104+
<th>For</th>
105+
<th>Runbook</th>
106+
</tr>
107+
</thead>
108+
<tbody>
109+
<tr>
110+
<td>Alert1</td>
111+
<td>TestSummary1</td>
112+
<td>TestAlert1</td>
113+
<td>TestSeverity1</td>
114+
<td><code>job:request_latency_seconds:mean5m{job=&#34;myjob&#34;} &gt; 0.5</code></td>
115+
<td></td>
116+
<td><a href="TestRunbookURL1" target="blank">TestRunbookURL1</td>
117+
</tr>
118+
<tr>
119+
<td>Alert2</td>
120+
<td>TestSummary2</td>
121+
<td>TestAlert2</td>
122+
<td>TestSeverity2</td>
123+
<td><code>(
124+
predict_linear(prometheus_notifications_queue_length{job=&#34;prometheus&#34;}[5m], 60 * 30)
125+
&gt;
126+
min_over_time(prometheus_notifications_queue_capacity{job=&#34;prometheus&#34;}[5m])
127+
)
128+
</code></td>
129+
<td></td>
130+
<td><a href="TestRunbookURL2" target="blank">TestRunbookURL2</td>
131+
</tr>
132+
</tbody>
133+
</table>
134+
</body>
135+
</html>

examples/markdown/expected.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@
2929

3030
|Name|Summary|Description|Severity|Expr|For|Runbook|
3131
|---|---|---|---|---|---|---|
32+
|Alert1|TestSummary1|TestAlert1|TestSeverity1|`job:request_latency_seconds:mean5m{job="myjob"} > 0.5`||[TestRunbookURL1](TestRunbookURL1)|
3233
|Alert2|TestSummary2|TestAlert2|TestSeverity2|`( predict_linear(prometheus_notifications_queue_length{job="prometheus"}[5m], 60 * 30) > min_over_time(prometheus_notifications_queue_capacity{job="prometheus"}[5m]) )`||[TestRunbookURL2](TestRunbookURL2)|

examples/ruleWithMultipleDocuments.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ spec:
2525
groups:
2626
- name: MultiAlert2
2727
rules:
28+
- alert: Alert1
29+
expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5
30+
annotations:
31+
summary: TestSummary1
32+
description: TestAlert1
33+
runbook_url: TestRunbookURL1
34+
labels:
35+
severity: TestSeverity1
2836
- alert: Alert2
2937
expr: |
3038
(

generate/generate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ func Generate(path string, output string, input string) (string, error) {
1919
return Markdown(path, input)
2020
case ".csv":
2121
return CSV(path, input)
22+
case ".html":
23+
return HTML(path, input)
2224
default:
2325
return "", errors.New("output format not supported")
2426
}

generate/html.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package generate
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"html/template"
7+
"log"
8+
)
9+
10+
// HTML finds all rules at the given path and its
11+
// subdirectories and generates an HTML table
12+
// from the found rules.
13+
func HTML(path string, input string) (string, error) {
14+
ruleGroups, err := getRuleGroups(path, input)
15+
if err != nil {
16+
return "", fmt.Errorf("get rule groups: %w", err)
17+
}
18+
19+
const htmlTemplate = `<html>
20+
<head>
21+
<title>Prometheus Alerts</title>
22+
<meta charset="utf-8" />
23+
<style>
24+
body{font-family:Microsoft Yahei,Helvetica,arial,sans-serif;font-size:14px;line-height:1.6;background-color:#fff;padding:30px;color:#516272;min-width:95%;margin:0 auto}body>*:first-child{margin-top:0!important}body>*:last-child{margin-bottom:0!important}a{color:#4183c4}a.absent{color:#c00}a.anchor{display:block;padding-left:30px;margin-left:-30px;cursor:pointer;position:absolute;top:0;left:0;bottom:0}h1,h2,h3,h4,h5,h6{margin:20px 0 10px;padding:0;font-weight:700;-webkit-font-smoothing:antialiased;cursor:text;position:relative}h1{font-size:28px;color:#2b3f52}h2{font-size:24px;border-bottom:1px solid #dde4e9;color:#2b3f52}h3{font-size:18px;color:#2b3f52}h4{font-size:16px;color:#2b3f52}h5{font-size:14px;color:#2b3f52}h6{color:#2b3f52;font-size:14px}p,blockquote,ul,ol,dl,li,table{margin:15px 0;color:#516272}body>h2:first-child{margin-top:0;padding-top:0}body>h1:first-child{margin-top:0;padding-top:0}body>h1:first-child+h2{margin-top:0;padding-top:0}body>h3:first-child,body>h4:first-child,body>h5:first-child,body>h6:first-child{margin-top:0;padding-top:0}a:first-child h1,a:first-child h2,a:first-child h3,a:first-child h4,a:first-child h5,a:first-child h6{margin-top:0;padding-top:0}h1 p,h2 p,h3 p,h4 p,h5 p,h6 p{margin-top:0}li p.first{display:inline-block}li{margin:0}ul,ol{padding-left:30px}ul :first-child,ol :first-child{margin-top:0}table{padding:0;border-collapse:collapse}table tr{border-top:1px solid #cccccc;background-color:#fff;margin:0;padding:0}table tr:nth-child(2n){background-color:#f8f8f8}table tr th{font-weight:700;border:1px solid #cccccc;margin:0;padding:6px 13px}table tr td{border:1px solid #cccccc;margin:0;padding:6px 13px}table tr th :first-child,table tr td :first-child{margin-top:0}table tr th :last-child,table tr td :last-child{margin-bottom:0}code{margin:0 2px;padding:0 5px;white-space:pre-wrap;word-break:break-all;display:block;border:1px solid #eaeaea;background-color:#f8f8f8;border-radius:3px}td code{margin:0;padding:0;white-space:pre}*{-webkit-print-color-adjust:exact}@media screen and (min-width: 914px){body{width:960px;margin:0 auto}}@media print{table,td{page-break-inside:avoid}td{word-wrap:break-word}}
25+
</style>
26+
</head>
27+
28+
<body>
29+
<nav>
30+
<ul>
31+
{{- range . }}
32+
<li><a href="#{{.Name}}">{{.Name}}</a></li>
33+
{{- end }}
34+
</ul>
35+
</nav>
36+
{{range .}}
37+
<h2 id="{{.Name}}">{{.Name}}</h2>
38+
<table>
39+
<thead>
40+
<tr>
41+
<th>Name</th>
42+
<th>Summary</th>
43+
<th>Description</th>
44+
<th>Severity</th>
45+
<th>Expr</th>
46+
<th>For</th>
47+
<th>Runbook</th>
48+
</tr>
49+
</thead>
50+
<tbody>
51+
{{- range .Rules}}
52+
<tr>
53+
<td>{{.Alert}}</td>
54+
<td>{{.Annotations.summary}}</td>
55+
{{- if .Annotations.message}}
56+
<td>{{.Annotations.message}}</td>
57+
{{- else if .Annotations.description}}
58+
<td>{{.Annotations.description}}</td>
59+
{{- else}}
60+
<td></td>
61+
{{- end}}
62+
<td>{{.Labels.severity}}</td>
63+
<td><code>{{.Expr}}</code></td>
64+
<td>{{.For}}</td>
65+
<td><a href="{{.Annotations.runbook_url}}" target="blank">{{.Annotations.runbook_url}}</td>
66+
</tr>
67+
{{- end}}
68+
</tbody>
69+
</table>
70+
{{- end}}
71+
</body>
72+
</html>
73+
`
74+
75+
parsedTemplate, _ := template.New("template").Parse(htmlTemplate)
76+
var doc bytes.Buffer
77+
err = parsedTemplate.Execute(&doc, ruleGroups)
78+
if err != nil {
79+
log.Println("Error executing template :", err)
80+
return "", nil
81+
}
82+
83+
return doc.String(), nil
84+
}

generate/html_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package generate
2+
3+
import (
4+
"io/ioutil"
5+
"testing"
6+
)
7+
8+
func TestHTML(t *testing.T) {
9+
expectedBytes, err := ioutil.ReadFile("../examples/html/expected.html")
10+
if err != nil {
11+
t.Fatal("read:", err)
12+
}
13+
14+
actual, err := Generate("../examples", ".html", "kubernetes")
15+
if err != nil {
16+
t.Fatal("generate:", err)
17+
}
18+
19+
expected := string(expectedBytes)
20+
if expected != actual {
21+
t.Errorf("Unexpected HTML. expected %v, actual %v", expected, actual)
22+
}
23+
}

0 commit comments

Comments
 (0)