add prometheus exporter

This commit is contained in:
Simon Rieger 2025-07-17 03:12:27 +02:00
parent 7ebba2f488
commit dccdc00eec
3 changed files with 71 additions and 18 deletions

8
go.mod
View file

@ -4,11 +4,14 @@ go 1.24.1
require (
github.com/docker/docker v28.3.2+incompatible
github.com/prometheus/client_golang v1.22.0
github.com/regclient/regclient v0.9.0
)
require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
@ -25,9 +28,13 @@ require (
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.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/pkg/errors v0.9.1 // 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
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@ -38,5 +45,6 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gotest.tools/v3 v3.5.2 // indirect
)

16
go.sum
View file

@ -2,8 +2,12 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
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/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@ -43,6 +47,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@ -53,6 +59,8 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
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/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI=
github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -64,6 +72,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/regclient/regclient v0.9.0 h1:c3hNJZvtv8lMqhP0jGCa4d9j2n4688VCfhCWddGfWfk=
github.com/regclient/regclient v0.9.0/go.mod h1:Adv7tukwdX+oDTszfILrjerGk55Pg2nKlbshj94U3rg=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=

65
main.go
View file

@ -4,16 +4,19 @@ import (
"context"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/regclient/regclient"
"github.com/regclient/regclient/types/ref"
)
// Wandelt ein ImageTag in registrykompatibles Format (ggf. registry hinzufügen)
// Hilfsfunktion: registrykompatibles Format aus repo:tag erzeugen
func toRegistryImage(imageTag string) (string, error) {
r := regexp.MustCompile(`^(?:(?P<registry>[^/]+)/)?(?P<repo>[^:]+)(?::(?P<tag>.+))?$`)
match := r.FindStringSubmatch(imageTag)
@ -32,28 +35,50 @@ func toRegistryImage(imageTag string) (string, error) {
return fmt.Sprintf("%s/%s:%s", registry, repo, tag), nil
}
// Extrahiert nur den reinen sha256:<...>-Digest
// Extrahiert nur den sha256:... Digest
func extractDigest(s string) string {
for _, part := range strings.Split(s, "@") {
if strings.HasPrefix(part, "sha256:") {
return part
}
}
return s // ggf. nur die ID, wenn kein Digest
return s
}
func main() {
// Prometheus Collector-Struktur
type dockerImageUpdateCollector struct {
desc *prometheus.Desc
}
func newDockerImageUpdateCollector() prometheus.Collector {
return &dockerImageUpdateCollector{
desc: prometheus.NewDesc(
"docker_image_update_available",
"Whether a new image digest is available for this local image tag (1=update, 0=current)",
[]string{"image"},
nil,
),
}
}
func (c *dockerImageUpdateCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.desc
}
func (c *dockerImageUpdateCollector) Collect(ch chan<- prometheus.Metric) {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
log.Fatal(err)
log.Println("Docker-Client-Fehler:", err)
return
}
defer cli.Close()
rc := regclient.New()
images, err := cli.ImageList(ctx, image.ListOptions{All: true})
if err != nil {
log.Fatal(err)
log.Println("Docker-ImageList-Fehler:", err)
return
}
for _, img := range images {
@ -62,34 +87,38 @@ func main() {
if err != nil {
continue
}
// Lokalen Digest extrahieren
var localDigest string
if len(img.RepoDigests) > 0 {
localDigest = extractDigest(img.RepoDigests[0])
} else {
localDigest = img.ID
}
// Remote-Digest bestimmen
refObj, err := ref.New(imageRef)
if err != nil {
fmt.Printf("ImageRef-Fehler bei %s: %v\n", tag, err)
continue
}
desc, err := rc.ManifestHead(ctx, refObj)
if err != nil {
fmt.Printf("Manifest nicht gefunden (%s): %v\n", tag, err)
continue
}
remoteDigest := desc.GetDigest().String()
fmt.Printf("Image: %s\n Local Digest: %s\n Remote Digest: %s\n", tag, localDigest, remoteDigest)
if localDigest == remoteDigest {
fmt.Println(" -> Kein Update verfügbar.")
} else {
fmt.Println(" -> Update verfügbar!")
var value float64 = 0
if localDigest != remoteDigest {
value = 1
}
ch <- prometheus.MustNewConstMetric(
c.desc,
prometheus.GaugeValue,
value,
tag,
)
}
}
}
func main() {
prometheus.MustRegister(newDockerImageUpdateCollector())
http.Handle("/metrics", promhttp.Handler())
log.Println("Prometheus Exporter läuft auf :9888/metrics")
log.Fatal(http.ListenAndServe(":9888", nil))
}