mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
And make healthcheck command use it by default. This makes the healthcheck command works whatever port the user has configured for the HTTP service.
210 lines
5.6 KiB
Go
210 lines
5.6 KiB
Go
// SPDX-FileCopyrightText: 2022 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
// Package httpserver handles the internal web server for akvorado.
|
|
package httpserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/chenyahui/gin-cache/persist"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/hlog"
|
|
"gopkg.in/tomb.v2"
|
|
|
|
"akvorado/common/daemon"
|
|
"akvorado/common/reporter"
|
|
)
|
|
|
|
// Component represents the HTTP compomenent.
|
|
type Component struct {
|
|
r *reporter.Reporter
|
|
d *Dependencies
|
|
t tomb.Tomb
|
|
config Configuration
|
|
|
|
mux *http.ServeMux
|
|
metrics metrics
|
|
address net.Addr
|
|
serviceName string
|
|
|
|
// GinRouter is the router exposed for /api
|
|
GinRouter *gin.Engine
|
|
cacheStore persist.CacheStore
|
|
}
|
|
|
|
// Dependencies define the dependencies of the HTTP component.
|
|
type Dependencies struct {
|
|
Daemon daemon.Component
|
|
}
|
|
|
|
// New creates a new HTTP component.
|
|
func New(r *reporter.Reporter, serviceName string, configuration Configuration, dependencies Dependencies) (*Component, error) {
|
|
c := Component{
|
|
r: r,
|
|
d: &dependencies,
|
|
config: configuration,
|
|
|
|
mux: http.NewServeMux(),
|
|
serviceName: serviceName,
|
|
GinRouter: gin.New(),
|
|
}
|
|
c.initMetrics()
|
|
c.d.Daemon.Track(&c.t, "common/http")
|
|
c.GinRouter.Use(gin.Recovery())
|
|
c.AddHandler("/api/", c.GinRouter)
|
|
if configuration.Profiler {
|
|
c.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
|
c.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
c.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
c.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
c.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
|
runtime.SetBlockProfileRate(int(10 * time.Millisecond.Nanoseconds())) // 1/10ms
|
|
runtime.SetMutexProfileFraction(1000) // 0.1%
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// AddHandler registers a new handler for the web server
|
|
func (c *Component) AddHandler(location string, handler http.Handler) {
|
|
l := c.r.With().Str("handler", location).Logger()
|
|
handler = hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
|
level := zerolog.InfoLevel
|
|
if r.URL.Path == "/api/v0/metrics" || r.URL.Path == "/api/v0/healthcheck" {
|
|
level = zerolog.DebugLevel
|
|
}
|
|
hlog.FromRequest(r).WithLevel(level).
|
|
Str("method", r.Method).
|
|
Stringer("url", r.URL).
|
|
Str("ip", r.RemoteAddr).
|
|
Str("user-agent", r.Header.Get("User-Agent")).
|
|
Int("status", status).
|
|
Int("size", size).
|
|
Dur("duration", duration).
|
|
Msg("HTTP request")
|
|
})(handler)
|
|
handler = hlog.NewHandler(l)(handler)
|
|
handler = promhttp.InstrumentHandlerResponseSize(
|
|
c.metrics.sizes.MustCurryWith(prometheus.Labels{"handler": location}), handler)
|
|
handler = promhttp.InstrumentHandlerCounter(
|
|
c.metrics.requests.MustCurryWith(prometheus.Labels{"handler": location}), handler)
|
|
handler = promhttp.InstrumentHandlerDuration(
|
|
c.metrics.durations.MustCurryWith(prometheus.Labels{"handler": location}), handler)
|
|
handler = promhttp.InstrumentHandlerInFlight(c.metrics.inflights, handler)
|
|
|
|
c.mux.Handle(location, handler)
|
|
}
|
|
|
|
// Start starts the HTTP component.
|
|
func (c *Component) Start() error {
|
|
var err error
|
|
c.cacheStore, err = c.config.Cache.Config.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
server := &http.Server{Handler: c.mux}
|
|
|
|
for _, lc := range []struct {
|
|
network string
|
|
address string
|
|
fatal bool
|
|
}{
|
|
// The regular one.
|
|
{
|
|
network: "tcp",
|
|
address: c.config.Listen,
|
|
fatal: true,
|
|
},
|
|
// Using abstract unix sockets for healthchecking
|
|
{
|
|
network: "unix",
|
|
address: "@akvorado",
|
|
fatal: false,
|
|
},
|
|
{
|
|
network: "unix",
|
|
address: fmt.Sprintf("@akvorado/%s", c.serviceName),
|
|
fatal: false,
|
|
},
|
|
} {
|
|
if lc.address == "" {
|
|
continue
|
|
}
|
|
if lc.network == "unix" && runtime.GOOS != "linux" {
|
|
continue
|
|
}
|
|
|
|
// Most of the time, if we have an error, it's here!
|
|
c.r.Info().Str("listen", lc.address).Msg("starting HTTP server")
|
|
listener, err := net.Listen(lc.network, lc.address)
|
|
if err != nil {
|
|
if lc.fatal {
|
|
return fmt.Errorf("unable to listen to %v: %w", c.config.Listen, err)
|
|
}
|
|
c.r.Info().Err(err).Msg("cannot start HTTP server")
|
|
continue
|
|
}
|
|
if lc.network == "tcp" {
|
|
c.address = listener.Addr()
|
|
}
|
|
server.Addr = listener.Addr().String()
|
|
|
|
// Serve requests
|
|
c.t.Go(func() error {
|
|
if err := server.Serve(listener); err != http.ErrServerClosed {
|
|
c.r.Err(err).Str("listen", lc.address).Msg("unable to start HTTP server")
|
|
return fmt.Errorf("unable to start HTTP server: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
// Gracefully stop when asked to
|
|
c.t.Go(func() error {
|
|
<-c.t.Dying()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := server.Shutdown(ctx); err != nil {
|
|
c.r.Err(err).Msg("unable to shutdown HTTP server")
|
|
return fmt.Errorf("unable to shutdown HTTP server: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// In case we have no server
|
|
c.t.Go(func() error {
|
|
<-c.t.Dying()
|
|
return nil
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the HTTP component
|
|
func (c *Component) Stop() error {
|
|
c.r.Info().Msg("stopping HTTP component")
|
|
defer c.r.Info().Msg("HTTP component stopped")
|
|
c.t.Kill(nil)
|
|
return c.t.Wait()
|
|
}
|
|
|
|
// LocalAddr returns the address the HTTP server is listening to.
|
|
func (c *Component) LocalAddr() net.Addr {
|
|
return c.address
|
|
}
|
|
|
|
func init() {
|
|
// Disable proxy for client
|
|
http.DefaultTransport.(*http.Transport).Proxy = nil
|
|
http.DefaultClient.Timeout = 30 * time.Second
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|