Skip to content

Commit 99b5b45

Browse files
tommyers-elasticalaudazzimrodm
authored
add 'alertRuleTemplates' function to readme generation (#3076)
* add 'alertRuleTemplate' function to readme generation * gracefully handle packages with no templates * remove 'techPreview' arg to alertRuleTemplates function * go with the os.Stat approach to checking if the template dir exists - it makes it less likely you render the preamble by mistake * remove manual formatting of the rule names * don't use headings for template names at all; just use '<strong>' formatting. * [Accessibility] Replace visual clues with more accessible terms. Co-authored-by: Arianna Laudazzi <[email protected]> * Split the logic to retrieve the template list from the logic to render it. * use linksMap to avoid hard coding the docs link * add unit tests * add sample package for functional tests * remove description field from sample package - it's not supported in kibana yet * fix merge * Update test/packages/other/alert_rule_templates/manifest.yml Co-authored-by: Mario Rodriguez Molins <[email protected]> * Update internal/docs/readme.go Co-authored-by: Mario Rodriguez Molins <[email protected]> --------- Co-authored-by: Arianna Laudazzi <[email protected]> Co-authored-by: Mario Rodriguez Molins <[email protected]>
1 parent 0bd3919 commit 99b5b45

File tree

10 files changed

+383
-0
lines changed

10 files changed

+383
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package docs
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"io/fs"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
)
15+
16+
type alertRuleTemplate struct {
17+
Attributes struct {
18+
Name string
19+
Description string
20+
}
21+
}
22+
23+
func renderAlertRuleTemplates(packageRoot string, linksMap linkMap) (string, error) {
24+
templatesDir := filepath.Join(packageRoot, "kibana", "alerting_rule_template")
25+
26+
if _, err := os.Stat(templatesDir); os.IsNotExist(err) {
27+
// no template directory in the package, do nothing
28+
return "", nil
29+
}
30+
31+
var templates []alertRuleTemplate
32+
33+
err := filepath.WalkDir(templatesDir, func(path string, d fs.DirEntry, err error) error {
34+
if err != nil {
35+
return err
36+
}
37+
38+
if path == templatesDir {
39+
return nil
40+
}
41+
42+
if d.IsDir() {
43+
return filepath.SkipDir
44+
}
45+
46+
if filepath.Ext(d.Name()) != ".json" {
47+
return nil
48+
}
49+
50+
rawTemplate, err := os.ReadFile(path)
51+
if err != nil {
52+
return fmt.Errorf("failed to read alert rule template file: %w", err)
53+
}
54+
55+
var template alertRuleTemplate
56+
if err := json.Unmarshal(rawTemplate, &template); err != nil {
57+
return fmt.Errorf("failed to unmarshal alert rule template JSON: %w", err)
58+
}
59+
60+
templates = append(templates, template)
61+
return nil
62+
})
63+
64+
if err != nil {
65+
return "", fmt.Errorf("parsing alert rule templates failed: %w", err)
66+
}
67+
68+
var builder strings.Builder
69+
70+
if len(templates) != 0 {
71+
docsLink, err := linksMap.RenderLink("alert-rule-templates", linkOptions{})
72+
if err != nil {
73+
docsLink = "https://www.elastic.co/docs"
74+
}
75+
76+
builder.WriteString(`Alert rule templates provide pre-defined configurations for creating alert rules in Kibana.
77+
78+
For more information, refer to the [Elastic documentation](` + docsLink + `).
79+
80+
Alert rule templates require Elastic Stack version 9.2.0 or later.
81+
82+
`)
83+
84+
builder.WriteString("The following alert rule templates are available:\n\n")
85+
86+
for _, template := range templates {
87+
builder.WriteString(fmt.Sprintf("**%s**\n\n", template.Attributes.Name))
88+
builder.WriteString(fmt.Sprintf("%s\n\n", template.Attributes.Description))
89+
}
90+
}
91+
92+
return builder.String(), nil
93+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package docs
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestRenderAlertRuleTemplates(t *testing.T) {
18+
cases := []struct {
19+
name string
20+
setupFunc func(t *testing.T) string
21+
expectError bool
22+
expectEmpty bool
23+
validateFunc func(t *testing.T, result string)
24+
}{
25+
{
26+
name: "no templates directory",
27+
setupFunc: func(t *testing.T) string {
28+
tmpDir := t.TempDir()
29+
return tmpDir
30+
},
31+
expectError: false,
32+
expectEmpty: true,
33+
},
34+
{
35+
name: "empty templates directory",
36+
setupFunc: func(t *testing.T) string {
37+
tmpDir := t.TempDir()
38+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
39+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
40+
return tmpDir
41+
},
42+
expectError: false,
43+
expectEmpty: true,
44+
},
45+
{
46+
name: "single valid template",
47+
setupFunc: func(t *testing.T) string {
48+
tmpDir := t.TempDir()
49+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
50+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
51+
52+
template := `{
53+
"attributes": {
54+
"name": "Test Alert Rule",
55+
"description": "This is a test alert rule description"
56+
}
57+
}`
58+
templateFile := filepath.Join(templatesDir, "test_rule.json")
59+
require.NoError(t, os.WriteFile(templateFile, []byte(template), 0o644))
60+
return tmpDir
61+
},
62+
expectError: false,
63+
expectEmpty: false,
64+
validateFunc: func(t *testing.T, result string) {
65+
assert.Contains(t, result, "Alert rule templates provide pre-defined configurations")
66+
assert.Contains(t, result, "**Test Alert Rule**")
67+
assert.Contains(t, result, "This is a test alert rule description")
68+
},
69+
},
70+
{
71+
name: "multiple valid templates",
72+
setupFunc: func(t *testing.T) string {
73+
tmpDir := t.TempDir()
74+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
75+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
76+
77+
template1 := `{
78+
"attributes": {
79+
"name": "First Rule",
80+
"description": "First description"
81+
}
82+
}`
83+
template2 := `{
84+
"attributes": {
85+
"name": "Second Rule",
86+
"description": "Second description"
87+
}
88+
}`
89+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "rule1.json"), []byte(template1), 0o644))
90+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "rule2.json"), []byte(template2), 0o644))
91+
return tmpDir
92+
},
93+
expectError: false,
94+
expectEmpty: false,
95+
validateFunc: func(t *testing.T, result string) {
96+
assert.Contains(t, result, "**First Rule**")
97+
assert.Contains(t, result, "First description")
98+
assert.Contains(t, result, "**Second Rule**")
99+
assert.Contains(t, result, "Second description")
100+
},
101+
},
102+
{
103+
name: "skip non-json files",
104+
setupFunc: func(t *testing.T) string {
105+
tmpDir := t.TempDir()
106+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
107+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
108+
109+
template := `{
110+
"attributes": {
111+
"name": "Valid Rule",
112+
"description": "Valid description"
113+
}
114+
}`
115+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "valid.json"), []byte(template), 0o644))
116+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "ignore.txt"), []byte("ignored"), 0o644))
117+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "README.md"), []byte("# readme"), 0o644))
118+
return tmpDir
119+
},
120+
expectError: false,
121+
expectEmpty: false,
122+
validateFunc: func(t *testing.T, result string) {
123+
assert.Contains(t, result, "**Valid Rule**")
124+
assert.NotContains(t, result, "ignored")
125+
assert.NotContains(t, result, "readme")
126+
},
127+
},
128+
{
129+
name: "skip subdirectories",
130+
setupFunc: func(t *testing.T) string {
131+
tmpDir := t.TempDir()
132+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
133+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
134+
135+
template := `{
136+
"attributes": {
137+
"name": "Root Rule",
138+
"description": "Root description"
139+
}
140+
}`
141+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "root.json"), []byte(template), 0o644))
142+
143+
// Create subdirectory with a file that should be skipped
144+
subDir := filepath.Join(templatesDir, "subdir")
145+
require.NoError(t, os.MkdirAll(subDir, 0o755))
146+
require.NoError(t, os.WriteFile(filepath.Join(subDir, "nested.json"), []byte(template), 0o644))
147+
return tmpDir
148+
},
149+
expectError: false,
150+
expectEmpty: false,
151+
validateFunc: func(t *testing.T, result string) {
152+
// Should only have one rule mentioned
153+
assert.Equal(t, 1, strings.Count(result, "**Root Rule**"))
154+
},
155+
},
156+
{
157+
name: "unreadable file",
158+
setupFunc: func(t *testing.T) string {
159+
tmpDir := t.TempDir()
160+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
161+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
162+
163+
unreadableFile := filepath.Join(templatesDir, "unreadable.json")
164+
require.NoError(t, os.WriteFile(unreadableFile, []byte("content"), 0o000))
165+
return tmpDir
166+
},
167+
expectError: true,
168+
expectEmpty: false,
169+
},
170+
{
171+
name: "invalid json file",
172+
setupFunc: func(t *testing.T) string {
173+
tmpDir := t.TempDir()
174+
templatesDir := filepath.Join(tmpDir, "kibana", "alerting_rule_template")
175+
require.NoError(t, os.MkdirAll(templatesDir, 0o755))
176+
177+
invalidJSON := `{ "attributes": { "name": "Invalid" }`
178+
require.NoError(t, os.WriteFile(filepath.Join(templatesDir, "invalid.json"), []byte(invalidJSON), 0o644))
179+
return tmpDir
180+
},
181+
expectError: true,
182+
expectEmpty: false,
183+
},
184+
}
185+
186+
for _, tc := range cases {
187+
t.Run(tc.name, func(t *testing.T) {
188+
packageRoot := tc.setupFunc(t)
189+
190+
result, err := renderAlertRuleTemplates(packageRoot, newEmptyLinkMap())
191+
192+
if tc.expectError {
193+
assert.Error(t, err)
194+
return
195+
}
196+
197+
require.NoError(t, err)
198+
199+
if tc.expectEmpty {
200+
assert.Empty(t, result)
201+
} else {
202+
assert.NotEmpty(t, result)
203+
if tc.validateFunc != nil {
204+
tc.validateFunc(t, result)
205+
}
206+
}
207+
})
208+
}
209+
}

