add first metrics

This commit is contained in:
Adrien PONSIN 2025-04-30 22:40:03 +02:00
parent 59b95ea315
commit 0dbd2c72eb
No known key found for this signature in database
GPG Key ID: 7B4D4A32C05C475E
6 changed files with 164 additions and 6 deletions

34
Dockerfile Normal file
View File

@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
FROM golang:1.24.2 AS build
WORKDIR /go/src/middleman
COPY . .
ARG VERSION=dev
# echo -n "not tied to a commit" | sha1sum
ARG COMMIT=dbf242029aeedcfe71af2e5843474d8f8e2e9d63
RUN go build -ldflags="-s -w -B gobuildid -X main.version=$VERSION -X main.commit=$COMMIT -linkmode external -extldflags -static" -buildmode=pie -trimpath -o middleman cmd/middleman/main.go
FROM scratch
COPY --from=build /etc/group /etc/group
COPY --from=build /etc/passwd /etc/passwd
WORKDIR /
COPY --from=build /go/src/middleman/middleman .
EXPOSE 2375/tcp
USER nobody:nogroup
HEALTHCHECK --interval=5s --timeout=2s CMD ["/middleman", "healthcheck"]
ENTRYPOINT ["/middleman"]
CMD ["serve"]

View File

@ -1,3 +1,42 @@
# middleman
Securely mount the Docker socket: apply fine-grained access control to Docker socket HTTP requests.
Securely mount the Docker socket: apply fine-grained access control to Docker socket HTTP requests.
```shell
$ openssl ecparam -check -name prime256v1 -genkey -noout -out key.pem -rand /dev/urandom
$ vim traefik.cfg
```
```
[req]
distinguished_name = req_distinguished_name
prompt = no
default_md = sha256
[req_distinguished_name]
CN = infra.local
[database]
basicConstraints = CA:false
authorityKeyIdentifier = keyid,issuer
subjectKeyIdentifier = hash
keyUsage = digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = infra.local
DNS.2 = *.infra.local
DNS.3 = localhost
IP.1 = 127.0.0.1
```
```shell
$ openssl req -new -key key.pem -out csr.pem -rand /dev/urandom -config traefik.cfg
$ openssl x509 -req -days 365 -in csr.pem -signkey key.pem -out cert.pem -extensions properties -extfile traefik.cfg
```
```shell
$ docker compose --project-name infra up --detach
$ docker compose --project-name infra down --remove-orphans --volumes
```

View File

