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  }