├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ ├── codeql-analysis.yml │ ├── dependabot-auto-approve.yaml │ └── dependabot-auto-merge.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── manifests ├── base │ ├── deployment.yaml │ └── kustomization.yaml ├── cluster │ ├── clusterrole.yaml │ └── kustomization.yaml └── scrape-config.yaml └── test-summary.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See GitHub's docs for more information on this file: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | version: 2 4 | updates: 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | 11 | # Maintain dependencies for Go modules 12 | - package-ecosystem: "gomod" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "master" 12 | 13 | env: 14 | REGISTRY: quay.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | docker: 19 | runs-on: ubuntu-latest 20 | if: github.actor != 'dependabot[bot]' 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | - name: Login to Quay.io Container Registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: quay.io 34 | username: utilitywarehouse+drone_ci 35 | password: ${{ secrets.SYSTEM_QUAY_TOKEN }} 36 | - name: Extract metadata (tags, labels) for Docker 37 | id: meta 38 | uses: docker/metadata-action@v5 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@v6 43 | with: 44 | context: . 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "30 22 * * 0" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-approve.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#approve-a-pull-request 2 | name: Dependabot auto-approve 3 | on: pull_request 4 | 5 | permissions: 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Approve a PR 19 | run: gh pr review --approve "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: Dependabot auto-merge 3 | on: pull_request 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2.4.0 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | - name: Enable auto-merge for Dependabot PRs 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | kube-summary-exporter 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-alpine AS build 2 | WORKDIR /src 3 | COPY go.* ./ 4 | RUN go mod download 5 | COPY . . 6 | ENV CGO_ENABLED=0 7 | RUN apk --no-cache add git \ 8 | && go test ./... \ 9 | && go build -o /kube-summary-exporter . 10 | 11 | FROM alpine:3.20 12 | COPY --from=build /kube-summary-exporter /kube-summary-exporter 13 | 14 | ENTRYPOINT [ "/kube-summary-exporter"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 UW - Utility Warehouse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE := quay.io/utilitywarehouse/kube-summary-exporter 2 | 3 | .PHONY: release 4 | release: 5 | @sd "$(IMAGE):latest" "$(IMAGE):$(VERSION)" $$(rg -l -- $(IMAGE) manifests/) 6 | @git add -- manifests/ 7 | @git commit -m "Release $(VERSION)" 8 | @sd "$(IMAGE):$(VERSION)" "$(IMAGE):latest" $$(rg -l -- "$(IMAGE)" manifests/) 9 | @git add -- manifests/ 10 | @git commit -m "Clean up release $(VERSION)" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kube-summary-exporter 2 | 3 | Exports Prometheus metrics for the Kubernetes Summary API. 4 | 5 | This exists because of: https://github.com/google/cadvisor/issues/2785 6 | 7 | Docker / Podman image available: 8 | `quay.io/utilitywarehouse/kube-summary-exporter` 9 | 10 | All available tags: 11 | https://quay.io/repository/utilitywarehouse/kube-summary-exporter?tab=tags 12 | 13 | ## Run locally 14 | 15 | To run exporter locally run `go run ./...` 16 | 17 | This will run server on default port `9779` 18 | 19 | Visiting http://localhost:9779/node/{node-name} will return metrics for the 20 | specified node. The app will look for the node in the `current-context` 21 | cluster set in kube config. 22 | 23 | You can also visit http://localhost:9779/nodes to retrieve metrics for all nodes in the cluster. 24 | 25 | [Here's an example scrape config.](manifests/scrape-config.yaml) 26 | 27 | ## Endpoints 28 | 29 | - `/`: Home page with links to other endpoints 30 | - `/nodes`: Metrics for all nodes in the cluster 31 | - `/node/{node}`: Metrics for a specific node 32 | - `/metrics`: Prometheus metrics about the exporter itself 33 | 34 | ## Command-line Flags 35 | 36 | - `--listen-address`: The address to listen on for HTTP requests (default ":9779") 37 | - `--kubeconfig`: Path to a kubeconfig file (if not provided, the app will try $KUBECONFIG, $HOME/.kube/config, or in-cluster config) 38 | 39 | ## Metrics 40 | 41 | | Metric | Description | Labels | 42 | | -------------------------------------------------- | -------------------------------------------------------------------- | -------------------------- | 43 | | kube_summary_container_logs_available_bytes | Number of bytes that aren't consumed by the container logs | node, pod, namespace, name | 44 | | kube_summary_container_logs_capacity_bytes | Number of bytes that can be consumed by the container logs | node, pod, namespace, name | 45 | | kube_summary_container_logs_inodes | Number of Inodes for logs | node, pod, namespace, name | 46 | | kube_summary_container_logs_inodes_free | Number of available Inodes for logs | node, pod, namespace, name | 47 | | kube_summary_container_logs_inodes_used | Number of used Inodes for logs | node, pod, namespace, name | 48 | | kube_summary_container_logs_used_bytes | Number of bytes that are consumed by the container logs | node, pod, namespace, name | 49 | | kube_summary_container_rootfs_available_bytes | Number of bytes that aren't consumed by the container | node, pod, namespace, name | 50 | | kube_summary_container_rootfs_capacity_bytes | Number of bytes that can be consumed by the container | node, pod, namespace, name | 51 | | kube_summary_container_rootfs_inodes | Number of Inodes | node, pod, namespace, name | 52 | | kube_summary_container_rootfs_inodes_free | Number of available Inodes | node, pod, namespace, name | 53 | | kube_summary_container_rootfs_inodes_used | Number of used Inodes | node, pod, namespace, name | 54 | | kube_summary_container_rootfs_used_bytes | Number of bytes that are consumed by the container | node, pod, namespace, name | 55 | | kube_summary_node_runtime_imagefs_available_bytes | Number of bytes of node Runtime ImageFS that aren't consumed | node | 56 | | kube_summary_node_runtime_imagefs_capacity_bytes | Number of bytes of node Runtime ImageFS that can be consumed | node | 57 | | kube_summary_node_runtime_imagefs_inodes | Number of Inodes for node Runtime ImageFS | node | 58 | | kube_summary_node_runtime_imagefs_inodes_free | Number of available Inodes for node Runtime ImageFS | node | 59 | | kube_summary_node_runtime_imagefs_inodes_used | Number of used Inodes for node Runtime ImageFS | node | 60 | | kube_summary_node_runtime_imagefs_used_bytes | Number of bytes of node Runtime ImageFS that are consumed | node | 61 | | kube_summary_pod_ephemeral_storage_available_bytes | Number of bytes of Ephemeral storage that aren't consumed by the pod | node, pod, namespace | 62 | | kube_summary_pod_ephemeral_storage_capacity_bytes | Number of bytes of Ephemeral storage that can be consumed by the pod | node, pod, namespace | 63 | | kube_summary_pod_ephemeral_storage_inodes | Number of Inodes for pod Ephemeral storage | node, pod, namespace | 64 | | kube_summary_pod_ephemeral_storage_inodes_free | Number of available Inodes for pod Ephemeral storage | node, pod, namespace | 65 | | kube_summary_pod_ephemeral_storage_inodes_used | Number of used Inodes for pod Ephemeral storage | node, pod, namespace | 66 | | kube_summary_pod_ephemeral_storage_used_bytes | Number of bytes of Ephemeral storage that are consumed by the pod | node, pod, namespace | 67 | 68 | ## Development 69 | 70 | ### Running Tests 71 | 72 | To run the tests, use the following command: 73 | ``` 74 | go test ./... 75 | ``` 76 | 77 | The main test file (`main_test.go`) includes a test for the `collectSummaryMetrics` function, which verifies that the metrics are collected correctly from a sample JSON file (`test-summary.json`). 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/utilitywarehouse/kube-summary-exporter 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/google/go-cmp v0.7.0 9 | github.com/gorilla/mux v1.8.1 10 | github.com/prometheus/client_golang v1.22.0 11 | k8s.io/apimachinery v0.33.1 12 | k8s.io/client-go v0.33.1 13 | k8s.io/kubelet v0.33.1 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 21 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 24 | github.com/go-openapi/jsonreference v0.21.0 // indirect 25 | github.com/go-openapi/swag v0.23.0 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/google/gnostic-models v0.6.9 // indirect 28 | github.com/google/uuid v1.6.0 // indirect 29 | github.com/josharian/intern v1.0.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/mailru/easyjson v0.7.7 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 35 | github.com/pkg/errors v0.9.1 // indirect 36 | github.com/prometheus/client_model v0.6.1 // indirect 37 | github.com/prometheus/common v0.62.0 // indirect 38 | github.com/prometheus/procfs v0.15.1 // indirect 39 | github.com/spf13/pflag v1.0.5 // indirect 40 | github.com/x448/float16 v0.8.4 // indirect 41 | golang.org/x/net v0.38.0 // indirect 42 | golang.org/x/oauth2 v0.27.0 // indirect 43 | golang.org/x/sys v0.31.0 // indirect 44 | golang.org/x/term v0.30.0 // indirect 45 | golang.org/x/text v0.23.0 // indirect 46 | golang.org/x/time v0.9.0 // indirect 47 | google.golang.org/protobuf v1.36.5 // indirect 48 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 49 | gopkg.in/inf.v0 v0.9.1 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | k8s.io/api v0.33.1 // indirect 52 | k8s.io/klog/v2 v2.130.1 // indirect 53 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 54 | k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 // indirect 55 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 56 | sigs.k8s.io/randfill v1.0.0 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 58 | sigs.k8s.io/yaml v1.4.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= 10 | github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 12 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 13 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 14 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 15 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 16 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 17 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 18 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 19 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 20 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 21 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 22 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 23 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 24 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 25 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 26 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 27 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 32 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 33 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 34 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 36 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 37 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 38 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 39 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 40 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 41 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 42 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 43 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 44 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 45 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 46 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 47 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 48 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 49 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 50 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 51 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 52 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 53 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 56 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 57 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 59 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 60 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 61 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 62 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 63 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 64 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 65 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 69 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 70 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 71 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 72 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 73 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 74 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 75 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 76 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 77 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 78 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 79 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 82 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 83 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 84 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 85 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 86 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 87 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 88 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 89 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 90 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 91 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 92 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 93 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 94 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 95 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 96 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 97 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 99 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 100 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 101 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 102 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 103 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 110 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 111 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 112 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 113 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 114 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 115 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 116 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 117 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 118 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 119 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 120 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 121 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 122 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 123 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 124 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 125 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 130 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 132 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 133 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 134 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 135 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 136 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 137 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 138 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 139 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 140 | k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= 141 | k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= 142 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 143 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 144 | k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= 145 | k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= 146 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 147 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 148 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 149 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 150 | k8s.io/kubelet v0.33.1 h1:x4LCw1/iZVWOKA4RoITnuB8gMHnw31HPB3S0EF0EexE= 151 | k8s.io/kubelet v0.33.1/go.mod h1:8WpdC9M95VmsqIdGSQrajXooTfT5otEj8pGWOm+KKfQ= 152 | k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= 153 | k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 154 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 155 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 156 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 157 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 158 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 159 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 160 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 161 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 162 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 163 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/gorilla/mux" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/tools/clientcmd" 19 | stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 20 | 21 | // Support auth providers in kubeconfig files 22 | _ "k8s.io/client-go/plugin/pkg/client/auth" 23 | ) 24 | 25 | var ( 26 | flagKubeConfigPath = flag.String("kubeconfig", "", "Path of a kubeconfig file, if not provided the app will try $KUBECONFIG, $HOME/.kube/config or in cluster config") 27 | flagListenAddress = flag.String("listen-address", ":9779", "Listen address") 28 | metricsNamespace = "kube_summary" 29 | ) 30 | 31 | type Collectors struct { 32 | containerLogsInodesFree *prometheus.GaugeVec 33 | containerLogsInodes *prometheus.GaugeVec 34 | containerLogsInodesUsed *prometheus.GaugeVec 35 | containerLogsAvailableBytes *prometheus.GaugeVec 36 | containerLogsCapacityBytes *prometheus.GaugeVec 37 | containerLogsUsedBytes *prometheus.GaugeVec 38 | containerRootFsInodesFree *prometheus.GaugeVec 39 | containerRootFsInodes *prometheus.GaugeVec 40 | containerRootFsInodesUsed *prometheus.GaugeVec 41 | containerRootFsAvailableBytes *prometheus.GaugeVec 42 | containerRootFsCapacityBytes *prometheus.GaugeVec 43 | containerRootFsUsedBytes *prometheus.GaugeVec 44 | podEphemeralStorageAvailableBytes *prometheus.GaugeVec 45 | podEphemeralStorageCapacityBytes *prometheus.GaugeVec 46 | podEphemeralStorageUsedBytes *prometheus.GaugeVec 47 | podEphemeralStorageInodesFree *prometheus.GaugeVec 48 | podEphemeralStorageInodes *prometheus.GaugeVec 49 | podEphemeralStorageInodesUsed *prometheus.GaugeVec 50 | nodeRuntimeImageFSAvailableBytes *prometheus.GaugeVec 51 | nodeRuntimeImageFSCapacityBytes *prometheus.GaugeVec 52 | nodeRuntimeImageFSUsedBytes *prometheus.GaugeVec 53 | nodeRuntimeImageFSInodesFree *prometheus.GaugeVec 54 | nodeRuntimeImageFSInodes *prometheus.GaugeVec 55 | nodeRuntimeImageFSInodesUsed *prometheus.GaugeVec 56 | } 57 | 58 | func newCollectors() *Collectors { 59 | return &Collectors{ 60 | containerLogsInodesFree: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 61 | Namespace: metricsNamespace, 62 | Name: "container_logs_inodes_free", 63 | Help: "Number of available Inodes for logs", 64 | }, []string{"node", "pod", "namespace", "name"}), 65 | containerLogsInodes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 66 | Namespace: metricsNamespace, 67 | Name: "container_logs_inodes", 68 | Help: "Number of Inodes for logs", 69 | }, []string{"node", "pod", "namespace", "name"}), 70 | containerLogsInodesUsed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 71 | Namespace: metricsNamespace, 72 | Name: "container_logs_inodes_used", 73 | Help: "Number of used Inodes for logs", 74 | }, []string{"node", "pod", "namespace", "name"}), 75 | containerLogsAvailableBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 76 | Namespace: metricsNamespace, 77 | Name: "container_logs_available_bytes", 78 | Help: "Number of bytes that aren't consumed by the container logs", 79 | }, []string{"node", "pod", "namespace", "name"}), 80 | containerLogsCapacityBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 81 | Namespace: metricsNamespace, 82 | Name: "container_logs_capacity_bytes", 83 | Help: "Number of bytes that can be consumed by the container logs", 84 | }, []string{"node", "pod", "namespace", "name"}), 85 | containerLogsUsedBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 86 | Namespace: metricsNamespace, 87 | Name: "container_logs_used_bytes", 88 | Help: "Number of bytes that are consumed by the container logs", 89 | }, []string{"node", "pod", "namespace", "name"}), 90 | containerRootFsInodesFree: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 91 | Namespace: metricsNamespace, 92 | Name: "container_rootfs_inodes_free", 93 | Help: "Number of available Inodes", 94 | }, []string{"node", "pod", "namespace", "name"}), 95 | containerRootFsInodes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 96 | Namespace: metricsNamespace, 97 | Name: "container_rootfs_inodes", 98 | Help: "Number of Inodes", 99 | }, []string{"node", "pod", "namespace", "name"}), 100 | containerRootFsInodesUsed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 101 | Namespace: metricsNamespace, 102 | Name: "container_rootfs_inodes_used", 103 | Help: "Number of used Inodes", 104 | }, []string{"node", "pod", "namespace", "name"}), 105 | containerRootFsAvailableBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 106 | Namespace: metricsNamespace, 107 | Name: "container_rootfs_available_bytes", 108 | Help: "Number of bytes that aren't consumed by the container", 109 | }, []string{"node", "pod", "namespace", "name"}), 110 | containerRootFsCapacityBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 111 | Namespace: metricsNamespace, 112 | Name: "container_rootfs_capacity_bytes", 113 | Help: "Number of bytes that can be consumed by the container", 114 | }, []string{"node", "pod", "namespace", "name"}), 115 | containerRootFsUsedBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 116 | Namespace: metricsNamespace, 117 | Name: "container_rootfs_used_bytes", 118 | Help: "Number of bytes that are consumed by the container", 119 | }, []string{"node", "pod", "namespace", "name"}), 120 | podEphemeralStorageAvailableBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 121 | Namespace: metricsNamespace, 122 | Name: "pod_ephemeral_storage_available_bytes", 123 | Help: "Number of bytes of Ephemeral storage that aren't consumed by the pod", 124 | }, []string{"node", "pod", "namespace"}), 125 | podEphemeralStorageCapacityBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 126 | Namespace: metricsNamespace, 127 | Name: "pod_ephemeral_storage_capacity_bytes", 128 | Help: "Number of bytes of Ephemeral storage that can be consumed by the pod", 129 | }, []string{"node", "pod", "namespace"}), 130 | podEphemeralStorageUsedBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 131 | Namespace: metricsNamespace, 132 | Name: "pod_ephemeral_storage_used_bytes", 133 | Help: "Number of bytes of Ephemeral storage that are consumed by the pod", 134 | }, []string{"node", "pod", "namespace"}), 135 | podEphemeralStorageInodesFree: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 136 | Namespace: metricsNamespace, 137 | Name: "pod_ephemeral_storage_inodes_free", 138 | Help: "Number of available Inodes for pod Ephemeral storage", 139 | }, []string{"node", "pod", "namespace"}), 140 | podEphemeralStorageInodes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 141 | Namespace: metricsNamespace, 142 | Name: "pod_ephemeral_storage_inodes", 143 | Help: "Number of Inodes for pod Ephemeral storage", 144 | }, []string{"node", "pod", "namespace"}), 145 | podEphemeralStorageInodesUsed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 146 | Namespace: metricsNamespace, 147 | Name: "pod_ephemeral_storage_inodes_used", 148 | Help: "Number of used Inodes for pod Ephemeral storage", 149 | }, []string{"node", "pod", "namespace"}), 150 | nodeRuntimeImageFSAvailableBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 151 | Namespace: metricsNamespace, 152 | Name: "node_runtime_imagefs_available_bytes", 153 | Help: "Number of bytes of node Runtime ImageFS that aren't consumed", 154 | }, []string{"node"}), 155 | nodeRuntimeImageFSCapacityBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 156 | Namespace: metricsNamespace, 157 | Name: "node_runtime_imagefs_capacity_bytes", 158 | Help: "Number of bytes of node Runtime ImageFS that can be consumed", 159 | }, []string{"node"}), 160 | nodeRuntimeImageFSUsedBytes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 161 | Namespace: metricsNamespace, 162 | Name: "node_runtime_imagefs_used_bytes", 163 | Help: "Number of bytes of node Runtime ImageFS that are consumed", 164 | }, []string{"node"}), 165 | nodeRuntimeImageFSInodesFree: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 166 | Namespace: metricsNamespace, 167 | Name: "node_runtime_imagefs_inodes_free", 168 | Help: "Number of available Inodes for node Runtime ImageFS", 169 | }, []string{"node"}), 170 | nodeRuntimeImageFSInodes: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 171 | Namespace: metricsNamespace, 172 | Name: "node_runtime_imagefs_inodes", 173 | Help: "Number of Inodes for node Runtime ImageFS", 174 | }, []string{"node"}), 175 | nodeRuntimeImageFSInodesUsed: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 176 | Namespace: metricsNamespace, 177 | Name: "node_runtime_imagefs_inodes_used", 178 | Help: "Number of used Inodes for node Runtime ImageFS", 179 | }, []string{"node"}), 180 | } 181 | } 182 | 183 | func (c *Collectors) register(registry *prometheus.Registry) { 184 | registry.MustRegister( 185 | c.containerLogsInodesFree, 186 | c.containerLogsInodes, 187 | c.containerLogsInodesUsed, 188 | c.containerLogsAvailableBytes, 189 | c.containerLogsCapacityBytes, 190 | c.containerLogsUsedBytes, 191 | c.containerRootFsInodesFree, 192 | c.containerRootFsInodes, 193 | c.containerRootFsInodesUsed, 194 | c.containerRootFsAvailableBytes, 195 | c.containerRootFsCapacityBytes, 196 | c.containerRootFsUsedBytes, 197 | c.podEphemeralStorageAvailableBytes, 198 | c.podEphemeralStorageCapacityBytes, 199 | c.podEphemeralStorageUsedBytes, 200 | c.podEphemeralStorageInodesFree, 201 | c.podEphemeralStorageInodes, 202 | c.podEphemeralStorageInodesUsed, 203 | c.nodeRuntimeImageFSAvailableBytes, 204 | c.nodeRuntimeImageFSCapacityBytes, 205 | c.nodeRuntimeImageFSUsedBytes, 206 | c.nodeRuntimeImageFSInodesFree, 207 | c.nodeRuntimeImageFSInodes, 208 | c.nodeRuntimeImageFSInodesUsed, 209 | ) 210 | } 211 | 212 | // collectSummaryMetrics collects metrics from a /stats/summary response 213 | func collectSummaryMetrics(summary *stats.Summary, collectors *Collectors) { 214 | nodeName := summary.Node.NodeName 215 | 216 | for _, pod := range summary.Pods { 217 | for _, container := range pod.Containers { 218 | if logs := container.Logs; logs != nil { 219 | if inodesFree := logs.InodesFree; inodesFree != nil { 220 | collectors.containerLogsInodesFree.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*inodesFree)) 221 | } 222 | if inodes := logs.Inodes; inodes != nil { 223 | collectors.containerLogsInodes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*inodes)) 224 | } 225 | if inodesUsed := logs.InodesUsed; inodesUsed != nil { 226 | collectors.containerLogsInodesUsed.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*inodesUsed)) 227 | } 228 | if availableBytes := logs.AvailableBytes; availableBytes != nil { 229 | collectors.containerLogsAvailableBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*availableBytes)) 230 | } 231 | if capacityBytes := logs.CapacityBytes; capacityBytes != nil { 232 | collectors.containerLogsCapacityBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*capacityBytes)) 233 | } 234 | if usedBytes := logs.UsedBytes; usedBytes != nil { 235 | collectors.containerLogsUsedBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*usedBytes)) 236 | } 237 | } 238 | if rootfs := container.Rootfs; rootfs != nil { 239 | if inodesFree := rootfs.InodesFree; inodesFree != nil { 240 | collectors.containerRootFsInodesFree.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*inodesFree)) 241 | } 242 | if inodes := rootfs.Inodes; inodes != nil { 243 | collectors.containerRootFsInodes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*inodes)) 244 | } 245 | if inodesUsed := rootfs.InodesUsed; inodesUsed != nil { 246 | collectors.containerRootFsInodesUsed.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*inodesUsed)) 247 | } 248 | if availableBytes := rootfs.AvailableBytes; availableBytes != nil { 249 | collectors.containerRootFsAvailableBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*availableBytes)) 250 | } 251 | if capacityBytes := rootfs.CapacityBytes; capacityBytes != nil { 252 | collectors.containerRootFsCapacityBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*capacityBytes)) 253 | } 254 | if usedBytes := rootfs.UsedBytes; usedBytes != nil { 255 | collectors.containerRootFsUsedBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace, container.Name).Set(float64(*usedBytes)) 256 | } 257 | } 258 | } 259 | 260 | if ephemeralStorage := pod.EphemeralStorage; ephemeralStorage != nil { 261 | if ephemeralStorage.AvailableBytes != nil { 262 | collectors.podEphemeralStorageAvailableBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace).Set(float64(*ephemeralStorage.AvailableBytes)) 263 | } 264 | if ephemeralStorage.CapacityBytes != nil { 265 | collectors.podEphemeralStorageCapacityBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace).Set(float64(*ephemeralStorage.CapacityBytes)) 266 | } 267 | if ephemeralStorage.UsedBytes != nil { 268 | collectors.podEphemeralStorageUsedBytes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace).Set(float64(*ephemeralStorage.UsedBytes)) 269 | } 270 | if ephemeralStorage.InodesFree != nil { 271 | collectors.podEphemeralStorageInodesFree.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace).Set(float64(*ephemeralStorage.InodesFree)) 272 | } 273 | if ephemeralStorage.Inodes != nil { 274 | collectors.podEphemeralStorageInodes.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace).Set(float64(*ephemeralStorage.Inodes)) 275 | } 276 | if ephemeralStorage.InodesUsed != nil { 277 | collectors.podEphemeralStorageInodesUsed.WithLabelValues(nodeName, pod.PodRef.Name, pod.PodRef.Namespace).Set(float64(*ephemeralStorage.InodesUsed)) 278 | } 279 | } 280 | } 281 | 282 | if runtime := summary.Node.Runtime; runtime != nil { 283 | if runtime.ImageFs.AvailableBytes != nil { 284 | collectors.nodeRuntimeImageFSAvailableBytes.WithLabelValues(nodeName).Set(float64(*runtime.ImageFs.AvailableBytes)) 285 | } 286 | if runtime.ImageFs.CapacityBytes != nil { 287 | collectors.nodeRuntimeImageFSCapacityBytes.WithLabelValues(nodeName).Set(float64(*runtime.ImageFs.CapacityBytes)) 288 | } 289 | if runtime.ImageFs.UsedBytes != nil { 290 | collectors.nodeRuntimeImageFSUsedBytes.WithLabelValues(nodeName).Set(float64(*runtime.ImageFs.UsedBytes)) 291 | } 292 | if runtime.ImageFs.InodesFree != nil { 293 | collectors.nodeRuntimeImageFSInodesFree.WithLabelValues(nodeName).Set(float64(*runtime.ImageFs.InodesFree)) 294 | } 295 | if runtime.ImageFs.Inodes != nil { 296 | collectors.nodeRuntimeImageFSInodes.WithLabelValues(nodeName).Set(float64(*runtime.ImageFs.Inodes)) 297 | } 298 | if runtime.ImageFs.InodesUsed != nil { 299 | collectors.nodeRuntimeImageFSInodesUsed.WithLabelValues(nodeName).Set(float64(*runtime.ImageFs.InodesUsed)) 300 | } 301 | } 302 | } 303 | 304 | // nodeHandler returns metrics for the /stats/summary API of the given node 305 | func nodeHandler(w http.ResponseWriter, r *http.Request, kubeClient *kubernetes.Clientset) { 306 | node := mux.Vars(r)["node"] 307 | 308 | ctx, cancel := timeoutContext(r) 309 | defer cancel() 310 | 311 | summary, err := nodeSummary(ctx, kubeClient, node) 312 | if err != nil { 313 | http.Error(w, fmt.Sprintf("Error querying /stats/summary for %s: %v", node, err), http.StatusInternalServerError) 314 | return 315 | } 316 | 317 | collectors := newCollectors() 318 | registry := prometheus.NewRegistry() 319 | collectors.register(registry) 320 | collectSummaryMetrics(summary, collectors) 321 | 322 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 323 | h.ServeHTTP(w, r) 324 | } 325 | 326 | // allNodesHandler returns metrics for all nodes in the cluster 327 | func allNodesHandler(w http.ResponseWriter, r *http.Request, kubeClient *kubernetes.Clientset) { 328 | ctx, cancel := timeoutContext(r) 329 | defer cancel() 330 | 331 | nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 332 | if err != nil { 333 | http.Error(w, fmt.Sprintf("Error listing nodes: %v", err), http.StatusInternalServerError) 334 | return 335 | } 336 | 337 | collectors := newCollectors() 338 | registry := prometheus.NewRegistry() 339 | collectors.register(registry) 340 | 341 | for _, node := range nodes.Items { 342 | summary, err := nodeSummary(ctx, kubeClient, node.Name) 343 | if err != nil { 344 | http.Error(w, fmt.Sprintf("Error querying /stats/summary for %s: %v", node.Name, err), http.StatusInternalServerError) 345 | return 346 | } 347 | collectSummaryMetrics(summary, collectors) 348 | } 349 | 350 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 351 | h.ServeHTTP(w, r) 352 | } 353 | 354 | // nodeSummary retrieves the summary for a single node 355 | func nodeSummary(ctx context.Context, kubeClient *kubernetes.Clientset, nodeName string) (*stats.Summary, error) { 356 | req := kubeClient.CoreV1().RESTClient().Get().Resource("nodes").Name(nodeName).SubResource("proxy").Suffix("stats/summary") 357 | resp, err := req.DoRaw(ctx) 358 | if err != nil { 359 | return nil, fmt.Errorf("error querying /stats/summary for %s: %v", nodeName, err) 360 | } 361 | 362 | summary := &stats.Summary{} 363 | if err := json.Unmarshal(resp, summary); err != nil { 364 | return nil, fmt.Errorf("error unmarshaling /stats/summary response for %s: %v", nodeName, err) 365 | } 366 | 367 | return summary, nil 368 | } 369 | 370 | // timeoutContext returns a context with timeout based on the X-Prometheus-Scrape-Timeout-Seconds header 371 | func timeoutContext(r *http.Request) (context.Context, context.CancelFunc) { 372 | if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { 373 | timeoutSeconds, err := strconv.ParseFloat(v, 64) 374 | if err == nil { 375 | return context.WithTimeout(r.Context(), time.Duration(timeoutSeconds*float64(time.Second))) 376 | } 377 | } 378 | return context.WithCancel(r.Context()) 379 | } 380 | 381 | // newKubeClient returns a Kubernetes client (clientset) with configurable 382 | // rate limits from a supplied kubeconfig path, the KUBECONFIG environment variable, 383 | // the default config file location ($HOME/.kube/config), or from the in-cluster 384 | // service account environment. 385 | func newKubeClient(path string) (*kubernetes.Clientset, error) { 386 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 387 | if path != "" { 388 | loadingRules.ExplicitPath = path 389 | } 390 | 391 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 392 | loadingRules, 393 | &clientcmd.ConfigOverrides{}, 394 | ) 395 | 396 | config, err := kubeConfig.ClientConfig() 397 | if err != nil { 398 | return nil, err 399 | } 400 | 401 | // Set rate limits to reduce client-side throttling 402 | config.QPS = 100 403 | config.Burst = 200 404 | 405 | return kubernetes.NewForConfig(config) 406 | } 407 | 408 | func main() { 409 | flag.Parse() 410 | 411 | kubeClient, err := newKubeClient(*flagKubeConfigPath) 412 | if err != nil { 413 | fmt.Printf("[Error] Cannot create kube client: %v", err) 414 | os.Exit(1) 415 | } 416 | 417 | r := mux.NewRouter() 418 | r.HandleFunc("/nodes", func(w http.ResponseWriter, r *http.Request) { 419 | allNodesHandler(w, r, kubeClient) 420 | }) 421 | r.HandleFunc("/node/{node}", func(w http.ResponseWriter, r *http.Request) { 422 | nodeHandler(w, r, kubeClient) 423 | }) 424 | r.Handle("/metrics", promhttp.Handler()) 425 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 426 | _, _ = w.Write([]byte(` 427 | Kube Summary Exporter 428 | 429 |

