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