mirror of https://github.com/hashicorp/consul
331 lines
9.0 KiB
Go
331 lines
9.0 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// HealthAny is special, and is used as a wild card,
|
|
// not as a specific state.
|
|
HealthAny = "any"
|
|
HealthPassing = "passing"
|
|
HealthWarning = "warning"
|
|
HealthCritical = "critical"
|
|
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:"
|
|
)
|
|
|
|
// 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
|
|
ServiceTags []string
|
|
|
|
Definition HealthCheckDefinition
|
|
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
}
|
|
|
|
// HealthCheckDefinition is used to store the details about
|
|
// a health check's execution.
|
|
type HealthCheckDefinition struct {
|
|
HTTP string
|
|
Header map[string][]string
|
|
Method string
|
|
TLSSkipVerify bool
|
|
TCP string
|
|
IntervalDuration time.Duration `json:"-"`
|
|
TimeoutDuration time.Duration `json:"-"`
|
|
DeregisterCriticalServiceAfterDuration time.Duration `json:"-"`
|
|
|
|
// DEPRECATED in Consul 1.4.1. Use the above time.Duration fields instead.
|
|
Interval ReadableDuration
|
|
Timeout ReadableDuration
|
|
DeregisterCriticalServiceAfter ReadableDuration
|
|
}
|
|
|
|
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
|
|
type Alias HealthCheckDefinition
|
|
out := &struct {
|
|
Interval string
|
|
Timeout string
|
|
DeregisterCriticalServiceAfter string
|
|
*Alias
|
|
}{
|
|
Interval: d.Interval.String(),
|
|
Timeout: d.Timeout.String(),
|
|
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
|
|
Alias: (*Alias)(d),
|
|
}
|
|
|
|
if d.IntervalDuration != 0 {
|
|
out.Interval = d.IntervalDuration.String()
|
|
} else if d.Interval != 0 {
|
|
out.Interval = d.Interval.String()
|
|
}
|
|
if d.TimeoutDuration != 0 {
|
|
out.Timeout = d.TimeoutDuration.String()
|
|
} else if d.Timeout != 0 {
|
|
out.Timeout = d.Timeout.String()
|
|
}
|
|
if d.DeregisterCriticalServiceAfterDuration != 0 {
|
|
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfterDuration.String()
|
|
} else if d.DeregisterCriticalServiceAfter != 0 {
|
|
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfter.String()
|
|
}
|
|
|
|
return json.Marshal(out)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Parse the values into both the time.Duration and old ReadableDuration fields.
|
|
var err error
|
|
if aux.Interval != "" {
|
|
if d.IntervalDuration, err = time.ParseDuration(aux.Interval); err != nil {
|
|
return err
|
|
}
|
|
d.Interval = ReadableDuration(d.IntervalDuration)
|
|
}
|
|
if aux.Timeout != "" {
|
|
if d.TimeoutDuration, err = time.ParseDuration(aux.Timeout); err != nil {
|
|
return err
|
|
}
|
|
d.Timeout = ReadableDuration(d.TimeoutDuration)
|
|
}
|
|
if aux.DeregisterCriticalServiceAfter != "" {
|
|
if d.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(aux.DeregisterCriticalServiceAfter); err != nil {
|
|
return err
|
|
}
|
|
d.DeregisterCriticalServiceAfter = ReadableDuration(d.DeregisterCriticalServiceAfterDuration)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// ServiceEntry is used for the health service endpoint
|
|
type ServiceEntry struct {
|
|
Node *Node
|
|
Service *AgentService
|
|
Checks HealthChecks
|
|
}
|
|
|
|
// 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
|
|
func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
|
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
|
|
|
|
var out HealthChecks
|
|
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
|
|
func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
|
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
|
|
|
|
var out HealthChecks
|
|
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) {
|
|
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)
|
|
}
|
|
|
|
// Connect is equivalent to Service except that it will only return services
|
|
// which are Connect-enabled and will returns the connection address for Connect
|
|
// client's to use which may be a proxy in front of the named service. If
|
|
// 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) {
|
|
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)
|
|
}
|
|
|
|
func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, connect bool) ([]*ServiceEntry, *QueryMeta, error) {
|
|
path := "/v1/health/service/" + service
|
|
if connect {
|
|
path = "/v1/health/connect/" + service
|
|
}
|
|
r := h.c.newRequest("GET", path)
|
|
r.setQueryOptions(q)
|
|
if len(tags) > 0 {
|
|
for _, tag := range tags {
|
|
r.params.Add("tag", tag)
|
|
}
|
|
}
|
|
if passingOnly {
|
|
r.params.Set(HealthPassing, "1")
|
|
}
|
|
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
|
|
}
|
|
|
|
// State is used to retrieve all the checks in a given state.
|
|
// The wildcard "any" state can also be used for all checks.
|
|
func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
|
|
switch state {
|
|
case HealthAny:
|
|
case HealthWarning:
|
|
case HealthCritical:
|
|
case HealthPassing:
|
|
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
|
|
|
|
var out HealthChecks
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return out, qm, nil
|
|
}
|