@ -7,6 +7,8 @@ import (
"gitea.illuad.fr/adrien/middleman"
"gitea.illuad.fr/adrien/middleman/flag"
"gitea.illuad.fr/adrien/middleman/pkg/fastrp"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup"
@ -16,6 +18,7 @@ import (
"net/url"
"regexp"
"slices"
"strconv"
"strings"
"time"
)
@ -70,7 +73,7 @@ const (
// defaultShutdownTimeout is the default server shutdown timeout.
defaultShutdownTimeout = 5 * time.Second
// defaultHealthcheckRetry is the default number of healthcheck retry
defaultHealthcheckRetry uint64 = 3
defaultHealthcheckRetry uint = 3
// defaultHealthEndpoint is the default health endpoint.
defaultHealthEndpoint = "/healthz"
// defaultHealthListenAddr is the proxy health endpoint default listen address and port.
@ -130,27 +133,39 @@ func Serve(group *errgroup.Group) *cli.Command {
}
}
// https://www.civo.com/learn/build-your-own-prometheus-exporter-in-go
func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
middleman.RequestHandled.Inc()
timer := prometheus.NewTimer(middleman.ProcessingRequest.WithLabelValues(r.URL.Path))
defer timer.ObserveDuration()
// metric: incoming request handled
log.Debug().Str("remote_addr", r.RemoteAddr).Str("method", r.Method).Str("path", r.URL.Path).Msg("incoming request")
if mr, ok := containerMethodRegex["*"]; ok {
// metric: wildcard container match
if code := ph.checkMethodAndRegex(mr, r, ""); code != http.StatusOK {
// metric: request failed to match the regex for wildcard container
http.Error(w, http.StatusText(code), code)
return
}
// metric: request successfully match the regex for a wildcard container
ph.frp.ServeHTTP(w, r)
return
}
// metric: non wildcard container match
host, _, _ := net.SplitHostPort(r.RemoteAddr)
for containerName, mr := range containerMethodRegex {
if ph.isContainerAuthorized(containerName, host) {
if code := ph.checkMethodAndRegex(mr, r, containerName); code != http.StatusOK {
// metric: request failed to match the regex for a non wildcard container
http.Error(w, http.StatusText(code), code)
return
}
// metric: request successfully match the regex for a non wildcard container
ph.frp.ServeHTTP(w, r)
return
}
}
// metric: container/client? is not authorized to dial with the proxy
logDeniedRequest(r, http.StatusUnauthorized, "this container is not on the list of authorized ones")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
@ -158,13 +173,16 @@ func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (ph *ProxyHandler) isContainerAuthorized(containerName, host string) bool {
resolvedIPs, err := net.LookupIP(containerName)
if err != nil {
// metric: failed to resolve
return false
}
for resolvedIP := range slices.Values(resolvedIPs) {
if resolvedIP.Equal(net.ParseIP(host)) {
if resolvedIP.Equal(net.ParseIP(host)) { // Should I check net.ParseIP do not return a nil value?
// metric: container is authorized
return true
}
}
// metric: container is unauthorized
return false
}
@ -191,13 +209,16 @@ func logAuthorizedRequest(r *http.Request, containerName, message string) {
func (ph *ProxyHandler) checkMethodAndRegex(mr methodRegex, r *http.Request, containerName string) int {
req, ok := mr[r.Method]
if !ok {
middleman.ProcessedRequest.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(http.StatusMethodNotAllowed))
logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container")
return http.StatusMethodNotAllowed
}
if !req.MatchString(r.URL.Path) {
middleman.ProcessedRequest.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(http.StatusForbidden))
logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method")
return http.StatusForbidden
}
middleman.ProcessedRequest.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(http.StatusOK))
logAuthorizedRequest(r, containerName, "incoming request matches a registered regular expression")
return http.StatusOK
}
@ -207,6 +228,20 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
if err := ctx.Err(); err != nil {
return err
}
registry := prometheus.NewRegistry()
metricsMux := http.NewServeMux()
metricsMux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
registry.MustRegister(middleman.RequestHandled, middleman.ProcessingRequest, middleman.ProcessedRequest)
metricsSrv := http.Server{
Addr: ":8107",
Handler: metricsMux,
}
s.group.Go(func() error {
if err := metricsSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
ap, err := parseAddrPorts(command.String(listenAddrFlagName), command.String(healthListenAddrFlagName))
if err != nil {
return err
@ -239,7 +274,7 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
if retry == 0 {
return err
}
log.Err(err).Uint64("retry_remaining", retry).Send()
log.Err(err).Uint("retry_remaining", retry).Send()
retry--
}
}
@ -283,7 +318,7 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
Stringer("shutdown_timeout", command.Duration(shutdownTimeoutFlagName)).
Bool("docker_socket_healthcheck_disabled", command.Bool(noDockerSocketHealthcheckFlagName)).
Bool("shutdown_on_failure_disabled", command.Bool(noShutdownFlagName)).
Uint64("number_of_healthcheck_retry", command.Uint(healthcheckRetryFlagName)).
Uint("number_of_healthcheck_retry", command.Uint(healthcheckRetryFlagName)).
Str("health_endpoint", command.String(healthEndpointFlagName)).
Stringer("health_endpoint_listen_addr", ap.healthAddrPort).
Strs("requests", command.StringSlice(addRequestsFlagName)).
@ -293,6 +328,9 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), command.Duration(shutdownTimeoutFlagName))
defer cancel()
if err = metricsSrv.Close(); err != nil {
log.Err(err).Send()
}
if err = healthSrv.Close(); err != nil {
log.Err(err).Send()
}

10
go.mod
View File

@ -4,12 +4,20 @@ go 1.24.2
require (
github.com/rs/zerolog v1.34.0
github.com/urfave/cli/v3 v3.1.1
github.com/urfave/cli/v3 v3.3.2
golang.org/x/sync v0.13.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
golang.org/x/sys v0.32.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)

20
go.sum
View File

@ -1,3 +1,7 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -9,7 +13,17 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
@ -20,6 +34,10 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/urfave/cli/v3 v3.1.1 h1:bNnl8pFI5dxPOjeONvFCDFoECLQsceDG4ejahs4Jtxk=
github.com/urfave/cli/v3 v3.1.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/urfave/cli/v3 v3.2.0 h1:m8WIXY0U9LCuUl5r+0fqLWDhNYWt6qvlW+GcF4EoXf8=
github.com/urfave/cli/v3 v3.2.0/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o=
github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
@ -36,3 +54,5 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=

19
metric.go Normal file
View File

@ -0,0 +1,19 @@
package middleman
import "github.com/prometheus/client_golang/prometheus"
var (
RequestHandled = prometheus.NewCounter(prometheus.CounterOpts{
Name: "http_requests_handled_total",
Help: "Total number of HTTP requests handled",
})
ProcessingRequest = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "time_spent_processing_request_seconds",
Help: "Number of seconds spent processing a request",
}, []string{"path"})
ProcessedRequest = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_processed_total",
Help: "Total number of HTTP requests processed",
ConstLabels: nil,
}, []string{"method", "path", "status"})
)