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 //------------------------------------------------------------------------------