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") }