internal/docs/readme.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ func renderReadme(fileName, packageRoot, templatePath string, linksMap linkMap)
225225
"generatedHeader": func() string {
226226
return doNotModifyStr
227227
},
228+
"alertRuleTemplates": func() (string, error) {
229+
return renderAlertRuleTemplates(packageRoot, linksMap)
230+
},
228231
}).ParseFiles(templatePath)
229232
if err != nil {
230233
return nil, fmt.Errorf("parsing README template failed (path: %s): %w", templatePath, err)

scripts/links_table.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ links:
22
elastic-main: "https://www.elastic.co/guide"
33
getting-started-observability: "https://www.elastic.co/guide/en/welcome-to-elastic/current/getting-started-observability.html"
44
elasticsearch-histograms: https://www.elastic.co/guide/en/elasticsearch/reference/current/histogram.html
5+
alert-rule-templates: https://www.elastic.co/docs/reference/fleet/alert-templates#alert-templates
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dependencies:
2+
ecs:
3+
reference: [email protected]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Readme
2+
3+
## Alert rule templates
4+
5+
{{ alertRuleTemplates }}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# newer versions go on top
2+
- version: "0.0.1"
3+
changes:
4+
- description: Initial draft of the package
5+
type: enhancement
6+
link: https://github.com/elastic/integrations/pull/1 # FIXME Replace with the real PR link
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Readme
2+
3+
## Alert rule templates
4+
5+
Alert rule templates provide pre-defined configurations for creating alert rules in Kibana.
6+
7+
For more information, refer to the [Elastic documentation](https://www.elastic.co/docs/reference/fleet/alert-templates#alert-templates).
8+
9+
Alert rule templates require Elastic Stack version 9.2.0 or later.
10+
11+
The following alert rule templates are available:
12+
13+
**[MongoDB Replication] Replica member down**
14+
15+
16+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"id": "sample-rule",
3+
"type": "alerting_rule_template",
4+
"attributes": {
5+
"name": "[MongoDB Replication] Replica member down",
6+
"tags": [
7+
"MongoDB",
8+
"Replication"
9+
],
10+
"ruleTypeId": ".es-query",
11+
"schedule": {
12+
"interval": "1m"
13+
},
14+
"params": {
15+
"searchType": "esqlQuery",
16+
"timeWindowSize": 5,
17+
"timeWindowUnit": "m",
18+
"esqlQuery": {
19+
"esql": "FROM metrics-mongodb.replstatus-default\n| STATS members_down = MAX(`mongodb.replstatus.members.down.count`) BY `mongodb.replstatus.set_name`, `service.address`\n| WHERE members_down > 0"
20+
},
21+
"groupBy": "row",
22+
"termSize": 5,
23+
"timeField": "@timestamp"
24+
},
25+
"alertDelay": {
26+
"active": 1
27+
}
28+
},
29+
"managed": true,
30+
"coreMigrationVersion": "8.8.0",
31+
"typeMigrationVersion": "10.1.0"
32+
}
33+

0 commit comments

Comments
 (0)