Skip to content

Commit 87c8623

Browse files
committed
Add enabled:false to disable a monitor while still listing it
1 parent 36ed3bf commit 87c8623

10 files changed

Lines changed: 89 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
All notable changes to Gjallar are documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6+
## [0.8.0] - 2026-07-06
7+
8+
### Added
9+
10+
- `enabled: false` on a monitor: keep it on the status page (shown as a grey
11+
DISABLED badge, excluded from the up/total count) without running any check
12+
or alert. Defaults to enabled.
13+
614
## [0.7.0] - 2026-07-06
715

816
### Added

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ Gjallar reloads its configuration on `SIGHUP` (`systemctl reload gjallar`).
6262
The new config is fully validated first — if it is broken, the running
6363
configuration is kept and the error is logged.
6464

65+
### Disabling a monitor
66+
67+
Set `enabled: false` on a monitor to stop checking and alerting on it while
68+
keeping it on the status page, shown with a grey **DISABLED** badge and
69+
excluded from the group's up/total count. Omitting `enabled` (or `true`)
70+
leaves it active.
71+
72+
```yaml
73+
- name: "legacy-api"
74+
type: http
75+
url: "http://legacy/health"
76+
enabled: false # listed as disabled, never checked
77+
```
78+
6579
### Groups
6680

6781
Give monitors an optional `group: "Hyperion"` and the status page shows them

gjallar.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ monitors:
4343
# --- HTTP/HTTPS: status code, body regex, TLS certificate expiry ---
4444
- name: website
4545
group: "Public" # optional: monitors sharing a group are shown together
46+
# enabled: false # optional: list it on the page as DISABLED, never check it
4647
type: http
4748
url: "https://example.com/api/health"
4849
method: GET # default GET

