From e8d3a207816c0158ef7e011ffc6379c5f3157a4f Mon Sep 17 00:00:00 2001 From: dcarrillo Date: Mon, 3 Nov 2025 18:36:13 +0100 Subject: [PATCH] New feature: prometheus metrics endpoint (#46) --- Makefile | 4 +- README.md | 71 ++++----- cmd/whatismyip.go | 10 ++ go.mod | 48 +++++-- go.sum | 105 +++++++++----- integration-tests/integration_test.go | 21 +++ internal/metrics/metrics.go | 122 ++++++++++++++++ internal/metrics/metrics_test.go | 200 ++++++++++++++++++++++++++ internal/setting/app.go | 7 + resolver/setup.go | 4 + server/prometheus.go | 48 +++++++ service/geo.go | 3 + service/port_scanner.go | 3 + 13 files changed, 564 insertions(+), 82 deletions(-) create mode 100644 internal/metrics/metrics.go create mode 100644 internal/metrics/metrics_test.go create mode 100644 server/prometheus.go diff --git a/Makefile b/Makefile index 81ece94..23e9a99 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ docker-run: docker-build-dev docker run --tty --interactive --rm \ --publish 8080:8080/tcp \ --publish 8081:8081/tcp \ + --publish 9100:9100/tcp \ --publish 8081:8081/udp \ --volume ${PWD}/test:/test \ ${DOCKER_URL}:${VERSION} \ @@ -61,4 +62,5 @@ docker-run: docker-build-dev -tls-bind :8081 \ -tls-crt /test/server.pem \ -tls-key /test/server.key \ - -enable-http3 + -enable-http3 \ + -metrics-bind :9100 diff --git a/README.md b/README.md index 6964e43..61ce0f1 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,27 @@ [![GitHub release](https://img.shields.io/github/release/dcarrillo/whatismyip.svg)](https://github.com/dcarrillo/whatismyip/releases/) [![License Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE) -- [What is my IP address](#what-is-my-ip-address) - - [Features](#features) - - [Endpoints](#endpoints) - - [DNS discovery](#dns-discovery) - - [Build](#build) - - [Usage](#usage) - - [Examples](#examples) - - [Run a default TCP server](#run-a-default-tcp-server) - - [Run a default TCP server with geo information enabled](#run-a-default-tcp-server-with-geo-information-enabled) - - [Run a TLS (HTTP/2) and enable "what is my DNS" with geo information](#run-a-tls-http2-and-enable-what-is-my-dns-with-geo-information) - - [Run an HTTP/3 server](#run-an-http3-server) - - [Run a default TCP server with a custom template and trust a pair of custom headers set by an upstream proxy](#run-a-default-tcp-server-with-a-custom-template-and-trust-a-pair-of-custom-headers-set-by-an-upstream-proxy) - - [Download](#download) - - [Docker](#docker) - - [Run a container locally using test databases](#run-a-container-locally-using-test-databases) - - [From Docker Hub](#from-docker-hub) +- [Features](#features) +- [Endpoints](#endpoints) +- [DNS discovery](#dns-discovery) +- [Build](#build) +- [Usage](#usage) +- [Examples](#examples) + - [Run a default TCP server](#run-a-default-tcp-server) + - [Run a default TCP server with geo information enabled](#run-a-default-tcp-server-with-geo-information-enabled) + - [Run a TLS (HTTP/2) and enable "what is my DNS" with geo information](#run-a-tls-http2-and-enable-what-is-my-dns-with-geo-information) + - [Run an HTTP/3 server](#run-an-http3-server) + - [Run a default TCP server with a custom template and trust a pair of custom headers set by an upstream proxy](#run-a-default-tcp-server-with-a-custom-template-and-trust-a-pair-of-custom-headers-set-by-an-upstream-proxy) +- [Download](#download) +- [Docker](#docker) + - [Run a container locally using test databases](#run-a-container-locally-using-test-databases) + - [From Docker Hub](#from-docker-hub) > [!NOTE] +> Since version 3.2.0, an optional prometheus metrics endpoint is available. +> > Since version 3.0.0, the geodb database is not mandatory; not adding the flags will disable the geo feature. +> > Since version 2.3.0, the application includes an optional client [DNS discovery](#dns-discovery) feature. Just another "what is my IP address" service, including geolocation, TCP open port checking, and headers information. Written in Go with high performance in mind, @@ -53,12 +55,13 @@ curl -L dns.ifconfig.es - TLS and HTTP/2. - Experimental HTTP/3 support. HTTP/3 requires a TLS server running (`-tls-bind`), as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server. -- Beta DNS discovery: A best-effort approach to discovering the DNS server that is resolving the client's requests. +- DNS discovery: A best-effort approach to discovering the DNS server that is resolving the client's requests. - Can run behind a proxy by trusting a custom header (usually `X-Real-IP`) to figure out the source IP address. It also supports a custom header to resolve the client port, if the proxy can only add a header for the IP (for example a fixed header from CDNs) the client port is shown as unknown. - IPv4 and IPv6. - Geolocation info including ASN. This feature is possible thanks to [maxmind](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data?lang=en) GeoLite2 databases. In order to use these databases, a license key is needed. Please visit Maxmind site for further instructions and get a free license. - Checking TCP open ports. - High performance. +- Prometheus metrics endpoint: Exports metrics (on a separete process/port) for HTTP requests, request duration, geo lookups, port scans, and DNS queries. - Self-contained server that can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored. - HTML templates for the landing page. - Text plain and JSON output. @@ -117,7 +120,7 @@ curl $(cat /proc/sys/kernel/random/uuid).dns.ifconfig.es ## Build -Golang >= 1.22 is required. +Golang >= 1.24 is required. `make build` @@ -126,33 +129,35 @@ Golang >= 1.22 is required. ```text Usage of whatismyip: -bind string - Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080") + Listening address (see https://pkg.go.dev/net?#Listen) (default ":8080") -disable-scan - Disable TCP port scanning functionality + Disable TCP port scanning functionality -enable-http3 - Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server. + Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server. -enable-secure-headers - Add sane security-related headers to every response + Add sane security-related headers to every response -geoip2-asn string - Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory) + Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory) -geoip2-city string - Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory) + Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory) + -metrics-bind string + Listening address for Prometheus metrics endpoint (see https://pkg.go.dev/net?#Listen). It enables the metrics available at the given address/port via the /metrics endpoint. -resolver string - Path to the resolver configuration. It actually enables the resolver for DNS client discovery. + Path to the resolver configuration. It actually enables the resolver for DNS client discovery. -template string - Path to the template file + Path to the template file -tls-bind string - Listening address for TLS (see https://pkg.go.dev/net?#Listen) + Listening address for TLS (see https://pkg.go.dev/net?#Listen) -tls-crt string - When using TLS, path to certificate file + When using TLS, path to certificate file -tls-key string - When using TLS, path to private key file + When using TLS, path to private key file -trusted-header string - Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown' + Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown' -trusted-port-header string - Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory + Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory -version - Output version information and exit + Output version information and exit ``` ## Examples @@ -197,7 +202,7 @@ Download the latest version from [github](https://github.com/dcarrillo/whatismyi ## Docker -An ultra-light (~4MB) image is available on [docker hub](https://hub.docker.com/r/dcarrillo/whatismyip). Since version `2.1.2`, the binary is compressed using [upx](https://github.com/upx/upx). +An ultra-light (~6MB) image is available on [docker hub](https://hub.docker.com/r/dcarrillo/whatismyip). Since version `2.1.2`, the binary is compressed using [upx](https://github.com/upx/upx). ### Run a container locally using test databases diff --git a/cmd/whatismyip.go b/cmd/whatismyip.go index 9e0eed9..e053aef 100644 --- a/cmd/whatismyip.go +++ b/cmd/whatismyip.go @@ -10,6 +10,7 @@ import ( "time" "github.com/dcarrillo/whatismyip/internal/httputils" + "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/resolver" "github.com/dcarrillo/whatismyip/server" @@ -54,6 +55,11 @@ func main() { router.Setup(engine, geoSvc) servers = slices.Concat(servers, setupHTTPServers(context.Background(), engine.Handler())) + if setting.App.PrometheusAddress != "" { + prometheusServer := server.NewPrometheusServer(context.Background()) + servers = append(servers, prometheusServer) + } + whatismyip := server.Setup(servers, geoSvc) whatismyip.Run() } @@ -65,6 +71,10 @@ func setupEngine() *gin.Engine { } engine := gin.New() engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter), gin.Recovery()) + if setting.App.PrometheusAddress != "" { + metrics.Enable() + engine.Use(metrics.GinMiddleware()) + } if setting.App.EnableSecureHeaders { engine.Use(secure.New(secure.Config{ BrowserXssFilter: true, diff --git a/go.mod b/go.mod index 1a56369..b3691d5 100644 --- a/go.mod +++ b/go.mod @@ -10,20 +10,25 @@ require ( github.com/miekg/dns v1.1.68 github.com/oschwald/maxminddb-golang v1.13.1 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/quic-go/quic-go v0.54.1 + github.com/prometheus/client_golang v1.23.2 + github.com/quic-go/quic-go v0.55.0 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.36.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + codeberg.org/chavacava/garif v0.2.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.1 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect @@ -33,25 +38,32 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.9 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgechev/dots v1.0.0 // indirect + github.com/mgechev/revive v1.12.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect @@ -61,19 +73,24 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect @@ -84,14 +101,15 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/mock v0.6.0 // indirect - golang.org/x/arch v0.21.0 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.44.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect - google.golang.org/protobuf v1.36.9 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index d7d316c..13342d0 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,27 @@ +codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= +codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= -github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -37,10 +45,14 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/secure v1.1.2 h1:6G8/NCOTSywWY7TeaH/0Yfaa6bfkE5ukkqtIm7lK11U= github.com/gin-contrib/secure v1.1.2/go.mod h1:xI3jI5/BpOYMCBtjgmIVrMA3kI7y9LwCFxs+eLf5S3w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -60,8 +72,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -76,26 +88,36 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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/mgechev/dots v1.0.0 h1:o+4OJ3OjWzgQHGJXKfJ8rbH4dqDugu5BiEy84nxg0k4= +github.com/mgechev/dots v1.0.0/go.mod h1:rykuMydC9t3wfkM+ccYH3U3ss03vZGg6h3hmOznXLH0= +github.com/mgechev/revive v1.12.0 h1:Q+/kkbbwerrVYPv9d9efaPGmAO/NsxwW/nE6ahpQaCU= +github.com/mgechev/revive v1.12.0/go.mod h1:VXsY2LsTigk8XU9BpZauVLjVrhICMOV3k1lpB3CXrp8= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -117,6 +139,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -133,16 +157,26 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= -github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -152,7 +186,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= @@ -163,8 +198,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -187,25 +222,29 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= -golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -221,22 +260,22 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -247,8 +286,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/integration-tests/integration_test.go b/integration-tests/integration_test.go index 914cf12..9102939 100644 --- a/integration-tests/integration_test.go +++ b/integration-tests/integration_test.go @@ -98,6 +98,7 @@ func TestContainerIntegration(t *testing.T) { "8000:8000", "8001:8001", "8001:8001/udp", + "9100:9100", "53531:53/udp", }, Cmd: []string{ @@ -110,6 +111,7 @@ func TestContainerIntegration(t *testing.T) { "-trusted-header", "X-Real-IP", "-enable-secure-headers", "-enable-http3", + "-metrics-bind", ":9100", "-resolver", "/resolver.yml", }, Files: []tc.ContainerFile{ @@ -232,6 +234,25 @@ func TestContainerIntegration(t *testing.T) { }) } + t.Run("RequestMetricsEndpoint", func(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:9100/metrics", nil) + assert.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + bodyStr := string(body) + + assert.Contains(t, bodyStr, "whatismyip_http_requests_total") + assert.Contains(t, bodyStr, "whatismyip_http_request_duration_seconds") + assert.Contains(t, bodyStr, "# HELP") + assert.Contains(t, bodyStr, "# TYPE") + }) + testWhatIsMyDNS(t) } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..0d93a9e --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,122 @@ +package metrics + +import ( + "fmt" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + enabled bool + initOnce sync.Once + requestsTotal *prometheus.CounterVec + requestDuration *prometheus.HistogramVec + requestsInFlight prometheus.Gauge + geoLookups *prometheus.CounterVec + portScans prometheus.Counter + dnsQueries *prometheus.CounterVec +) + +func Enable() { + initOnce.Do(func() { + enabled = true + + requestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "whatismyip_http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "path", "status"}, + ) + + requestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "whatismyip_http_request_duration_seconds", + Help: "HTTP request latency in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "path"}, + ) + + requestsInFlight = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "whatismyip_http_requests_in_flight", + Help: "Current number of HTTP requests being processed", + }, + ) + + geoLookups = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "whatismyip_geo_lookups_total", + Help: "Total number of geo lookups", + }, + []string{"type"}, + ) + + portScans = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "whatismyip_port_scans_total", + Help: "Total number of port scan requests", + }, + ) + + dnsQueries = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "whatismyip_dns_queries_total", + Help: "Total number of DNS queries", + }, + []string{"query_type", "rcode"}, + ) + }) +} + +func GinMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if !enabled { + c.Next() + return + } + + start := time.Now() + path := c.FullPath() + if path == "" { + path = "/404" // group 404s + } + + requestsInFlight.Inc() + defer requestsInFlight.Dec() + + c.Next() + + duration := time.Since(start).Seconds() + status := c.Writer.Status() + + requestsTotal.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%dxx", status/100)).Inc() + requestDuration.WithLabelValues(c.Request.Method, path).Observe(duration) + } +} + +func RecordGeoLookup(lookupType string) { + if !enabled { + return + } + geoLookups.WithLabelValues(lookupType).Inc() +} + +func RecordPortScan() { + if !enabled { + return + } + portScans.Inc() +} + +func RecordDNSQuery(queryType string, rcode string) { + if !enabled { + return + } + dnsQueries.WithLabelValues(queryType, rcode).Inc() +} diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go new file mode 100644 index 0000000..33c7a62 --- /dev/null +++ b/internal/metrics/metrics_test.go @@ -0,0 +1,200 @@ +package metrics + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" +) + +func TestDisabledMetrics_Middleware(t *testing.T) { + if enabled { + t.Skip("Skipping disabled test - metrics already enabled") + } + + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest("GET", "/test", nil) + + middleware := GinMiddleware() + + assert.NotPanics(t, func() { + middleware(c) + }) +} + +func TestDisabledMetrics_GeoLookup(t *testing.T) { + if enabled { + t.Skip("Skipping disabled test - metrics already enabled") + } + + assert.NotPanics(t, func() { + RecordGeoLookup("city") + }) +} + +func TestDisabledMetrics_PortScan(t *testing.T) { + if enabled { + t.Skip("Skipping disabled test - metrics already enabled") + } + + assert.NotPanics(t, func() { + RecordPortScan() + }) +} + +func TestDisabledMetrics_DNSQuery(t *testing.T) { + if enabled { + t.Skip("Skipping disabled test - metrics already enabled") + } + + assert.NotPanics(t, func() { + RecordDNSQuery("A", "NOERROR") + }) +} + +func TestEnable(t *testing.T) { + Enable() + + assert.True(t, enabled, "Enable() should set enabled to true") + assert.NotNil(t, requestsTotal, "requestsTotal should be initialized") + assert.NotNil(t, requestDuration, "requestDuration should be initialized") + assert.NotNil(t, requestsInFlight, "requestsInFlight should be initialized") + assert.NotNil(t, geoLookups, "geoLookups should be initialized") + assert.NotNil(t, portScans, "portScans should be initialized") + assert.NotNil(t, dnsQueries, "dnsQueries should be initialized") +} + +func TestEnableIdempotent(t *testing.T) { + Enable() + firstRequestsTotal := requestsTotal + + Enable() + Enable() + + assert.Equal(t, firstRequestsTotal, requestsTotal, "Enable() should be idempotent") +} + +func TestGinMiddleware_StatusCategories(t *testing.T) { + Enable() + + testCases := []struct { + status int + category string + }{ + {200, "2xx"}, + {201, "2xx"}, + {301, "3xx"}, + {404, "4xx"}, + {500, "5xx"}, + } + + for _, tc := range testCases { + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(GinMiddleware()) + router.GET("/test-status", func(c *gin.Context) { + c.Status(tc.status) + }) + + initialCount := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/test-status", tc.category)) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test-status", nil) + router.ServeHTTP(w, req) + + count := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/test-status", tc.category)) + assert.Equal(t, initialCount+1, count, "Expected count for category %s to increase by 1", tc.category) + } +} + +func TestGinMiddleware_404(t *testing.T) { + Enable() + + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(GinMiddleware()) + + initialCount := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/404", "4xx")) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/nonexistent-path", nil) + router.ServeHTTP(w, req) + + count := testutil.ToFloat64(requestsTotal.WithLabelValues("GET", "/404", "4xx")) + assert.Equal(t, initialCount+1, count, "Expected count to increase by 1 for empty path (404)") +} + +func TestGinMiddleware_RecordsDuration(t *testing.T) { + Enable() + + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(GinMiddleware()) + router.GET("/test-duration", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/test-duration", nil) + router.ServeHTTP(w, req) + + metric := requestDuration.WithLabelValues("GET", "/test-duration") + assert.NotNil(t, metric, "Expected histogram metric to exist") +} + +func TestRecordGeoLookup(t *testing.T) { + Enable() + + initialCityCount := testutil.ToFloat64(geoLookups.WithLabelValues("city")) + initialCountryCount := testutil.ToFloat64(geoLookups.WithLabelValues("asn")) + + RecordGeoLookup("city") + RecordGeoLookup("city") + RecordGeoLookup("asn") + + cityCount := testutil.ToFloat64(geoLookups.WithLabelValues("city")) + assert.Equal(t, initialCityCount+2, cityCount, "Expected city lookups to increase by 2") + + countryCount := testutil.ToFloat64(geoLookups.WithLabelValues("asn")) + assert.Equal(t, initialCountryCount+1, countryCount, "Expected country lookups to increase by 1") +} + +func TestRecordPortScan(t *testing.T) { + Enable() + + initialCount := testutil.ToFloat64(portScans) + + RecordPortScan() + RecordPortScan() + + count := testutil.ToFloat64(portScans) + assert.Equal(t, initialCount+2, count, "Expected port scans to increase by 2") +} + +func TestRecordDNSQuery(t *testing.T) { + Enable() + + initialACount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NOERROR")) + initialAAAACount := testutil.ToFloat64(dnsQueries.WithLabelValues("AAAA", "NOERROR")) + initialNXDOMAINCount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NXDOMAIN")) + + RecordDNSQuery("A", "NOERROR") + RecordDNSQuery("A", "NOERROR") + RecordDNSQuery("AAAA", "NOERROR") + RecordDNSQuery("A", "NXDOMAIN") + + aCount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NOERROR")) + assert.Equal(t, initialACount+2, aCount, "Expected A NOERROR queries to increase by 2") + + aaaaCount := testutil.ToFloat64(dnsQueries.WithLabelValues("AAAA", "NOERROR")) + assert.Equal(t, initialAAAACount+1, aaaaCount, "Expected AAAA NOERROR queries to increase by 1") + + nxdomainCount := testutil.ToFloat64(dnsQueries.WithLabelValues("A", "NXDOMAIN")) + assert.Equal(t, initialNXDOMAINCount+1, nxdomainCount, "Expected A NXDOMAIN queries to increase by 1") +} diff --git a/internal/setting/app.go b/internal/setting/app.go index 8db53c1..78b0373 100644 --- a/internal/setting/app.go +++ b/internal/setting/app.go @@ -37,6 +37,7 @@ type settings struct { TLSAddress string TLSCrtPath string TLSKeyPath string + PrometheusAddress string TrustedHeader string TrustedPortHeader string EnableSecureHeaders bool @@ -87,6 +88,12 @@ func Setup(args []string) (output string, err error) { ) flags.StringVar(&App.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file") flags.StringVar(&App.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file") + flags.StringVar( + &App.PrometheusAddress, + "metrics-bind", + "", + "Listening address for Prometheus metrics endpoint (see https://pkg.go.dev/net?#Listen). It enables the metrics available at the given address/port via the /metrics endpoint.", + ) flags.StringVar( &App.TrustedHeader, "trusted-header", diff --git a/resolver/setup.go b/resolver/setup.go index 8b933ab..00beabf 100644 --- a/resolver/setup.go +++ b/resolver/setup.go @@ -5,6 +5,7 @@ import ( "net" "strings" + "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/validator/uuid" "github.com/miekg/dns" @@ -59,6 +60,7 @@ func (rsv *Resolver) blackHole(w dns.ResponseWriter, r *dns.Msg) { msg.SetRcode(r, dns.RcodeRefused) w.WriteMsg(msg) logger(w, r.Question[0], msg.Rcode) + metrics.RecordDNSQuery(dns.TypeToString[r.Question[0].Qtype], dns.RcodeToString[msg.Rcode]) } func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) { @@ -78,6 +80,7 @@ func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) { logger(w, q, msg.Rcode) } w.WriteMsg(msg) + metrics.RecordDNSQuery(dns.TypeToString[q.Qtype], dns.RcodeToString[msg.Rcode]) return } } @@ -96,6 +99,7 @@ func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) { w.WriteMsg(msg) logger(w, q, msg.Rcode) + metrics.RecordDNSQuery(dns.TypeToString[q.Qtype], dns.RcodeToString[msg.Rcode]) } func (rsv *Resolver) getIP(question dns.Question, msg *dns.Msg) int { diff --git a/server/prometheus.go b/server/prometheus.go new file mode 100644 index 0000000..50848c1 --- /dev/null +++ b/server/prometheus.go @@ -0,0 +1,48 @@ +package server + +import ( + "context" + "errors" + "log" + "net/http" + + "github.com/dcarrillo/whatismyip/internal/setting" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type Prometheus struct { + server *http.Server + ctx context.Context +} + +func NewPrometheusServer(ctx context.Context) *Prometheus { + return &Prometheus{ + ctx: ctx, + } +} + +func (p *Prometheus) Start() { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + + p.server = &http.Server{ + Addr: setting.App.PrometheusAddress, + Handler: mux, + ReadTimeout: setting.App.Server.ReadTimeout, + WriteTimeout: setting.App.Server.WriteTimeout, + } + + log.Printf("Starting Prometheus server listening on %s", setting.App.PrometheusAddress) + go func() { + if err := p.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal(err) + } + }() +} + +func (p *Prometheus) Stop() { + log.Print("Stopping Prometheus server...") + if err := p.server.Shutdown(p.ctx); err != nil { + log.Printf("Prometheus server forced to shutdown: %s", err) + } +} diff --git a/service/geo.go b/service/geo.go index af2ab4f..a734b5d 100644 --- a/service/geo.go +++ b/service/geo.go @@ -6,6 +6,7 @@ import ( "net" "sync" + "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/models" ) @@ -41,6 +42,7 @@ func (g *Geo) LookUpCity(ip net.IP) *models.GeoRecord { return nil } + metrics.RecordGeoLookup("city") return record } @@ -51,6 +53,7 @@ func (g *Geo) LookUpASN(ip net.IP) *models.ASNRecord { return nil } + metrics.RecordGeoLookup("asn") return record } diff --git a/service/port_scanner.go b/service/port_scanner.go index c24f348..4e71ba8 100644 --- a/service/port_scanner.go +++ b/service/port_scanner.go @@ -3,6 +3,8 @@ package service import ( "net" "time" + + "github.com/dcarrillo/whatismyip/internal/metrics" ) const scannerTimeOut = 3 * time.Second @@ -20,5 +22,6 @@ func (p *PortScanner) IsPortOpen() (bool, error) { defer conn.Close() } + metrics.RecordPortScan() return true, nil }