Repository navigation
Expand file tree
/
Copy pathevaluate.go
More file actions
175 lines (151 loc) · 5.31 KB
/
Copy pathevaluate.go
File metadata and controls
175 lines (151 loc) · 5.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package githubstatus
import (
"errors"
"fmt"
"maps"
"slices"
)
// Thresholds define when GitHub should be considered unhealthy.
type Thresholds struct {
// Overall is the minimum overall indicator that counts as unhealthy,
// e.g. IndicatorMajor. Leave empty to disable the overall check.
Overall Indicator
// Components maps a component name (see the Component* constants) to the
// minimum status that counts as unhealthy, e.g. PartialOutage.
Components map[string]ComponentStatus
// FailOnMaintenance treats planned maintenance as unhealthy. It applies
// to the overall status (whether or not Overall is set) and to every
// component listed in Components.
FailOnMaintenance bool
}
// Validate reports an error if any threshold can never be exceeded, such as
// a misspelled value, "none" or "operational". Without this check, such a
// threshold would match every non-healthy status.
//
// The returned error wraps one *ParseError per invalid threshold.
func (t Thresholds) Validate() error {
var errs []error
if t.Overall != "" && t.Overall.Severity() == 0 {
errs = append(errs, &ParseError{Kind: "indicator", Value: string(t.Overall)})
}
for _, name := range slices.Sorted(maps.Keys(t.Components)) {
if st := t.Components[name]; st.Severity() == 0 {
errs = append(errs, &ParseError{Kind: "component status", Value: string(st), Component: name})
}
}
return errors.Join(errs...)
}
// Reason explains why a Violation was reported.
type Reason string
const (
// ReasonSeverity means the observed status met or exceeded the threshold.
ReasonSeverity Reason = "severity"
// ReasonMaintenance means planned maintenance was in progress and
// Thresholds.FailOnMaintenance was set.
ReasonMaintenance Reason = "maintenance"
)
// Violation describes a single threshold that was met or exceeded.
type Violation struct {
// Component is the component name, or empty for the overall status.
Component string
// Reason is why the violation was reported.
Reason Reason
// Indicator is the observed overall indicator. Set only when Component
// is empty.
Indicator Indicator
// IndicatorThreshold is the configured overall threshold. It may be
// empty for maintenance violations when no Overall threshold was set.
IndicatorThreshold Indicator
// Status is the observed component status. Set only when Component is
// not empty.
Status ComponentStatus
// StatusThreshold is the configured threshold for the component.
StatusThreshold ComponentStatus
}
// IsOverall reports whether the violation concerns the overall status rather
// than a single component.
func (v Violation) IsOverall() bool {
return v.Component == ""
}
func (v Violation) String() string {
name := v.Component
if v.IsOverall() {
name = "Overall"
}
switch {
case v.Reason == ReasonMaintenance:
return name + " (under maintenance)"
case v.IsOverall():
return fmt.Sprintf("%s (%s >= %s)", name, v.Indicator, v.IndicatorThreshold)
default:
return fmt.Sprintf("%s (%s >= %s)", name, v.Status, v.StatusThreshold)
}
}
// Result is the outcome of evaluating a Summary against Thresholds.
type Result struct {
// Violations lists every threshold that was met or exceeded: the
// overall status first, then components in the order the API returns
// them.
Violations []Violation
// MissingComponents lists, in sorted order, names in
// Thresholds.Components that did not appear in the summary. This
// usually means a typo or a component that GitHub has renamed, so the
// threshold was never checked. Missing components do not affect
// Healthy; decide for yourself whether to treat them as failures.
MissingComponents []string
}
// Healthy reports whether no thresholds were violated.
func (r Result) Healthy() bool {
return len(r.Violations) == 0
}
// Evaluate checks the summary against the thresholds.
//
// It returns an error, and no result, if t fails Validate.
func (s *Summary) Evaluate(t Thresholds) (Result, error) {
if err := t.Validate(); err != nil {
return Result{}, err
}
var r Result
ind := s.Status.Indicator
if t.Overall != "" && ind.Severity() >= t.Overall.Severity() {
r.Violations = append(r.Violations, Violation{
Reason: ReasonSeverity, Indicator: ind, IndicatorThreshold: t.Overall,
})
} else if t.FailOnMaintenance && ind.IsMaintenance() {
r.Violations = append(r.Violations, Violation{
Reason: ReasonMaintenance, Indicator: ind, IndicatorThreshold: t.Overall,
})
}
found := make(map[string]bool, len(t.Components))
for _, c := range s.Components {
th, ok := t.Components[c.Name]
if !ok {
continue
}
found[c.Name] = true
if c.Status.Severity() >= th.Severity() {
r.Violations = append(r.Violations, Violation{
Component: c.Name, Reason: ReasonSeverity, Status: c.Status, StatusThreshold: th,
})
} else if t.FailOnMaintenance && c.Status.IsMaintenance() {
r.Violations = append(r.Violations, Violation{
Component: c.Name, Reason: ReasonMaintenance, Status: c.Status, StatusThreshold: th,
})
}
}
for _, name := range slices.Sorted(maps.Keys(t.Components)) {
if !found[name] {
r.MissingComponents = append(r.MissingComponents, name)
}
}
return r, nil
}
// Component returns the component with the given name, if present.
func (s *Summary) Component(name string) (Component, bool) {
for _, c := range s.Components {
if c.Name == name {
return c, true
}
}
return Component{}, false
}