internal/alert/engine.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type Engine struct {
4343
func NewEngine(cfg *config.Config, st *store.Store, notifiers map[string]Notifier) (*Engine, error) {
4444
e := &Engine{st: st, notifiers: notifiers, states: map[string]*monitorState{}}
4545
for _, m := range cfg.Monitors {
46+
if !m.IsEnabled() {
47+
continue // disabled monitors produce no results, need no state
48+
}
4649
s := &monitorState{threshold: m.FailureThreshold, realert: m.Realert.D(), notifiers: m.Alerts}
4750
open, err := st.HasOpenIncident(m.Name)
4851
if err != nil {

internal/config/config.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ type Alert struct {
6060

6161
type Monitor struct {
6262
Name string `yaml:"name"`
63-
Group string `yaml:"group"` // optional; groups monitors on the status page
64-
Type string `yaml:"type"` // http | postgres | oracle | ping | prometheus
63+
Group string `yaml:"group"` // optional; groups monitors on the status page
64+
Enabled *bool `yaml:"enabled"` // default true; false = listed but not checked
65+
Type string `yaml:"type"` // http | postgres | oracle | ping | prometheus
6566
Interval Duration `yaml:"interval"`
6667
Timeout Duration `yaml:"timeout"`
6768
FailureThreshold int `yaml:"failure_threshold"`
@@ -97,6 +98,11 @@ type Monitor struct {
9798
TimestampField string `yaml:"timestamp_field"` // freshness = hours since max(this field)
9899
}
99100

101+
// IsEnabled reports whether the monitor should be scheduled. A monitor with no
102+
// `enabled` key defaults to enabled; `enabled: false` lists it on the status
103+
// page as intentionally disabled without running any check or alert.
104+
func (m Monitor) IsEnabled() bool { return m.Enabled == nil || *m.Enabled }
105+
100106
var monitorTypes = map[string]bool{
101107
"http": true, "postgres": true, "oracle": true, "ping": true, "prometheus": true, "redis": true,
102108
"elasticsearch": true,

internal/config/config_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,35 @@ monitors:
163163
}
164164
}
165165

166+
func TestEnabledDefault(t *testing.T) {
167+
cfg, err := Load(writeConfig(t, `
168+
monitors:
169+
- name: a
170+
type: ping
171+
host: h
172+
- name: b
173+
type: ping
174+
host: h
175+
enabled: false
176+
- name: c
177+
type: ping
178+
host: h
179+
enabled: true
180+
`))
181+
if err != nil {
182+
t.Fatal(err)
183+
}
184+
if !cfg.Monitors[0].IsEnabled() {
185+
t.Error("a: default should be enabled")
186+
}
187+
if cfg.Monitors[1].IsEnabled() {
188+
t.Error("b: enabled:false should be disabled")
189+
}
190+
if !cfg.Monitors[2].IsEnabled() {
191+
t.Error("c: enabled:true should be enabled")
192+
}
193+
}
194+
166195
func TestLoadErrors(t *testing.T) {
167196
cases := []struct {
168197
name, yaml, wantErr string

internal/web/server.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ type IncidentView struct {
9292
type GroupView struct {
9393
Name string // empty = ungrouped
9494
Up int
95-
Total int
95+
Total int // enabled monitors only
96+
Disabled int
9697
Monitors []MonitorView
9798
}
9899

@@ -164,6 +165,10 @@ func (s *Server) overview() overviewData {
164165
}
165166
g := &d.Groups[gi]
166167
g.Monitors = append(g.Monitors, v)
168+
if v.Status == "disabled" {
169+
g.Disabled++
170+
continue // disabled monitors don't count toward up/total health
171+
}
167172
g.Total++
168173
if v.Status == "up" {
169174
g.Up++
@@ -181,6 +186,10 @@ func (s *Server) overview() overviewData {
181186

182187
func (s *Server) monitorView(m config.Monitor) MonitorView {
183188
v := MonitorView{Name: m.Name, Type: m.Type, Status: "pending", Latency: "—", Uptime24h: "—"}
189+
if !m.IsEnabled() {
190+
v.Status = "disabled"
191+
return v // no checks run, nothing to read from the store
192+
}
184193

185194
results, err := s.st.RecentResults(m.Name, tickCount)
186195
if err != nil {
@@ -229,12 +238,15 @@ func (s *Server) detail(name string) (detailData, bool) {
229238
Type: mon.Type,
230239
Status: "pending", Uptime24h: "—", Uptime30d: "—",
231240
}
241+
if !mon.IsEnabled() {
242+
d.Status = "disabled"
243+
}
232244
rows, err := s.st.RecentResults(name, historyCount)
233245
if err != nil {
234246
slog.Error("loading results", "monitor", name, "error", err)
235247
}
236248
d.Rows = rows
237-
if len(rows) > 0 {
249+
if mon.IsEnabled() && len(rows) > 0 {
238250
if rows[0].OK {
239251
d.Status = "up"
240252
} else {

internal/web/static/style.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ h2 { border-left: 4px solid var(--red); padding-left: 10px; }
8282
.pill.up { background: #1f2a1f; color: var(--ok); border: 1px solid #3a4a3a; }
8383
.pill.down { background: var(--red-dark); color: #fff; border: 1px solid var(--red); }
8484
.pill.pending { background: #26262c; color: var(--muted); border: 1px solid var(--border); }
85+
.pill.disabled { background: #26262c; color: var(--muted); border: 1px dashed #4a4a52; }
86+
.card.disabled { opacity: .55; }
87+
.card.disabled .ticks { display: none; }
8588

8689
/* uptime bars (oldest → newest) */
8790
.ticks { display: flex; gap: 2px; margin-top: 10px; }

internal/web/templates/_monitors.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
{{if .Name}}
44
<div class="group-head">
55
<h2>{{.Name}}</h2>
6-
<span class="group-count{{if lt .Up .Total}} bad{{end}}">{{.Up}}/{{.Total}} up</span>
6+
<span class="group-count{{if lt .Up .Total}} bad{{end}}">{{.Up}}/{{.Total}} up{{if .Disabled}} · {{.Disabled}} disabled{{end}}</span>
77
</div>
88
{{end}}
99
<section class="cards">
1010
{{range .Monitors}}
11-
<article class="card">
11+
<article class="card{{if eq .Status "disabled"}} disabled{{end}}">
1212
<div class="card-head">
1313
<a class="mon-name" href="/monitor/{{.Name}}">{{.Name}}</a>
1414
<span class="mon-type">{{.Type}}</span>

main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ func prepare(cfg *config.Config) (*prepared, error) {
113113
p := &prepared{checkers: make([]check.Checker, len(cfg.Monitors))}
114114
var err error
115115
for i, m := range cfg.Monitors {
116+
if !m.IsEnabled() {
117+
continue // disabled monitors are listed but never built or run
118+
}
116119
if p.checkers[i], err = check.New(m); err != nil {
117120
return nil, err
118121
}
@@ -164,6 +167,9 @@ func start(cfg *config.Config, p *prepared) (*instance, error) {
164167
// consumer (one SQLite writer, lock-free alert state machine).
165168
results := make(chan check.Result, len(cfg.Monitors))
166169
for i, m := range cfg.Monitors {
170+
if !m.IsEnabled() {
171+
continue // disabled: shown on the page, never scheduled
172+
}
167173
inst.runners.Add(1)
168174
go func(m config.Monitor, c check.Checker) {
169175
defer inst.runners.Done()
@@ -218,7 +224,7 @@ func (i *instance) stop() {
218224
func pingSelfTest(cfg *config.Config) error {
219225
tested := map[bool]bool{}
220226
for _, m := range cfg.Monitors {
221-
if m.Type == "ping" && !tested[m.Privileged] {
227+
if m.Type == "ping" && m.IsEnabled() && !tested[m.Privileged] {
222228
tested[m.Privileged] = true
223229
if err := check.SelfTestPing(m.Privileged); err != nil {
224230
return err

0 commit comments

Comments
 (0)