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 ( serviceHealth = "service" connectHealth = "connect" ingressHealth = "ingress" ) 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 Type string Namespace string `json:",omitempty"` Partition string `json:",omitempty"` 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 Body string TLSServerName string TLSSkipVerify bool TCP string UDP string GRPC string OSService string GRPCUseTLS bool 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 (t *HealthCheckDefinition) UnmarshalJSON(data []byte) (err error) { type Alias HealthCheckDefinition aux := &struct { IntervalDuration interface{} TimeoutDuration interface{} DeregisterCriticalServiceAfterDuration interface{} *Alias }{ Alias: (*Alias)(t), } if err := json.Unmarshal(data, &aux); err != nil { return err } // Parse the values into both the time.Duration and old ReadableDuration fields. if aux.IntervalDuration == nil { t.IntervalDuration = time.Duration(t.Interval) } else { switch v := aux.IntervalDuration.(type) { case string: if t.IntervalDuration, err = time.ParseDuration(v); err != nil { return err } case float64: t.IntervalDuration = time.Duration(v) } t.Interval = ReadableDuration(t.IntervalDuration) } if aux.TimeoutDuration == nil { t.TimeoutDuration = time.Duration(t.Timeout) } else { switch v := aux.TimeoutDuration.(type) { case string: if t.TimeoutDuration, err = time.ParseDuration(v); err != nil { return err } case float64: t.TimeoutDuration = time.Duration(v) } t.Timeout = ReadableDuration(t.TimeoutDuration) } if aux.DeregisterCriticalServiceAfterDuration == nil { t.DeregisterCriticalServiceAfterDuration = time.Duration(t.DeregisterCriticalServiceAfter) } else { switch v := aux.DeregisterCriticalServiceAfterDuration.(type) { case string: if t.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(v); err != nil { return err } case float64: t.DeregisterCriticalServiceAfterDuration = time.Duration(v) } t.DeregisterCriticalServiceAfter = ReadableDuration(t.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 := 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 := h.c.doRequest(r) if err != nil { return nil, nil, err } defer closeResponseBody(resp) if err := requireOK(resp); err != nil { return nil, nil, err } 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 := h.c.doRequest(r) if err != nil { return nil, nil, err } defer closeResponseBody(resp) if err := requireOK(resp); err != nil { return nil, nil, err } 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, serviceHealth) } func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { return h.service(service, tags, passingOnly, q, serviceHealth) } // 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, connectHealth) } func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { return h.service(service, tags, passingOnly, q, connectHealth) } // Ingress is equivalent to Connect except that it will only return associated // ingress gateways for the requested service. func (h *Health) Ingress(service string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { var tags []string return h.service(service, tags, passingOnly, q, ingressHealth) } func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, healthType string) ([]*ServiceEntry, *QueryMeta, error) { var path string switch healthType { case connectHealth: path = "/v1/health/connect/" + service case ingressHealth: path = "/v1/health/ingress/" + service default: path = "/v1/health/service/" + 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 := h.c.doRequest(r) if err != nil { return nil, nil, err } defer closeResponseBody(resp) if err := requireOK(resp); err != nil { return nil, nil, err } 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 := h.c.doRequest(r) if err != nil { return nil, nil, err } defer closeResponseBody(resp) if err := requireOK(resp); err != nil { return nil, nil, err } 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 }