github.com/blend/go-sdk@v1.20220411.3/status/freeform.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package status 9 10 import ( 11 "context" 12 "net/http" 13 "sync" 14 "time" 15 16 "github.com/blend/go-sdk/ex" 17 "github.com/blend/go-sdk/logger" 18 "github.com/blend/go-sdk/web" 19 ) 20 21 // NewFreeform returns a new freeform check aggregator. 22 func NewFreeform(opts ...FreeformOption) *Freeform { 23 ff := &Freeform{ 24 ServiceChecks: make(map[string]Checker), 25 } 26 for _, opt := range opts { 27 opt(ff) 28 } 29 return ff 30 } 31 32 // FreeformOption mutates a freeform check. 33 type FreeformOption func(*Freeform) 34 35 // OptFreeformTimeout sets the timeout. 36 func OptFreeformTimeout(d time.Duration) FreeformOption { 37 return func(ff *Freeform) { 38 ff.Timeout = d 39 } 40 } 41 42 // OptFreeformLog sets the logger. 43 func OptFreeformLog(log logger.Log) FreeformOption { 44 return func(ff *Freeform) { 45 ff.Log = log 46 } 47 } 48 49 // Freeform holds sla check actions. 50 type Freeform struct { 51 // Timeout serves as an overall timeout, but is 52 // enforced per action. 53 Timeout time.Duration 54 // ServiceChecks are the individual checks 55 // we'll try as part of the status. 56 ServiceChecks map[string]Checker 57 // Log is a reference to a logger for 58 // situations where there are errors. 59 Log logger.Log 60 } 61 62 // Endpoint implements the handler for a given list of services. 63 func (f Freeform) Endpoint(servicesToCheck ...string) web.Action { 64 return func(r *web.Ctx) web.Result { 65 results, err := f.CheckStatuses(r.Context(), servicesToCheck...) 66 if err != nil { 67 return web.JSON.InternalError(err) 68 } 69 70 statusCode := http.StatusOK 71 for _, status := range results { 72 if !status { 73 statusCode = http.StatusServiceUnavailable 74 break 75 } 76 } 77 return web.JSON.Status(statusCode, results) 78 } 79 } 80 81 // CheckStatuses runs the check statuses for a given list of service names. 82 func (f Freeform) CheckStatuses(ctx context.Context, servicesToCheck ...string) (FreeformResult, error) { 83 servicesOrDefault, err := f.serviceChecksOrDefault(servicesToCheck...) 84 if err != nil { 85 return nil, err 86 } 87 88 results := make(chan freeformCheckResult, len(servicesOrDefault)) 89 wg := sync.WaitGroup{} 90 wg.Add(len(servicesOrDefault)) 91 for serviceName, check := range servicesOrDefault { 92 go func(ictx context.Context, sn string, c Checker) { 93 defer wg.Done() 94 results <- f.getCheckStatus(ictx, sn, c) 95 }(ctx, serviceName, check) 96 } 97 wg.Wait() 98 99 // handle results 100 output := make(FreeformResult) 101 resultCount := len(results) 102 for x := 0; x < resultCount; x++ { 103 res := <-results 104 output[res.ServiceName] = res.Ok 105 } 106 return output, nil 107 } 108 109 // getCheckStatus runs a check for a given serviceName. 110 func (f Freeform) getCheckStatus(ctx context.Context, serviceName string, checkAction Checker) (res freeformCheckResult) { 111 res.ServiceName = serviceName 112 defer func() { 113 if r := recover(); r != nil { 114 res.Err = ex.Append(res.Err, ex.New(r)) 115 } 116 if res.Err != nil { 117 logger.MaybeErrorContext(ctx, f.Log, res.Err) 118 } else { 119 res.Ok = true 120 } 121 return 122 }() 123 timeoutCtx, cancel := context.WithTimeout(ctx, f.timeoutOrDefault()) 124 defer cancel() 125 res.Err = checkAction.Check(timeoutCtx) 126 return 127 } 128 129 // 130 // Private / Internal 131 // 132 133 // timeoutOrDefault returns the timeout or a default. 134 func (f Freeform) timeoutOrDefault() time.Duration { 135 if f.Timeout > 0 { 136 return f.Timeout 137 } 138 return DefaultFreeformTimeout 139 } 140 141 // serviceChecksOrDefault returns either the full ServiceCheck's list 142 // or a subset of those checks. If any element of the subset is not found, 143 // an error will be returned. 144 func (f Freeform) serviceChecksOrDefault(servicesToCheck ...string) (map[string]Checker, error) { 145 if len(servicesToCheck) == 0 { 146 return f.ServiceChecks, nil 147 } 148 149 servicesChecks := make(map[string]Checker) 150 for _, serviceName := range servicesToCheck { 151 check, ok := f.ServiceChecks[serviceName] 152 if !ok { 153 return nil, ex.New(ErrServiceCheckNotDefined, ex.OptMessagef("service: %s", serviceName)) 154 } 155 servicesChecks[serviceName] = check 156 } 157 return servicesChecks, nil 158 }