2015-01-06 18:40:00 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2018-12-12 17:14:02 +00:00
|
|
|
"encoding/json"
|
2015-01-06 18:40:00 +00:00
|
|
|
"fmt"
|
2016-11-29 21:15:20 +00:00
|
|
|
"strings"
|
2018-12-12 17:14:02 +00:00
|
|
|
"time"
|
2015-01-06 18:40:00 +00:00
|
|
|
)
|
|
|
|
|
2016-03-24 18:26:07 +00:00
|
|
|
const (
|
|
|
|
// HealthAny is special, and is used as a wild card,
|
|
|
|
// not as a specific state.
|
|
|
|
HealthAny = "any"
|
|
|
|
HealthPassing = "passing"
|
|
|
|
HealthWarning = "warning"
|
|
|
|
HealthCritical = "critical"
|
2016-11-29 21:15:20 +00:00
|
|
|
HealthMaint = "maintenance"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// NodeMaint is the special key set by a node in maintenance mode.
|
|
|
|
NodeMaint = "_node_maintenance"
|
|
|
|
|
|
|
|
// ServiceMaintPrefix is the prefix for a service in maintenance mode.
|
|
|
|
ServiceMaintPrefix = "_service_maintenance:"
|
2016-03-24 18:26:07 +00:00
|
|
|
)
|
|
|
|
|
2015-01-06 18:40:00 +00:00
|
|
|
// HealthCheck is used to represent a single check
|
|
|
|
type HealthCheck struct {
|
|
|
|
Node string
|
|
|
|
CheckID string
|
|
|
|
Name string
|
|
|
|
Status string
|
|
|
|
Notes string
|
|
|
|
Output string
|
|
|
|
ServiceID string
|
|
|
|
ServiceName string
|
2017-04-27 23:03:05 +00:00
|
|
|
ServiceTags []string
|
2017-10-20 20:39:13 +00:00
|
|
|
|
2017-11-01 21:25:46 +00:00
|
|
|
Definition HealthCheckDefinition
|
2018-12-12 17:14:02 +00:00
|
|
|
|
|
|
|
CreateIndex uint64
|
|
|
|
ModifyIndex uint64
|
2017-11-01 21:25:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HealthCheckDefinition is used to store the details about
|
|
|
|
// a health check's execution.
|
|
|
|
type HealthCheckDefinition struct {
|
2017-10-26 02:17:41 +00:00
|
|
|
HTTP string
|
|
|
|
Header map[string][]string
|
|
|
|
Method string
|
|
|
|
TLSSkipVerify bool
|
|
|
|
TCP string
|
2018-12-12 17:14:02 +00:00
|
|
|
Interval time.Duration
|
|
|
|
Timeout time.Duration
|
|
|
|
DeregisterCriticalServiceAfter time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
|
|
|
|
type Alias HealthCheckDefinition
|
|
|
|
return json.Marshal(&struct {
|
|
|
|
Interval string
|
|
|
|
Timeout string
|
|
|
|
DeregisterCriticalServiceAfter string
|
|
|
|
*Alias
|
|
|
|
}{
|
|
|
|
Interval: d.Interval.String(),
|
|
|
|
Timeout: d.Timeout.String(),
|
|
|
|
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
|
|
|
|
Alias: (*Alias)(d),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *HealthCheckDefinition) UnmarshalJSON(data []byte) error {
|
|
|
|
type Alias HealthCheckDefinition
|
|
|
|
aux := &struct {
|
|
|
|
Interval string
|
|
|
|
Timeout string
|
|
|
|
DeregisterCriticalServiceAfter string
|
|
|
|
*Alias
|
|
|
|
}{
|
|
|
|
Alias: (*Alias)(d),
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
if aux.Interval != "" {
|
|
|
|
if d.Interval, err = time.ParseDuration(aux.Interval); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if aux.Timeout != "" {
|
|
|
|
if d.Timeout, err = time.ParseDuration(aux.Timeout); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if aux.DeregisterCriticalServiceAfter != "" {
|
|
|
|
if d.DeregisterCriticalServiceAfter, err = time.ParseDuration(aux.DeregisterCriticalServiceAfter); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2015-01-06 18:40:00 +00:00
|
|
|
}
|
|
|
|
|
2016-11-29 21:15:20 +00:00
|
|
|
// HealthChecks is a collection of HealthCheck structs.
|
|
|
|
type HealthChecks []*HealthCheck
|
|
|
|
|
|
|
|
// AggregatedStatus returns the "best" status for the list of health checks.
|
|
|
|
// Because a given entry may have many service and node-level health checks
|
|
|
|
// attached, this function determines the best representative of the status as
|
|
|
|
// as single string using the following heuristic:
|
|
|
|
//
|
|
|
|
// maintenance > critical > warning > passing
|
|
|
|
//
|
|
|
|
func (c HealthChecks) AggregatedStatus() string {
|
|
|
|
var passing, warning, critical, maintenance bool
|
|
|
|
for _, check := range c {
|
|
|
|
id := string(check.CheckID)
|
|
|
|
if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) {
|
|
|
|
maintenance = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch check.Status {
|
|
|
|
case HealthPassing:
|
|
|
|
passing = true
|
|
|
|
case HealthWarning:
|
|
|
|
warning = true
|
|
|
|
case HealthCritical:
|
|
|
|
critical = true
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case maintenance:
|
|
|
|
return HealthMaint
|
|
|
|
case critical:
|
|
|
|
return HealthCritical
|
|
|
|
case warning:
|
|
|
|
return HealthWarning
|
|
|
|
case passing:
|
|
|
|
return HealthPassing
|
|
|
|
default:
|
|
|
|
return HealthPassing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-06 18:40:00 +00:00
|
|
|
// ServiceEntry is used for the health service endpoint
|
|
|
|
type ServiceEntry struct {
|
|
|
|
Node *Node
|
|
|
|
Service *AgentService
|
2016-11-30 02:17:00 +00:00
|
|
|
Checks HealthChecks
|
2015-01-06 18:40:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Health can be used to query the Health endpoints
|
|
|
|
type Health struct {
|
|
|
|
c *Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// Health returns a handle to the health endpoints
|
|
|
|
func (c *Client) Health() *Health {
|
|
|
|
return &Health{c}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Node is used to query for checks belonging to a given node
|
2016-11-30 02:17:00 +00:00
|
|
|
func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
2015-01-06 18:40:00 +00:00
|
|
|
r := h.c.newRequest("GET", "/v1/health/node/"+node)
|
|
|
|
r.setQueryOptions(q)
|
|
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
qm := &QueryMeta{}
|
|
|
|
parseQueryMeta(resp, qm)
|
|
|
|
qm.RequestTime = rtt
|
|
|
|
|
2016-11-30 02:17:00 +00:00
|
|
|
var out HealthChecks
|
2015-01-06 18:40:00 +00:00
|
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return out, qm, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checks is used to return the checks associated with a service
|
2016-11-30 02:17:00 +00:00
|
|
|
func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
2015-01-06 18:40:00 +00:00
|
|
|
r := h.c.newRequest("GET", "/v1/health/checks/"+service)
|
|
|
|
r.setQueryOptions(q)
|
|
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
qm := &QueryMeta{}
|
|
|
|
parseQueryMeta(resp, qm)
|
|
|
|
qm.RequestTime = rtt
|
|
|
|
|
2016-11-30 02:17:00 +00:00
|
|
|
var out HealthChecks
|
2015-01-06 18:40:00 +00:00
|
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return out, qm, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Service is used to query health information along with service info
|
|
|
|
// for a given service. It can optionally do server-side filtering on a tag
|
|
|
|
// or nodes with passing health checks only.
|
|
|
|
func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
2018-10-11 11:50:05 +00:00
|
|
|
var tags []string
|
|
|
|
if tag != "" {
|
|
|
|
tags = []string{tag}
|
|
|
|
}
|
|
|
|
return h.service(service, tags, passingOnly, q, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
|
|
|
return h.service(service, tags, passingOnly, q, false)
|
2018-03-26 15:51:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Connect is equivalent to Service except that it will only return services
|
|
|
|
// which are Connect-enabled and will returns the connection address for Connect
|
2018-06-07 09:17:44 +00:00
|
|
|
// client's to use which may be a proxy in front of the named service. If
|
2018-03-26 15:51:43 +00:00
|
|
|
// passingOnly is true only instances where both the service and any proxy are
|
|
|
|
// healthy will be returned.
|
|
|
|
func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
2018-10-11 11:50:05 +00:00
|
|
|
var tags []string
|
|
|
|
if tag != "" {
|
|
|
|
tags = []string{tag}
|
|
|
|
}
|
|
|
|
return h.service(service, tags, passingOnly, q, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
|
|
|
return h.service(service, tags, passingOnly, q, true)
|
2018-03-26 15:51:43 +00:00
|
|
|
}
|
|
|
|
|
2018-10-11 11:50:05 +00:00
|
|
|
func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, connect bool) ([]*ServiceEntry, *QueryMeta, error) {
|
2018-03-26 15:51:43 +00:00
|
|
|
path := "/v1/health/service/" + service
|
|
|
|
if connect {
|
|
|
|
path = "/v1/health/connect/" + service
|
|
|
|
}
|
|
|
|
r := h.c.newRequest("GET", path)
|
2015-01-06 18:40:00 +00:00
|
|
|
r.setQueryOptions(q)
|
2018-10-11 11:50:05 +00:00
|
|
|
if len(tags) > 0 {
|
|
|
|
for _, tag := range tags {
|
|
|
|
r.params.Add("tag", tag)
|
|
|
|
}
|
2015-01-06 18:40:00 +00:00
|
|
|
}
|
|
|
|
if passingOnly {
|
2016-03-24 18:26:07 +00:00
|
|
|
r.params.Set(HealthPassing, "1")
|
2015-01-06 18:40:00 +00:00
|
|
|
}
|
|
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
qm := &QueryMeta{}
|
|
|
|
parseQueryMeta(resp, qm)
|
|
|
|
qm.RequestTime = rtt
|
|
|
|
|
|
|
|
var out []*ServiceEntry
|
|
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return out, qm, nil
|
|
|
|
}
|
|
|
|
|
2015-09-11 19:24:54 +00:00
|
|
|
// State is used to retrieve all the checks in a given state.
|
2015-01-06 18:40:00 +00:00
|
|
|
// The wildcard "any" state can also be used for all checks.
|
2016-11-30 02:17:00 +00:00
|
|
|
func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
2015-01-06 18:40:00 +00:00
|
|
|
switch state {
|
2016-03-24 18:26:07 +00:00
|
|
|
case HealthAny:
|
|
|
|
case HealthWarning:
|
|
|
|
case HealthCritical:
|
|
|
|
case HealthPassing:
|
2015-01-06 18:40:00 +00:00
|
|
|
default:
|
|
|
|
return nil, nil, fmt.Errorf("Unsupported state: %v", state)
|
|
|
|
}
|
|
|
|
r := h.c.newRequest("GET", "/v1/health/state/"+state)
|
|
|
|
r.setQueryOptions(q)
|
|
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
qm := &QueryMeta{}
|
|
|
|
parseQueryMeta(resp, qm)
|
|
|
|
qm.RequestTime = rtt
|
|
|
|
|
2016-11-30 02:17:00 +00:00
|
|
|
var out HealthChecks
|
2015-01-06 18:40:00 +00:00
|
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return out, qm, nil
|
|
|
|
}
|