github.com/Jeffail/benthos/v3@v3.65.0/lib/api/api.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"net/http/pprof"
    11  	"runtime"
    12  	"sync"
    13  	"time"
    14  
    15  	httpdocs "github.com/Jeffail/benthos/v3/internal/http/docs"
    16  	"github.com/Jeffail/benthos/v3/lib/log"
    17  	"github.com/Jeffail/benthos/v3/lib/metrics"
    18  	"github.com/gorilla/mux"
    19  	yaml "gopkg.in/yaml.v3"
    20  )
    21  
    22  //------------------------------------------------------------------------------
    23  
    24  // Config contains the configuration fields for the Benthos API.
    25  type Config struct {
    26  	Address        string              `json:"address" yaml:"address"`
    27  	Enabled        bool                `json:"enabled" yaml:"enabled"`
    28  	ReadTimeout    string              `json:"read_timeout" yaml:"read_timeout"`
    29  	RootPath       string              `json:"root_path" yaml:"root_path"`
    30  	DebugEndpoints bool                `json:"debug_endpoints" yaml:"debug_endpoints"`
    31  	CertFile       string              `json:"cert_file" yaml:"cert_file"`
    32  	KeyFile        string              `json:"key_file" yaml:"key_file"`
    33  	CORS           httpdocs.ServerCORS `json:"cors" yaml:"cors"`
    34  }
    35  
    36  // NewConfig creates a new API config with default values.
    37  func NewConfig() Config {
    38  	return Config{
    39  		Address:        "0.0.0.0:4195",
    40  		Enabled:        true,
    41  		ReadTimeout:    "5s",
    42  		RootPath:       "/benthos",
    43  		DebugEndpoints: false,
    44  		CertFile:       "",
    45  		KeyFile:        "",
    46  		CORS:           httpdocs.NewServerCORS(),
    47  	}
    48  }
    49  
    50  //------------------------------------------------------------------------------
    51  
    52  // OptFunc applies an option to an API type during construction.
    53  type OptFunc func(t *Type)
    54  
    55  // OptWithMiddleware adds an HTTP middleware to the Benthos API.
    56  func OptWithMiddleware(m func(http.Handler) http.Handler) OptFunc {
    57  	return func(t *Type) {
    58  		t.server.Handler = m(t.server.Handler)
    59  	}
    60  }
    61  
    62  // OptWithTLS replaces the tls options of the HTTP server.
    63  func OptWithTLS(tls *tls.Config) OptFunc {
    64  	return func(t *Type) {
    65  		t.server.TLSConfig = tls
    66  	}
    67  }
    68  
    69  //------------------------------------------------------------------------------
    70  
    71  // Type implements the Benthos HTTP API.
    72  type Type struct {
    73  	conf         Config
    74  	endpoints    map[string]string
    75  	endpointsMut sync.Mutex
    76  
    77  	ctx    context.Context
    78  	cancel func()
    79  
    80  	handlers    map[string]http.HandlerFunc
    81  	handlersMut sync.RWMutex
    82  
    83  	log    log.Modular
    84  	mux    *mux.Router
    85  	server *http.Server
    86  }
    87  
    88  // New creates a new Benthos HTTP API.
    89  func New(
    90  	version string,
    91  	dateBuilt string,
    92  	conf Config,
    93  	wholeConf interface{},
    94  	log log.Modular,
    95  	stats metrics.Type,
    96  	opts ...OptFunc,
    97  ) (*Type, error) {
    98  	gMux := mux.NewRouter()
    99  	server := &http.Server{Addr: conf.Address}
   100  
   101  	var err error
   102  	if server.Handler, err = conf.CORS.WrapHandler(gMux); err != nil {
   103  		return nil, fmt.Errorf("bad CORS configuration: %w", err)
   104  	}
   105  
   106  	if conf.CertFile != "" || conf.KeyFile != "" {
   107  		if conf.CertFile == "" || conf.KeyFile == "" {
   108  			return nil, errors.New("both cert_file and key_file must be specified, or neither")
   109  		}
   110  	}
   111  
   112  	if tout := conf.ReadTimeout; len(tout) > 0 {
   113  		if server.ReadTimeout, err = time.ParseDuration(tout); err != nil {
   114  			return nil, fmt.Errorf("failed to parse read timeout string: %v", err)
   115  		}
   116  	}
   117  	t := &Type{
   118  		conf:      conf,
   119  		endpoints: map[string]string{},
   120  		handlers:  map[string]http.HandlerFunc{},
   121  		mux:       gMux,
   122  		server:    server,
   123  		log:       log,
   124  	}
   125  	t.ctx, t.cancel = context.WithCancel(context.Background())
   126  
   127  	handlePing := func(w http.ResponseWriter, r *http.Request) {
   128  		w.Write([]byte("pong"))
   129  	}
   130  
   131  	handleStackTrace := func(w http.ResponseWriter, r *http.Request) {
   132  		stackSlice := make([]byte, 1024*100)
   133  		s := runtime.Stack(stackSlice, true)
   134  		w.Write(stackSlice[:s])
   135  	}
   136  
   137  	handlePrintJSONConfig := func(w http.ResponseWriter, r *http.Request) {
   138  		var g interface{}
   139  		var err error
   140  		if node, ok := wholeConf.(yaml.Node); ok {
   141  			err = node.Decode(&g)
   142  		} else {
   143  			g = node
   144  		}
   145  		var resBytes []byte
   146  		if err == nil {
   147  			resBytes, err = json.Marshal(g)
   148  		}
   149  		if err != nil {
   150  			w.WriteHeader(http.StatusBadGateway)
   151  			return
   152  		}
   153  		w.Write(resBytes)
   154  	}
   155  
   156  	handlePrintYAMLConfig := func(w http.ResponseWriter, r *http.Request) {
   157  		resBytes, err := yaml.Marshal(wholeConf)
   158  		if err != nil {
   159  			w.WriteHeader(http.StatusBadGateway)
   160  			return
   161  		}
   162  		w.Write(resBytes)
   163  	}
   164  
   165  	handleVersion := func(w http.ResponseWriter, r *http.Request) {
   166  		fmt.Fprintf(w, "{\"version\":\"%v\", \"built\":\"%v\"}", version, dateBuilt)
   167  	}
   168  
   169  	handleEndpoints := func(w http.ResponseWriter, r *http.Request) {
   170  		t.endpointsMut.Lock()
   171  		defer t.endpointsMut.Unlock()
   172  
   173  		resBytes, err := json.Marshal(t.endpoints)
   174  		if err != nil {
   175  			w.WriteHeader(http.StatusBadGateway)
   176  		} else {
   177  			w.Write(resBytes)
   178  		}
   179  	}
   180  
   181  	if t.conf.DebugEndpoints {
   182  		t.RegisterEndpoint(
   183  			"/debug/config/json", "DEBUG: Returns the loaded config as JSON.",
   184  			handlePrintJSONConfig,
   185  		)
   186  		t.RegisterEndpoint(
   187  			"/debug/config/yaml", "DEBUG: Returns the loaded config as YAML.",
   188  			handlePrintYAMLConfig,
   189  		)
   190  		t.RegisterEndpoint(
   191  			"/debug/stack", "DEBUG: Returns a snapshot of the current service stack trace.",
   192  			handleStackTrace,
   193  		)
   194  		t.RegisterEndpoint(
   195  			"/debug/pprof/profile", "DEBUG: Responds with a pprof-formatted cpu profile.",
   196  			pprof.Profile,
   197  		)
   198  		t.RegisterEndpoint(
   199  			"/debug/pprof/heap", "DEBUG: Responds with a pprof-formatted heap profile.",
   200  			pprof.Index,
   201  		)
   202  		t.RegisterEndpoint(
   203  			"/debug/pprof/block", "DEBUG: Responds with a pprof-formatted block profile.",
   204  			pprof.Index,
   205  		)
   206  		t.RegisterEndpoint(
   207  			"/debug/pprof/mutex", "DEBUG: Responds with a pprof-formatted mutex profile.",
   208  			pprof.Index,
   209  		)
   210  		t.RegisterEndpoint(
   211  			"/debug/pprof/symbol", "DEBUG: looks up the program counters listed"+
   212  				" in the request, responding with a table mapping program"+
   213  				" counters to function names.",
   214  			pprof.Symbol,
   215  		)
   216  		t.RegisterEndpoint(
   217  			"/debug/pprof/trace",
   218  			"DEBUG: Responds with the execution trace in binary form."+
   219  				" Tracing lasts for duration specified in seconds GET"+
   220  				" parameter, or for 1 second if not specified.",
   221  			pprof.Trace,
   222  		)
   223  	}
   224  
   225  	t.RegisterEndpoint("/ping", "Ping me.", handlePing)
   226  	t.RegisterEndpoint("/version", "Returns the service version.", handleVersion)
   227  	t.RegisterEndpoint("/endpoints", "Returns this map of endpoints.", handleEndpoints)
   228  
   229  	// If we want to expose a JSON stats endpoint we register the endpoints.
   230  	if wHandlerFunc, ok := stats.(metrics.WithHandlerFunc); ok {
   231  		t.RegisterEndpoint(
   232  			"/stats", "Returns a JSON object of service metrics.",
   233  			wHandlerFunc.HandlerFunc(),
   234  		)
   235  		t.RegisterEndpoint(
   236  			"/metrics", "Returns a JSON object of service metrics.",
   237  			wHandlerFunc.HandlerFunc(),
   238  		)
   239  	}
   240  
   241  	for _, opt := range opts {
   242  		opt(t)
   243  	}
   244  
   245  	return t, nil
   246  }
   247  
   248  // RegisterEndpoint registers a http.HandlerFunc under a path with a
   249  // description that will be displayed under the /endpoints path.
   250  func (t *Type) RegisterEndpoint(path, desc string, handlerFunc http.HandlerFunc) {
   251  	t.endpointsMut.Lock()
   252  	defer t.endpointsMut.Unlock()
   253  
   254  	t.endpoints[path] = desc
   255  
   256  	t.handlersMut.Lock()
   257  	defer t.handlersMut.Unlock()
   258  
   259  	if _, exists := t.handlers[path]; !exists {
   260  		wrapHandler := func(w http.ResponseWriter, r *http.Request) {
   261  			t.handlersMut.RLock()
   262  			h := t.handlers[path]
   263  			t.handlersMut.RUnlock()
   264  			h(w, r)
   265  		}
   266  		t.mux.HandleFunc(path, wrapHandler)
   267  		t.mux.HandleFunc(t.conf.RootPath+path, wrapHandler)
   268  	}
   269  	t.handlers[path] = handlerFunc
   270  }
   271  
   272  // ListenAndServe launches the API and blocks until the server closes or fails.
   273  func (t *Type) ListenAndServe() error {
   274  	if !t.conf.Enabled {
   275  		<-t.ctx.Done()
   276  		return nil
   277  	}
   278  	t.log.Infof(
   279  		"Listening for HTTP requests at: %v\n",
   280  		"http://"+t.conf.Address,
   281  	)
   282  	if t.server.TLSConfig != nil {
   283  		return t.server.ListenAndServeTLS("", "")
   284  	}
   285  	if len(t.conf.CertFile) > 0 {
   286  		return t.server.ListenAndServeTLS(t.conf.CertFile, t.conf.KeyFile)
   287  	}
   288  	return t.server.ListenAndServe()
   289  }
   290  
   291  // Shutdown attempts to close the http server.
   292  func (t *Type) Shutdown(ctx context.Context) error {
   293  	t.cancel()
   294  	return t.server.Shutdown(ctx)
   295  }
   296  
   297  //------------------------------------------------------------------------------