blob: 9faf6b665aa89af40a44de647e68ebbe0bfcbca5 [file] [log] [blame]
William Kurkianea869482019-04-09 15:16:11 -04001package api
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 "time"
8)
9
10const (
11 // HealthAny is special, and is used as a wild card,
12 // not as a specific state.
13 HealthAny = "any"
14 HealthPassing = "passing"
15 HealthWarning = "warning"
16 HealthCritical = "critical"
17 HealthMaint = "maintenance"
18)
19
20const (
21 // NodeMaint is the special key set by a node in maintenance mode.
22 NodeMaint = "_node_maintenance"
23
24 // ServiceMaintPrefix is the prefix for a service in maintenance mode.
25 ServiceMaintPrefix = "_service_maintenance:"
26)
27
28// HealthCheck is used to represent a single check
29type HealthCheck struct {
30 Node string
31 CheckID string
32 Name string
33 Status string
34 Notes string
35 Output string
36 ServiceID string
37 ServiceName string
38 ServiceTags []string
39
40 Definition HealthCheckDefinition
41
42 CreateIndex uint64
43 ModifyIndex uint64
44}
45
46// HealthCheckDefinition is used to store the details about
47// a health check's execution.
48type HealthCheckDefinition struct {
49 HTTP string
50 Header map[string][]string
51 Method string
52 TLSSkipVerify bool
53 TCP string
54 IntervalDuration time.Duration `json:"-"`
55 TimeoutDuration time.Duration `json:"-"`
56 DeregisterCriticalServiceAfterDuration time.Duration `json:"-"`
57
58 // DEPRECATED in Consul 1.4.1. Use the above time.Duration fields instead.
59 Interval ReadableDuration
60 Timeout ReadableDuration
61 DeregisterCriticalServiceAfter ReadableDuration
62}
63
64func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
65 type Alias HealthCheckDefinition
66 out := &struct {
67 Interval string
68 Timeout string
69 DeregisterCriticalServiceAfter string
70 *Alias
71 }{
72 Interval: d.Interval.String(),
73 Timeout: d.Timeout.String(),
74 DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
75 Alias: (*Alias)(d),
76 }
77
78 if d.IntervalDuration != 0 {
79 out.Interval = d.IntervalDuration.String()
80 } else if d.Interval != 0 {
81 out.Interval = d.Interval.String()
82 }
83 if d.TimeoutDuration != 0 {
84 out.Timeout = d.TimeoutDuration.String()
85 } else if d.Timeout != 0 {
86 out.Timeout = d.Timeout.String()
87 }
88 if d.DeregisterCriticalServiceAfterDuration != 0 {
89 out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfterDuration.String()
90 } else if d.DeregisterCriticalServiceAfter != 0 {
91 out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfter.String()
92 }
93
94 return json.Marshal(out)
95}
96
97func (d *HealthCheckDefinition) UnmarshalJSON(data []byte) error {
98 type Alias HealthCheckDefinition
99 aux := &struct {
100 Interval string
101 Timeout string
102 DeregisterCriticalServiceAfter string
103 *Alias
104 }{
105 Alias: (*Alias)(d),
106 }
107 if err := json.Unmarshal(data, &aux); err != nil {
108 return err
109 }
110
111 // Parse the values into both the time.Duration and old ReadableDuration fields.
112 var err error
113 if aux.Interval != "" {
114 if d.IntervalDuration, err = time.ParseDuration(aux.Interval); err != nil {
115 return err
116 }
117 d.Interval = ReadableDuration(d.IntervalDuration)
118 }
119 if aux.Timeout != "" {
120 if d.TimeoutDuration, err = time.ParseDuration(aux.Timeout); err != nil {
121 return err
122 }
123 d.Timeout = ReadableDuration(d.TimeoutDuration)
124 }
125 if aux.DeregisterCriticalServiceAfter != "" {
126 if d.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(aux.DeregisterCriticalServiceAfter); err != nil {
127 return err
128 }
129 d.DeregisterCriticalServiceAfter = ReadableDuration(d.DeregisterCriticalServiceAfterDuration)
130 }
131 return nil
132}
133
134// HealthChecks is a collection of HealthCheck structs.
135type HealthChecks []*HealthCheck
136
137// AggregatedStatus returns the "best" status for the list of health checks.
138// Because a given entry may have many service and node-level health checks
139// attached, this function determines the best representative of the status as
140// as single string using the following heuristic:
141//
142// maintenance > critical > warning > passing
143//
144func (c HealthChecks) AggregatedStatus() string {
145 var passing, warning, critical, maintenance bool
146 for _, check := range c {
147 id := string(check.CheckID)
148 if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) {
149 maintenance = true
150 continue
151 }
152
153 switch check.Status {
154 case HealthPassing:
155 passing = true
156 case HealthWarning:
157 warning = true
158 case HealthCritical:
159 critical = true
160 default:
161 return ""
162 }
163 }
164
165 switch {
166 case maintenance:
167 return HealthMaint
168 case critical:
169 return HealthCritical
170 case warning:
171 return HealthWarning
172 case passing:
173 return HealthPassing
174 default:
175 return HealthPassing
176 }
177}
178
179// ServiceEntry is used for the health service endpoint
180type ServiceEntry struct {
181 Node *Node
182 Service *AgentService
183 Checks HealthChecks
184}
185
186// Health can be used to query the Health endpoints
187type Health struct {
188 c *Client
189}
190
191// Health returns a handle to the health endpoints
192func (c *Client) Health() *Health {
193 return &Health{c}
194}
195
196// Node is used to query for checks belonging to a given node
197func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
198 r := h.c.newRequest("GET", "/v1/health/node/"+node)
199 r.setQueryOptions(q)
200 rtt, resp, err := requireOK(h.c.doRequest(r))
201 if err != nil {
202 return nil, nil, err
203 }
204 defer resp.Body.Close()
205
206 qm := &QueryMeta{}
207 parseQueryMeta(resp, qm)
208 qm.RequestTime = rtt
209
210 var out HealthChecks
211 if err := decodeBody(resp, &out); err != nil {
212 return nil, nil, err
213 }
214 return out, qm, nil
215}
216
217// Checks is used to return the checks associated with a service
218func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
219 r := h.c.newRequest("GET", "/v1/health/checks/"+service)
220 r.setQueryOptions(q)
221 rtt, resp, err := requireOK(h.c.doRequest(r))
222 if err != nil {
223 return nil, nil, err
224 }
225 defer resp.Body.Close()
226
227 qm := &QueryMeta{}
228 parseQueryMeta(resp, qm)
229 qm.RequestTime = rtt
230
231 var out HealthChecks
232 if err := decodeBody(resp, &out); err != nil {
233 return nil, nil, err
234 }
235 return out, qm, nil
236}
237
238// Service is used to query health information along with service info
239// for a given service. It can optionally do server-side filtering on a tag
240// or nodes with passing health checks only.
241func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
242 var tags []string
243 if tag != "" {
244 tags = []string{tag}
245 }
246 return h.service(service, tags, passingOnly, q, false)
247}
248
249func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
250 return h.service(service, tags, passingOnly, q, false)
251}
252
253// Connect is equivalent to Service except that it will only return services
254// which are Connect-enabled and will returns the connection address for Connect
255// client's to use which may be a proxy in front of the named service. If
256// passingOnly is true only instances where both the service and any proxy are
257// healthy will be returned.
258func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
259 var tags []string
260 if tag != "" {
261 tags = []string{tag}
262 }
263 return h.service(service, tags, passingOnly, q, true)
264}
265
266func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
267 return h.service(service, tags, passingOnly, q, true)
268}
269
270func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, connect bool) ([]*ServiceEntry, *QueryMeta, error) {
271 path := "/v1/health/service/" + service
272 if connect {
273 path = "/v1/health/connect/" + service
274 }
275 r := h.c.newRequest("GET", path)
276 r.setQueryOptions(q)
277 if len(tags) > 0 {
278 for _, tag := range tags {
279 r.params.Add("tag", tag)
280 }
281 }
282 if passingOnly {
283 r.params.Set(HealthPassing, "1")
284 }
285 rtt, resp, err := requireOK(h.c.doRequest(r))
286 if err != nil {
287 return nil, nil, err
288 }
289 defer resp.Body.Close()
290
291 qm := &QueryMeta{}
292 parseQueryMeta(resp, qm)
293 qm.RequestTime = rtt
294
295 var out []*ServiceEntry
296 if err := decodeBody(resp, &out); err != nil {
297 return nil, nil, err
298 }
299 return out, qm, nil
300}
301
302// State is used to retrieve all the checks in a given state.
303// The wildcard "any" state can also be used for all checks.
304func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
305 switch state {
306 case HealthAny:
307 case HealthWarning:
308 case HealthCritical:
309 case HealthPassing:
310 default:
311 return nil, nil, fmt.Errorf("Unsupported state: %v", state)
312 }
313 r := h.c.newRequest("GET", "/v1/health/state/"+state)
314 r.setQueryOptions(q)
315 rtt, resp, err := requireOK(h.c.doRequest(r))
316 if err != nil {
317 return nil, nil, err
318 }
319 defer resp.Body.Close()
320
321 qm := &QueryMeta{}
322 parseQueryMeta(resp, qm)
323 qm.RequestTime = rtt
324
325 var out HealthChecks
326 if err := decodeBody(resp, &out); err != nil {
327 return nil, nil, err
328 }
329 return out, qm, nil
330}