diff --git a/.github/workflows/docker-exporter.yaml b/.github/workflows/docker-exporter.yaml new file mode 100644 index 0000000..45004eb --- /dev/null +++ b/.github/workflows/docker-exporter.yaml @@ -0,0 +1,45 @@ +name: Build and Push Docker-Exporter Image + +on: + push: + branches: [ * ] + paths: + - 'repository-mirror/**' + - '.github/workflows/repository-mirror.yaml' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Generate container metadata + id: meta + uses: docker/metadata-action@v3 + with: + images: ghcr.io/obmondo/dockerfiles/repository-mirror + tags: | + type=semver,pattern={{version}},value=v1.27.0 + flavor: | + latest=false + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & push container image + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./repository-mirror + labels: ${{ steps.meta.outputs.labels }} + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/docker-exporter/Dockerfile b/docker-exporter/Dockerfile new file mode 100644 index 0000000..aa74876 --- /dev/null +++ b/docker-exporter/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.22-alpine3.21 AS base + +RUN adduser -D -H docker-exporter + +ENV GO111MODULE=on \ + CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 + +WORKDIR /build + +COPY . . + +RUN go mod download + +FROM base AS build + +RUN go build -o docker-exporter -tags prod main.go + +FROM scratch AS prod + +COPY --from=build /etc/passwd /etc/passwd +COPY --from=build /etc/group /etc/group +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +COPY --from=build /build/docker-exporter / + +USER docker-exporter:docker-exporter + +CMD ["./docker-exporter"] diff --git a/docker-exporter/LICENSE b/docker-exporter/LICENSE new file mode 100644 index 0000000..5749f1e --- /dev/null +++ b/docker-exporter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 David Borzek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docker-exporter/README.md b/docker-exporter/README.md new file mode 100644 index 0000000..f8432a5 --- /dev/null +++ b/docker-exporter/README.md @@ -0,0 +1,95 @@ +# docker exporter + +[![Go Report Card](https://goreportcard.com/badge/github.com/davidborzek/docker-exporter)](https://goreportcard.com/report/github.com/davidborzek/docker-exporter) + +Simple and lightweight Prometheus exporter for docker container metrics. + +## Prerequisites + +- [Go](https://golang.org/doc/) + +## Installation + +### Using Docker + +The exporter is available as a Docker image. +You can run it using the following example: + +``` +$ docker run \ + -u root \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -p 8080:8080 \ + ghcr.io/davidborzek/docker-exporter:latest +``` + +> Note: To run Docker Exporter, you'll need to mount the Docker socket from your host system. This operation necessitates root privileges or the user running the command to be a member of the Docker group. It's important to note that mounting the Docker socket grants the container unrestricted access to Docker. For a more secure approach, consider utilizing the Docker Socket Proxy, which is further explained below for additional information. + +### Running with [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) + +``` +$ docker run \ + -e "DOCKER_HOST=tcp://localhost:2375" \ + -p 8080:8080 \ + ghcr.io/davidborzek/docker-exporter:latest +``` + +> Note: the [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy#not-always-needed) needs to have container access enabled. (`CONTAINERS=1`) + +### Prometheus config + +Once you have configured the exporter, update your `prometheus.yml` scrape config: + +```yaml +scrape_configs: + - job_name: "docker_exporter" + static_configs: + - targets: ["localhost:8080"] +``` + +### Config + +| Flag | Description | Default Value | Environment Variable | +| ---------------- | ---------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------------ | +| `--port` | The port of docker exporter server. | `8080` | `DOCKER_EXPORTER_PORT` | +| `--host` | The host of docker exporter server. | | `DOCKER_EXPORTER_HOST` | +| `--auth-token` | Optional auth token for the docker exporter server. If no token is set authentication is disabled. | | `DOCKER_EXPORTER_AUTH_TOKEN` | +| `--log-level` | Log level for the exporter. | `info` | `DOCKER_EXPORTER_LOG_LEVEL` | +| `--ignore-label` | Set the label name for ignoring docker containers. (See [Ignoring Containers](#ignoring-containers)) | `docker-exporter.ignore` | `DOCKER_EXPORTER_IGNORE_LABEL` | + +### Exported Metrics + +| Metric Name | Description | Labels | +| ------------------------------------------- | ---------------------------------- | ----------------------- | +| docker_container_block_io_read_bytes | Block I/O read bytes total | name | +| docker_container_block_io_write_bytes | Block I/O write bytes total | name | +| docker_container_cpu_usage_percentage | CPU usage in percentage | name | +| docker_container_info | Infos about the container | name, image_name, image | +| docker_container_memory_total_bytes | Total memory in bytes | name | +| docker_container_memory_usage_bytes | Memory usage in bytes | name | +| docker_container_memory_usage_percentage | Memory usage in percentage | name | +| docker_container_network_rx_bytes | Network received bytes total | name, network | +| docker_container_network_rx_dropped_packets | Network dropped packets total | name, network | +| docker_container_network_rx_errors | Network received errors | name, network | +| docker_container_network_rx_packets | Network received packets total | name, network | +| docker_container_network_tx_bytes | Network sent bytes total | name, network | +| docker_container_network_tx_dropped_packets | Network dropped packets total | name, network | +| docker_container_network_tx_errors | Network sent errors | name, network | +| docker_container_network_tx_packets | Network sent packets total | name, network | +| docker_container_pids_current | Current number of pids | name | +| docker_container_state | State of the container | name, state | +| docker_container_uptime | Uptime of the container in seconds | name | +| docker_exporter_scrape_duration | Duration of the scrape in seconds | | +| docker_exporter_scrape_errors | Number of scrape errors | | + +### Ignoring Containers + +You can ignore containers by setting the label `docker-exporter.ignore` on the container. The label name can be configured with the `--ignore-label` flag. + +```yaml +services: + nginx: + image: nginx + labels: + docker-exporter.ignore: "true" +``` diff --git a/docker-exporter/cmd/main.go b/docker-exporter/cmd/main.go new file mode 100644 index 0000000..f5cacb3 --- /dev/null +++ b/docker-exporter/cmd/main.go @@ -0,0 +1,123 @@ +package cmd + +import ( + "fmt" + "net" + "net/http" + "os" + "strings" + + "github.com/davidborzek/docker-exporter/internal/clock" + "github.com/davidborzek/docker-exporter/internal/collector" + "github.com/davidborzek/docker-exporter/internal/handler" + "github.com/prometheus/client_golang/prometheus" + "github.com/urfave/cli/v2" + + log "github.com/sirupsen/logrus" +) + +const ( + version = "v0.3.0" +) + +var ( + flags = []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Value: "8080", + Usage: "The port of docker exporter server", + EnvVars: []string{"DOCKER_EXPORTER_PORT"}, + }, + &cli.StringFlag{ + Name: "host", + Usage: "The host of docker exporter server", + EnvVars: []string{"DOCKER_EXPORTER_HOST"}, + }, + &cli.StringFlag{ + Name: "auth-token", + Usage: "Optional auth token for the docker exporter server. If no token is set authentication is disabled.", + EnvVars: []string{"DOCKER_EXPORTER_AUTH_TOKEN"}, + }, + &cli.StringFlag{ + Name: "log-level", + Usage: "Log level", + Value: "info", + EnvVars: []string{"DOCKER_EXPORTER_LOG_LEVEL"}, + }, + &cli.StringFlag{ + Name: "ignore-label", + Usage: "Label to ignore containers", + Value: "docker-exporter.ignore", + EnvVars: []string{"DOCKER_EXPORTER_IGNORE_LABEL"}, + }, + } +) + +func parseLogLevel(level string) log.Level { + switch strings.ToLower(level) { + case "debug": + return log.DebugLevel + case "info": + return log.InfoLevel + case "warning": + return log.WarnLevel + case "error": + return log.ErrorLevel + case "fatal": + return log.FatalLevel + } + + log.WithField("level", level). + Warn("invalid log level provided - falling back to 'info'") + + return log.InfoLevel +} + +func start(ctx *cli.Context) error { + log.SetFormatter(&log.TextFormatter{ + FullTimestamp: true, + }) + + log.SetLevel( + parseLogLevel(ctx.String("log-level")), + ) + + log.WithField("pid", os.Getpid()). + Info("docker prometheus exporter started") + + token := ctx.String("auth-token") + if len(token) > 0 { + log.Info("authentication is enabled") + } + + dc, err := collector.NewDockerCollector(clock.NewClock(), ctx.String("ignore-label")) + if err != nil { + log.WithError(err). + Fatal("failed to create docker collector") + } + + prometheus.MustRegister(dc) + + h := handler.New(token) + + addr := net.JoinHostPort( + ctx.String("host"), ctx.String("port")) + log.WithField("addr", addr). + Infof("starting the http server") + + return http.ListenAndServe(addr, h) +} + +func Main(args []string) { + app := cli.App{ + Name: "Docker Prometheus exporter", + Usage: "Export Docker metrics to prometheus format", + Action: start, + Flags: flags, + Version: version, + } + + if err := app.Run(args); err != nil { + fmt.Println(err.Error()) + } +} diff --git a/docker-exporter/go.mod b/docker-exporter/go.mod new file mode 100644 index 0000000..e60315e --- /dev/null +++ b/docker-exporter/go.mod @@ -0,0 +1,62 @@ +module github.com/davidborzek/docker-exporter + +go 1.22 + +toolchain go1.23.4 + +require ( + github.com/docker/docker v27.4.0+incompatible + github.com/prometheus/client_golang v1.20.5 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli/v2 v2.27.5 + go.uber.org/mock v0.5.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // 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.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // 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/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/docker-exporter/go.sum b/docker-exporter/go.sum new file mode 100644 index 0000000..e26f58a --- /dev/null +++ b/docker-exporter/go.sum @@ -0,0 +1,302 @@ +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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.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/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.0+incompatible h1:g9b6wZTblhMgzOT2tspESstfw6ySZ9kdm94BLDKaZac= +github.com/docker/docker v25.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA= +github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY= +github.com/docker/docker v25.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +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 v25.0.4+incompatible h1:XITZTrq+52tZyZxUOtFIahUf3aH367FLxJzt9vZeAF8= +github.com/docker/docker v25.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.0.0+incompatible h1:Ng2qi+gdKADUa/VM+6b6YaY2nlZhk/lVJiKR/2bMudU= +github.com/docker/docker v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA= +github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE= +github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.0+incompatible h1:W1G9MPNbskA6VZWL7b3ZljTh0pXI68FpINx0GKaOdaM= +github.com/docker/docker v26.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.1+incompatible h1:oI+4kkAgIwwb54b9OC7Xc3hSgu1RlJA/Lln/DF72djQ= +github.com/docker/docker v26.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.2+incompatible h1:UVX5ZOrrfTGZZYEP+ZDq3Xn9PdHNXaSYMFPDumMqG2k= +github.com/docker/docker v26.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= +github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= +github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.0.1+incompatible h1:AbszR+lCnR3f297p/g0arbQoyhAkImxQOR/XO9YZeIg= +github.com/docker/docker v27.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.0.2+incompatible h1:mNhCtgXNV1fIRns102grG7rdzIsGGCq1OlOD0KunZos= +github.com/docker/docker v27.0.2+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 v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs= +github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= +github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.3.0+incompatible h1:BNb1QY6o4JdKpqwi9IB+HUYcRRrVN4aGFUTvDmWYK1A= +github.com/docker/docker v27.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A= +github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +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-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/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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +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/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= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +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.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +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.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +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_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= +github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8= +github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +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.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +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.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= +github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY= +github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= +github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +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.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/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.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/docker-exporter/internal/clock/clock.go b/docker-exporter/internal/clock/clock.go new file mode 100644 index 0000000..8411cd1 --- /dev/null +++ b/docker-exporter/internal/clock/clock.go @@ -0,0 +1,29 @@ +package clock + +import "time" + +//go:generate mockgen -destination=../mock/clock.go -package mock -typed -source clock.go + +type Clock interface { + Parse(layout, value string) (time.Time, error) + Since(t time.Time) time.Duration + Now() time.Time +} + +type realClock struct{} + +func NewClock() Clock { + return &realClock{} +} + +func (realClock) Parse(layout, value string) (time.Time, error) { + return time.Parse(layout, value) +} + +func (realClock) Since(t time.Time) time.Duration { + return time.Since(t) +} + +func (realClock) Now() time.Time { + return time.Now() +} diff --git a/docker-exporter/internal/collector/collector.go b/docker-exporter/internal/collector/collector.go new file mode 100644 index 0000000..7b44932 --- /dev/null +++ b/docker-exporter/internal/collector/collector.go @@ -0,0 +1,342 @@ +package collector + +import ( + "context" + "encoding/json" + "strconv" + "strings" + "sync" + "time" + + "github.com/davidborzek/docker-exporter/internal/clock" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +type DockerCollector struct { + ignoreLabel string + client *client.Client + clock clock.Clock +} + +func NewDockerCollector(clk clock.Clock, ignoreLabel string) (*DockerCollector, error) { + client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, err + } + + return NewWithClient(client, clk, ignoreLabel), nil +} + +func NewWithClient(client *client.Client, clk clock.Clock, ignoreLabel string) *DockerCollector { + return &DockerCollector{ + client: client, + clock: clk, + ignoreLabel: ignoreLabel, + } +} + +func (c *DockerCollector) Describe(_ chan<- *prometheus.Desc) {} + +func (c *DockerCollector) Collect(ch chan<- prometheus.Metric) { + now := c.clock.Now() + + ctx := context.Background() + + containers, err := c.client.ContainerList( + ctx, + container.ListOptions{ + All: true, + }, + ) + + if err != nil { + log.WithError(err). + Error("failed to fetch container list") + c.collectScrapeError(ch) + + } else { + var wg sync.WaitGroup + + for _, container := range containers { + wg.Add(1) + go c.collectContainerMetrics(ctx, container, ch, &wg) + } + + wg.Wait() + } + + ch <- prometheus.MustNewConstMetric(scrapeDuration, + prometheus.GaugeValue, + c.clock.Since(now).Seconds(), + ) +} + +func (c *DockerCollector) collectContainerMetrics(ctx context.Context, container types.Container, ch chan<- prometheus.Metric, wg *sync.WaitGroup) { + defer wg.Done() + + if c.isContainerIgnored(container) { + return + } + + name := containerName(container) + inspect, err := c.client.ContainerInspect(ctx, container.ID) + if err != nil { + log.WithError(err).WithField("id", container.ID). + Error("error inspecting container") + c.collectScrapeError(ch) + return + } + + ch <- prometheus.MustNewConstMetric(containerInfo, + prometheus.GaugeValue, + 1, + name, + inspect.Config.Image, + inspect.Image, + ) + + ch <- prometheus.MustNewConstMetric( + containerStateMetric, prometheus.GaugeValue, 1, name, container.State, + ) + + if container.State != "running" { + return + } + + ch <- prometheus.MustNewConstMetric(containerUptime, + prometheus.GaugeValue, + c.calculateUptime(inspect), + name, + ) + + stats, err := c.containerStats(ctx, container.ID) + if err != nil { + log.WithError(err).WithField("id", container.ID). + Error("error getting stats for container") + c.collectScrapeError(ch) + return + } + + c.cpuMetrics(ch, name, stats) + c.memoryMetrics(ch, name, stats) + c.networkMetrics(ch, name, stats) + c.blockIOMetrics(ch, name, stats) + c.pidsMetrics(ch, name, stats) +} + +func (c *DockerCollector) cpuMetrics(ch chan<- prometheus.Metric, name string, stats *types.StatsJSON) { + cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage) + onlineCPUs := getOnlineCPUs(stats) + + cpuPercent := 0.0 + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0 + } + + ch <- prometheus.MustNewConstMetric(cpuUsagePercentage, + prometheus.GaugeValue, + cpuPercent, + name, + ) + + ch <- prometheus.MustNewConstMetric(cpuOnlineCPUs, + prometheus.GaugeValue, + onlineCPUs, + name, + ) +} + +func (c *DockerCollector) memoryMetrics(ch chan<- prometheus.Metric, name string, stats *types.StatsJSON) { + mem := calculateMemUsageUnixNoCache(stats.MemoryStats) + memLimit := float64(stats.MemoryStats.Limit) + + memPercent := 0.0 + if memLimit > 0 { + memPercent = mem / memLimit * 100.0 + } + + ch <- prometheus.MustNewConstMetric(memoryTotalBytes, + prometheus.GaugeValue, + memLimit, + name, + ) + + ch <- prometheus.MustNewConstMetric(memoryUsageBytes, + prometheus.GaugeValue, + mem, + name, + ) + + ch <- prometheus.MustNewConstMetric(memoryUsagePercentage, + prometheus.GaugeValue, + memPercent, + name, + ) +} + +func (c *DockerCollector) networkMetrics(ch chan<- prometheus.Metric, name string, stats *types.StatsJSON) { + for networkName, network := range stats.Networks { + ch <- prometheus.MustNewConstMetric(networkRxBytes, + prometheus.GaugeValue, + float64(network.RxBytes), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkRxPackets, + prometheus.GaugeValue, + float64(network.RxPackets), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkRxDroppedPackets, + prometheus.GaugeValue, + float64(network.RxDropped), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkRxErrors, + prometheus.GaugeValue, + float64(network.RxErrors), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkTxBytes, + prometheus.GaugeValue, + float64(network.TxBytes), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkTxPackets, + prometheus.GaugeValue, + float64(network.TxPackets), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkTxDroppedPackets, + prometheus.GaugeValue, + float64(network.TxDropped), + name, networkName, + ) + + ch <- prometheus.MustNewConstMetric(networkTxErrors, + prometheus.GaugeValue, + float64(network.TxErrors), + name, networkName, + ) + } +} + +func (c *DockerCollector) blockIOMetrics(ch chan<- prometheus.Metric, name string, stats *types.StatsJSON) { + var blkRead, blkWrite uint64 + for _, bioEntry := range stats.BlkioStats.IoServiceBytesRecursive { + if len(bioEntry.Op) == 0 { + continue + } + switch bioEntry.Op[0] { + case 'r', 'R': + blkRead = blkRead + bioEntry.Value + case 'w', 'W': + blkWrite = blkWrite + bioEntry.Value + } + } + + ch <- prometheus.MustNewConstMetric(blockIOReadBytes, + prometheus.GaugeValue, + float64(blkRead), + name, + ) + + ch <- prometheus.MustNewConstMetric(blockIOWriteBytes, + prometheus.GaugeValue, + float64(blkWrite), + name, + ) +} + +func (c *DockerCollector) pidsMetrics(ch chan<- prometheus.Metric, name string, stats *types.StatsJSON) { + ch <- prometheus.MustNewConstMetric(pidsCurrent, + prometheus.GaugeValue, + float64(stats.PidsStats.Current), + name, + ) +} + +// containerStats gets the stats of a single containers. +func (c *DockerCollector) containerStats(ctx context.Context, containerID string) (*types.StatsJSON, error) { + r, err := c.client.ContainerStats(ctx, containerID, false) + if err != nil { + return nil, err + } + + var stats types.StatsJSON + decoder := json.NewDecoder(r.Body) + + if err := decoder.Decode(&stats); err != nil { + return nil, err + } + + return &stats, err +} + +func (c *DockerCollector) calculateUptime(container types.ContainerJSON) float64 { + startTime, err := c.clock.Parse(time.RFC3339Nano, container.State.StartedAt) + if err != nil { + return 0 + } + + return c.clock.Since(startTime).Seconds() +} + +func (c *DockerCollector) isContainerIgnored(container types.Container) bool { + ignore, ok := container.Labels[c.ignoreLabel] + if !ok { + return false + } + + b, err := strconv.ParseBool(ignore) + if err != nil { + return false + } + + return b +} + +func (c *DockerCollector) collectScrapeError(ch chan<- prometheus.Metric) { + ch <- prometheus.MustNewConstMetric(scrapeErrors, prometheus.CounterValue, 1) +} + +func calculateMemUsageUnixNoCache(mem types.MemoryStats) float64 { + if v, isCgroup1 := mem.Stats["total_inactive_file"]; isCgroup1 && v < mem.Usage { + return float64(mem.Usage - v) + } + if v := mem.Stats["inactive_file"]; v < mem.Usage { + return float64(mem.Usage - v) + } + return float64(mem.Usage) +} + +// containerName returns the first name of a container +// without the leading slash. +func containerName(c types.Container) string { + if len(c.Names) == 0 { + return "" + } + + return strings.TrimLeft(c.Names[0], "/") +} + +// getOnlineCPUs returns the number of online CPUs. +func getOnlineCPUs(stats *types.StatsJSON) float64 { + onlineCPUs := float64(stats.CPUStats.OnlineCPUs) + + if onlineCPUs == 0.0 { + onlineCPUs = float64(len(stats.CPUStats.CPUUsage.PercpuUsage)) + } + + return onlineCPUs +} diff --git a/docker-exporter/internal/collector/collector_test.go b/docker-exporter/internal/collector/collector_test.go new file mode 100644 index 0000000..599f0f7 --- /dev/null +++ b/docker-exporter/internal/collector/collector_test.go @@ -0,0 +1,427 @@ +package collector_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/davidborzek/docker-exporter/internal/collector" + "github.com/davidborzek/docker-exporter/internal/mock" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/prometheus/client_golang/prometheus/testutil" + "go.uber.org/mock/gomock" +) + +const ignoreLabel = "docker-exporter.ignore" + +func TestCollectMetrics(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + srv := httptest.NewServer(http.HandlerFunc(mockDockerApi)) + defer srv.Close() + + cli, err := client.NewClientWithOpts( + client.WithHost(srv.URL), + client.WithHTTPClient(&http.Client{}), + ) + + if err != nil { + panic(err) + } + + mockClock := mock.NewMockClock(ctrl) + mockClock.EXPECT(). + Now(). + Return(time.Now()). + Times(1) + + mockClock.EXPECT(). + Parse(gomock.Any(), gomock.Any()). + DoAndReturn(func(s1, s2 string) (time.Time, error) { + return time.Parse(s1, s2) + }). + Times(1) + + // The first call to Since is for the uptime of the container + mockClock.EXPECT(). + Since(gomock.Any()). + Return(1 * time.Second). + Times(1) + + // The second call to Since is for the scrape duration + mockClock.EXPECT(). + Since(gomock.Any()). + Return(2 * time.Second). + Times(1) + + dc := collector.NewWithClient(cli, mockClock, ignoreLabel) + + const expected = ` + # HELP docker_container_block_io_read_bytes Block I/O read bytes total + # TYPE docker_container_block_io_read_bytes gauge + docker_container_block_io_read_bytes{name="testName"} 9999 + # HELP docker_container_block_io_write_bytes Block I/O write bytes total + # TYPE docker_container_block_io_write_bytes gauge + docker_container_block_io_write_bytes{name="testName"} 7777 + # HELP docker_container_cpu_online_cpus Number of online CPUs + # TYPE docker_container_cpu_online_cpus gauge + docker_container_cpu_online_cpus{name="testName"} 4 + # HELP docker_container_cpu_usage_percentage CPU usage in percentage + # TYPE docker_container_cpu_usage_percentage gauge + docker_container_cpu_usage_percentage{name="testName"} 0 + # HELP docker_container_info Infos about the container + # TYPE docker_container_info gauge + docker_container_info{image="sha256:d3751d33f9cd5049c4af2b462735457e4d3baf130bcbb87f389e349fbaeb20b9",image_name="myImage",name="testName"} 1 + # HELP docker_container_memory_total_bytes Total memory in bytes + # TYPE docker_container_memory_total_bytes gauge + docker_container_memory_total_bytes{name="testName"} 8e+09 + # HELP docker_container_memory_usage_bytes Memory usage in bytes + # TYPE docker_container_memory_usage_bytes gauge + docker_container_memory_usage_bytes{name="testName"} 9999 + # HELP docker_container_memory_usage_percentage Memory usage in percentage + # TYPE docker_container_memory_usage_percentage gauge + docker_container_memory_usage_percentage{name="testName"} 0.0001249875 + # HELP docker_container_network_rx_bytes Network received bytes total + # TYPE docker_container_network_rx_bytes gauge + docker_container_network_rx_bytes{name="testName",network="eth0"} 135 + # HELP docker_container_network_rx_dropped_packets Network dropped packets total + # TYPE docker_container_network_rx_dropped_packets gauge + docker_container_network_rx_dropped_packets{name="testName",network="eth0"} 3 + # HELP docker_container_network_rx_errors Network received errors + # TYPE docker_container_network_rx_errors gauge + docker_container_network_rx_errors{name="testName",network="eth0"} 1 + # HELP docker_container_network_rx_packets Network received packets total + # TYPE docker_container_network_rx_packets gauge + docker_container_network_rx_packets{name="testName",network="eth0"} 246 + # HELP docker_container_network_tx_bytes Network sent bytes total + # TYPE docker_container_network_tx_bytes gauge + docker_container_network_tx_bytes{name="testName",network="eth0"} 975 + # HELP docker_container_network_tx_dropped_packets Network dropped packets total + # TYPE docker_container_network_tx_dropped_packets gauge + docker_container_network_tx_dropped_packets{name="testName",network="eth0"} 2 + # HELP docker_container_network_tx_errors Network sent errors + # TYPE docker_container_network_tx_errors gauge + docker_container_network_tx_errors{name="testName",network="eth0"} 4 + # HELP docker_container_network_tx_packets Network sent packets total + # TYPE docker_container_network_tx_packets gauge + docker_container_network_tx_packets{name="testName",network="eth0"} 864 + # HELP docker_container_pids_current Current number of pids + # TYPE docker_container_pids_current gauge + docker_container_pids_current{name="testName"} 12 + # HELP docker_container_state State of the container + # TYPE docker_container_state gauge + docker_container_state{name="testName",state="running"} 1 + # HELP docker_container_uptime Uptime of the container in seconds + # TYPE docker_container_uptime gauge + docker_container_uptime{name="testName"} 1.0 + # HELP docker_exporter_scrape_duration Duration of the scrape in seconds + # TYPE docker_exporter_scrape_duration gauge + docker_exporter_scrape_duration 2 + ` + + if err := testutil.CollectAndCompare(dc, strings.NewReader(expected)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestCollectMetricsShouldCollectErrorWhenContainerListFails(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + srv := httptest.NewServer(http.HandlerFunc(mockErrorDockerApi)) + defer srv.Close() + + cli, err := client.NewClientWithOpts( + client.WithHost(srv.URL), + client.WithHTTPClient(&http.Client{}), + ) + + if err != nil { + panic(err) + } + + mockClock := mock.NewMockClock(ctrl) + mockClock.EXPECT(). + Now(). + Return(time.Now()). + Times(1) + + mockClock.EXPECT(). + Since(gomock.Any()). + Return(2 * time.Second). + Times(1) + + dc := collector.NewWithClient(cli, mockClock, ignoreLabel) + + const expected = ` + # HELP docker_exporter_scrape_errors Number of scrape errors + # TYPE docker_exporter_scrape_errors counter + docker_exporter_scrape_errors 1 + # HELP docker_exporter_scrape_duration Duration of the scrape in seconds + # TYPE docker_exporter_scrape_duration gauge + docker_exporter_scrape_duration 2 + ` + + if err := testutil.CollectAndCompare(dc, strings.NewReader(expected)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestCollectMetricsShouldCollectErrorWhenContainerInspectFails(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + srv := httptest.NewServer(http.HandlerFunc(mockContainerInspectErrorDockerApi)) + defer srv.Close() + + cli, err := client.NewClientWithOpts( + client.WithHost(srv.URL), + client.WithHTTPClient(&http.Client{}), + ) + + if err != nil { + panic(err) + } + + mockClock := mock.NewMockClock(ctrl) + mockClock.EXPECT(). + Now(). + Return(time.Now()). + Times(1) + + mockClock.EXPECT(). + Since(gomock.Any()). + Return(2 * time.Second). + Times(1) + + dc := collector.NewWithClient(cli, mockClock, ignoreLabel) + + const expected = ` + # HELP docker_exporter_scrape_errors Number of scrape errors + # TYPE docker_exporter_scrape_errors counter + docker_exporter_scrape_errors 1 + # HELP docker_exporter_scrape_duration Duration of the scrape in seconds + # TYPE docker_exporter_scrape_duration gauge + docker_exporter_scrape_duration 2 + ` + + if err := testutil.CollectAndCompare(dc, strings.NewReader(expected)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func TestCollectMetricsShouldCollectErrorWhenContainerStatsFails(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + srv := httptest.NewServer(http.HandlerFunc(mockContainerStatsErrorDockerApi)) + defer srv.Close() + + cli, err := client.NewClientWithOpts( + client.WithHost(srv.URL), + client.WithHTTPClient(&http.Client{}), + ) + + if err != nil { + panic(err) + } + + mockClock := mock.NewMockClock(ctrl) + mockClock.EXPECT(). + Now(). + Return(time.Now()). + Times(1) + + mockClock.EXPECT(). + Parse(gomock.Any(), gomock.Any()). + DoAndReturn(func(s1, s2 string) (time.Time, error) { + return time.Parse(s1, s2) + }). + Times(1) + + // The first call to Since is for the uptime of the container + mockClock.EXPECT(). + Since(gomock.Any()). + Return(1 * time.Second). + Times(1) + + // The second call to Since is for the scrape duration + mockClock.EXPECT(). + Since(gomock.Any()). + Return(2 * time.Second). + Times(1) + + dc := collector.NewWithClient(cli, mockClock, ignoreLabel) + + const expected = ` + # HELP docker_container_info Infos about the container + # TYPE docker_container_info gauge + docker_container_info{image="sha256:d3751d33f9cd5049c4af2b462735457e4d3baf130bcbb87f389e349fbaeb20b9",image_name="myImage",name="testName"} 1 + # HELP docker_container_state State of the container + # TYPE docker_container_state gauge + docker_container_state{name="testName",state="running"} 1 + # HELP docker_container_uptime Uptime of the container in seconds + # TYPE docker_container_uptime gauge + docker_container_uptime{name="testName"} 1 + # HELP docker_exporter_scrape_duration Duration of the scrape in seconds + # TYPE docker_exporter_scrape_duration gauge + docker_exporter_scrape_duration 2 + # HELP docker_exporter_scrape_errors Number of scrape errors + # TYPE docker_exporter_scrape_errors counter + docker_exporter_scrape_errors 1 + ` + + if err := testutil.CollectAndCompare(dc, strings.NewReader(expected)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } +} + +func buildInspectResponse() types.ContainerJSON { + return types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + State: &types.ContainerState{ + StartedAt: "2023-09-17T12:00:00.00Z", + }, + Image: "sha256:d3751d33f9cd5049c4af2b462735457e4d3baf130bcbb87f389e349fbaeb20b9", + }, + Config: &container.Config{ + Image: "myImage", + }, + } + +} + +func buildContainerListResponse() []types.Container { + return []types.Container{ + { + ID: "testID", + Names: []string{"/testName"}, + State: "running", + }, + { + ID: "testIDIgnored", + Names: []string{"/testNameIgnored"}, + State: "running", + Labels: map[string]string{ + ignoreLabel: "true", + }, + }, + } +} + +func buildStatsResponse() types.StatsJSON { + return types.StatsJSON{ + Stats: types.Stats{ + BlkioStats: types.BlkioStats{ + IoServiceBytesRecursive: []types.BlkioStatEntry{ + { + Op: "read", + Value: 9999, + }, + { + Op: "write", + Value: 7777, + }, + }, + }, + MemoryStats: types.MemoryStats{ + Usage: 9999, + MaxUsage: 99999, + Limit: 8000000000, + Stats: map[string]uint64{ + "total_inactive_file": 121212, + }, + }, + CPUStats: types.CPUStats{ + CPUUsage: types.CPUUsage{ + TotalUsage: 8888, + }, + SystemUsage: 202, + OnlineCPUs: 4, + }, + PreCPUStats: types.CPUStats{ + CPUUsage: types.CPUUsage{ + TotalUsage: 1111, + }, + SystemUsage: 1223, + OnlineCPUs: 4, + }, + PidsStats: types.PidsStats{ + Current: 12, + }, + }, + Networks: map[string]types.NetworkStats{ + "eth0": { + RxBytes: 135, + RxPackets: 246, + RxDropped: 3, + RxErrors: 1, + TxBytes: 975, + TxPackets: 864, + TxDropped: 2, + TxErrors: 4, + }, + }, + } +} + +func mockJsonResponse(w http.ResponseWriter, r *http.Request, body any) { + raw, err := json.Marshal(body) + if err != nil { + panic(err) + } + + w.Write(raw) +} + +func mockDockerApi(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "stats") { + mockJsonResponse(w, r, buildStatsResponse()) + return + } + + if strings.Contains(r.URL.Path, "testID") { + mockJsonResponse(w, r, buildInspectResponse()) + return + } + + mockJsonResponse(w, r, buildContainerListResponse()) +} + +func mockErrorDockerApi(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) +} + +func mockContainerInspectErrorDockerApi(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "stats") { + mockJsonResponse(w, r, buildStatsResponse()) + return + } + + if strings.Contains(r.URL.Path, "testID") { + w.WriteHeader(http.StatusInternalServerError) + return + } + + mockJsonResponse(w, r, buildContainerListResponse()) +} + +func mockContainerStatsErrorDockerApi(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "stats") { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if strings.Contains(r.URL.Path, "testID") { + mockJsonResponse(w, r, buildInspectResponse()) + return + } + + mockJsonResponse(w, r, buildContainerListResponse()) +} diff --git a/docker-exporter/internal/collector/metrics.go b/docker-exporter/internal/collector/metrics.go new file mode 100644 index 0000000..ec5ba7f --- /dev/null +++ b/docker-exporter/internal/collector/metrics.go @@ -0,0 +1,172 @@ +package collector + +import "github.com/prometheus/client_golang/prometheus" + +var ( + containerStateMetric = prometheus.NewDesc( + "docker_container_state", + "State of the container", + []string{"name", "state"}, + nil, + ) + + containerInfo = prometheus.NewDesc( + "docker_container_info", + "Infos about the container", + []string{"name", "image_name", "image"}, + nil, + ) + + containerUptime = prometheus.NewDesc( + "docker_container_uptime", + "Uptime of the container in seconds", + []string{"name"}, + nil, + ) + + scrapeDuration = prometheus.NewDesc( + "docker_exporter_scrape_duration", + "Duration of the scrape in seconds", + nil, + nil, + ) + + scrapeErrors = prometheus.NewDesc( + "docker_exporter_scrape_errors", + "Number of scrape errors", + nil, + nil, + ) + + /* + CPU Metrics + */ + + cpuUsagePercentage = prometheus.NewDesc( + "docker_container_cpu_usage_percentage", + "CPU usage in percentage", + []string{"name"}, + nil, + ) + + cpuOnlineCPUs = prometheus.NewDesc( + "docker_container_cpu_online_cpus", + "Number of online CPUs", + []string{"name"}, + nil, + ) + + /* + Memory Metrics + */ + + memoryUsageBytes = prometheus.NewDesc( + "docker_container_memory_usage_bytes", + "Memory usage in bytes", + []string{"name"}, + nil, + ) + + memoryTotalBytes = prometheus.NewDesc( + "docker_container_memory_total_bytes", + "Total memory in bytes", + []string{"name"}, + nil, + ) + + memoryUsagePercentage = prometheus.NewDesc( + "docker_container_memory_usage_percentage", + "Memory usage in percentage", + []string{"name"}, + nil, + ) + + /* + Network Metrics + */ + + networkRxBytes = prometheus.NewDesc( + "docker_container_network_rx_bytes", + "Network received bytes total", + []string{"name", "network"}, + nil, + ) + + networkRxPackets = prometheus.NewDesc( + "docker_container_network_rx_packets", + "Network received packets total", + []string{"name", "network"}, + nil, + ) + + networkRxDroppedPackets = prometheus.NewDesc( + "docker_container_network_rx_dropped_packets", + "Network dropped packets total", + []string{"name", "network"}, + nil, + ) + + networkRxErrors = prometheus.NewDesc( + "docker_container_network_rx_errors", + "Network received errors", + []string{"name", "network"}, + nil, + ) + + networkTxBytes = prometheus.NewDesc( + "docker_container_network_tx_bytes", + "Network sent bytes total", + []string{"name", "network"}, + nil, + ) + + networkTxPackets = prometheus.NewDesc( + "docker_container_network_tx_packets", + "Network sent packets total", + []string{"name", "network"}, + nil, + ) + + networkTxDroppedPackets = prometheus.NewDesc( + "docker_container_network_tx_dropped_packets", + "Network dropped packets total", + []string{"name", "network"}, + nil, + ) + + networkTxErrors = prometheus.NewDesc( + "docker_container_network_tx_errors", + "Network sent errors", + []string{"name", "network"}, + nil, + ) + + /* + BlockIO Metrics + */ + + blockIOReadBytes = prometheus.NewDesc( + "docker_container_block_io_read_bytes", + "Block I/O read bytes total", + []string{"name"}, + nil, + ) + + blockIOWriteBytes = prometheus.NewDesc( + "docker_container_block_io_write_bytes", + "Block I/O write bytes total", + []string{"name"}, + nil, + ) + + /* + PIDs Metrics + */ + + pidsCurrent = prometheus.NewDesc( + "docker_container_pids_current", + "Current number of pids", + []string{"name"}, + nil, + ) +) diff --git a/docker-exporter/internal/handler/handler.go b/docker-exporter/internal/handler/handler.go new file mode 100644 index 0000000..8d91355 --- /dev/null +++ b/docker-exporter/internal/handler/handler.go @@ -0,0 +1,26 @@ +package handler + +import ( + "net/http" +) + +type handler struct { + expectedToken string + mux *http.ServeMux +} + +func New(authToken string) *handler { + s := &handler{ + expectedToken: authToken, + mux: http.NewServeMux(), + } + + s.mux.HandleFunc("/metrics", s.handleMetrics()) + s.mux.HandleFunc("/health", s.handleHealth()) + + return s +} + +func (s *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + s.mux.ServeHTTP(rw, r) +} diff --git a/docker-exporter/internal/handler/health.go b/docker-exporter/internal/handler/health.go new file mode 100644 index 0000000..e8bb2dc --- /dev/null +++ b/docker-exporter/internal/handler/health.go @@ -0,0 +1,19 @@ +package handler + +import ( + "net/http" + "time" +) + +// handleHealth is a basic health route handler. +func (*handler) handleHealth() func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Disable caching + w.Header().Set("Expires", time.Unix(0, 0).Format(time.RFC1123)) + w.Header().Set("Cache-Control", "no-cache, private, max-age=0") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("X-Accel-Expires", "0") + + w.WriteHeader(http.StatusOK) + } +} diff --git a/docker-exporter/internal/handler/health_test.go b/docker-exporter/internal/handler/health_test.go new file mode 100644 index 0000000..73c1317 --- /dev/null +++ b/docker-exporter/internal/handler/health_test.go @@ -0,0 +1,24 @@ +package handler_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/davidborzek/docker-exporter/internal/handler" + "github.com/stretchr/testify/assert" +) + +func TestHealthHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + h := handler.New("") + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) +} diff --git a/docker-exporter/internal/handler/metrics.go b/docker-exporter/internal/handler/metrics.go new file mode 100644 index 0000000..4ad4fda --- /dev/null +++ b/docker-exporter/internal/handler/metrics.go @@ -0,0 +1,38 @@ +package handler + +import ( + "errors" + "net/http" + "strings" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// authenticate authenticates a request when a token is configured. +func (s *handler) authenticate(r *http.Request) error { + if len(s.expectedToken) == 0 { + return nil + } + + token := strings.ReplaceAll( + r.Header.Get("Authorization"), + "Bearer ", "") + + if token != s.expectedToken { + return errors.New("authentication failed") + } + + return nil +} + +// handleMetrics is a prometheus metrics handler. +func (s *handler) handleMetrics() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if err := s.authenticate(r); err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + promhttp.Handler().ServeHTTP(w, r) + } +} diff --git a/docker-exporter/internal/handler/metrics_test.go b/docker-exporter/internal/handler/metrics_test.go new file mode 100644 index 0000000..91fcd92 --- /dev/null +++ b/docker-exporter/internal/handler/metrics_test.go @@ -0,0 +1,73 @@ +package handler_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/davidborzek/docker-exporter/internal/handler" + "github.com/stretchr/testify/assert" +) + +const ( + authToken = "someToken" +) + +func TestMetricsHandlerReturnsOK(t *testing.T) { + req, err := http.NewRequest("GET", "/metrics", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + h := handler.New("") + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) +} + +func TestMetricsHandlerReturnsUnauthorizedForEmptyAuthorizationHeader(t *testing.T) { + req, err := http.NewRequest("GET", "/metrics", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + h := handler.New(authToken) + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) +} + +func TestMetricsHandlerReturnsUnauthorizedForInvalidToken(t *testing.T) { + req, err := http.NewRequest("GET", "/metrics", nil) + req.Header.Add("Authorization", "Bearer invalidToken") + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + h := handler.New(authToken) + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) +} + +func TestMetricsHandlerReturnsOKForValidToken(t *testing.T) { + req, err := http.NewRequest("GET", "/metrics", nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authToken)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + h := handler.New(authToken) + + h.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) +} diff --git a/docker-exporter/internal/mock/clock.go b/docker-exporter/internal/mock/clock.go new file mode 100644 index 0000000..8d37f4e --- /dev/null +++ b/docker-exporter/internal/mock/clock.go @@ -0,0 +1,155 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: clock.go +// +// Generated by this command: +// +// mockgen -destination=../mock/clock.go -package mock -typed -source clock.go +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + time "time" + + gomock "go.uber.org/mock/gomock" +) + +// MockClock is a mock of Clock interface. +type MockClock struct { + ctrl *gomock.Controller + recorder *MockClockMockRecorder +} + +// MockClockMockRecorder is the mock recorder for MockClock. +type MockClockMockRecorder struct { + mock *MockClock +} + +// NewMockClock creates a new mock instance. +func NewMockClock(ctrl *gomock.Controller) *MockClock { + mock := &MockClock{ctrl: ctrl} + mock.recorder = &MockClockMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClock) EXPECT() *MockClockMockRecorder { + return m.recorder +} + +// Now mocks base method. +func (m *MockClock) Now() time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Now") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// Now indicates an expected call of Now. +func (mr *MockClockMockRecorder) Now() *MockClockNowCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClock)(nil).Now)) + return &MockClockNowCall{Call: call} +} + +// MockClockNowCall wrap *gomock.Call +type MockClockNowCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockClockNowCall) Return(arg0 time.Time) *MockClockNowCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockClockNowCall) Do(f func() time.Time) *MockClockNowCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockClockNowCall) DoAndReturn(f func() time.Time) *MockClockNowCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Parse mocks base method. +func (m *MockClock) Parse(layout, value string) (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Parse", layout, value) + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Parse indicates an expected call of Parse. +func (mr *MockClockMockRecorder) Parse(layout, value any) *MockClockParseCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parse", reflect.TypeOf((*MockClock)(nil).Parse), layout, value) + return &MockClockParseCall{Call: call} +} + +// MockClockParseCall wrap *gomock.Call +type MockClockParseCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockClockParseCall) Return(arg0 time.Time, arg1 error) *MockClockParseCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockClockParseCall) Do(f func(string, string) (time.Time, error)) *MockClockParseCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockClockParseCall) DoAndReturn(f func(string, string) (time.Time, error)) *MockClockParseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Since mocks base method. +func (m *MockClock) Since(t time.Time) time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Since", t) + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// Since indicates an expected call of Since. +func (mr *MockClockMockRecorder) Since(t any) *MockClockSinceCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Since", reflect.TypeOf((*MockClock)(nil).Since), t) + return &MockClockSinceCall{Call: call} +} + +// MockClockSinceCall wrap *gomock.Call +type MockClockSinceCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockClockSinceCall) Return(arg0 time.Duration) *MockClockSinceCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockClockSinceCall) Do(f func(time.Time) time.Duration) *MockClockSinceCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockClockSinceCall) DoAndReturn(f func(time.Time) time.Duration) *MockClockSinceCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/docker-exporter/main.go b/docker-exporter/main.go new file mode 100644 index 0000000..fee00c5 --- /dev/null +++ b/docker-exporter/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "os" + + "github.com/davidborzek/docker-exporter/cmd" +) + +func main() { + cmd.Main(os.Args) +} diff --git a/docker-exporter/renovate.json b/docker-exporter/renovate.json new file mode 100644 index 0000000..e772561 --- /dev/null +++ b/docker-exporter/renovate.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "packageRules": [ + { + "matchUpdateTypes": ["major", "minor", "patch"], + "automerge": true + } + ], + "extends": ["config:base"] +}