diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..218014a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,41 @@
+# http://editorconfig.org
+
+# A special property that should be specified at the top of the file outside of any sections.
+# Set to true to stop .editorconfig file search on the current file.
+root = true
+
+[*]
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+continuation_indent_size = 2
+max_line_length = 120
+
+[*.sh]
+end_of_line = lf
+
+# shfmt options, equivalent to: -ln=bash -bn -ci -sr
+shell_variant = bash
+binary_next_line = true
+switch_case_indent = true
+space_redirects = true
+
+[*.{bat,cmd,ps1}]
+end_of_line = crlf
+
+[*.go]
+indent_style = tab
+indent_size = 4
+
+[*.py]
+indent_size = 4
+continuation_indent_size = 4
+
+[Makefile]
+indent_style = tab
+end_of_line = lf
+
+[.gitmodules]
+indent_style = tab
diff --git a/.envrc b/.envrc
index 8b169e1..ffee1ab 100644
--- a/.envrc
+++ b/.envrc
@@ -1,5 +1,34 @@
+#!/usr/bin/env bash
+
watch_file .tool-versions
asdf_has golang || asdf plugin-add golang
+asdf_has editorconfig-checker || asdf plugin-add editorconfig-checker
+asdf_has hadolint || asdf plugin-add hadolint
+asdf_has python || asdf plugin-add python
asdf install | sed '/is already installed/d'
use asdf
+
+has pipx || use pipx
+has pre-commit \
+ || pipx install 'pre-commit>=4.3'
+has detect-secrets \
+ || pipx install 'detect-secrets>=1.5'
+
+use pre-commit
+
+# Install Go tools using go install for better Go module integration
+has gomarkdoc \
+ || go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0
+has gosec \
+ || go install github.com/securego/gosec/v2/cmd/gosec@v2.22.7
+has golangci-lint \
+ || go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
+has govulncheck \
+ || go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
+has goimports \
+ || go install golang.org/x/tools/cmd/goimports@v0.36.0
+has gocyclo \
+ || go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0
+
+layout python-venv
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 34c15d4..30e8176 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -1,3 +1,4 @@
+---
name: Docker
# This workflow uses actions that are not certified by GitHub.
@@ -7,11 +8,11 @@ name: Docker
on:
push:
- branches: [ main ]
+ branches: [main]
# Publish semver tags as releases.
- tags: [ 'v*.*.*' ]
+ tags: ['v*.*.*']
pull_request:
- branches: [ main ]
+ branches: [main]
env:
# Use docker.io for Docker Hub if empty
@@ -29,13 +30,13 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v5
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
- uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
+ uses: docker/login-action@v3.5.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -45,14 +46,14 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
- uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+ uses: docker/metadata-action@v5.8.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
- uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
+ uses: docker/build-push-action@v6.18.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
diff --git a/.gitignore b/.gitignore
index e65e461..b9d1e34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,26 @@
*.iws
*.iml
out/
-vendor/
+
+### Go template
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (comment the line below if you want to checkin your vendor folder)
+vendor/
+
+# Go workspace file
+go.work
### Node template
# Logs
@@ -69,5 +88,15 @@ build/
*#
#*
+### vscode
+.vscode/
+
### Direnv
-.direnv/
\ No newline at end of file
+.direnv/
+
+### Project specific
+.cache/
+
+# Security scanning reports
+gosec-report.json
+gosec-report.sarif
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..fd71639
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,78 @@
+---
+default_stages:
+ - pre-commit
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks.git
+ rev: v5.0.0
+ hooks:
+ - id: trailing-whitespace
+ files: \.(conf|j2|js|json|rb|md|py|sh|tf|tm?pl|txt|yaml|yml|go)$
+ - id: check-case-conflict
+ - id: check-json
+ - id: check-toml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ exclude: '^\.idea/.*$'
+ - repo: https://github.com/adrienverge/yamllint.git
+ rev: v1.35.1
+ hooks:
+ - id: yamllint
+ - repo: https://github.com/Yelp/detect-secrets.git
+ rev: v1.5.0
+ hooks:
+ - id: detect-secrets
+ args:
+ - '--baseline'
+ - '.secrets.baseline'
+ - '--exclude-secrets'
+ - '.*fake.*'
+ exclude: 'package-lock\.json$|Pipfile\.lock$|poetry\.lock$|go\.sum$|^.secrets.baseline$'
+ - repo: https://github.com/hadolint/hadolint
+ rev: v2.13.1-beta
+ hooks:
+ - id: hadolint
+ - repo: https://github.com/editorconfig-checker/editorconfig-checker.python
+ rev: 3.0.3
+ hooks:
+ - id: editorconfig-checker-system
+ alias: ec
+ # Go-specific hooks
+ - repo: https://github.com/dnephin/pre-commit-golang
+ rev: v0.5.1
+ hooks:
+ - id: go-fmt
+ - id: go-vet
+ - id: go-imports
+ - id: go-cyclo
+ args: [-over=15]
+ - id: go-mod-tidy
+ - id: go-unit-tests
+ - id: golangci-lint
+ - repo: local
+ hooks:
+ - id: gosec
+ name: gosec security scanner
+ entry: gosec
+ language: system
+ files: '\.go$'
+ pass_filenames: false
+ args: ['./...']
+ - id: go-no-replacement
+ name: Avoid committing debug statements
+ entry: 'github\.com/(docker|prometheus)/'
+ language: fail
+ files: go\.(mod|sum)$
+ - id: govulncheck
+ name: govulncheck
+ entry: govulncheck
+ language: system
+ files: '\.go$'
+ pass_filenames: false
+ args: ['./...']
+ - id: gomarkdoc
+ name: Generate Go documentation
+ entry: gomarkdoc
+ language: system
+ files: '\.go$'
+ pass_filenames: false
+ args: ['--embed', '--include-unexported', '--output', 'README.md', './...']
diff --git a/.secrets.baseline b/.secrets.baseline
new file mode 100644
index 0000000..c660b35
--- /dev/null
+++ b/.secrets.baseline
@@ -0,0 +1,127 @@
+{
+ "version": "1.5.0",
+ "plugins_used": [
+ {
+ "name": "ArtifactoryDetector"
+ },
+ {
+ "name": "AWSKeyDetector"
+ },
+ {
+ "name": "AzureStorageKeyDetector"
+ },
+ {
+ "name": "Base64HighEntropyString",
+ "limit": 4.5
+ },
+ {
+ "name": "BasicAuthDetector"
+ },
+ {
+ "name": "CloudantDetector"
+ },
+ {
+ "name": "DiscordBotTokenDetector"
+ },
+ {
+ "name": "GitHubTokenDetector"
+ },
+ {
+ "name": "GitLabTokenDetector"
+ },
+ {
+ "name": "HexHighEntropyString",
+ "limit": 3.0
+ },
+ {
+ "name": "IbmCloudIamDetector"
+ },
+ {
+ "name": "IbmCosHmacDetector"
+ },
+ {
+ "name": "IPPublicDetector"
+ },
+ {
+ "name": "JwtTokenDetector"
+ },
+ {
+ "name": "KeywordDetector",
+ "keyword_exclude": ""
+ },
+ {
+ "name": "MailchimpDetector"
+ },
+ {
+ "name": "NpmDetector"
+ },
+ {
+ "name": "OpenAIDetector"
+ },
+ {
+ "name": "PrivateKeyDetector"
+ },
+ {
+ "name": "PypiTokenDetector"
+ },
+ {
+ "name": "SendGridDetector"
+ },
+ {
+ "name": "SlackDetector"
+ },
+ {
+ "name": "SoftlayerDetector"
+ },
+ {
+ "name": "SquareOAuthDetector"
+ },
+ {
+ "name": "StripeDetector"
+ },
+ {
+ "name": "TelegramBotTokenDetector"
+ },
+ {
+ "name": "TwilioKeyDetector"
+ }
+ ],
+ "filters_used": [
+ {
+ "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
+ },
+ {
+ "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
+ "min_level": 2
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_indirect_reference"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_likely_id_string"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_lock_file"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_potential_uuid"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_sequential_string"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_swagger_file"
+ },
+ {
+ "path": "detect_secrets.filters.heuristic.is_templated_secret"
+ }
+ ],
+ "results": {},
+ "generated_at": "2025-08-13T20:35:50Z"
+}
diff --git a/.tool-versions b/.tool-versions
index 009efa3..1ef9b39 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1 +1,4 @@
-golang 1.22.5
+golang 1.24.6
+editorconfig-checker 3.4.0
+hadolint 2.12.0
+python 3.13.6
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 0000000..8a231cd
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,10 @@
+---
+extends: default
+
+rules:
+ line-length:
+ max: 120
+ indentation:
+ spaces: 2
+ truthy:
+ allowed-values: ['true', 'false', 'on', 'off']
diff --git a/Dockerfile b/Dockerfile
index 5b56fb0..8621b7d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,8 @@
-FROM golang:1.22-alpine as builder
+FROM golang:1.24.6-alpine AS builder
-RUN apk update && apk add \
+# Ignore version pinning for build tools as they change frequently
+# hadolint ignore=DL3018
+RUN apk update --no-cache && apk add --no-cache \
git \
ca-certificates
@@ -8,16 +10,16 @@ COPY *.go go.mod go.sum $GOPATH/src/docker_state_exporter/
WORKDIR $GOPATH/src/docker_state_exporter/
-RUN go mod vendor -v
-RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/docker_state_exporter
+RUN go mod vendor -v && \
+ CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/docker_state_exporter
-FROM alpine:3
+FROM alpine:3.21
-RUN apk -U --no-cache upgrade
+RUN apk upgrade --no-cache
COPY --from=builder /go/bin/docker_state_exporter /go/bin/docker_state_exporter
EXPOSE 8080
ENTRYPOINT ["/go/bin/docker_state_exporter"]
-CMD ["-listen-address=:8080"]
\ No newline at end of file
+CMD ["-listen-address=:8080"]
diff --git a/README.md b/README.md
index 10eeb38..9445aff 100644
--- a/README.md
+++ b/README.md
@@ -59,9 +59,272 @@ This exporter also exports the standard
[Go Collector](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#NewGoCollector)
and [Process Collector](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#NewProcessCollector).
+## API Documentation
+
+
+
+
+
+# docker\_state\_exporter
+
+```go
+import "github.com/AdaptiveConsulting/docker_state_exporter"
+```
+
+Package main implements a Docker state exporter for Prometheus metrics.
+
+This exporter connects to the Docker daemon and exports container state information as Prometheus metrics, including health status, running state, OOM killed status, and restart counts.
+
+The exporter provides the following metrics:
+
+- container\_state\_health\_status: Container health status \(none, starting, healthy, unhealthy\)
+- container\_state\_status: Container status \(paused, restarting, running, removing, dead, created, exited\)
+- container\_state\_oomkilled: Whether the container was killed by OOMKiller
+- container\_state\_startedat: Unix timestamp when the container started
+- container\_state\_finishedat: Unix timestamp when the container finished
+- container\_restartcount: Number of times the container has been restarted
+
+Usage:
+
+```
+docker_state_exporter [flags]
+```
+
+Flags:
+
+```
+-listen-address string
+ The address to listen on for HTTP requests (default ":8080")
+-add-container-labels
+ Add labels from docker containers as metric labels (default true)
+-cache-period int
+ The period of time the collector will reuse the results of docker inspect
+ before polling again, in seconds (default 1)
+```
+
+Examples:
+
+```
+# Start the exporter on default port 8080
+docker_state_exporter
+
+# Start on custom port with 5-second cache
+docker_state_exporter -listen-address=:9090 -cache-period=5
+
+# Start without container labels
+docker_state_exporter -add-container-labels=false
+```
+
+The exporter exposes metrics at /metrics and provides a health check at /\-/healthy.
+
+## Index
+
+- [Variables](<#variables>)
+- [func errCheck\(err error\)](<#errCheck>)
+- [func init\(\)](<#init>)
+- [func main\(\)](<#main>)
+- [type descSource](<#descSource>)
+ - [func \(desc \*descSource\) Desc\(labels prometheus.Labels\) \*prometheus.Desc](<#descSource.Desc>)
+- [type dockerHealthCollector](<#dockerHealthCollector>)
+ - [func \(c \*dockerHealthCollector\) Collect\(ch chan\<\- prometheus.Metric\)](<#dockerHealthCollector.Collect>)
+ - [func \(c \*dockerHealthCollector\) Describe\(ch chan\<\- \*prometheus.Desc\)](<#dockerHealthCollector.Describe>)
+ - [func \(c \*dockerHealthCollector\) collectContainer\(\)](<#dockerHealthCollector.collectContainer>)
+ - [func \(c \*dockerHealthCollector\) collectMetrics\(ch chan\<\- prometheus.Metric\)](<#dockerHealthCollector.collectMetrics>)
+- [type loggerWrapper](<#loggerWrapper>)
+ - [func \(l \*loggerWrapper\) Println\(v ...interface\{\}\)](<#loggerWrapper.Println>)
+
+
+## Variables
+
+
+
+```go
+var (
+ // addContainerLabels controls whether to include Docker container labels as Prometheus metric labels.
+ // When enabled, all labels from the container will be added to the metrics with a "container_label_" prefix.
+ addContainerLabels bool
+
+ // cachePeriod defines how long to cache Docker inspect results before polling again.
+ // This reduces the load on the Docker API while maintaining reasonable freshness of data.
+ cachePeriod time.Duration
+)
+```
+
+
+
+```go
+var (
+ namespace = "container_state_"
+ healthStatusDesc = descSource{
+ namespace + "health_status",
+ "Container health status."}
+ statusDesc = descSource{
+ namespace + "status",
+ "Container status."}
+ oomkilledDesc = descSource{
+ namespace + "oomkilled",
+ "Container was killed by OOMKiller."}
+ startedatDesc = descSource{
+ namespace + "startedat",
+ "Time when the Container started."}
+ finishedatDesc = descSource{
+ namespace + "finishedat",
+ "Time when the Container finished."}
+ restartcountDesc = descSource{
+ "container_restartcount",
+ "Number of times the container has been restarted"}
+)
+```
+
+Define loggers.
+
+```go
+var (
+ normalLogger = log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
+ errorLogger = log.NewJSONLogger(log.NewSyncWriter(os.Stderr))
+)
+```
+
+Define flags.
+
+```go
+var (
+ address = flag.String(
+ "listen-address",
+ ":8080",
+ "The address to listen on for HTTP requests.",
+ )
+)
+```
+
+
+## func [errCheck]()
+
+```go
+func errCheck(err error)
+```
+
+
+
+
+## func [init]()
+
+```go
+func init()
+```
+
+
+
+
+## func [main]()
+
+```go
+func main()
+```
+
+
+
+
+## type [descSource]()
+
+descSource provides a helper for creating Prometheus metric descriptions with consistent naming and help text.
+
+```go
+type descSource struct {
+ name string // Metric name
+ help string // Help text describing the metric
+}
+```
+
+
+### func \(\*descSource\) [Desc]()
+
+```go
+func (desc *descSource) Desc(labels prometheus.Labels) *prometheus.Desc
+```
+
+Desc creates a Prometheus metric description with the given labels. It returns a new prometheus.Desc that can be used to create metrics.
+
+
+## type [dockerHealthCollector]()
+
+dockerHealthCollector implements the prometheus.Collector interface to collect Docker container state metrics. It caches container information for a configurable period to reduce Docker API calls while maintaining reasonable freshness of data.
+
+The collector exports metrics about container health status, running state, OOM kill status, start/finish times, and restart counts.
+
+```go
+type dockerHealthCollector struct {
+ mu sync.Mutex // Protects concurrent access to cache
+ containerClient *client.Client // Docker client for API calls
+ containerInfoCache []container.InspectResponse // Cached container information
+ lastseen time.Time // Last time cache was refreshed
+}
+```
+
+
+### func \(\*dockerHealthCollector\) [Collect]()
+
+```go
+func (c *dockerHealthCollector) Collect(ch chan<- prometheus.Metric)
+```
+
+Collect is called by Prometheus when collecting metrics. It fetches current container state \(using cache if fresh enough\) and sends metrics to the channel.
+
+
+### func \(\*dockerHealthCollector\) [Describe]()
+
+```go
+func (c *dockerHealthCollector) Describe(ch chan<- *prometheus.Desc)
+```
+
+Describe sends the super\-set of all possible descriptors of metrics collected by this Collector to the provided channel and returns once the last descriptor has been sent.
+
+
+### func \(\*dockerHealthCollector\) [collectContainer]()
+
+```go
+func (c *dockerHealthCollector) collectContainer()
+```
+
+
+
+
+### func \(\*dockerHealthCollector\) [collectMetrics]()
+
+```go
+func (c *dockerHealthCollector) collectMetrics(ch chan<- prometheus.Metric)
+```
+
+
+
+
+## type [loggerWrapper]()
+
+
+
+```go
+type loggerWrapper struct {
+ Logger *log.Logger
+}
+```
+
+
+### func \(\*loggerWrapper\) [Println]()
+
+```go
+func (l *loggerWrapper) Println(v ...interface{})
+```
+
+
+
+Generated by [gomarkdoc]()
+
+
+
+
## Performance
-The polling of docker inspect commands is set to every one second.
+The polling of docker inspect commands is set to every one second.
TODO: Allow for polling interval customization.
diff --git a/go.mod b/go.mod
index 0db9fa0..301a052 100644
--- a/go.mod
+++ b/go.mod
@@ -1,50 +1,48 @@
module github.com/AdaptiveConsulting/docker_state_exporter
-go 1.22
+go 1.24
require (
- github.com/docker/docker v27.0.3+incompatible
- github.com/go-kit/kit v0.13.0
- github.com/prometheus/client_golang v1.19.1
+ github.com/docker/docker v28.3.3+incompatible
+ github.com/go-kit/log v0.2.1
+ github.com/prometheus/client_golang v1.23.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // 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
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.0 // 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.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.55.0 // indirect
- github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.65.0 // indirect
+ github.com/prometheus/procfs v0.16.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
- golang.org/x/mod v0.19.0 // indirect
- golang.org/x/sys v0.22.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
- golang.org/x/tools v0.23.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect
- google.golang.org/grpc v1.61.1 // indirect
- google.golang.org/protobuf v1.34.2 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
gotest.tools/v3 v3.5.1 // indirect
)
diff --git a/go.sum b/go.sum
index b020e9f..a90304a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,60 +1,58 @@
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/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
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/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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
-github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ=
-github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
-github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
+github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
-github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+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=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -63,119 +61,85 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
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.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
-github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
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.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
-github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-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.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
-github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-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/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
+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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
-go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
-go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
-go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
-go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
-go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
-go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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/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.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
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=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
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.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
-golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
-golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
-golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
-google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk=
-google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
-google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
+google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
+google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
diff --git a/main.go b/main.go
index 083dc06..4c9a482 100644
--- a/main.go
+++ b/main.go
@@ -1,3 +1,43 @@
+// Package main implements a Docker state exporter for Prometheus metrics.
+//
+// This exporter connects to the Docker daemon and exports container state information
+// as Prometheus metrics, including health status, running state, OOM killed status,
+// and restart counts.
+//
+// The exporter provides the following metrics:
+// - container_state_health_status: Container health status (none, starting, healthy, unhealthy)
+// - container_state_status: Container status (paused, restarting, running, removing, dead, created, exited)
+// - container_state_oomkilled: Whether the container was killed by OOMKiller
+// - container_state_startedat: Unix timestamp when the container started
+// - container_state_finishedat: Unix timestamp when the container finished
+// - container_restartcount: Number of times the container has been restarted
+//
+// Usage:
+//
+// docker_state_exporter [flags]
+//
+// Flags:
+//
+// -listen-address string
+// The address to listen on for HTTP requests (default ":8080")
+// -add-container-labels
+// Add labels from docker containers as metric labels (default true)
+// -cache-period int
+// The period of time the collector will reuse the results of docker inspect
+// before polling again, in seconds (default 1)
+//
+// Examples:
+//
+// # Start the exporter on default port 8080
+// docker_state_exporter
+//
+// # Start on custom port with 5-second cache
+// docker_state_exporter -listen-address=:9090 -cache-period=5
+//
+// # Start without container labels
+// docker_state_exporter -add-container-labels=false
+//
+// The exporter exposes metrics at /metrics and provides a health check at /-/healthy.
package main
import (
@@ -13,32 +53,46 @@ import (
"syscall"
"time"
- "github.com/docker/docker/api/types"
- tcontainer "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
+ // addContainerLabels controls whether to include Docker container labels as Prometheus metric labels.
+ // When enabled, all labels from the container will be added to the metrics with a "container_label_" prefix.
addContainerLabels bool
- cachePeriod time.Duration
+
+ // cachePeriod defines how long to cache Docker inspect results before polling again.
+ // This reduces the load on the Docker API while maintaining reasonable freshness of data.
+ cachePeriod time.Duration
)
+// dockerHealthCollector implements the prometheus.Collector interface to collect
+// Docker container state metrics. It caches container information for a configurable
+// period to reduce Docker API calls while maintaining reasonable freshness of data.
+//
+// The collector exports metrics about container health status, running state,
+// OOM kill status, start/finish times, and restart counts.
type dockerHealthCollector struct {
- mu sync.Mutex
- containerClient *client.Client
- containerInfoCache []types.ContainerJSON
- lastseen time.Time
+ mu sync.Mutex // Protects concurrent access to cache
+ containerClient *client.Client // Docker client for API calls
+ containerInfoCache []container.InspectResponse // Cached container information
+ lastseen time.Time // Last time cache was refreshed
}
+// descSource provides a helper for creating Prometheus metric descriptions
+// with consistent naming and help text.
type descSource struct {
- name string
- help string
+ name string // Metric name
+ help string // Help text describing the metric
}
+// Desc creates a Prometheus metric description with the given labels.
+// It returns a new prometheus.Desc that can be used to create metrics.
func (desc *descSource) Desc(labels prometheus.Labels) *prometheus.Desc {
return prometheus.NewDesc(desc.name, desc.help, nil, labels)
}
@@ -65,6 +119,9 @@ var (
"Number of times the container has been restarted"}
)
+// Describe sends the super-set of all possible descriptors of metrics
+// collected by this Collector to the provided channel and returns once
+// the last descriptor has been sent.
func (c *dockerHealthCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- healthStatusDesc.Desc(nil)
ch <- statusDesc.Desc(nil)
@@ -74,6 +131,8 @@ func (c *dockerHealthCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- restartcountDesc.Desc(nil)
}
+// Collect is called by Prometheus when collecting metrics. It fetches current
+// container state (using cache if fresh enough) and sends metrics to the channel.
func (c *dockerHealthCollector) Collect(ch chan<- prometheus.Metric) {
c.mu.Lock()
defer c.mu.Unlock()
@@ -91,14 +150,16 @@ func (c *dockerHealthCollector) collectMetrics(ch chan<- prometheus.Metric) {
rep := regexp.MustCompile("[^a-zA-Z0-9_]")
- if addContainerLabels {
+ if addContainerLabels && info.Config != nil {
for k, v := range info.Config.Labels {
label := strings.ToLower("container_label_" + k)
labels[rep.ReplaceAllLiteralString(label, "_")] = v
}
}
labels["id"] = "/docker/" + info.ID
- labels["image"] = info.Config.Image
+ if info.Config != nil {
+ labels["image"] = info.Config.Image
+ }
labels["name"] = strings.TrimPrefix(info.Name, "/")
b2f := func(b bool) float64 {
@@ -115,10 +176,21 @@ func (c *dockerHealthCollector) collectMetrics(ch chan<- prometheus.Metric) {
return dst
}
+ // Health status metrics - handle nil health
for _, lv := range []string{"none", "starting", "healthy", "unhealthy"} {
tmpLabels := mapcopy(labels)
tmpLabels["status"] = lv
- ch <- prometheus.MustNewConstMetric(healthStatusDesc.Desc(tmpLabels), prometheus.GaugeValue, b2f(info.State.Health.Status == lv))
+ var value float64
+ if info.State.Health != nil {
+ value = b2f(info.State.Health.Status == lv)
+ } else {
+ value = b2f(lv == "none")
+ }
+ ch <- prometheus.MustNewConstMetric(
+ healthStatusDesc.Desc(tmpLabels),
+ prometheus.GaugeValue,
+ value,
+ )
}
for _, lv := range []string{"paused", "restarting", "running", "removing", "dead", "created", "exited"} {
tmpLabels := mapcopy(labels)
@@ -137,22 +209,17 @@ func (c *dockerHealthCollector) collectMetrics(ch chan<- prometheus.Metric) {
}
func (c *dockerHealthCollector) collectContainer() {
- containers, err := c.containerClient.ContainerList(context.Background(), tcontainer.ListOptions{All: true})
+ containers, err := c.containerClient.ContainerList(context.Background(), container.ListOptions{All: true})
errCheck(err)
- c.containerInfoCache = []types.ContainerJSON{}
+ c.containerInfoCache = []container.InspectResponse{}
for _, container := range containers {
info, err := c.containerClient.ContainerInspect(context.Background(), container.ID)
errCheck(err)
c.containerInfoCache = append(c.containerInfoCache, info)
- if info.Config == nil {
- info.Config = &tcontainer.Config{Labels: map[string]string{}}
- }
-
- if info.State.Health == nil {
- info.State.Health = &types.Health{Status: "none"}
- }
+ // Note: We don't modify the info struct as it's from the Docker API
+ // The collectMetrics function will handle nil checks appropriately
}
}
@@ -161,7 +228,10 @@ type loggerWrapper struct {
}
func (l *loggerWrapper) Println(v ...interface{}) {
- (*l.Logger).Log("messages", v)
+ if err := (*l.Logger).Log("messages", v); err != nil {
+ // fallback to stderr if logging fails
+ fmt.Fprintf(os.Stderr, "Failed to log: %v\n", err)
+ }
}
// Define loggers.
@@ -172,14 +242,20 @@ var (
func errCheck(err error) {
if err != nil {
- errorLogger.Log("message", err)
+ if logErr := errorLogger.Log("message", err); logErr != nil {
+ fmt.Fprintf(os.Stderr, "Failed to log error: %v, original error: %v\n", logErr, err)
+ }
os.Exit(1)
}
}
// Define flags.
var (
- address = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.")
+ address = flag.String(
+ "listen-address",
+ ":8080",
+ "The address to listen on for HTTP requests.",
+ )
)
func init() {
@@ -188,14 +264,22 @@ func init() {
errorLogger = log.With(errorLogger, "timestamp", log.DefaultTimestampUTC)
errorLogger = log.With(errorLogger, "severity", "error")
prometheus.MustRegister(collectors.NewBuildInfoCollector())
- cachePeriod = time.Duration(*flag.Int("cache-period", 1, "The period of time the collector will reuse the results of docker inspect before polling again, in seconds")) * time.Second
+ cachePeriodFlag := flag.Int(
+ "cache-period",
+ 1,
+ "Period to reuse docker inspect results before polling again, in seconds",
+ )
+ cachePeriod = time.Duration(*cachePeriodFlag) * time.Second
flag.BoolVar(&addContainerLabels, "add-container-labels", true, "Add labels from docker containers as metric labels")
}
func main() {
flag.Parse()
- client, err := client.NewClientWithOpts()
+ client, err := client.NewClientWithOpts(
+ client.FromEnv,
+ client.WithAPIVersionNegotiation(),
+ )
errCheck(err)
defer client.Close()
@@ -218,9 +302,18 @@ func main() {
prometheus.DefaultGatherer,
promhttp.HandlerOpts{ErrorLog: &loggerWrapper{Logger: &errorLogger}, EnableOpenMetrics: true}))
- normalLogger.Log("message", "Server listening...", "address", address)
+ if err := normalLogger.Log("message", "Server listening...", "address", address); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to log server start: %v\n", err)
+ }
- server := &http.Server{Addr: *address, Handler: nil}
+ server := &http.Server{
+ Addr: *address,
+ Handler: nil,
+ ReadHeaderTimeout: 30 * time.Second,
+ ReadTimeout: 60 * time.Second,
+ WriteTimeout: 60 * time.Second,
+ IdleTimeout: 120 * time.Second,
+ }
go func() {
err = server.ListenAndServe()
@@ -232,12 +325,18 @@ func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
<-quit
- normalLogger.Log("message", "Server shutting down...")
+ if err := normalLogger.Log("message", "Server shutting down..."); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to log server shutdown: %v\n", err)
+ }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
- errorLogger.Log("message", fmt.Sprintf("Failed to gracefully shutdown: %v", err))
+ if logErr := errorLogger.Log("message", fmt.Sprintf("Failed to gracefully shutdown: %v", err)); logErr != nil {
+ fmt.Fprintf(os.Stderr, "Failed to log shutdown error: %v, original error: %v\n", logErr, err)
+ }
+ }
+ if err := normalLogger.Log("message", "Server shutdown"); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to log server shutdown complete: %v\n", err)
}
- normalLogger.Log("message", "Server shutdown")
}