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 # 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"
"gitea.illuad.fr/adrien/middleman/flag" "gitea.illuad.fr/adrien/middleman/flag"
"gitea.illuad.fr/adrien/middleman/pkg/fastrp" "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/rs/zerolog/log"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -16,6 +18,7 @@ import (
"net/url" "net/url"
"regexp" "regexp"
"slices" "slices"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -70,7 +73,7 @@ const (
// defaultShutdownTimeout is the default server shutdown timeout. // defaultShutdownTimeout is the default server shutdown timeout.
defaultShutdownTimeout = 5 * time.Second defaultShutdownTimeout = 5 * time.Second
// defaultHealthcheckRetry is the default number of healthcheck retry // defaultHealthcheckRetry is the default number of healthcheck retry
defaultHealthcheckRetry uint64 = 3 defaultHealthcheckRetry uint = 3
// defaultHealthEndpoint is the default health endpoint. // defaultHealthEndpoint is the default health endpoint.
defaultHealthEndpoint = "/healthz" defaultHealthEndpoint = "/healthz"
// defaultHealthListenAddr is the proxy health endpoint default listen address and port. // 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) { 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") log.Debug().Str("remote_addr", r.RemoteAddr).Str("method", r.Method).Str("path", r.URL.Path).Msg("incoming request")
if mr, ok := containerMethodRegex["*"]; ok { if mr, ok := containerMethodRegex["*"]; ok {
// metric: wildcard container match
if code := ph.checkMethodAndRegex(mr, r, ""); code != http.StatusOK { 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) http.Error(w, http.StatusText(code), code)
return return
} }
// metric: request successfully match the regex for a wildcard container
ph.frp.ServeHTTP(w, r) ph.frp.ServeHTTP(w, r)
return return
} }
// metric: non wildcard container match
host, _, _ := net.SplitHostPort(r.RemoteAddr) host, _, _ := net.SplitHostPort(r.RemoteAddr)
for containerName, mr := range containerMethodRegex { for containerName, mr := range containerMethodRegex {
if ph.isContainerAuthorized(containerName, host) { if ph.isContainerAuthorized(containerName, host) {
if code := ph.checkMethodAndRegex(mr, r, containerName); code != http.StatusOK { 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) http.Error(w, http.StatusText(code), code)
return return
} }
// metric: request successfully match the regex for a non wildcard container
ph.frp.ServeHTTP(w, r) ph.frp.ServeHTTP(w, r)
return 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") logDeniedRequest(r, http.StatusUnauthorized, "this container is not on the list of authorized ones")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 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 { func (ph *ProxyHandler) isContainerAuthorized(containerName, host string) bool {
resolvedIPs, err := net.LookupIP(containerName) resolvedIPs, err := net.LookupIP(containerName)
if err != nil { if err != nil {
// metric: failed to resolve
return false return false
} }
for resolvedIP := range slices.Values(resolvedIPs) { 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 return true
} }
} }
// metric: container is unauthorized
return false 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 { func (ph *ProxyHandler) checkMethodAndRegex(mr methodRegex, r *http.Request, containerName string) int {
req, ok := mr[r.Method] req, ok := mr[r.Method]
if !ok { 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") logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container")
return http.StatusMethodNotAllowed return http.StatusMethodNotAllowed
} }
if !req.MatchString(r.URL.Path) { 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") logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method")
return http.StatusForbidden 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") logAuthorizedRequest(r, containerName, "incoming request matches a registered regular expression")
return http.StatusOK return http.StatusOK
} }
@ -207,6 +228,20 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err 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)) ap, err := parseAddrPorts(command.String(listenAddrFlagName), command.String(healthListenAddrFlagName))
if err != nil { if err != nil {
return err return err
@ -239,7 +274,7 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
if retry == 0 { if retry == 0 {
return err return err
} }
log.Err(err).Uint64("retry_remaining", retry).Send() log.Err(err).Uint("retry_remaining", retry).Send()
retry-- retry--
} }
} }
@ -283,7 +318,7 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
Stringer("shutdown_timeout", command.Duration(shutdownTimeoutFlagName)). Stringer("shutdown_timeout", command.Duration(shutdownTimeoutFlagName)).
Bool("docker_socket_healthcheck_disabled", command.Bool(noDockerSocketHealthcheckFlagName)). Bool("docker_socket_healthcheck_disabled", command.Bool(noDockerSocketHealthcheckFlagName)).
Bool("shutdown_on_failure_disabled", command.Bool(noShutdownFlagName)). 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)). Str("health_endpoint", command.String(healthEndpointFlagName)).
Stringer("health_endpoint_listen_addr", ap.healthAddrPort). Stringer("health_endpoint_listen_addr", ap.healthAddrPort).
Strs("requests", command.StringSlice(addRequestsFlagName)). Strs("requests", command.StringSlice(addRequestsFlagName)).
@ -293,6 +328,9 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
<-ctx.Done() <-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), command.Duration(shutdownTimeoutFlagName)) shutdownCtx, cancel := context.WithTimeout(context.Background(), command.Duration(shutdownTimeoutFlagName))
defer cancel() defer cancel()
if err = metricsSrv.Close(); err != nil {
log.Err(err).Send()
}
if err = healthSrv.Close(); err != nil { if err = healthSrv.Close(); err != nil {
log.Err(err).Send() log.Err(err).Send()
} }

10
go.mod
View File

@ -4,12 +4,20 @@ go 1.24.2
require ( require (
github.com/rs/zerolog v1.34.0 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 golang.org/x/sync v0.13.0
) )
require ( 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-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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 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/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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 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.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 h1:bNnl8pFI5dxPOjeONvFCDFoECLQsceDG4ejahs4Jtxk=
github.com/urfave/cli/v3 v3.1.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 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 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 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.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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"})
)