261 строка
6.7 KiB
Go
261 строка
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"net/url"
|
|
|
|
"github.com/Azure/applicationhealth-extension-linux/internal/telemetry"
|
|
"github.com/go-kit/log"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type HealthStatus string
|
|
type CustomMetricsStatus string
|
|
|
|
const (
|
|
Initializing HealthStatus = "Initializing"
|
|
Healthy HealthStatus = "Healthy"
|
|
Unhealthy HealthStatus = "Unhealthy"
|
|
Unknown HealthStatus = "Unknown"
|
|
)
|
|
|
|
const (
|
|
Empty string = ""
|
|
)
|
|
|
|
func (p HealthStatus) GetStatusType() StatusType {
|
|
switch p {
|
|
case Initializing:
|
|
return StatusTransitioning
|
|
case Unknown:
|
|
return StatusError
|
|
default:
|
|
return StatusSuccess
|
|
}
|
|
}
|
|
|
|
func (p HealthStatus) GetStatusTypeForAppHealthStatus() StatusType {
|
|
switch p {
|
|
case Unhealthy, Unknown:
|
|
return StatusError
|
|
default:
|
|
return StatusSuccess
|
|
}
|
|
}
|
|
|
|
func (p HealthStatus) GetMessageForAppHealthStatus() string {
|
|
if p.GetStatusTypeForAppHealthStatus() == StatusError {
|
|
return "Application found to be unhealthy"
|
|
} else {
|
|
return "Application found to be healthy"
|
|
}
|
|
}
|
|
|
|
type HealthProbe interface {
|
|
evaluate(lg log.Logger) (ProbeResponse, error)
|
|
address() string
|
|
healthStatusAfterGracePeriodExpires() HealthStatus
|
|
}
|
|
|
|
type TcpHealthProbe struct {
|
|
Address string
|
|
}
|
|
|
|
type HttpHealthProbe struct {
|
|
HttpClient *http.Client
|
|
Address string
|
|
}
|
|
|
|
func NewHealthProbe(lg log.Logger, cfg *handlerSettings) HealthProbe {
|
|
var p HealthProbe
|
|
p = new(DefaultHealthProbe)
|
|
switch cfg.protocol() {
|
|
case "tcp":
|
|
p = &TcpHealthProbe{
|
|
Address: "localhost:" + strconv.Itoa(cfg.port()),
|
|
}
|
|
sendTelemetry(lg, telemetry.EventLevelInfo, telemetry.AppHealthProbeTask, fmt.Sprintf("Creating %s probe targeting %s", cfg.protocol(), p.address()))
|
|
case "http":
|
|
fallthrough
|
|
case "https":
|
|
p = NewHttpHealthProbe(cfg.protocol(), cfg.requestPath(), cfg.port())
|
|
sendTelemetry(lg, telemetry.EventLevelInfo, telemetry.AppHealthProbeTask, fmt.Sprintf("Creating %s probe targeting %s", cfg.protocol(), p.address()))
|
|
default:
|
|
sendTelemetry(lg, telemetry.EventLevelInfo, telemetry.AppHealthProbeTask, "Configuration not provided. Using default reporting.")
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
func (p *TcpHealthProbe) evaluate(lg log.Logger) (ProbeResponse, error) {
|
|
conn, err := net.DialTimeout("tcp", p.address(), 30*time.Second)
|
|
var probeResponse ProbeResponse
|
|
if err != nil {
|
|
probeResponse.ApplicationHealthState = Unhealthy
|
|
return probeResponse, err
|
|
}
|
|
|
|
tcpConn, ok := conn.(*net.TCPConn)
|
|
if !ok {
|
|
probeResponse.ApplicationHealthState = Unhealthy
|
|
return probeResponse, errUnableToConvertType
|
|
}
|
|
|
|
tcpConn.SetLinger(0)
|
|
tcpConn.Close()
|
|
|
|
probeResponse.ApplicationHealthState = Healthy
|
|
return probeResponse, nil
|
|
}
|
|
|
|
func (p *TcpHealthProbe) address() string {
|
|
return p.Address
|
|
}
|
|
|
|
func (p *TcpHealthProbe) healthStatusAfterGracePeriodExpires() HealthStatus {
|
|
return Unhealthy
|
|
}
|
|
|
|
// constructAddress constructs a URL string from the given protocol, port, and request path.
|
|
// If the protocol is "http" and the port is not 0 or 80, the port number is included in the URL string.
|
|
// If the protocol is "https" and the port is not 0 or 443, the port number is included in the URL string.
|
|
func constructAddress(protocol string, port int, requestPath string) string {
|
|
portString := ""
|
|
if protocol == "http" && port != 0 && port != 80 {
|
|
portString = ":" + strconv.Itoa(port)
|
|
} else if protocol == "https" && port != 0 && port != 443 {
|
|
portString = ":" + strconv.Itoa(port)
|
|
}
|
|
|
|
u := url.URL{
|
|
Scheme: protocol,
|
|
Host: "localhost" + portString,
|
|
Path: requestPath,
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
func NewHttpHealthProbe(protocol string, requestPath string, port int) *HttpHealthProbe {
|
|
p := new(HttpHealthProbe)
|
|
|
|
timeout := time.Duration(30 * time.Second)
|
|
|
|
var transport *http.Transport
|
|
if protocol == "https" {
|
|
transport = &http.Transport{
|
|
// Ignore authentication/certificate failures - just validate that the localhost
|
|
// endpoint responds with HTTP.OK
|
|
// MinVersion set to tls1.0 because as after go 1.18, default min version changed
|
|
// from tls1.0 to tls1.2 and we want to support customers who are using tls1.0.
|
|
// tls MaxVersion is set to tls1.3 by default.
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MinVersion: tls.VersionTLS10,
|
|
},
|
|
}
|
|
p.HttpClient = &http.Client{
|
|
CheckRedirect: noRedirect,
|
|
Timeout: timeout,
|
|
Transport: transport,
|
|
}
|
|
} else {
|
|
p.HttpClient = &http.Client{
|
|
CheckRedirect: noRedirect,
|
|
Timeout: timeout,
|
|
}
|
|
}
|
|
|
|
p.Address = constructAddress(protocol, port, requestPath)
|
|
|
|
return p
|
|
}
|
|
|
|
func (p *HttpHealthProbe) evaluate(lg log.Logger) (ProbeResponse, error) {
|
|
req, err := http.NewRequest("GET", p.address(), nil)
|
|
var probeResponse ProbeResponse
|
|
if err != nil {
|
|
probeResponse.ApplicationHealthState = Unknown
|
|
return probeResponse, err
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "ApplicationHealthExtension/1.0")
|
|
resp, err := p.HttpClient.Do(req)
|
|
// non-2xx status code doesn't return err
|
|
// err is returned if a timeout occurred
|
|
if err != nil {
|
|
probeResponse.ApplicationHealthState = Unknown
|
|
return probeResponse, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
// non 2xx status code
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
probeResponse.ApplicationHealthState = Unknown
|
|
return probeResponse, errors.New(fmt.Sprintf("Unsuccessful response status code %v", resp.StatusCode))
|
|
}
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
probeResponse.ApplicationHealthState = Unknown
|
|
return probeResponse, err
|
|
}
|
|
|
|
if err := json.Unmarshal(bodyBytes, &probeResponse); err != nil {
|
|
probeResponse.ApplicationHealthState = Unknown
|
|
return probeResponse, err
|
|
}
|
|
|
|
if err := probeResponse.validateCustomMetrics(); err != nil {
|
|
sendTelemetry(lg, telemetry.EventLevelError, telemetry.AppHealthProbeTask, err.Error(), "error", err)
|
|
}
|
|
|
|
if err := probeResponse.validateApplicationHealthState(); err != nil {
|
|
probeResponse.ApplicationHealthState = Unknown
|
|
return probeResponse, err
|
|
}
|
|
|
|
return probeResponse, nil
|
|
}
|
|
|
|
func (p *HttpHealthProbe) address() string {
|
|
return p.Address
|
|
}
|
|
|
|
func (p *HttpHealthProbe) healthStatusAfterGracePeriodExpires() HealthStatus {
|
|
return Unknown
|
|
}
|
|
|
|
var (
|
|
errNoRedirect = errors.New("No redirect allowed")
|
|
errUnableToConvertType = errors.New("Unable to convert type")
|
|
)
|
|
|
|
func noRedirect(req *http.Request, via []*http.Request) error {
|
|
return errNoRedirect
|
|
}
|
|
|
|
type DefaultHealthProbe struct {
|
|
}
|
|
|
|
func (p DefaultHealthProbe) evaluate(lg log.Logger) (ProbeResponse, error) {
|
|
var probeResponse ProbeResponse
|
|
probeResponse.ApplicationHealthState = Healthy
|
|
return probeResponse, nil
|
|
}
|
|
|
|
func (p DefaultHealthProbe) address() string {
|
|
return ""
|
|
}
|
|
|
|
func (p DefaultHealthProbe) healthStatusAfterGracePeriodExpires() HealthStatus {
|
|
return Unhealthy
|
|
}
|