Files
akvorado/common/httpserver/root.go
Vincent Bernat bd37c1d553 common/httpserver: listen on an abstract Unix socket
And make healthcheck command use it by default. This makes the
healthcheck command works whatever port the user has configured for the
HTTP service.
2025-11-24 11:29:45 +01:00

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)
}