Kube Summary Exporter

430 |

Retrieve metrics for all nodes

431 |

Retrieve metrics for 'example-node'

432 |

Metrics

433 | 434 | `)) 435 | }) 436 | 437 | fmt.Printf("Listening on %s\n", *flagListenAddress) 438 | fmt.Printf("error: %v\n", http.ListenAndServe(*flagListenAddress, r)) 439 | } 440 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/prometheus/client_golang/prometheus" 10 | _ "k8s.io/client-go/plugin/pkg/client/auth" 11 | stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 12 | ) 13 | 14 | func Test_collectSummaryMetrics(t *testing.T) { 15 | expectedOut := `# HELP kube_summary_container_logs_available_bytes Number of bytes that aren't consumed by the container logs 16 | # TYPE kube_summary_container_logs_available_bytes gauge 17 | kube_summary_container_logs_available_bytes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 9.0016837632e+10 18 | # HELP kube_summary_container_logs_capacity_bytes Number of bytes that can be consumed by the container logs 19 | # TYPE kube_summary_container_logs_capacity_bytes gauge 20 | kube_summary_container_logs_capacity_bytes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 1.01535985664e+11 21 | # HELP kube_summary_container_logs_inodes Number of Inodes for logs 22 | # TYPE kube_summary_container_logs_inodes gauge 23 | kube_summary_container_logs_inodes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 2.5474432e+07 24 | # HELP kube_summary_container_logs_inodes_free Number of available Inodes for logs 25 | # TYPE kube_summary_container_logs_inodes_free gauge 26 | kube_summary_container_logs_inodes_free{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 2.5355212e+07 27 | # HELP kube_summary_container_logs_inodes_used Number of used Inodes for logs 28 | # TYPE kube_summary_container_logs_inodes_used gauge 29 | kube_summary_container_logs_inodes_used{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 1 30 | # HELP kube_summary_container_logs_used_bytes Number of bytes that are consumed by the container logs 31 | # TYPE kube_summary_container_logs_used_bytes gauge 32 | kube_summary_container_logs_used_bytes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 8192 33 | # HELP kube_summary_container_rootfs_available_bytes Number of bytes that aren't consumed by the container 34 | # TYPE kube_summary_container_rootfs_available_bytes gauge 35 | kube_summary_container_rootfs_available_bytes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 9.0016837632e+10 36 | # HELP kube_summary_container_rootfs_capacity_bytes Number of bytes that can be consumed by the container 37 | # TYPE kube_summary_container_rootfs_capacity_bytes gauge 38 | kube_summary_container_rootfs_capacity_bytes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 1.01535985664e+11 39 | # HELP kube_summary_container_rootfs_inodes Number of Inodes 40 | # TYPE kube_summary_container_rootfs_inodes gauge 41 | kube_summary_container_rootfs_inodes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 2.5474432e+07 42 | # HELP kube_summary_container_rootfs_inodes_free Number of available Inodes 43 | # TYPE kube_summary_container_rootfs_inodes_free gauge 44 | kube_summary_container_rootfs_inodes_free{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 2.5355212e+07 45 | # HELP kube_summary_container_rootfs_inodes_used Number of used Inodes 46 | # TYPE kube_summary_container_rootfs_inodes_used gauge 47 | kube_summary_container_rootfs_inodes_used{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 14 48 | # HELP kube_summary_container_rootfs_used_bytes Number of bytes that are consumed by the container 49 | # TYPE kube_summary_container_rootfs_used_bytes gauge 50 | kube_summary_container_rootfs_used_bytes{name="dev-server",namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 114688 51 | # HELP kube_summary_pod_ephemeral_storage_available_bytes Number of bytes of Ephemeral storage that aren't consumed by the pod 52 | # TYPE kube_summary_pod_ephemeral_storage_available_bytes gauge 53 | kube_summary_pod_ephemeral_storage_available_bytes{namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 9.0016837632e+10 54 | # HELP kube_summary_pod_ephemeral_storage_capacity_bytes Number of bytes of Ephemeral storage that can be consumed by the pod 55 | # TYPE kube_summary_pod_ephemeral_storage_capacity_bytes gauge 56 | kube_summary_pod_ephemeral_storage_capacity_bytes{namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 1.01535985664e+11 57 | # HELP kube_summary_pod_ephemeral_storage_inodes Number of Inodes for pod Ephemeral storage 58 | # TYPE kube_summary_pod_ephemeral_storage_inodes gauge 59 | kube_summary_pod_ephemeral_storage_inodes{namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 2.5474432e+07 60 | # HELP kube_summary_pod_ephemeral_storage_inodes_free Number of available Inodes for pod Ephemeral storage 61 | # TYPE kube_summary_pod_ephemeral_storage_inodes_free gauge 62 | kube_summary_pod_ephemeral_storage_inodes_free{namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 2.5355212e+07 63 | # HELP kube_summary_pod_ephemeral_storage_inodes_used Number of used Inodes for pod Ephemeral storage 64 | # TYPE kube_summary_pod_ephemeral_storage_inodes_used gauge 65 | kube_summary_pod_ephemeral_storage_inodes_used{namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 63 66 | # HELP kube_summary_pod_ephemeral_storage_used_bytes Number of bytes of Ephemeral storage that are consumed by the pod 67 | # TYPE kube_summary_pod_ephemeral_storage_used_bytes gauge 68 | kube_summary_pod_ephemeral_storage_used_bytes{namespace="mon",node="test.eu-west-1.compute.internal",pod="dev-server-0"} 1.33947392e+08 69 | ` 70 | 71 | d, err := os.ReadFile("test-summary.json") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | var summary stats.Summary 77 | collectors := newCollectors() 78 | 79 | err = json.Unmarshal(d, &summary) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | collectSummaryMetrics(&summary, collectors) 85 | 86 | tmpfile, err := os.CreateTemp("", "test-summary.prom") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | defer os.Remove(tmpfile.Name()) 91 | 92 | registry := prometheus.NewRegistry() 93 | collectors.register(registry) 94 | 95 | if err := prometheus.WriteToTextfile(tmpfile.Name(), registry); err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | fileBytes, err := os.ReadFile(tmpfile.Name()) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | if diff := cmp.Diff(string(fileBytes), expectedOut); diff != "" { 105 | t.Errorf("collectSummaryMetrics() metrics mismatch (-want +got):\n%s", diff) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /manifests/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: kube-summary-exporter 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | labels: 10 | name: kube-summary-exporter 11 | name: kube-summary-exporter 12 | spec: 13 | ports: 14 | - name: kube-summary-exporter 15 | protocol: TCP 16 | port: 9779 17 | targetPort: 9779 18 | selector: 19 | app: kube-summary-exporter 20 | --- 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | name: kube-summary-exporter 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: kube-summary-exporter 30 | template: 31 | metadata: 32 | name: kube-summary-exporter 33 | labels: 34 | app: kube-summary-exporter 35 | spec: 36 | serviceAccountName: kube-summary-exporter 37 | containers: 38 | - name: kube-summary-exporter 39 | image: quay.io/utilitywarehouse/kube-summary-exporter:latest 40 | ports: 41 | - name: tcp 42 | containerPort: 9779 43 | -------------------------------------------------------------------------------- /manifests/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | -------------------------------------------------------------------------------- /manifests/cluster/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: kube-summary-exporter 5 | rules: 6 | - apiGroups: [""] 7 | resources: ["nodes/proxy"] 8 | verbs: ["get"] 9 | --- 10 | apiVersion: rbac.authorization.k8s.io/v1 11 | kind: ClusterRoleBinding 12 | metadata: 13 | name: kube-summary-exporter 14 | roleRef: 15 | apiGroup: rbac.authorization.k8s.io 16 | kind: ClusterRole 17 | name: kube-summary-exporter 18 | subjects: 19 | - kind: ServiceAccount 20 | name: kube-summary-exporter 21 | namespace: sys-prom 22 | -------------------------------------------------------------------------------- /manifests/cluster/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - clusterrole.yaml 5 | -------------------------------------------------------------------------------- /manifests/scrape-config.yaml: -------------------------------------------------------------------------------- 1 | # Scrape config for the Summary API exporter. 2 | # This assumes that the exporter is available at kube-summary-exporter:9779 3 | - job_name: "kubernetes-summary" 4 | kubernetes_sd_configs: 5 | - role: node 6 | relabel_configs: 7 | - source_labels: [__meta_kubernetes_node_label_role] 8 | action: replace 9 | target_label: role 10 | - source_labels: [__meta_kubernetes_node_name] 11 | regex: (.+) 12 | target_label: __metrics_path__ 13 | replacement: /node/${1} 14 | - target_label: __address__ 15 | replacement: kube-summary-exporter:9779 16 | -------------------------------------------------------------------------------- /test-summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "nodeName": "test.eu-west-1.compute.internal" 4 | }, 5 | "pods": [ 6 | { 7 | "podRef": { 8 | "name": "dev-server-0", 9 | "namespace": "mon", 10 | "uid": "93b0b04d-1ebe-4ad0-ada0-c29172c1ab9c" 11 | }, 12 | "startTime": "2022-11-30T11:42:08Z", 13 | "containers": [ 14 | { 15 | "name": "dev-server", 16 | "startTime": "2022-11-30T11:42:10Z", 17 | "cpu": { 18 | "time": "2022-11-30T14:14:40Z", 19 | "usageNanoCores": 268106954, 20 | "usageCoreNanoSeconds": 6650306864000 21 | }, 22 | "memory": { 23 | "time": "2022-11-30T14:14:40Z", 24 | "availableBytes": 552603648, 25 | "usageBytes": 539209728, 26 | "workingSetBytes": 495972352, 27 | "rssBytes": 118317056, 28 | "pageFaults": 14962868, 29 | "majorPageFaults": 3500 30 | }, 31 | "rootfs": { 32 | "time": "2022-11-30T14:14:37Z", 33 | "availableBytes": 90016837632, 34 | "capacityBytes": 101535985664, 35 | "usedBytes": 114688, 36 | "inodesFree": 25355212, 37 | "inodes": 25474432, 38 | "inodesUsed": 14 39 | }, 40 | "logs": { 41 | "time": "2022-11-30T14:14:41Z", 42 | "availableBytes": 90016837632, 43 | "capacityBytes": 101535985664, 44 | "usedBytes": 8192, 45 | "inodesFree": 25355212, 46 | "inodes": 25474432, 47 | "inodesUsed": 1 48 | } 49 | } 50 | ], 51 | "cpu": { 52 | "time": "2022-11-30T14:14:27Z", 53 | "usageNanoCores": 390813248, 54 | "usageCoreNanoSeconds": 8446621581000 55 | }, 56 | "memory": { 57 | "time": "2022-11-30T14:14:27Z", 58 | "availableBytes": 1310638080, 59 | "usageBytes": 835383296, 60 | "workingSetBytes": 786513920, 61 | "rssBytes": 241803264, 62 | "pageFaults": 23482811, 63 | "majorPageFaults": 7232 64 | }, 65 | "network": { 66 | "time": "2022-11-30T14:14:40Z", 67 | "name": "eth0", 68 | "rxBytes": 14522059300, 69 | "rxErrors": 0, 70 | "txBytes": 14549131546, 71 | "txErrors": 0, 72 | "interfaces": [ 73 | { 74 | "name": "tunl0", 75 | "rxBytes": 0, 76 | "rxErrors": 0, 77 | "txBytes": 0, 78 | "txErrors": 0 79 | }, 80 | { 81 | "name": "eth0", 82 | "rxBytes": 14522059300, 83 | "rxErrors": 0, 84 | "txBytes": 14549131546, 85 | "txErrors": 0 86 | } 87 | ] 88 | }, 89 | "volume": [ 90 | { 91 | "time": "2022-11-30T14:12:48Z", 92 | "availableBytes": 90016899072, 93 | "capacityBytes": 101535985664, 94 | "usedBytes": 12288, 95 | "inodesFree": 25355211, 96 | "inodes": 25474432, 97 | "inodesUsed": 2, 98 | "name": "plugins" 99 | }, 100 | { 101 | "time": "2022-11-30T14:12:48Z", 102 | "availableBytes": 90016899072, 103 | "capacityBytes": 101535985664, 104 | "usedBytes": 133500928, 105 | "inodesFree": 25355211, 106 | "inodes": 25474432, 107 | "inodesUsed": 2, 108 | "name": "var-files" 109 | } 110 | ], 111 | "ephemeral-storage": { 112 | "time": "2022-11-30T14:14:41Z", 113 | "availableBytes": 90016837632, 114 | "capacityBytes": 101535985664, 115 | "usedBytes": 133947392, 116 | "inodesFree": 25355212, 117 | "inodes": 25474432, 118 | "inodesUsed": 63 119 | }, 120 | "process_stats": { 121 | "process_count": 0 122 | } 123 | } 124 | ] 125 | } 126 | --------------------------------------------------------------------------------