github.com/splucs/witchcraft-go-server@v1.7.0/status/status.go (about)

     1  // Copyright (c) 2018 Palantir Technologies. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package status
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"sync/atomic"
    21  
    22  	"github.com/palantir/witchcraft-go-logging/wlog/svclog/svc1log"
    23  	"github.com/palantir/witchcraft-go-server/conjure/witchcraft/api/health"
    24  	"github.com/palantir/witchcraft-go-server/rest"
    25  	"github.com/palantir/witchcraft-go-server/witchcraft/refreshable"
    26  )
    27  
    28  var HealthStateStatusCodes = map[health.HealthState]int{
    29  	health.HealthStateHealthy:   http.StatusOK,
    30  	health.HealthStateUnknown:   500,
    31  	health.HealthStateDeferring: 518,
    32  	health.HealthStateSuspended: 519,
    33  	health.HealthStateRepairing: 520,
    34  	health.HealthStateWarning:   521,
    35  	health.HealthStateError:     522,
    36  	health.HealthStateTerminal:  523,
    37  }
    38  
    39  type healthStatusWithCode struct {
    40  	statusCode int
    41  	checks     map[health.CheckType]health.HealthCheckResult
    42  }
    43  
    44  // Source provides status that should be sent as a response.
    45  type Source interface {
    46  	Status() (respStatus int, metadata interface{})
    47  }
    48  
    49  // HealthCheckSource provides the SLS health status that should be sent as a response.
    50  // Refer to the SLS specification for more information.
    51  type HealthCheckSource interface {
    52  	HealthStatus(ctx context.Context) health.HealthStatus
    53  }
    54  
    55  type combinedHealthCheckSource struct {
    56  	healthCheckSources []HealthCheckSource
    57  }
    58  
    59  func NewCombinedHealthCheckSource(healthCheckSources ...HealthCheckSource) HealthCheckSource {
    60  	return &combinedHealthCheckSource{
    61  		healthCheckSources: healthCheckSources,
    62  	}
    63  }
    64  
    65  func (c *combinedHealthCheckSource) HealthStatus(ctx context.Context) health.HealthStatus {
    66  	result := health.HealthStatus{
    67  		Checks: map[health.CheckType]health.HealthCheckResult{},
    68  	}
    69  	for _, healthCheckSource := range c.healthCheckSources {
    70  		for k, v := range healthCheckSource.HealthStatus(ctx).Checks {
    71  			result.Checks[k] = v
    72  		}
    73  	}
    74  	return result
    75  }
    76  
    77  // HealthHandler is responsible for checking the health-check-shared-secret if it is provided and
    78  // invoking a HealthCheckSource if the secret is correct or unset.
    79  type healthHandlerImpl struct {
    80  	healthCheckSharedSecret refreshable.String
    81  	check                   HealthCheckSource
    82  	previousHealth          *atomic.Value
    83  }
    84  
    85  func NewHealthCheckHandler(checkSource HealthCheckSource, sharedSecret refreshable.String) http.Handler {
    86  	previousHealth := &atomic.Value{}
    87  	previousHealth.Store(&healthStatusWithCode{
    88  		statusCode: 0,
    89  		checks:     map[health.CheckType]health.HealthCheckResult{},
    90  	})
    91  	return &healthHandlerImpl{
    92  		healthCheckSharedSecret: sharedSecret,
    93  		check:                   checkSource,
    94  		previousHealth:          previousHealth,
    95  	}
    96  }
    97  
    98  func (h *healthHandlerImpl) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    99  	metadata, newHealthStatusCode := h.computeNewHealthStatus(req)
   100  	newHealth := &healthStatusWithCode{
   101  		statusCode: newHealthStatusCode,
   102  		checks:     metadata.Checks,
   103  	}
   104  	previousHealth := h.previousHealth.Load()
   105  	if previousHealth != nil {
   106  		if previousHealthTyped, ok := previousHealth.(*healthStatusWithCode); ok {
   107  			logIfHealthChanged(req.Context(), previousHealthTyped, newHealth)
   108  		}
   109  	}
   110  
   111  	h.previousHealth.Store(newHealth)
   112  
   113  	rest.WriteJSONResponse(w, metadata, newHealthStatusCode)
   114  }
   115  
   116  func (h *healthHandlerImpl) computeNewHealthStatus(req *http.Request) (health.HealthStatus, int) {
   117  	if sharedSecret := h.healthCheckSharedSecret.CurrentString(); sharedSecret != "" {
   118  		token, err := rest.ParseBearerTokenHeader(req)
   119  		if err != nil || sharedSecret != token {
   120  			return health.HealthStatus{}, http.StatusUnauthorized
   121  		}
   122  	}
   123  	metadata := h.check.HealthStatus(req.Context())
   124  	return metadata, healthStatusCode(metadata)
   125  }
   126  
   127  func healthStatusCode(metadata health.HealthStatus) int {
   128  	worst := http.StatusOK
   129  	for _, result := range metadata.Checks {
   130  		code := HealthStateStatusCodes[result.State]
   131  		if worst < code {
   132  			worst = code
   133  		}
   134  	}
   135  	return worst
   136  }
   137  
   138  func logIfHealthChanged(ctx context.Context, previousHealth, newHealth *healthStatusWithCode) {
   139  	if previousHealth.statusCode != newHealth.statusCode {
   140  		params := map[string]interface{}{
   141  			"previousHealthStatusCode": previousHealth.statusCode,
   142  			"newHealthStatusCode":      newHealth.statusCode,
   143  			"newHealthStatus":          newHealth.checks,
   144  		}
   145  		if newHealth.statusCode == http.StatusOK {
   146  			svc1log.FromContext(ctx).Info("Health status code changed.", svc1log.SafeParams(params))
   147  		} else {
   148  			svc1log.FromContext(ctx).Error("Health status code changed.", svc1log.SafeParams(params))
   149  		}
   150  		return
   151  	} else if checksDiffer(previousHealth.checks, newHealth.checks) {
   152  		svc1log.FromContext(ctx).Info("Health checks content changed without status change.", svc1log.SafeParams(map[string]interface{}{
   153  			"statusCode":      newHealth.statusCode,
   154  			"newHealthStatus": newHealth.checks,
   155  		}))
   156  	}
   157  }
   158  
   159  func checksDiffer(previousChecks, newChecks map[health.CheckType]health.HealthCheckResult) bool {
   160  	if len(previousChecks) != len(newChecks) {
   161  		return true
   162  	}
   163  	for previousCheckType, previouscheckResult := range previousChecks {
   164  		newCheckResult, checkTypePresent := newChecks[previousCheckType]
   165  		if !checkTypePresent {
   166  			return true
   167  		}
   168  		if previouscheckResult.State != newCheckResult.State {
   169  			return true
   170  		}
   171  	}
   172  	return false
   173  }