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 }