diff --git a/README.md b/README.md index 7606ca6..2ff18c9 100644 --- a/README.md +++ b/README.md @@ -12,28 +12,27 @@ Specifications: * Watches for secrets and configmaps changes and updates the in-memory cache * Prometheus instrumentation (RED metrics) * Dependency management with golang/dep -* Structured logging with zerolog -* Error handling with pkg/errors +* Structured logging with zap +* Tracing with Istio and Jaeger * Helm chart Web API: -* `GET /` prints runtime information, environment variables, labels and annotations +* `GET /` prints runtime information * `GET /version` prints podinfo version and git commit hash -* `GET /metrics` http requests duration and Go runtime metrics +* `GET /metrics` return HTTP requests duration and Go runtime metrics * `GET /healthz` used by Kubernetes liveness probe * `GET /readyz` used by Kubernetes readiness probe * `POST /readyz/enable` signals the Kubernetes LB that this instance is ready to receive traffic * `POST /readyz/disable` signals the Kubernetes LB to stop sending requests to this instance -* `GET /error` returns code 500 and logs the error +* `GET /status/{code}` returns the status code * `GET /panic` crashes the process with exit code 255 -* `POST /echo` echos the posted content, logs the SHA1 hash of the content -* `GET /echoheaders` prints the request HTTP headers -* `POST /job` long running job, json body: `{"wait":2}` -* `GET /configs` prints the configmaps and/or secrets mounted in the `config` volume +* `POST /echo` forwards the call to the backend service and echos the posted content +* `GET /headers` returns a JSON with the request HTTP headers +* `GET /delay/{seconds}` waits for the specified period +* `GET /configs` returns a JSON with configmaps and/or secrets mounted in the `config` volume * `POST /write` writes the posted content to disk at /data/hash and returns the SHA1 hash of the content -* `POST /read` receives a SHA1 hash and returns the content of the file /data/hash if exists -* `POST /backend` forwards the call to the backend service on `http://backend-podinfo:9898/echo` +* `GET /read/{hash}` returns the content of the file /data/hash if exists ### Guides diff --git a/pkg/server/api.go b/pkg/server/api.go deleted file mode 100644 index eb383b5..0000000 --- a/pkg/server/api.go +++ /dev/null @@ -1,178 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "time" - - "github.com/rs/zerolog/log" - "github.com/stefanprodan/k8s-podinfo/pkg/version" -) - -func (s *Server) apiInfo(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/info" && r.Method != http.MethodGet { - w.WriteHeader(http.StatusNotFound) - return - } - - host, _ := os.Hostname() - color := os.Getenv("color") - if len(color) < 1 { - color = "blue" - } - - msg := os.Getenv("message") - if len(msg) < 1 { - msg = fmt.Sprintf("Greetings from podinfo v%v", version.VERSION) - } - - data := struct { - Message string `json:"message"` - Version string `json:"version"` - Revision string `json:"revision"` - Hostname string `json:"hostname"` - Color string `json:"color"` - }{ - Message: msg, - Version: version.VERSION, - Revision: version.GITCOMMIT, - Hostname: host, - Color: color, - } - - d, err := json.Marshal(data) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusOK) - w.Write(d) -} - -func (s *Server) apiEcho(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/echo" && r.Method != http.MethodPost { - w.WriteHeader(http.StatusNotFound) - return - } - - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - jsonError(w, "invalid request body", http.StatusBadRequest) - return - } - - backendURL := os.Getenv("backendURL") - if len(backendURL) > 0 { - backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body)) - if err != nil { - log.Error().Err(err).Msgf("%v backend call failed", r.URL.Path) - jsonError(w, "backend call failed", http.StatusInternalServerError) - return - } - - // forward headers - copyTracingHeaders(r, backendReq) - setVersionHeaders(backendReq) - - // TODO: make the timeout configurable - ctx, cancel := context.WithTimeout(backendReq.Context(), 2*time.Minute) - defer cancel() - - // call backend - resp, err := http.DefaultClient.Do(backendReq.WithContext(ctx)) - if err != nil { - log.Error().Err(err).Msgf("backend call to %s failed", backendURL) - jsonError(w, "backend call failed", http.StatusInternalServerError) - return - } - - defer resp.Body.Close() - - // copy error status from backend and exit - if resp.StatusCode >= 400 { - w.WriteHeader(resp.StatusCode) - return - } - - // forward the received body - rbody, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Error().Err(err).Msgf("%v reading the backend request body failed", r.URL.Path) - jsonError(w, "backend call failed", http.StatusInternalServerError) - return - } - - // set logLevel=info when load testing - log.Debug().Msgf("Payload received %v from backend: %s", r.URL.Path, string(rbody)) - - setResponseHeaders(w) - w.Write(rbody) - } else { - setResponseHeaders(w) - w.Write(body) - } -} - -func copyTracingHeaders(from *http.Request, to *http.Request) { - headers := []string{ - "x-request-id", - "x-b3-traceid", - "x-b3-spanid", - "x-b3-parentspanid", - "x-b3-sampled", - "x-b3-flags", - "x-ot-span-context", - } - - for i := range headers { - headerValue := from.Header.Get(headers[i]) - if len(headerValue) > 0 { - to.Header.Set(headers[i], headerValue) - } - } -} - -func setVersionHeaders(r *http.Request) { - r.Header.Set("X-API-Version", version.VERSION) - r.Header.Set("X-API-Revision", version.GITCOMMIT) -} - -func setResponseHeaders(w http.ResponseWriter) { - color := os.Getenv("color") - if len(color) < 1 { - color = "blue" - } - w.Header().Set("X-Color", color) - w.WriteHeader(http.StatusAccepted) -} - -func jsonError(w http.ResponseWriter, error string, code int) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(code) - - data := struct { - Code int `json:"code"` - Message string `json:"message"` - }{ - Code: code, - Message: error, - } - - body, err := json.Marshal(data) - if err != nil { - log.Debug().Err(err).Msg("jsonError marshal failed") - } else { - w.Write(body) - } -} diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go deleted file mode 100644 index 68f30e2..0000000 --- a/pkg/server/handlers.go +++ /dev/null @@ -1,346 +0,0 @@ -package server - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "html/template" - "io/ioutil" - "net/http" - "os" - "path" - "strings" - "sync/atomic" - "time" - - "github.com/rs/zerolog/log" - "github.com/stefanprodan/k8s-podinfo/pkg/version" - "gopkg.in/yaml.v2" -) - -func (s *Server) index(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - w.WriteHeader(http.StatusNotFound) - return - } - - log.Debug().Msgf("Request %s received from %s on %s", r.Header.Get("x-request-id"), r.RemoteAddr, r.RequestURI) - - if strings.Contains(r.UserAgent(), "Mozilla") { - uiPath := os.Getenv("uiPath") - if len(uiPath) < 1 { - uiPath = "ui" - } - tmpl, err := template.New("vue.html").ParseFiles(path.Join(uiPath, "vue.html")) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(path.Join(uiPath, "vue.html") + err.Error())) - return - } - - host, _ := os.Hostname() - data := struct { - Title string - }{ - Title: host, - } - - if err := tmpl.Execute(w, data); err != nil { - http.Error(w, path.Join(uiPath, "vue.html")+err.Error(), http.StatusInternalServerError) - } - - } else { - resp, err := makeResponse() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - d, err := yaml.Marshal(resp) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusOK) - w.Write(d) - } - -} - -func (s *Server) echo(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "POST": - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - hash := hash(string(body)) - log.Debug().Msgf("Payload received from %s hash %s", r.RemoteAddr, hash) - w.WriteHeader(http.StatusAccepted) - w.Write(body) - default: - w.WriteHeader(http.StatusNotAcceptable) - } -} - -func (s *Server) echoHeaders(w http.ResponseWriter, r *http.Request) { - d, err := yaml.Marshal(r.Header) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusOK) - w.Write(d) -} - -func (s *Server) backend(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "POST": - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - backendURL := os.Getenv("backendURL") - if len(backendURL) > 0 { - backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body)) - if err != nil { - log.Error().Msgf("Backend call failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - // forward tracing headers - copyTracingHeaders(r, backendReq) - setVersionHeaders(backendReq) - - resp, err := http.DefaultClient.Do(backendReq) - if err != nil { - log.Error().Msgf("Backend call failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - defer resp.Body.Close() - if resp.StatusCode >= 500 { - w.WriteHeader(resp.StatusCode) - return - } - rbody, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Error().Msgf("Reading the backend request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - log.Debug().Msgf("Payload received from backend: %s", string(rbody)) - - setResponseHeaders(w) - w.Write(rbody) - } else { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Backend not specified, set backendURL env var")) - } - default: - w.WriteHeader(http.StatusNotAcceptable) - } -} - -func (s *Server) job(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "POST": - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - log.Debug().Msgf("Payload received from %s: %s", r.RemoteAddr, string(body)) - - job := struct { - Wait int `json:"wait"` - }{ - Wait: 0, - } - err = json.Unmarshal(body, &job) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - if job.Wait > 0 { - time.Sleep(time.Duration(job.Wait) * time.Second) - } - w.WriteHeader(http.StatusAccepted) - w.Write([]byte("Job done")) - default: - w.WriteHeader(http.StatusNotAcceptable) - } -} - -func (s *Server) write(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "POST": - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - hash := hash(string(body)) - err = ioutil.WriteFile(path.Join(dataPath, hash), body, 0644) - if err != nil { - log.Error().Msgf("Writing file to /data failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - log.Debug().Msgf("Write command received from %s hash %s", r.RemoteAddr, hash) - w.WriteHeader(http.StatusAccepted) - w.Write([]byte(hash)) - default: - w.WriteHeader(http.StatusNotAcceptable) - } -} - -func (s *Server) read(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "POST": - body, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Error().Msgf("Reading the request body failed: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - hash := string(body) - content, err := ioutil.ReadFile(path.Join(dataPath, hash)) - if err != nil { - log.Error().Msgf("Reading file from /data/%s failed: %v", hash, err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - log.Debug().Msgf("Read command received from %s hash %s", r.RemoteAddr, hash) - w.WriteHeader(http.StatusAccepted) - w.Write([]byte(content)) - default: - w.WriteHeader(http.StatusNotAcceptable) - } -} - -func (s *Server) configs(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - files := make(map[string]string) - if watcher != nil { - watcher.Cache.Range(func(key interface{}, value interface{}) bool { - files[key.(string)] = value.(string) - return true - }) - } - - d, err := yaml.Marshal(files) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusOK) - w.Write(d) - default: - w.WriteHeader(http.StatusNotAcceptable) - } -} - -func (s *Server) version(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/version" { - w.WriteHeader(http.StatusNotFound) - return - } - - resp := map[string]string{ - "version": version.VERSION, - "commit": version.GITCOMMIT, - } - - d, err := yaml.Marshal(resp) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusOK) - w.Write(d) -} - -func (s *Server) healthz(w http.ResponseWriter, r *http.Request) { - if atomic.LoadInt32(&healthy) == 1 { - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - return - } - w.WriteHeader(http.StatusServiceUnavailable) -} - -func (s *Server) readyz(w http.ResponseWriter, r *http.Request) { - if atomic.LoadInt32(&ready) == 1 { - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - return - } - w.WriteHeader(http.StatusServiceUnavailable) -} - -func (s *Server) enable(w http.ResponseWriter, r *http.Request) { - atomic.StoreInt32(&ready, 1) -} - -func (s *Server) disable(w http.ResponseWriter, r *http.Request) { - atomic.StoreInt32(&ready, 0) -} - -func (s *Server) error(w http.ResponseWriter, r *http.Request) { - log.Error().Msg("Error triggered") - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Internal server error")) - return -} - -func (s *Server) panic(w http.ResponseWriter, r *http.Request) { - log.Fatal().Msg("Kill switch triggered") -} - -func hash(input string) string { - h := sha256.New() - h.Write([]byte(input)) - return hex.EncodeToString(h.Sum(nil)) -} diff --git a/pkg/server/handlers_test.go b/pkg/server/handlers_test.go deleted file mode 100644 index 9affc84..0000000 --- a/pkg/server/handlers_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - "net/http/httptest" - "regexp" - "strings" - "testing" -) - -func TestYamlResponse(t *testing.T) { - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - srv := &Server{} - handler := http.HandlerFunc(srv.index) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Fatalf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - expected := "external_ip" - r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected)) - if !r.MatchString(rr.Body.String()) { - t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", - rr.Body.String(), expected) - } -} - -func TestHealthzNotReady(t *testing.T) { - req, err := http.NewRequest("GET", "/healthz", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - srv := &Server{} - handler := http.HandlerFunc(srv.healthz) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusServiceUnavailable { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusServiceUnavailable) - } -} - -func TestReadyzNotReady(t *testing.T) { - req, err := http.NewRequest("GET", "/readyz", nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - srv := &Server{} - handler := http.HandlerFunc(srv.readyz) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusServiceUnavailable { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusServiceUnavailable) - } -} - -func TestEchoResponse(t *testing.T) { - expected := "test" - req, err := http.NewRequest("POST", "/echo", strings.NewReader(expected)) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - srv := &Server{} - handler := http.HandlerFunc(srv.echo) - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusAccepted { - t.Fatalf("handler returned wrong status code: got %v want %v", - status, http.StatusAccepted) - } - - if rr.Body.String() != expected { - t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s", - rr.Body.String(), expected) - } -} diff --git a/pkg/server/response.go b/pkg/server/response.go deleted file mode 100644 index 7880a3f..0000000 --- a/pkg/server/response.go +++ /dev/null @@ -1,143 +0,0 @@ -package server - -import ( - "bufio" - "crypto/tls" - "encoding/json" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" -) - -type Response struct { - Runtime map[string]string `json:"runtime" yaml:"runtime"` - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` - Environment map[string]string `json:"environment" yaml:"environment"` -} - -func makeResponse() (*Response, error) { - labels, err := filesToMap("/etc/podinfod/metadata/labels") - if err != nil { - return nil, err - } - - annotations, err := filesToMap("/etc/podinfod/metadata/annotations") - if err != nil { - return nil, err - } - - resp := &Response{ - Environment: envToMap(), - Runtime: runtimeToMap(), - Labels: labels, - Annotations: annotations, - } - - return resp, nil -} - -func filesToMap(dir string) (map[string]string, error) { - list := make(map[string]string, 0) - if _, err := os.Stat(dir); err != nil { - // path not found - return list, nil - } - files := make([]string, 0) - err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { - files = append(files, path) - - return nil - }) - if err != nil { - return nil, errors.Wrapf(err, "Reading from %v failed", dir) - } - for _, path := range files { - file, err := os.Open(path) - if err != nil { - continue - } - s := bufio.NewScanner(file) - for s.Scan() { - kv := strings.Split(s.Text(), "=") - if len(kv) > 1 { - list[kv[0]] = strings.Replace(kv[1], "\"", "", -1) - } else { - list[kv[0]] = "" - } - } - file.Close() - } - return list, nil -} - -func envToMap() map[string]string { - list := make(map[string]string, 0) - for _, env := range os.Environ() { - kv := strings.Split(env, "=") - if len(kv) > 1 { - list[kv[0]] = strings.Replace(kv[1], "\"", "", -1) - } else { - list[kv[0]] = "" - } - } - return list -} - -func runtimeToMap() map[string]string { - info := map[string]string{ - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "version": runtime.Version(), - "max_procs": strconv.FormatInt(int64(runtime.GOMAXPROCS(0)), 10), - "num_goroutine": strconv.FormatInt(int64(runtime.NumGoroutine()), 10), - "num_cpu": strconv.FormatInt(int64(runtime.NumCPU()), 10), - "external_ip": findIp("http://httpbin.org/ip"), - } - return info -} - -func findIp(url string) string { - ip := "" - client := &http.Client{ - Transport: &http.Transport{ - DisableKeepAlives: false, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - Timeout: time.Duration(1 * time.Second), - } - - req, _ := http.NewRequest("GET", url, nil) - res, err := client.Do(req) - if err != nil { - log.Error().Err(errors.Wrapf(err, "cannot connect to %s", url)).Msg("timeout") - return ip - } - - if res.Body != nil { - defer res.Body.Close() - - if res.StatusCode == http.StatusOK { - contents, err := ioutil.ReadAll(res.Body) - if err != nil { - return ip - } - jsonMap := make(map[string]string) - err = json.Unmarshal([]byte(contents), &jsonMap) - if err != nil { - return ip - } - return jsonMap["origin"] - } - } - - return ip -} diff --git a/pkg/server/server.go b/pkg/server/server.go deleted file mode 100644 index aa3651e..0000000 --- a/pkg/server/server.go +++ /dev/null @@ -1,133 +0,0 @@ -package server - -import ( - "context" - "net/http" - "net/http/pprof" - "os" - "runtime" - "sync/atomic" - "time" - - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/rs/zerolog/log" - "github.com/stefanprodan/k8s-podinfo/pkg/fscache" -) - -var ( - healthy int32 - ready int32 - dataPath string - watcher *fscache.Watcher -) - -type Server struct { - mux *http.ServeMux -} - -func NewServer(options ...func(*Server)) *Server { - - s := &Server{mux: http.NewServeMux()} - - for _, f := range options { - f(s) - } - - s.mux.HandleFunc("/", s.index) - s.mux.HandleFunc("/healthz", s.healthz) - s.mux.HandleFunc("/readyz", s.readyz) - s.mux.HandleFunc("/readyz/enable", s.enable) - s.mux.HandleFunc("/readyz/disable", s.disable) - s.mux.HandleFunc("/echo", s.echo) - s.mux.HandleFunc("/echoheaders", s.echoHeaders) - s.mux.HandleFunc("/backend", s.backend) - s.mux.HandleFunc("/job", s.job) - s.mux.HandleFunc("/read", s.read) - s.mux.HandleFunc("/write", s.write) - s.mux.HandleFunc("/error", s.error) - s.mux.HandleFunc("/panic", s.panic) - s.mux.HandleFunc("/configs", s.configs) - s.mux.HandleFunc("/version", s.version) - s.mux.Handle("/metrics", promhttp.Handler()) - - // Register pprof handlers - s.mux.HandleFunc("/debug/pprof/", pprof.Index) - s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace) - - // API - s.mux.HandleFunc("/api/info", s.apiInfo) - s.mux.HandleFunc("/api/echo", s.apiEcho) - - return s -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Server", runtime.Version()) - - s.mux.ServeHTTP(w, r) -} - -func ListenAndServe(port string, timeout time.Duration, stopCh <-chan struct{}) { - inst := NewInstrument() - srv := &http.Server{ - Addr: ":" + port, - Handler: inst.Wrap(NewServer()), - ReadTimeout: 5 * time.Second, - WriteTimeout: 1 * time.Minute, - IdleTimeout: 15 * time.Second, - } - - atomic.StoreInt32(&healthy, 1) - atomic.StoreInt32(&ready, 1) - - // local storage path - dataPath = os.Getenv("data") - if len(dataPath) < 1 { - dataPath = "/data" - } - - // config path - configPath := os.Getenv("configPath") - if len(configPath) > 0 { - var err error - watcher, err = fscache.NewWatch(configPath) - if err != nil { - log.Error().Err(err).Msgf("%s watch error", configPath) - } else { - watcher.Watch() - } - } - - // run server in background - go func() { - if err := srv.ListenAndServe(); err != http.ErrServerClosed { - log.Fatal().Err(err).Msg("HTTP server crashed") - } - }() - - // wait for SIGTERM or SIGINT - <-stopCh - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - // all calls to /healthz and /readyz will fail from now on - atomic.StoreInt32(&healthy, 0) - atomic.StoreInt32(&ready, 0) - - log.Info().Msgf("Shutting down HTTP server with timeout: %v", timeout) - - // wait for Kubernetes readiness probe - // to remove this instance from the load balancer - // the readiness check interval must lower than the timeout - time.Sleep(timeout) - - // attempt graceful shutdown - if err := srv.Shutdown(ctx); err != nil { - log.Error().Err(err).Msg("HTTP server graceful shutdown failed") - } else { - log.Info().Msg("HTTP server stopped") - } -}