├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── master.yaml │ ├── pull_request.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── collector ├── billing.go ├── bucket.go ├── database.go ├── exporter.go ├── loadbalancer.go └── redis.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── hack ├── grafana │ ├── grafana.default │ └── provisioning │ │ ├── dashboards │ │ ├── db-details.json │ │ └── db-overview.json │ │ └── datasources │ │ └── datasource.yml └── prometheus │ ├── alert.rules │ └── prometheus.yml ├── main.go └── scaleway.env.default /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | .github/* -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | target-branch: "master" 5 | directory: "/" 6 | labels: 7 | - "dependencies" 8 | schedule: 9 | interval: "daily" 10 | - package-ecosystem: "github-actions" 11 | target-branch: "master" 12 | directory: "/" 13 | labels: 14 | - "dependencies" 15 | - "CI" 16 | schedule: 17 | interval: "daily" 18 | - package-ecosystem: docker 19 | target-branch: "master" 20 | directory: "/" 21 | labels: 22 | - "dependencies" 23 | - "CI" 24 | schedule: 25 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/master.yaml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v3 15 | with: 16 | go-version: 1.19 17 | 18 | - uses: actions/checkout@v3 19 | 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v3 22 | with: 23 | version: v1.50 24 | build: 25 | runs-on: ubuntu-latest 26 | needs: lint 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | 31 | - name: Set up QEMU 32 | uses: docker/setup-qemu-action@v2 33 | 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v2 36 | 37 | - name: Login to DockerHub 38 | uses: docker/login-action@v2 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USERNAME }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | 43 | - name: Login to GitHub Container Registry 44 | uses: docker/login-action@v2 45 | with: 46 | registry: ghcr.io 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Docker meta 51 | id: meta 52 | uses: docker/metadata-action@v4 53 | with: 54 | images: | 55 | ghcr.io/${{ github.repository }} 56 | yoannm/scaleway_exporter 57 | tags: | 58 | type=ref,event=branch 59 | type=sha 60 | 61 | - name: Build and push 62 | uses: docker/build-push-action@v3 63 | with: 64 | context: . 65 | file: ./Dockerfile 66 | target: bin 67 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x 68 | push: true 69 | build-args: | 70 | VERSION=master 71 | REVISION=${{ github.sha }} 72 | tags: ${{ steps.meta.outputs.tags }} 73 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | - 'main' 8 | 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v3 15 | with: 16 | go-version: 1.19 17 | 18 | - uses: actions/checkout@v3 19 | 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v3 22 | with: 23 | version: v1.50 24 | build: 25 | runs-on: ubuntu-latest 26 | needs: lint 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | 31 | - name: Set up QEMU 32 | uses: docker/setup-qemu-action@v2 33 | 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v2 36 | 37 | - name: Login to DockerHub 38 | uses: docker/login-action@v2 39 | with: 40 | username: ${{ secrets.DOCKERHUB_USERNAME }} 41 | password: ${{ secrets.DOCKERHUB_TOKEN }} 42 | 43 | - name: Login to GitHub Container Registry 44 | uses: docker/login-action@v2 45 | with: 46 | registry: ghcr.io 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Docker meta 51 | id: meta 52 | uses: docker/metadata-action@v4 53 | with: 54 | images: | 55 | ghcr.io/${{ github.repository }} 56 | yoannm/scaleway_exporter 57 | tags: | 58 | type=ref,event=pr 59 | type=sha 60 | 61 | - name: Build and push 62 | uses: docker/build-push-action@v3 63 | with: 64 | context: . 65 | file: ./Dockerfile 66 | target: bin 67 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x 68 | push: true 69 | build-args: | 70 | VERSION=master 71 | REVISION=${{ github.sha }} 72 | tags: ${{ steps.meta.outputs.tags }} 73 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | lint: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.19 16 | 17 | - uses: actions/checkout@v3 18 | 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v3 21 | with: 22 | version: v1.50 23 | build: 24 | runs-on: ubuntu-latest 25 | needs: lint 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | 30 | - name: Get the version 31 | id: get_version 32 | uses: battila7/get-version-action@v2 33 | 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v2 36 | 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v2 39 | 40 | - name: Login to DockerHub 41 | uses: docker/login-action@v2 42 | with: 43 | username: ${{ secrets.DOCKERHUB_USERNAME }} 44 | password: ${{ secrets.DOCKERHUB_TOKEN }} 45 | 46 | - name: Login to GitHub Container Registry 47 | uses: docker/login-action@v2 48 | with: 49 | registry: ghcr.io 50 | username: ${{ github.repository_owner }} 51 | password: ${{ secrets.PAT_GITHUB }} 52 | 53 | - name: Docker meta 54 | id: meta 55 | uses: docker/metadata-action@v4 56 | with: 57 | images: | 58 | ghcr.io/${{ github.repository }} 59 | yoannm/scaleway_exporter 60 | tags: | 61 | type=ref,event=branch 62 | type=sha 63 | type=semver,pattern={{version}} 64 | type=semver,pattern={{major}}.{{minor}} 65 | 66 | - name: Build and push 67 | uses: docker/build-push-action@v3 68 | with: 69 | context: . 70 | file: ./Dockerfile 71 | target: bin 72 | build-args: | 73 | VERSION=${{ steps.get_version.outputs.version }} 74 | REVISION=${{ github.sha }} 75 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x 76 | push: true 77 | tags: ${{ steps.meta.outputs.tags }} 78 | labels: ${{ steps.meta.outputs.labels }} 79 | 80 | - name: Create Release 81 | id: create_release 82 | uses: actions/create-release@v1 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | with: 86 | tag_name: ${{ steps.get_version.outputs.version }} 87 | release_name: Release ${{ steps.get_version.outputs.version }} 88 | draft: false 89 | prerelease: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Goland files 2 | .idea 3 | 4 | # Dependency directories (remove the comment below to include it) 5 | # vendor/ 6 | 7 | bin/ 8 | scaleway_exporter 9 | 10 | *.env 11 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # This code is licensed under the terms of the MIT license. 2 | 3 | ## Golden config for golangci-lint v1.50.1 4 | # 5 | # This is the best config for golangci-lint based on my experience and opinion. 6 | # It is very strict, but not extremely strict. 7 | # Feel free to adopt and change it for your needs. 8 | 9 | run: 10 | # Timeout for analysis, e.g. 30s, 5m. 11 | # Default: 1m 12 | timeout: 3m 13 | 14 | 15 | # This file contains only configs which differ from defaults. 16 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 17 | linters-settings: 18 | 19 | lll: 20 | # Max line length, lines longer will be reported. 21 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option. 22 | # Default: 120. 23 | line-length: 180 24 | # Tab width in spaces. 25 | # Default: 1 26 | tab-width: 1 27 | 28 | errcheck: 29 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 30 | # Such cases aren't reported by default. 31 | # Default: false 32 | check-type-assertions: true 33 | 34 | exhaustive: 35 | # Program elements to check for exhaustiveness. 36 | # Default: [ switch ] 37 | check: 38 | - switch 39 | - map 40 | 41 | gocognit: 42 | # Minimal code complexity to report 43 | # Default: 30 (but we recommend 10-20) 44 | min-complexity: 20 45 | 46 | gocritic: 47 | # Settings passed to gocritic. 48 | # The settings key is the name of a supported gocritic checker. 49 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 50 | settings: 51 | captLocal: 52 | # Whether to restrict checker to params only. 53 | # Default: true 54 | paramsOnly: false 55 | underef: 56 | # Whether to skip (*x).method() calls where x is a pointer receiver. 57 | # Default: true 58 | skipRecvDeref: false 59 | 60 | gomodguard: 61 | blocked: 62 | # List of blocked modules. 63 | # Default: [] 64 | modules: 65 | - github.com/golang/protobuf: 66 | recommendations: 67 | - google.golang.org/protobuf 68 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" 69 | - github.com/satori/go.uuid: 70 | recommendations: 71 | - github.com/google/uuid 72 | reason: "satori's package is not maintained" 73 | - github.com/gofrs/uuid: 74 | recommendations: 75 | - github.com/google/uuid 76 | reason: "gofrs' package is not go module" 77 | 78 | govet: 79 | # Enable all analyzers. 80 | # Default: false 81 | enable-all: true 82 | # Disable analyzers by name. 83 | # Run `go tool vet help` to see all analyzers. 84 | # Default: [] 85 | disable: 86 | - fieldalignment # too strict 87 | # Settings per analyzer. 88 | settings: 89 | shadow: 90 | # Whether to be strict about shadowing; can be noisy. 91 | # Default: false 92 | strict: true 93 | 94 | nakedret: 95 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 96 | # Default: 30 97 | max-func-lines: 0 98 | 99 | nolintlint: 100 | # Exclude following linters from requiring an explanation. 101 | # Default: [] 102 | allow-no-explanation: [ funlen, gocognit, lll ] 103 | # Enable to require an explanation of nonzero length after each nolint directive. 104 | # Default: false 105 | require-explanation: true 106 | # Enable to require nolint directives to mention the specific linter being suppressed. 107 | # Default: false 108 | require-specific: true 109 | 110 | rowserrcheck: 111 | # database/sql is always checked 112 | # Default: [] 113 | packages: 114 | - github.com/jmoiron/sqlx 115 | 116 | tenv: 117 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. 118 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. 119 | # Default: false 120 | all: true 121 | 122 | 123 | linters: 124 | disable-all: true 125 | enable: 126 | ## enabled by default 127 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 128 | - gosimple # specializes in simplifying a code 129 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 130 | - ineffassign # detects when assignments to existing variables are not used 131 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 132 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code 133 | - unused # checks for unused constants, variables, functions and types 134 | ## disabled by default 135 | - asasalint # checks for pass []any as any in variadic func(...any) 136 | - asciicheck # checks that your code does not contain non-ASCII identifiers 137 | - bidichk # checks for dangerous unicode character sequences 138 | - bodyclose # checks whether HTTP response body is closed successfully 139 | - dupl # tool for code clone detection 140 | - durationcheck # checks for two durations multiplied together 141 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 142 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 143 | - execinquery # checks query string in Query function which reads your Go src files and warning it finds 144 | - exhaustive # checks exhaustiveness of enum switch statements 145 | - exportloopref # checks for pointers to enclosing loop variables 146 | - forbidigo # forbids identifiers 147 | - gochecknoglobals # checks that no global variables exist 148 | - gochecknoinits # checks that no init functions are present in Go code 149 | - gocognit # computes and checks the cognitive complexity of functions 150 | - goconst # finds repeated strings that could be replaced by a constant 151 | - gocritic # provides diagnostics that check for bugs, performance and style issues 152 | - gocyclo # computes and checks the cyclomatic complexity of functions 153 | - godot # checks if comments end in a period 154 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 155 | - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 156 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations 157 | - goprintffuncname # checks that printf-like functions are named with f at the end 158 | - gosec # inspects source code for security problems 159 | - lll # reports long lines 160 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 161 | - makezero # finds slice declarations with non-zero initial length 162 | - nakedret # finds naked returns in functions greater than a specified function length 163 | - nestif # reports deeply nested if statements 164 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 165 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 166 | - noctx # finds sending http request without context.Context 167 | - nolintlint # reports ill-formed or insufficient nolint directives 168 | - nonamedreturns # reports all named returns 169 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 170 | - predeclared # finds code that shadows one of Go's predeclared identifiers 171 | - promlinter # checks Prometheus metrics naming via promlint 172 | - reassign # checks that package variables are not reassigned 173 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 174 | - rowserrcheck # checks whether Err of rows is checked successfully 175 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 176 | - stylecheck # is a replacement for golint 177 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 178 | - testableexamples # checks if examples are testable (have an expected output) 179 | - testpackage # makes you use a separate _test package 180 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 181 | - unconvert # removes unnecessary type conversions 182 | - unparam # reports unused function parameters 183 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 184 | - wastedassign # finds wasted assignment statements 185 | - whitespace # detects leading and trailing whitespace 186 | 187 | ## you may want to enable 188 | #- decorder # checks declaration order and count of types, constants, variables and functions 189 | #- exhaustruct # checks if all structure fields are initialized 190 | #- gci # controls golang package import order and makes it always deterministic 191 | #- godox # detects FIXME, TODO and other comment keywords 192 | #- goheader # checks is file header matches to pattern 193 | #- interfacebloat # checks the number of methods inside an interface 194 | #- ireturn # accept interfaces, return concrete types 195 | #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated 196 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope 197 | #- wrapcheck # checks that errors returned from external packages are wrapped 198 | 199 | ## disabled 200 | #- containedctx # detects struct contained context.Context field 201 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context 202 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages 203 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 204 | #- dupword # [useless without config] checks for duplicate words in the source code 205 | #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted 206 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions 207 | #- goerr113 # [too strict] checks the errors handling expressions 208 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed 209 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed 210 | #- grouper # analyzes expression groups 211 | #- importas # enforces consistent import aliases 212 | #- maintidx # measures the maintainability index of each function 213 | #- misspell # [useless] finds commonly misspelled English words in comments 214 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity 215 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test 216 | #- tagliatelle # checks the struct tags 217 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers 218 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines 219 | 220 | ## deprecated 221 | #- deadcode # [deprecated, replaced by unused] finds unused code 222 | #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized 223 | #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes 224 | #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible 225 | #- interfacer # [deprecated] suggests narrower interface types 226 | #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted 227 | #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name 228 | #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs 229 | #- structcheck # [deprecated, replaced by unused] finds unused struct fields 230 | #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants 231 | 232 | 233 | issues: 234 | # Maximum count of issues with the same text. 235 | # Set to 0 to disable. 236 | # Default: 3 237 | max-same-issues: 50 238 | 239 | exclude-rules: 240 | - source: "^//\\s*go:generate\\s" 241 | linters: [ lll ] 242 | - source: "(noinspection|TODO)" 243 | linters: [ godot ] 244 | - source: "//noinspection" 245 | linters: [ gocritic ] 246 | - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" 247 | linters: [ errorlint ] 248 | - path: "_test\\.go" 249 | linters: 250 | - bodyclose 251 | - dupl 252 | - funlen 253 | - goconst 254 | - gosec 255 | - noctx 256 | - wrapcheck -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM --platform=${BUILDPLATFORM} golang:1.19-alpine AS base 4 | WORKDIR /src 5 | ENV CGO_ENABLED=0 6 | COPY go.* . 7 | RUN --mount=type=cache,target=/go/pkg/mod \ 8 | go mod download 9 | 10 | FROM base AS build 11 | ARG TARGETOS 12 | ARG TARGETARCH 13 | ARG VERSION 14 | ARG REVISION 15 | RUN --mount=target=. \ 16 | --mount=type=cache,target=/go/pkg/mod \ 17 | --mount=type=cache,target=/root/.cache/go-build \ 18 | GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X main.Version=${VERSION} -X main.BuildDate=$(date -Iseconds) -X main.Revision=${REVISION}" -o /out/scaleway-exporter . 19 | 20 | FROM golangci/golangci-lint:v1.50-alpine AS lint-base 21 | 22 | FROM base AS lint 23 | RUN --mount=target=. \ 24 | --mount=from=lint-base,src=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \ 25 | --mount=type=cache,target=/go/pkg/mod \ 26 | --mount=type=cache,target=/root/.cache/go-build \ 27 | --mount=type=cache,target=/root/.cache/golangci-lint \ 28 | golangci-lint run --timeout 10m0s ./... 29 | 30 | FROM alpine AS bin-unix 31 | COPY --from=build /out/scaleway-exporter / 32 | CMD ["/scaleway-exporter"] 33 | 34 | FROM bin-unix AS bin-linux 35 | FROM bin-unix AS bin-darwin 36 | 37 | FROM scratch AS bin-windows 38 | COPY --from=build /out/scaleway-exporter /scaleway-exporter.exe 39 | 40 | FROM bin-${TARGETOS} as bin 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yoann MALLEMANCHE 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PLATFORM=local 2 | 3 | REPOSITORY=yoannm/scaleway_exporter 4 | VERSION=0.2.0 5 | 6 | export DOCKER_BUILDKIT=1 7 | export COMPOSE_DOCKER_CLI_BUILD=1 8 | 9 | .PHONY: docker 10 | docker: build-image push-image 11 | 12 | .PHONY: build-image 13 | build-image: 14 | @docker build . -t ${REPOSITORY}:${VERSION} \ 15 | --target bin \ 16 | --platform ${PLATFORM} 17 | 18 | .PHONY: push-image 19 | push-image: 20 | @docker push ${REPOSITORY}:${VERSION} 21 | 22 | .PHONY: bin/scaleway_exporter 23 | bin/scaleway_exporter: 24 | @docker build . --target bin \ 25 | --output bin/ \ 26 | --platform ${PLATFORM} 27 | 28 | .PHONY: lint 29 | lint: 30 | @docker build . --target lint 31 | 32 | .PHONY: compose-build 33 | compose: 34 | @docker-compose build 35 | 36 | .PHONY: compose-up 37 | compose: 38 | @docker-compose up -d 39 | 40 | .PHONY: compose 41 | compose: compose-build compose-up -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scaleway Exporter 2 | 3 | Prometheus exporter for various metrics about your [Scaleway Elements](https://www.scaleway.com/en/elements/) loadbalancers and managed databases, written in Go. 4 | 5 | ## How to 6 | 7 | ``` 8 | $ export SCALEWAY_ACCESS_KEY= 9 | $ export SCALEWAY_SECRET_KEY= 10 | $ ./scaleway_exporter 11 | level=info ts=2022-07-19T13:25:40.352520863Z caller=main.go:83 msg="Scaleway Region is set to ALL" 12 | level=info ts=2022-07-19T13:25:40.352550422Z caller=main.go:89 msg="starting scaleway_exporter" version= revision= buildDate= goVersion=go1.18.3 13 | level=info ts=2022-07-19T13:25:40.352691527Z caller=main.go:145 msg=listening addr=:9503 14 | ``` 15 | 16 | By default, all the collectors are enabled (buckets, databases, loadbalancer, redis) over all Scaleway regions and zones. 17 | If needed, you can disable certain collections by adding the `disable-bucket-collector`, `disable-database-collector`, `disable-redis-collector` or `disable-loadbalancer-collector` flags to the command line. 18 | You can also limit the scraped region by setting the environment variable `SCALEWAY_REGION=fr-par` and the zone with the environment variable `SCALEWAY_ZONE=fr-par-1` for instance. 19 | 20 | ## TODO 21 | 22 | - [ ] Add more documentation 23 | - [ ] Example prometheus rules 24 | - [ ] Example grafana dashboard 25 | - [ ] Proper CI 26 | - [x] Cross Region metrics pulling 27 | - [ ] More metrics ? (Container Registry size is available) 28 | - [x] Ability to filter the kind of product (only database for example) 29 | - [ ] Register a new default port as it's using one from [another Scaleway Exporter](https://github.com/promhippie/scw_exporter) ? (see [prometheus documentation](https://github.com/prometheus/prometheus/wiki/Default-port-allocations)) 30 | 31 | ## Acknowledgements 32 | 33 | This exporter is **heavily** inspired by the one for [DigitalOcean](https://github.com/metalmatze/digitalocean_exporter) -------------------------------------------------------------------------------- /collector/billing.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/go-kit/log" 10 | "github.com/go-kit/log/level" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/scaleway/scaleway-sdk-go/api/account/v2" 13 | "github.com/scaleway/scaleway-sdk-go/scw" 14 | ) 15 | 16 | // BillingCollector collects metrics about all buckets. 17 | type BillingCollector struct { 18 | logger log.Logger 19 | errors *prometheus.CounterVec 20 | timeout time.Duration 21 | client *scw.Client 22 | accountClient *account.API 23 | organizationID string 24 | 25 | Consumptions *prometheus.Desc 26 | Update *prometheus.Desc 27 | } 28 | 29 | // NewBillingCollector returns a new BucketCollector. 30 | func NewBillingCollector(logger log.Logger, errors *prometheus.CounterVec, client *scw.Client, timeout time.Duration, organizationID string) *BillingCollector { 31 | errors.WithLabelValues("bucket").Add(0) 32 | 33 | _ = level.Info(logger).Log("msg", "Billing collector enabled") 34 | 35 | return &BillingCollector{ 36 | logger: logger, 37 | errors: errors, 38 | timeout: timeout, 39 | client: client, 40 | accountClient: account.NewAPI(client), 41 | organizationID: organizationID, 42 | 43 | Consumptions: prometheus.NewDesc( 44 | "scaleway_billing_consumptions", 45 | "Consumptions", 46 | []string{"project_id", "project_name", "category", "operation_path", "description", "currency_code"}, nil, 47 | ), 48 | 49 | Update: prometheus.NewDesc( 50 | "scaleway_billing_update_timestamp_seconds", 51 | "Timestamp of the last update", 52 | nil, nil, 53 | ), 54 | } 55 | } 56 | 57 | // Describe sends the super-set of all possible descriptors of metrics 58 | // collected by this Collector. 59 | func (c *BillingCollector) Describe(ch chan<- *prometheus.Desc) { 60 | ch <- c.Consumptions 61 | } 62 | 63 | type ConsumptionValue struct { 64 | CurrencyCode string `json:"currency_code"` 65 | Units int32 `json:"units"` 66 | Nanos int32 `json:"nanos"` 67 | } 68 | 69 | type Consumption struct { 70 | Description string `json:"description"` 71 | ProjectID string `json:"project_id"` 72 | Category string `json:"category"` 73 | OperationPath string `json:"operation_path"` 74 | Value ConsumptionValue `json:"value"` 75 | } 76 | 77 | type BillingResponse struct { 78 | Consumptions []*Consumption `json:"consumptions"` 79 | UpdatedAt time.Time `json:"updated_at"` 80 | } 81 | 82 | // Collect is called by the Prometheus registry when collecting metrics. 83 | func (c *BillingCollector) Collect(ch chan<- prometheus.Metric) { 84 | _, cancel := context.WithTimeout(context.Background(), c.timeout) 85 | defer cancel() 86 | 87 | response, err := c.accountClient.ListProjects(&account.ListProjectsRequest{OrganizationID: c.organizationID}, scw.WithAllPages()) 88 | 89 | if err != nil { 90 | c.errors.WithLabelValues("billing").Add(1) 91 | _ = level.Warn(c.logger).Log("msg", "can't fetch the list of projects", "err", err) 92 | 93 | return 94 | } 95 | 96 | if len(response.Projects) == 0 { 97 | c.errors.WithLabelValues("billing").Add(1) 98 | _ = level.Error(c.logger).Log("msg", "No projects were found, perhaps you are missing the 'ProjectManager' permission") 99 | 100 | return 101 | } 102 | 103 | projects := make(map[string]string) 104 | 105 | for _, project := range response.Projects { 106 | projects[project.ID] = project.Name 107 | } 108 | 109 | query := url.Values{} 110 | 111 | query.Set("organization_id", c.organizationID) 112 | 113 | var billingResponse BillingResponse 114 | 115 | err = c.client.Do(&scw.ScalewayRequest{ 116 | Method: "GET", 117 | Path: "/billing/v2alpha1/consumption", 118 | Query: query, 119 | Headers: http.Header{}, 120 | }, &billingResponse) 121 | 122 | if err != nil { 123 | c.errors.WithLabelValues("billing").Add(1) 124 | _ = level.Warn(c.logger).Log( 125 | "msg", "Could not fetch the billing data, perhaps you are missing the 'BillingReadOnly' permission'", 126 | "err", err, 127 | ) 128 | 129 | return 130 | } 131 | 132 | for _, consumption := range billingResponse.Consumptions { 133 | ch <- prometheus.MustNewConstMetric( 134 | c.Consumptions, 135 | prometheus.GaugeValue, 136 | float64(consumption.Value.Units)+float64(consumption.Value.Nanos)/1e9, 137 | consumption.ProjectID, 138 | projects[consumption.ProjectID], 139 | consumption.Category, 140 | consumption.OperationPath, 141 | consumption.Description, 142 | consumption.Value.CurrencyCode, 143 | ) 144 | } 145 | 146 | ch <- prometheus.MustNewConstMetric( 147 | c.Update, 148 | prometheus.GaugeValue, 149 | float64(billingResponse.UpdatedAt.Unix()), 150 | ) 151 | } 152 | -------------------------------------------------------------------------------- /collector/bucket.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "sort" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/aws/aws-sdk-go/aws" 14 | "github.com/aws/aws-sdk-go/aws/credentials" 15 | "github.com/aws/aws-sdk-go/aws/session" 16 | "github.com/aws/aws-sdk-go/service/s3" 17 | "github.com/go-kit/log" 18 | "github.com/go-kit/log/level" 19 | "github.com/prometheus/client_golang/prometheus" 20 | "github.com/scaleway/scaleway-sdk-go/scw" 21 | ) 22 | 23 | // BucketCollector collects metrics about all buckets. 24 | type BucketCollector struct { 25 | logger log.Logger 26 | errors *prometheus.CounterVec 27 | endpoints []Endpoint 28 | timeout time.Duration 29 | 30 | ObjectCount *prometheus.Desc 31 | Bandwidth *prometheus.Desc 32 | StorageUsage *prometheus.Desc 33 | } 34 | 35 | type Endpoint struct { 36 | client *scw.Client 37 | region scw.Region 38 | s3Client *s3.S3 39 | } 40 | 41 | // NewBucketCollector returns a new BucketCollector. 42 | func NewBucketCollector(logger log.Logger, errors *prometheus.CounterVec, client *scw.Client, timeout time.Duration, regions []scw.Region) *BucketCollector { 43 | errors.WithLabelValues("bucket").Add(0) 44 | 45 | _ = level.Info(logger).Log("msg", "Bucket collector enabled") 46 | 47 | accessKey, _ := client.GetAccessKey() 48 | 49 | secretKey, _ := client.GetSecretKey() 50 | 51 | endpoints := make([]Endpoint, len(regions)) 52 | 53 | for i, region := range regions { 54 | newSession, err := session.NewSession(&aws.Config{ 55 | Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), 56 | Region: aws.String(fmt.Sprint(region)), 57 | }) 58 | 59 | if err != nil { 60 | _ = level.Error(logger).Log("msg", "can't create a S3 client", "err", err) 61 | os.Exit(1) 62 | } 63 | 64 | s3Client := s3.New(newSession, &aws.Config{ 65 | Endpoint: aws.String("https://s3." + fmt.Sprint(region) + ".scw.cloud"), 66 | S3ForcePathStyle: aws.Bool(true), 67 | }) 68 | 69 | endpoints[i] = Endpoint{ 70 | client: client, 71 | s3Client: s3Client, 72 | region: region, 73 | } 74 | } 75 | return &BucketCollector{ 76 | logger: logger, 77 | errors: errors, 78 | endpoints: endpoints, 79 | timeout: timeout, 80 | 81 | ObjectCount: prometheus.NewDesc( 82 | "scaleway_s3_object_total", 83 | "Number of objects, excluding parts", 84 | []string{"name", "region", "public"}, nil, 85 | ), 86 | Bandwidth: prometheus.NewDesc( 87 | "scaleway_s3_bandwidth_bytes", 88 | "Bucket's Bandwidth usage", 89 | []string{"name", "region", "public"}, nil, 90 | ), 91 | StorageUsage: prometheus.NewDesc( 92 | "scaleway_s3_storage_usage_bytes", 93 | "Bucket's Storage usage", 94 | []string{"name", "region", "public", "storage_class"}, nil, 95 | ), 96 | } 97 | } 98 | 99 | // Describe sends the super-set of all possible descriptors of metrics 100 | // collected by this Collector. 101 | func (c *BucketCollector) Describe(ch chan<- *prometheus.Desc) { 102 | ch <- c.ObjectCount 103 | ch <- c.Bandwidth 104 | ch <- c.StorageUsage 105 | } 106 | 107 | type BucketInfo struct { 108 | CurrentObjects int64 `json:"current_objects"` 109 | CurrentSize int64 `json:"current_size"` 110 | CurrentSegments int64 `json:"current_segments"` 111 | IsPublic bool `json:"is_public"` 112 | Status string `json:"status"` 113 | UpdatedAt time.Time `json:"updated_at"` 114 | } 115 | 116 | type BucketInfoList struct { 117 | CurrentObjects int64 `json:"current_objects"` 118 | CurrentSize int64 `json:"current_size"` 119 | QuotaBuckets int64 `json:"quota_buckets"` 120 | QuotaObjects int64 `json:"quota_objects"` 121 | QuotaSize int64 `json:"quota_size"` 122 | Buckets map[string]BucketInfo `json:"buckets"` 123 | } 124 | 125 | type BucketInfoRequestBody struct { 126 | ProjectID string `json:"project_id"` 127 | BucketsName []string `json:"buckets_name"` 128 | } 129 | 130 | // Metric InstanceMetrics: instance metrics. 131 | type Metric struct { 132 | // Timeseries: time series of metrics of a given bucket 133 | Timeseries []*scw.TimeSeries `json:"timeseries"` 134 | } 135 | 136 | type MetricName string 137 | 138 | const ( 139 | ObjectCount MetricName = "object_count" 140 | StorageUsage MetricName = "storage_usage" 141 | BytesSent MetricName = "bytes_sent" 142 | ) 143 | 144 | type HandleSimpleMetricOptions struct { 145 | Bucket string 146 | MetricName MetricName 147 | Desc *prometheus.Desc 148 | labels []string 149 | Endpoint Endpoint 150 | } 151 | 152 | type HandleMultiMetricsOptions struct { 153 | Bucket string 154 | MetricName MetricName 155 | Desc *prometheus.Desc 156 | labels []string 157 | Endpoint Endpoint 158 | GetExtraLabel func(*scw.TimeSeries) string 159 | } 160 | 161 | // Collect is called by the Prometheus registry when collecting metrics. 162 | func (c *BucketCollector) Collect(ch chan<- prometheus.Metric) { 163 | _, cancel := context.WithTimeout(context.Background(), c.timeout) 164 | defer cancel() 165 | 166 | for _, endpoint := range c.endpoints { 167 | buckets, err := endpoint.s3Client.ListBuckets(&s3.ListBucketsInput{}) 168 | 169 | if err != nil { 170 | c.errors.WithLabelValues("bucket").Add(1) 171 | _ = level.Warn(c.logger).Log("msg", "can't fetch the list of buckets", "region", endpoint.region, "err", err) 172 | 173 | return 174 | } 175 | 176 | scwReq := &scw.ScalewayRequest{ 177 | Method: "POST", 178 | Path: "/object-private/v1/regions/" + fmt.Sprint(endpoint.region) + "/buckets-info/", 179 | } 180 | 181 | var bucketNames []string 182 | 183 | for _, bucket := range buckets.Buckets { 184 | bucketNames = append(bucketNames, *bucket.Name) 185 | } 186 | 187 | projectID := strings.Split(*buckets.Owner.ID, ":")[0] 188 | 189 | _ = level.Debug(c.logger).Log( 190 | "msg", fmt.Sprintf("found %d buckets", len(bucketNames)), 191 | "region", endpoint.region, 192 | "bucketNames", fmt.Sprintf("%s", bucketNames), 193 | ) 194 | 195 | err = scwReq.SetBody(&BucketInfoRequestBody{ProjectID: projectID, BucketsName: bucketNames}) 196 | 197 | if err != nil { 198 | c.errors.WithLabelValues("bucket").Add(1) 199 | _ = level.Warn(c.logger).Log("msg", "can't fetch details of buckets", "region", endpoint.region, "err", err) 200 | 201 | return 202 | } 203 | 204 | var response BucketInfoList 205 | 206 | err = endpoint.client.Do(scwReq, &response) 207 | 208 | if err != nil { 209 | c.errors.WithLabelValues("bucket").Add(1) 210 | _ = level.Warn(c.logger).Log("msg", "can't fetch details of buckets", "region", endpoint.region, "err", err) 211 | 212 | return 213 | } 214 | 215 | var wg sync.WaitGroup 216 | defer wg.Wait() 217 | 218 | for name, bucket := range response.Buckets { 219 | wg.Add(1) 220 | 221 | _ = level.Debug(c.logger).Log( 222 | "msg", fmt.Sprintf("Fetching metrics for bucket : %s", name), 223 | "region", endpoint.region, 224 | ) 225 | go c.FetchMetricsForBucket(&wg, ch, name, bucket, endpoint) 226 | } 227 | } 228 | } 229 | 230 | func (c *BucketCollector) FetchMetricsForBucket(parentWg *sync.WaitGroup, ch chan<- prometheus.Metric, name string, bucket BucketInfo, endpoint Endpoint) { 231 | defer parentWg.Done() 232 | 233 | labels := []string{name, fmt.Sprint(endpoint.region), fmt.Sprint(bucket.IsPublic)} 234 | 235 | // TODO check if it is possible to add bucket tag as labels 236 | // for _, tags := range instance.Tags { 237 | // labels = append(labels, tags) 238 | // } 239 | 240 | var wg sync.WaitGroup 241 | defer wg.Wait() 242 | 243 | wg.Add(3) 244 | 245 | go c.HandleSimpleMetric(&wg, ch, &HandleSimpleMetricOptions{ 246 | Bucket: name, 247 | MetricName: ObjectCount, 248 | labels: labels, 249 | Desc: c.ObjectCount, 250 | Endpoint: endpoint, 251 | }) 252 | 253 | go c.HandleSimpleMetric(&wg, ch, &HandleSimpleMetricOptions{ 254 | Bucket: name, 255 | MetricName: BytesSent, 256 | labels: labels, 257 | Desc: c.Bandwidth, 258 | Endpoint: endpoint, 259 | }) 260 | 261 | go c.HandleMultiMetrics(&wg, ch, &HandleMultiMetricsOptions{ 262 | Bucket: name, 263 | MetricName: StorageUsage, 264 | labels: labels, 265 | Endpoint: endpoint, 266 | Desc: c.StorageUsage, 267 | GetExtraLabel: func(timeseries *scw.TimeSeries) string { 268 | return timeseries.Metadata["type"] 269 | }, 270 | }) 271 | } 272 | 273 | func (c *BucketCollector) HandleSimpleMetric(parentWg *sync.WaitGroup, ch chan<- prometheus.Metric, options *HandleSimpleMetricOptions) { 274 | defer parentWg.Done() 275 | 276 | var response Metric 277 | 278 | err := c.FetchMetric(options.Bucket, options.MetricName, &response, options.Endpoint) 279 | 280 | if err != nil { 281 | c.errors.WithLabelValues("bucket").Add(1) 282 | _ = level.Warn(c.logger).Log( 283 | "msg", "can't fetch the metric", 284 | "region", options.Endpoint.region, 285 | "metric", options.MetricName, 286 | "bucket", options.Bucket, 287 | "err", err, 288 | ) 289 | 290 | return 291 | } 292 | 293 | for _, timeseries := range response.Timeseries { 294 | sort.Slice(timeseries.Points, func(i, j int) bool { 295 | return timeseries.Points[i].Timestamp.Before(timeseries.Points[j].Timestamp) 296 | }) 297 | 298 | if len(timeseries.Points) == 0 { 299 | c.errors.WithLabelValues("bucket").Add(1) 300 | _ = level.Warn(c.logger).Log( 301 | "msg", "no data were returned for the metric", 302 | "region", options.Endpoint.region, 303 | "metric", options.MetricName, 304 | "bucket", options.Bucket, 305 | "err", err, 306 | ) 307 | 308 | continue 309 | } 310 | 311 | value := float64(timeseries.Points[len(timeseries.Points)-1].Value) 312 | 313 | ch <- prometheus.MustNewConstMetric(options.Desc, prometheus.GaugeValue, value, options.labels...) 314 | } 315 | } 316 | 317 | func (c *BucketCollector) HandleMultiMetrics(parentWg *sync.WaitGroup, ch chan<- prometheus.Metric, options *HandleMultiMetricsOptions) { 318 | defer parentWg.Done() 319 | 320 | var response Metric 321 | 322 | err := c.FetchMetric(options.Bucket, options.MetricName, &response, options.Endpoint) 323 | 324 | if err != nil { 325 | c.errors.WithLabelValues("bucket").Add(1) 326 | _ = level.Warn(c.logger).Log( 327 | "msg", "can't fetch the metric", 328 | "region", options.Endpoint.region, 329 | "metric", options.MetricName, 330 | "bucket", options.Bucket, 331 | "err", err, 332 | ) 333 | 334 | return 335 | } 336 | 337 | for _, timeseries := range response.Timeseries { 338 | sort.Slice(timeseries.Points, func(i, j int) bool { 339 | return timeseries.Points[i].Timestamp.Before(timeseries.Points[j].Timestamp) 340 | }) 341 | 342 | extraLabel := options.GetExtraLabel(timeseries) 343 | 344 | if len(timeseries.Points) == 0 { 345 | c.errors.WithLabelValues("bucket").Add(1) 346 | _ = level.Warn(c.logger).Log( 347 | "msg", "no data were returned for the metric", 348 | "region", options.Endpoint.region, 349 | "bucket", options.Bucket, 350 | "metric", options.MetricName, 351 | "extra_label", extraLabel, 352 | "err", err, 353 | ) 354 | 355 | continue 356 | } 357 | 358 | value := float64(timeseries.Points[len(timeseries.Points)-1].Value) 359 | 360 | allLabels := append(append([]string{}, options.labels...), extraLabel) 361 | 362 | ch <- prometheus.MustNewConstMetric(options.Desc, prometheus.GaugeValue, value, allLabels...) 363 | } 364 | } 365 | 366 | func (c *BucketCollector) FetchMetric(bucket string, metricName MetricName, response *Metric, endpoint Endpoint) error { 367 | query := url.Values{} 368 | 369 | query.Add("start_date", time.Now().Add(-1*time.Hour).Format(time.RFC3339)) 370 | query.Add("end_date", time.Now().Format(time.RFC3339)) 371 | query.Add("metric_name", fmt.Sprint(metricName)) 372 | 373 | scwReq := &scw.ScalewayRequest{ 374 | Method: "GET", 375 | Path: "/object-private/v1/regions/" + fmt.Sprint(endpoint.region) + "/buckets/" + bucket + "/metrics", 376 | Query: query, 377 | } 378 | 379 | err := endpoint.client.Do(scwReq, &response) 380 | 381 | if err != nil { 382 | return err 383 | } 384 | 385 | return nil 386 | } 387 | -------------------------------------------------------------------------------- /collector/database.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "sync" 8 | "time" 9 | 10 | "github.com/go-kit/log" 11 | "github.com/go-kit/log/level" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 14 | "github.com/scaleway/scaleway-sdk-go/scw" 15 | ) 16 | 17 | // DatabaseCollector collects metrics about all databases. 18 | type DatabaseCollector struct { 19 | logger log.Logger 20 | errors *prometheus.CounterVec 21 | client *scw.Client 22 | rdbClient *rdb.API 23 | timeout time.Duration 24 | regions []scw.Region 25 | 26 | Up *prometheus.Desc 27 | CPUs *prometheus.Desc 28 | Memory *prometheus.Desc 29 | Connection *prometheus.Desc 30 | Disk *prometheus.Desc 31 | } 32 | 33 | // NewDatabaseCollector returns a new DatabaseCollector. 34 | func NewDatabaseCollector(logger log.Logger, errors *prometheus.CounterVec, client *scw.Client, timeout time.Duration, regions []scw.Region) *DatabaseCollector { 35 | errors.WithLabelValues("database").Add(0) 36 | 37 | _ = level.Info(logger).Log("msg", "Database collector enabled") 38 | 39 | labels := []string{"id", "name", "region", "engine", "type"} 40 | 41 | labelsNode := []string{"id", "name", "node"} 42 | 43 | return &DatabaseCollector{ 44 | logger: logger, 45 | errors: errors, 46 | client: client, 47 | rdbClient: rdb.NewAPI(client), 48 | timeout: timeout, 49 | regions: regions, 50 | 51 | Up: prometheus.NewDesc( 52 | "scaleway_database_up", 53 | "If 1 the database is up and running, 0.5 in autohealing, 0 otherwise", 54 | labels, nil, 55 | ), 56 | CPUs: prometheus.NewDesc( 57 | "scaleway_database_cpu_usage_percent", 58 | "Database's CPUs percentage usage", 59 | labelsNode, nil, 60 | ), 61 | Memory: prometheus.NewDesc( 62 | "scaleway_database_memory_usage_percent", 63 | "Database's memory percentage usage", 64 | labelsNode, nil, 65 | ), 66 | Connection: prometheus.NewDesc( 67 | "scaleway_database_total_connections", 68 | "Database's connection count", 69 | labelsNode, nil, 70 | ), 71 | Disk: prometheus.NewDesc( 72 | "scaleway_database_disk_usage_percent", 73 | "Database's disk percentage usage", 74 | labelsNode, nil, 75 | ), 76 | } 77 | } 78 | 79 | // Describe sends the super-set of all possible descriptors of metrics 80 | // collected by this Collector. 81 | func (c *DatabaseCollector) Describe(ch chan<- *prometheus.Desc) { 82 | ch <- c.Up 83 | ch <- c.CPUs 84 | ch <- c.Memory 85 | ch <- c.Connection 86 | ch <- c.Disk 87 | } 88 | 89 | // Collect is called by the Prometheus registry when collecting metrics. 90 | func (c *DatabaseCollector) Collect(ch chan<- prometheus.Metric) { 91 | _, cancel := context.WithTimeout(context.Background(), c.timeout) 92 | defer cancel() 93 | 94 | var wg sync.WaitGroup 95 | defer wg.Wait() 96 | 97 | for _, region := range c.regions { 98 | // create a list to hold our databases 99 | response, err := c.rdbClient.ListInstances(&rdb.ListInstancesRequest{Region: region}, scw.WithAllPages()) 100 | 101 | if err != nil { 102 | c.errors.WithLabelValues("database").Add(1) 103 | _ = level.Warn(c.logger).Log( 104 | "msg", "can't fetch the list of databases", 105 | "region", region, 106 | "err", err, 107 | ) 108 | 109 | return 110 | } 111 | 112 | _ = level.Debug(c.logger).Log( 113 | "msg", fmt.Sprintf("found %d database instances", len(response.Instances)), 114 | "region", region, 115 | ) 116 | 117 | for _, instance := range response.Instances { 118 | wg.Add(1) 119 | 120 | _ = level.Debug(c.logger).Log("msg", fmt.Sprintf("Fetching metrics for database instance : %s", instance.Name)) 121 | 122 | go c.FetchMetricsForInstance(&wg, ch, instance) 123 | } 124 | } 125 | } 126 | 127 | func (c *DatabaseCollector) FetchMetricsForInstance(parentWg *sync.WaitGroup, ch chan<- prometheus.Metric, instance *rdb.Instance) { 128 | defer parentWg.Done() 129 | 130 | labels := []string{ 131 | instance.ID, 132 | instance.Name, 133 | instance.Region.String(), 134 | instance.Engine, 135 | instance.NodeType, 136 | } 137 | 138 | // TODO check if it is possible to add database tag as labels 139 | // for _, tags := range instance.Tags { 140 | // labels = append(labels, tags) 141 | // } 142 | 143 | var active float64 144 | 145 | switch instance.Status { 146 | case rdb.InstanceStatusReady: 147 | active = 1.0 148 | case rdb.InstanceStatusBackuping: 149 | active = 1.0 150 | case rdb.InstanceStatusAutohealing: 151 | active = 0.5 152 | case rdb.InstanceStatusProvisioning: 153 | active = 0.5 154 | case rdb.InstanceStatusConfiguring: 155 | active = 0.5 156 | case rdb.InstanceStatusDeleting: 157 | active = 0.5 158 | case rdb.InstanceStatusSnapshotting: 159 | active = 0.5 160 | case rdb.InstanceStatusRestarting: 161 | active = 0.5 162 | case rdb.InstanceStatusUnknown: 163 | active = 0.0 164 | case rdb.InstanceStatusError: 165 | active = 0.0 166 | case rdb.InstanceStatusLocked: 167 | active = 0.0 168 | case rdb.InstanceStatusInitializing: 169 | active = 0.0 170 | case rdb.InstanceStatusDiskFull: 171 | active = 0.0 172 | default: 173 | active = 0.0 174 | } 175 | 176 | ch <- prometheus.MustNewConstMetric( 177 | c.Up, 178 | prometheus.GaugeValue, 179 | active, 180 | labels..., 181 | ) 182 | 183 | metricResponse, err := c.rdbClient.GetInstanceMetrics(&rdb.GetInstanceMetricsRequest{Region: instance.Region, InstanceID: instance.ID}) 184 | 185 | if err != nil { 186 | c.errors.WithLabelValues("database").Add(1) 187 | _ = level.Warn(c.logger).Log( 188 | "msg", "can't fetch the metric for the instance", 189 | "err", err, 190 | "region", instance.Region, 191 | "instanceId", instance.ID, 192 | "instanceName", instance.Name, 193 | ) 194 | 195 | return 196 | } 197 | 198 | for _, timeseries := range metricResponse.Timeseries { 199 | labelsNode := []string{ 200 | instance.ID, 201 | instance.Name, 202 | timeseries.Metadata["node"], 203 | } 204 | 205 | var series *prometheus.Desc 206 | 207 | switch timeseries.Name { 208 | case "cpu_usage_percent": 209 | series = c.CPUs 210 | case "mem_usage_percent": 211 | series = c.Memory 212 | case "total_connections": 213 | series = c.Connection 214 | case "disk_usage_percent": 215 | series = c.Disk 216 | default: 217 | _ = level.Debug(c.logger).Log( 218 | "msg", "unmapped scaleway metric", 219 | "err", err, 220 | "region", instance.Region, 221 | "instanceId", instance.ID, 222 | "instanceName", instance.Name, 223 | "scwMetric", timeseries.Name, 224 | ) 225 | continue 226 | } 227 | 228 | if len(timeseries.Points) == 0 { 229 | c.errors.WithLabelValues("database").Add(1) 230 | _ = level.Warn(c.logger).Log( 231 | "msg", "no data were returned for the metric", 232 | "instanceName", instance.Name, 233 | "instanceId", instance.ID, 234 | "metric", timeseries.Name, 235 | "region", instance.Region, 236 | "err", err, 237 | ) 238 | 239 | continue 240 | } 241 | 242 | sort.Slice(timeseries.Points, func(i, j int) bool { 243 | return timeseries.Points[i].Timestamp.Before(timeseries.Points[j].Timestamp) 244 | }) 245 | 246 | value := float64(timeseries.Points[len(timeseries.Points)-1].Value) 247 | 248 | ch <- prometheus.MustNewConstMetric(series, prometheus.GaugeValue, value, labelsNode...) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /collector/exporter.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-kit/log" 7 | "github.com/go-kit/log/level" 8 | "github.com/prometheus/client_golang/prometheus" 9 | ) 10 | 11 | // ExporterCollector collects metrics, mostly runtime, about this exporter in general. 12 | type ExporterCollector struct { 13 | logger log.Logger 14 | version string 15 | revision string 16 | buildDate string 17 | goVersion string 18 | startTime time.Time 19 | 20 | StartTime *prometheus.Desc 21 | BuildInfo *prometheus.Desc 22 | } 23 | 24 | // logger, Version, Revision, BuildDate, GoVersion, StartTime 25 | 26 | // NewExporterCollector returns a new ExporterCollector. 27 | func NewExporterCollector(logger log.Logger, version string, revision string, buildDate string, goVersion string, startTime time.Time) *ExporterCollector { 28 | return &ExporterCollector{ 29 | logger: logger, 30 | 31 | version: version, 32 | revision: revision, 33 | buildDate: buildDate, 34 | goVersion: goVersion, 35 | startTime: startTime, 36 | 37 | StartTime: prometheus.NewDesc( 38 | "scaleway_start_time", 39 | "Unix timestamp of the start time", 40 | nil, nil, 41 | ), 42 | BuildInfo: prometheus.NewDesc( 43 | "scaleway_build_info", 44 | "A metric with a constant '1' value labeled by version, revision, and branch from which the node_exporter was built.", 45 | []string{"version", "revision", "builddate", "goversion"}, nil, 46 | ), 47 | } 48 | } 49 | 50 | // Describe sends the super-set of all possible descriptors of metrics 51 | // collected by this Collector. 52 | func (c *ExporterCollector) Describe(ch chan<- *prometheus.Desc) { 53 | ch <- c.StartTime 54 | } 55 | 56 | // Collect is called by the Prometheus registry when collecting metrics. 57 | func (c *ExporterCollector) Collect(ch chan<- prometheus.Metric) { 58 | _ = level.Debug(c.logger).Log( 59 | "starttime", c.startTime.Unix(), 60 | "version", c.version, 61 | "revision", c.revision, 62 | "buildDate", c.buildDate, 63 | "goVersion", c.goVersion, 64 | "startTime", c.startTime, 65 | ) 66 | 67 | ch <- prometheus.MustNewConstMetric( 68 | c.StartTime, 69 | prometheus.GaugeValue, 70 | float64(c.startTime.Unix()), 71 | ) 72 | ch <- prometheus.MustNewConstMetric( 73 | c.BuildInfo, 74 | prometheus.GaugeValue, 75 | 1.0, 76 | c.version, c.revision, c.buildDate, c.goVersion, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /collector/loadbalancer.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "sort" 10 | "sync" 11 | "time" 12 | 13 | "github.com/go-kit/log" 14 | "github.com/go-kit/log/level" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/scaleway/scaleway-sdk-go/api/lb/v1" 17 | "github.com/scaleway/scaleway-sdk-go/scw" 18 | ) 19 | 20 | // LoadBalancerCollector collects metrics about all loadbalancers. 21 | type LoadBalancerCollector struct { 22 | logger log.Logger 23 | errors *prometheus.CounterVec 24 | client *scw.Client 25 | lbClient *lb.ZonedAPI 26 | timeout time.Duration 27 | zones []scw.Zone 28 | 29 | Up *prometheus.Desc 30 | NetworkReceive *prometheus.Desc 31 | NetworkTransmit *prometheus.Desc 32 | Connection *prometheus.Desc 33 | NewConnection *prometheus.Desc 34 | } 35 | 36 | // NewLoadBalancerCollector returns a new LoadBalancerCollector. 37 | func NewLoadBalancerCollector(logger log.Logger, errors *prometheus.CounterVec, client *scw.Client, timeout time.Duration, zones []scw.Zone) *LoadBalancerCollector { 38 | errors.WithLabelValues("loadbalancer").Add(0) 39 | 40 | _ = level.Info(logger).Log("msg", "Loadbalancer collector enabled") 41 | 42 | labels := []string{"id", "name", "zone", "type"} 43 | 44 | return &LoadBalancerCollector{ 45 | logger: logger, 46 | errors: errors, 47 | client: client, 48 | lbClient: lb.NewZonedAPI(client), 49 | timeout: timeout, 50 | zones: zones, 51 | 52 | Up: prometheus.NewDesc( 53 | "scaleway_loadbalancer_up", 54 | "If 1 the loadbalancer is up and running, 0.5 when migrating, 0 otherwise", 55 | labels, nil, 56 | ), 57 | NetworkReceive: prometheus.NewDesc( 58 | "scaleway_loadbalancer_network_receive_bits_sec", 59 | "LoadBalancer's ", // TODO 60 | labels, nil, 61 | ), 62 | NetworkTransmit: prometheus.NewDesc( 63 | "scaleway_loadbalancer_network_transmit_bits_sec", 64 | "LoadBalancer's ", // TODO 65 | labels, nil, 66 | ), 67 | Connection: prometheus.NewDesc( 68 | "scaleway_loadbalancer_total_connections", 69 | "LoadBalancer's ", // TODO 70 | labels, nil, 71 | ), 72 | NewConnection: prometheus.NewDesc( 73 | "scaleway_loadbalancer_new_connection_rate_sec", 74 | "LoadBalancer's ", // TODO 75 | labels, nil, 76 | ), 77 | } 78 | } 79 | 80 | // Describe sends the super-set of all possible descriptors of metrics. 81 | // collected by this Collector. 82 | func (c *LoadBalancerCollector) Describe(ch chan<- *prometheus.Desc) { 83 | ch <- c.Up 84 | ch <- c.NetworkReceive 85 | ch <- c.NetworkTransmit 86 | ch <- c.Connection 87 | ch <- c.NewConnection 88 | } 89 | 90 | // LbMetrics InstanceMetrics: instance metrics. 91 | type LbMetrics struct { 92 | // Timeseries: time series of metrics of a given instance 93 | Timeseries []*scw.TimeSeries `json:"timeseries"` 94 | } 95 | 96 | // Collect is called by the Prometheus registry when collecting metrics. 97 | func (c *LoadBalancerCollector) Collect(ch chan<- prometheus.Metric) { 98 | _, cancel := context.WithTimeout(context.Background(), c.timeout) 99 | defer cancel() 100 | 101 | for _, zone := range c.zones { 102 | // create a list to hold our loadbalancers 103 | response, err := c.lbClient.ListLBs(&lb.ZonedAPIListLBsRequest{Zone: zone}, scw.WithAllPages()) 104 | 105 | if err != nil { 106 | var responseError *scw.ResponseError 107 | 108 | switch { 109 | case errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotImplemented: 110 | _ = level.Debug(c.logger).Log("msg", "Loadbalancer is not supported in this zone", "zone", zone) 111 | return 112 | default: 113 | c.errors.WithLabelValues("loadbalancer").Add(1) 114 | _ = level.Warn(c.logger).Log("msg", "can't fetch the list of loadbalancers", "err", err, "zone", zone) 115 | 116 | return 117 | } 118 | } 119 | 120 | _ = level.Debug(c.logger).Log("msg", fmt.Sprintf("found %d loadbalancer instances", len(response.LBs)), "zone", zone) 121 | 122 | var wg sync.WaitGroup 123 | defer wg.Wait() 124 | 125 | for _, loadbalancer := range response.LBs { 126 | wg.Add(1) 127 | 128 | _ = level.Debug(c.logger).Log("msg", fmt.Sprintf("Fetching metrics for loadbalancer : %s", loadbalancer.Name), "zone", zone) 129 | 130 | go c.FetchLoadbalancerMetrics(&wg, ch, loadbalancer) 131 | } 132 | } 133 | } 134 | 135 | func (c *LoadBalancerCollector) FetchLoadbalancerMetrics(parentWg *sync.WaitGroup, ch chan<- prometheus.Metric, loadbalancer *lb.LB) { 136 | defer parentWg.Done() 137 | 138 | labels := []string{ 139 | loadbalancer.ID, 140 | loadbalancer.Name, 141 | loadbalancer.Zone.String(), 142 | loadbalancer.Type, 143 | } 144 | 145 | // TODO check if it is possible to add loadbalancer tag as labels 146 | // for _, tags := range instance.Tags { 147 | // labels = append(labels, tags) 148 | // } 149 | 150 | var active float64 151 | 152 | switch loadbalancer.Status { 153 | case lb.LBStatusReady: 154 | active = 1.0 155 | case lb.LBStatusDeleting: 156 | active = 0.5 157 | case lb.LBStatusCreating: 158 | active = 0.5 159 | case lb.LBStatusMigrating: 160 | active = 0.5 161 | case lb.LBStatusToDelete: 162 | active = 0.5 163 | case lb.LBStatusError: 164 | active = 0.0 165 | case lb.LBStatusUnknown: 166 | active = 0.0 167 | case lb.LBStatusLocked: 168 | active = 0.0 169 | case lb.LBStatusPending: 170 | active = 0.0 171 | case lb.LBStatusStopped: 172 | active = 0.0 173 | case lb.LBStatusToCreate: 174 | active = 0.0 175 | default: 176 | active = 0.0 177 | } 178 | 179 | ch <- prometheus.MustNewConstMetric(c.Up, prometheus.GaugeValue, active, labels...) 180 | 181 | query := url.Values{} 182 | 183 | query.Add("start_date", time.Now().Add(-1*time.Hour).Format(time.RFC3339)) 184 | query.Add("end_date", time.Now().Format(time.RFC3339)) 185 | 186 | scwReq := &scw.ScalewayRequest{ 187 | Method: "GET", 188 | Path: "/lb-private/v1/zones/" + fmt.Sprint(loadbalancer.Zone) + "/lbs/" + fmt.Sprint(loadbalancer.ID) + "/metrics", 189 | Query: query, 190 | Headers: http.Header{}, 191 | } 192 | 193 | var metricResponse LbMetrics 194 | 195 | err := c.client.Do(scwReq, &metricResponse) 196 | 197 | if err != nil { 198 | c.errors.WithLabelValues("loadbalancer").Add(1) 199 | _ = level.Warn(c.logger).Log( 200 | "msg", "can't fetch the metric for the loadbalancer", 201 | "zone", loadbalancer.Zone, 202 | "loadbalancerId", loadbalancer.ID, 203 | "loadbalancerName", loadbalancer.Name, 204 | "err", err, 205 | ) 206 | 207 | return 208 | } 209 | 210 | for _, timeseries := range metricResponse.Timeseries { 211 | var series *prometheus.Desc 212 | 213 | switch timeseries.Name { 214 | case "node_network_receive_bits_sec": 215 | series = c.NetworkReceive 216 | case "node_network_transmit_bits_sec": 217 | series = c.NetworkTransmit 218 | case "current_connection_rate_sec": 219 | series = c.Connection 220 | case "current_new_connection_rate_sec": 221 | series = c.NewConnection 222 | case "server_status": 223 | // Should export metric for this ? 224 | continue 225 | default: 226 | _ = level.Debug(c.logger).Log( 227 | "msg", "unmapped scaleway metric", 228 | "zone", loadbalancer.Zone, 229 | "loadbalancerId", loadbalancer.ID, 230 | "loadbalancerName", loadbalancer.Name, 231 | "scwMetric", timeseries.Name, 232 | "err", err, 233 | ) 234 | continue 235 | } 236 | 237 | if len(timeseries.Points) == 0 { 238 | c.errors.WithLabelValues("database").Add(1) 239 | _ = level.Warn(c.logger).Log( 240 | "msg", "no data were returned for the metric", 241 | "loadbalancerName", loadbalancer.Name, 242 | "loadbalancerId", loadbalancer.ID, 243 | "zone", loadbalancer.Zone, 244 | "metric", timeseries.Name, 245 | "err", err, 246 | ) 247 | 248 | continue 249 | } 250 | 251 | sort.Slice(timeseries.Points, func(i, j int) bool { 252 | return timeseries.Points[i].Timestamp.Before(timeseries.Points[j].Timestamp) 253 | }) 254 | 255 | value := float64(timeseries.Points[len(timeseries.Points)-1].Value) 256 | 257 | ch <- prometheus.MustNewConstMetric(series, prometheus.GaugeValue, value, labels...) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /collector/redis.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "sort" 9 | "sync" 10 | "time" 11 | 12 | "github.com/go-kit/log" 13 | "github.com/go-kit/log/level" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/scaleway/scaleway-sdk-go/api/redis/v1" 16 | "github.com/scaleway/scaleway-sdk-go/scw" 17 | ) 18 | 19 | // RedisCollector collects metrics about all redis nodes. 20 | type RedisCollector struct { 21 | logger log.Logger 22 | errors *prometheus.CounterVec 23 | client *scw.Client 24 | redisClient *redis.API 25 | timeout time.Duration 26 | zones []scw.Zone 27 | 28 | CPUUsagePercent *prometheus.Desc 29 | MemUsagePercent *prometheus.Desc 30 | DBMemoryUsagePercent *prometheus.Desc 31 | } 32 | 33 | // NewRedisCollector returns a new RedisCollector. 34 | func NewRedisCollector(logger log.Logger, errors *prometheus.CounterVec, client *scw.Client, timeout time.Duration, zones []scw.Zone) *RedisCollector { 35 | errors.WithLabelValues("redis").Add(0) 36 | 37 | _ = level.Info(logger).Log("msg", "Redis collector enabled") 38 | 39 | labels := []string{"id", "name", "node"} 40 | 41 | return &RedisCollector{ 42 | logger: logger, 43 | errors: errors, 44 | client: client, 45 | redisClient: redis.NewAPI(client), 46 | timeout: timeout, 47 | zones: zones, 48 | 49 | CPUUsagePercent: prometheus.NewDesc( 50 | "scaleway_redis_cpu_usage_percent", 51 | "The redis node CPU usage percentage", 52 | labels, nil, 53 | ), 54 | MemUsagePercent: prometheus.NewDesc( 55 | "scaleway_redis_memory_usage_percent", 56 | "The redis node memory usage percentage", 57 | labels, nil, 58 | ), 59 | DBMemoryUsagePercent: prometheus.NewDesc( 60 | "scaleway_redis_db_memory_usage_percent", 61 | "The redis node database memory usage percentage", 62 | labels, nil, 63 | ), 64 | } 65 | } 66 | 67 | // Describe sends the super-set of all possible descriptors of metrics 68 | // collected by this Collector. 69 | func (c *RedisCollector) Describe(ch chan<- *prometheus.Desc) { 70 | ch <- c.CPUUsagePercent 71 | ch <- c.MemUsagePercent 72 | ch <- c.DBMemoryUsagePercent 73 | } 74 | 75 | // Collect is called by the Prometheus registry when collecting metrics. 76 | func (c *RedisCollector) Collect(ch chan<- prometheus.Metric) { 77 | _, cancel := context.WithTimeout(context.Background(), c.timeout) 78 | defer cancel() 79 | 80 | for _, zone := range c.zones { 81 | clusterList, err := c.redisClient.ListClusters(&redis.ListClustersRequest{Zone: zone}) 82 | 83 | if err != nil { 84 | var responseError *scw.ResponseError 85 | 86 | switch { 87 | case errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotImplemented: 88 | _ = level.Debug(c.logger).Log("msg", "Loadbalancer is not supported in this zone", "zone", zone) 89 | return 90 | default: 91 | c.errors.WithLabelValues("clusters").Add(1) 92 | _ = level.Warn(c.logger).Log("msg", "can't fetch the list of clusters", "err", err, "zone", zone) 93 | 94 | return 95 | } 96 | } 97 | 98 | var wg sync.WaitGroup 99 | defer wg.Wait() 100 | 101 | for _, cluster := range clusterList.Clusters { 102 | wg.Add(1) 103 | 104 | _ = level.Debug(c.logger).Log("msg", fmt.Sprintf("Fetching metrics for cluster : %s", cluster.ID), "zone", zone) 105 | 106 | go c.FetchRedisMetrics(&wg, ch, zone, cluster) 107 | } 108 | } 109 | } 110 | 111 | func (c *RedisCollector) FetchRedisMetrics(parentWg *sync.WaitGroup, ch chan<- prometheus.Metric, zone scw.Zone, cluster *redis.Cluster) { 112 | defer parentWg.Done() 113 | 114 | metricResponse, err := c.redisClient.GetClusterMetrics(&redis.GetClusterMetricsRequest{ 115 | Zone: zone, 116 | ClusterID: cluster.ID, 117 | }) 118 | 119 | if err != nil { 120 | c.errors.WithLabelValues("redis").Add(1) 121 | _ = level.Warn(c.logger).Log( 122 | "msg", "can't fetch the metric for the redis cluster", 123 | "clusterName", cluster.Name, 124 | "clusterId", cluster.ID, 125 | "zone", zone, 126 | "err", err, 127 | ) 128 | 129 | return 130 | } 131 | 132 | for _, timeseries := range metricResponse.Timeseries { 133 | labels := []string{ 134 | cluster.ID, 135 | cluster.Name, 136 | timeseries.Metadata["node"], 137 | } 138 | 139 | var series *prometheus.Desc 140 | 141 | switch timeseries.Name { 142 | case "cpu_usage_percent": 143 | series = c.CPUUsagePercent 144 | case "mem_usage_percent": 145 | series = c.MemUsagePercent 146 | case "db_memory_usage_percent": 147 | series = c.DBMemoryUsagePercent 148 | default: 149 | _ = level.Debug(c.logger).Log( 150 | "msg", "unmapped scaleway metric", 151 | "scwMetric", timeseries.Name, 152 | "clusterName", cluster.Name, 153 | "clusterId", cluster.ID, 154 | "zone", zone, 155 | "err", err, 156 | ) 157 | continue 158 | } 159 | 160 | if len(timeseries.Points) == 0 { 161 | c.errors.WithLabelValues("redis").Add(1) 162 | _ = level.Warn(c.logger).Log( 163 | "msg", "no data were returned for the metric", 164 | "metric", timeseries.Name, 165 | "clusterName", cluster.Name, 166 | "clusterId", cluster.ID, 167 | "zone", zone, 168 | "err", err, 169 | ) 170 | 171 | continue 172 | } 173 | 174 | sort.Slice(timeseries.Points, func(i, j int) bool { 175 | return timeseries.Points[i].Timestamp.Before(timeseries.Points[j].Timestamp) 176 | }) 177 | 178 | value := float64(timeseries.Points[len(timeseries.Points)-1].Value) 179 | 180 | ch <- prometheus.MustNewConstMetric(series, prometheus.GaugeValue, value, labels...) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | exporter: 4 | build: 5 | context: . 6 | args: 7 | VERSION: development 8 | REVISION: 0000000 9 | ports: 10 | - 9503:9503 11 | env_file: 12 | - scaleway.env 13 | 14 | prometheus: 15 | image: prom/prometheus:v2.41.0 16 | volumes: 17 | - ./hack/prometheus/:/etc/prometheus/ 18 | command: 19 | - '--config.file=/etc/prometheus/prometheus.yml' 20 | - '--storage.tsdb.path=/prometheus' 21 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 22 | - '--web.console.templates=/usr/share/prometheus/consoles' 23 | ports: 24 | - 9090:9090 25 | 26 | alertmanager: 27 | image: prom/alertmanager:v0.25.0 28 | depends_on: 29 | - prometheus 30 | ports: 31 | - 9093:9093 32 | 33 | grafana: 34 | image: grafana/grafana:9.3.2 35 | depends_on: 36 | - prometheus 37 | ports: 38 | - 3000:3000 39 | env_file: 40 | - hack/grafana/grafana.default 41 | volumes: 42 | - ./hack/grafana/provisioning/:/etc/grafana/provisioning/ -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yoannma/scaleway_exporter 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/alexflint/go-arg v1.4.3 7 | github.com/aws/aws-sdk-go v1.44.184 8 | github.com/go-kit/kit v0.12.0 9 | github.com/joho/godotenv v1.4.0 10 | github.com/prometheus/client_golang v1.14.0 11 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12 12 | ) 13 | 14 | require ( 15 | github.com/alexflint/go-scalar v1.2.0 // indirect 16 | github.com/beorn7/perks v1.0.1 // indirect 17 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 18 | github.com/go-kit/log v0.2.1 // indirect 19 | github.com/go-logfmt/logfmt v0.5.1 // indirect 20 | github.com/golang/protobuf v1.5.2 // indirect 21 | github.com/jmespath/go-jmespath v0.4.0 // indirect 22 | github.com/kr/pretty v0.3.1 // indirect 23 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 24 | github.com/prometheus/client_model v0.3.0 // indirect 25 | github.com/prometheus/common v0.39.0 // indirect 26 | github.com/prometheus/procfs v0.9.0 // indirect 27 | golang.org/x/sys v0.4.0 // indirect 28 | google.golang.org/protobuf v1.28.1 // indirect 29 | gopkg.in/yaml.v2 v2.4.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= 2 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= 3 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 4 | github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= 5 | github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 6 | github.com/aws/aws-sdk-go v1.44.184 h1:/MggyE66rOImXJKl1HqhLQITvWvqIV7w1Q4MaG6FHUo= 7 | github.com/aws/aws-sdk-go v1.44.184/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 8 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 9 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 10 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 11 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= 17 | github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= 18 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 19 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 20 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 21 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 22 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 24 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 25 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 26 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 29 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 30 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 31 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 32 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 33 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 34 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 35 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 36 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 37 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 38 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 39 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 40 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 41 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 42 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 45 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= 46 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 47 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 48 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 49 | github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= 50 | github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= 51 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 52 | github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= 53 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 54 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 55 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12 h1:Aaz4T7dZp7cB2cv7D/tGtRdSMh48sRaDYr7Jh0HV4qQ= 56 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 61 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 62 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 63 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 64 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 67 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 68 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 69 | golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= 70 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 80 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 82 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 83 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 84 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 85 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 86 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 87 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 88 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 89 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 90 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 91 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 92 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 95 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 96 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 97 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 99 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 100 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 101 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 102 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 103 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 104 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 105 | -------------------------------------------------------------------------------- /hack/grafana/grafana.default: -------------------------------------------------------------------------------- 1 | GF_AUTH_ANONYMOUS_ENABLED="true" 2 | GF_AUTH_ANONYMOUS_ORG_NAME="Main Org." 3 | GF_AUTH_ANONYMOUS_ORG_ROLE="Admin" 4 | GF_AUTH_BASIC_ENABLED="false" 5 | GF_AUTH_DISABLE_LOGIN_FORM="true" 6 | GF_AUTH_DISABLE_SIGNOUT_MENU="true" 7 | GF_AUTH_PROXY_ENABLED="true" 8 | GF_USERS_ALLOW_SIGN_UP=false -------------------------------------------------------------------------------- /hack/grafana/provisioning/dashboards/db-details.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 1, 19 | "iteration": 1604707893787, 20 | "links": [], 21 | "panels": [ 22 | { 23 | "datasource": null, 24 | "fieldConfig": { 25 | "defaults": { 26 | "custom": {}, 27 | "mappings": [ 28 | { 29 | "from": "", 30 | "id": 0, 31 | "text": "Ready", 32 | "to": "", 33 | "type": 1, 34 | "value": "1" 35 | }, 36 | { 37 | "from": "", 38 | "id": 1, 39 | "text": "Auto Healing", 40 | "to": "", 41 | "type": 1, 42 | "value": "0.5" 43 | }, 44 | { 45 | "from": "", 46 | "id": 2, 47 | "text": "Down", 48 | "to": "", 49 | "type": 1, 50 | "value": "0" 51 | } 52 | ], 53 | "thresholds": { 54 | "mode": "absolute", 55 | "steps": [ 56 | { 57 | "color": "dark-red", 58 | "value": null 59 | }, 60 | { 61 | "color": "yellow", 62 | "value": 0.5 63 | }, 64 | { 65 | "color": "dark-green", 66 | "value": 1 67 | } 68 | ] 69 | } 70 | }, 71 | "overrides": [] 72 | }, 73 | "gridPos": { 74 | "h": 6, 75 | "w": 3, 76 | "x": 0, 77 | "y": 0 78 | }, 79 | "id": 9, 80 | "options": { 81 | "colorMode": "value", 82 | "graphMode": "none", 83 | "justifyMode": "auto", 84 | "orientation": "auto", 85 | "reduceOptions": { 86 | "calcs": [ 87 | "lastNotNull" 88 | ], 89 | "fields": "", 90 | "values": false 91 | }, 92 | "textMode": "auto" 93 | }, 94 | "pluginVersion": "7.2.0", 95 | "targets": [ 96 | { 97 | "expr": "scaleway_database_up{name=\"$instance\"}", 98 | "interval": "", 99 | "legendFormat": "", 100 | "refId": "A" 101 | } 102 | ], 103 | "timeFrom": null, 104 | "timeShift": null, 105 | "title": "Status", 106 | "type": "stat" 107 | }, 108 | { 109 | "datasource": null, 110 | "fieldConfig": { 111 | "defaults": { 112 | "custom": {}, 113 | "mappings": [], 114 | "thresholds": { 115 | "mode": "absolute", 116 | "steps": [ 117 | { 118 | "color": "rgb(221, 221, 221)", 119 | "value": null 120 | } 121 | ] 122 | } 123 | }, 124 | "overrides": [] 125 | }, 126 | "gridPos": { 127 | "h": 6, 128 | "w": 5, 129 | "x": 3, 130 | "y": 0 131 | }, 132 | "id": 11, 133 | "options": { 134 | "colorMode": "value", 135 | "graphMode": "none", 136 | "justifyMode": "auto", 137 | "orientation": "auto", 138 | "reduceOptions": { 139 | "calcs": [ 140 | "first" 141 | ], 142 | "fields": "/^engine$/", 143 | "values": false 144 | }, 145 | "textMode": "auto" 146 | }, 147 | "pluginVersion": "7.2.0", 148 | "targets": [ 149 | { 150 | "expr": "scaleway_database_up{name=\"$instance\"}", 151 | "instant": true, 152 | "interval": "", 153 | "legendFormat": "", 154 | "refId": "A" 155 | } 156 | ], 157 | "timeFrom": null, 158 | "timeShift": null, 159 | "title": "Engine", 160 | "transformations": [ 161 | { 162 | "id": "labelsToFields", 163 | "options": {} 164 | } 165 | ], 166 | "type": "stat" 167 | }, 168 | { 169 | "datasource": null, 170 | "fieldConfig": { 171 | "defaults": { 172 | "custom": {}, 173 | "mappings": [], 174 | "thresholds": { 175 | "mode": "absolute", 176 | "steps": [ 177 | { 178 | "color": "rgb(200, 200, 200)", 179 | "value": null 180 | } 181 | ] 182 | }, 183 | "unit": "none" 184 | }, 185 | "overrides": [] 186 | }, 187 | "gridPos": { 188 | "h": 6, 189 | "w": 4, 190 | "x": 8, 191 | "y": 0 192 | }, 193 | "id": 15, 194 | "options": { 195 | "colorMode": "value", 196 | "graphMode": "area", 197 | "justifyMode": "auto", 198 | "orientation": "auto", 199 | "reduceOptions": { 200 | "calcs": [ 201 | "last" 202 | ], 203 | "fields": "/^node$/", 204 | "values": false 205 | }, 206 | "textMode": "auto" 207 | }, 208 | "pluginVersion": "7.2.0", 209 | "repeat": null, 210 | "repeatDirection": "h", 211 | "targets": [ 212 | { 213 | "expr": "topk(1, scaleway_database_cpu_usage_percent{name=\"$instance\"})", 214 | "instant": true, 215 | "interval": "", 216 | "intervalFactor": 1, 217 | "legendFormat": "{{node}}", 218 | "refId": "A" 219 | } 220 | ], 221 | "timeFrom": null, 222 | "timeShift": null, 223 | "title": "Master", 224 | "transformations": [ 225 | { 226 | "id": "labelsToFields", 227 | "options": {} 228 | } 229 | ], 230 | "type": "stat" 231 | }, 232 | { 233 | "datasource": null, 234 | "fieldConfig": { 235 | "defaults": { 236 | "custom": {}, 237 | "mappings": [], 238 | "thresholds": { 239 | "mode": "absolute", 240 | "steps": [ 241 | { 242 | "color": "green", 243 | "value": null 244 | }, 245 | { 246 | "color": "orange", 247 | "value": 60 248 | }, 249 | { 250 | "color": "dark-red", 251 | "value": 80 252 | } 253 | ] 254 | }, 255 | "unit": "percent" 256 | }, 257 | "overrides": [] 258 | }, 259 | "gridPos": { 260 | "h": 6, 261 | "w": 3, 262 | "x": 12, 263 | "y": 0 264 | }, 265 | "id": 16, 266 | "options": { 267 | "orientation": "auto", 268 | "reduceOptions": { 269 | "calcs": [ 270 | "last" 271 | ], 272 | "fields": "", 273 | "values": false 274 | }, 275 | "showThresholdLabels": false, 276 | "showThresholdMarkers": true 277 | }, 278 | "pluginVersion": "7.2.0", 279 | "repeatDirection": "h", 280 | "targets": [ 281 | { 282 | "expr": "topk(1, scaleway_database_cpu_usage_percent{name=\"$instance\"})", 283 | "instant": true, 284 | "interval": "", 285 | "intervalFactor": 1, 286 | "legendFormat": "{{node}}", 287 | "refId": "A" 288 | } 289 | ], 290 | "timeFrom": null, 291 | "timeShift": null, 292 | "title": "CPU Usage", 293 | "type": "gauge" 294 | }, 295 | { 296 | "datasource": null, 297 | "gridPos": { 298 | "h": 1, 299 | "w": 24, 300 | "x": 0, 301 | "y": 6 302 | }, 303 | "id": 13, 304 | "title": "Row title", 305 | "type": "row" 306 | }, 307 | { 308 | "aliasColors": {}, 309 | "bars": false, 310 | "dashLength": 10, 311 | "dashes": false, 312 | "datasource": null, 313 | "fieldConfig": { 314 | "defaults": { 315 | "custom": {}, 316 | "links": [] 317 | }, 318 | "overrides": [] 319 | }, 320 | "fill": 1, 321 | "fillGradient": 0, 322 | "gridPos": { 323 | "h": 12, 324 | "w": 12, 325 | "x": 0, 326 | "y": 7 327 | }, 328 | "hiddenSeries": false, 329 | "id": 2, 330 | "interval": "1m", 331 | "legend": { 332 | "avg": false, 333 | "current": false, 334 | "max": false, 335 | "min": false, 336 | "show": true, 337 | "total": false, 338 | "values": false 339 | }, 340 | "lines": true, 341 | "linewidth": 1, 342 | "nullPointMode": "null", 343 | "options": { 344 | "alertThreshold": true 345 | }, 346 | "percentage": false, 347 | "pluginVersion": "7.2.0", 348 | "pointradius": 2, 349 | "points": false, 350 | "renderer": "flot", 351 | "seriesOverrides": [], 352 | "spaceLength": 10, 353 | "stack": false, 354 | "steppedLine": false, 355 | "targets": [ 356 | { 357 | "expr": "scaleway_database_cpu_usage_percent{name=\"$instance\"}", 358 | "instant": false, 359 | "interval": "1m", 360 | "legendFormat": "{{node}}", 361 | "refId": "A" 362 | } 363 | ], 364 | "thresholds": [], 365 | "timeFrom": null, 366 | "timeRegions": [], 367 | "timeShift": null, 368 | "title": "CPU Usage", 369 | "tooltip": { 370 | "shared": true, 371 | "sort": 0, 372 | "value_type": "individual" 373 | }, 374 | "type": "graph", 375 | "xaxis": { 376 | "buckets": null, 377 | "mode": "time", 378 | "name": null, 379 | "show": true, 380 | "values": [] 381 | }, 382 | "yaxes": [ 383 | { 384 | "format": "percent", 385 | "label": null, 386 | "logBase": 1, 387 | "max": "100", 388 | "min": null, 389 | "show": true 390 | }, 391 | { 392 | "format": "short", 393 | "label": null, 394 | "logBase": 1, 395 | "max": null, 396 | "min": null, 397 | "show": true 398 | } 399 | ], 400 | "yaxis": { 401 | "align": false, 402 | "alignLevel": null 403 | } 404 | }, 405 | { 406 | "aliasColors": {}, 407 | "bars": false, 408 | "dashLength": 10, 409 | "dashes": false, 410 | "datasource": null, 411 | "fieldConfig": { 412 | "defaults": { 413 | "custom": {}, 414 | "links": [] 415 | }, 416 | "overrides": [] 417 | }, 418 | "fill": 1, 419 | "fillGradient": 0, 420 | "gridPos": { 421 | "h": 12, 422 | "w": 12, 423 | "x": 12, 424 | "y": 7 425 | }, 426 | "hiddenSeries": false, 427 | "id": 3, 428 | "interval": "1m", 429 | "legend": { 430 | "avg": false, 431 | "current": false, 432 | "max": false, 433 | "min": false, 434 | "show": true, 435 | "total": false, 436 | "values": false 437 | }, 438 | "lines": true, 439 | "linewidth": 1, 440 | "nullPointMode": "null", 441 | "options": { 442 | "alertThreshold": true 443 | }, 444 | "percentage": false, 445 | "pluginVersion": "7.2.0", 446 | "pointradius": 2, 447 | "points": false, 448 | "renderer": "flot", 449 | "seriesOverrides": [], 450 | "spaceLength": 10, 451 | "stack": false, 452 | "steppedLine": false, 453 | "targets": [ 454 | { 455 | "expr": "scaleway_database_memory_usage_percent{name=\"$instance\"}", 456 | "instant": false, 457 | "interval": "1m", 458 | "legendFormat": "{{node}}", 459 | "refId": "A" 460 | } 461 | ], 462 | "thresholds": [], 463 | "timeFrom": null, 464 | "timeRegions": [], 465 | "timeShift": null, 466 | "title": "RAM Usage", 467 | "tooltip": { 468 | "shared": true, 469 | "sort": 0, 470 | "value_type": "individual" 471 | }, 472 | "type": "graph", 473 | "xaxis": { 474 | "buckets": null, 475 | "mode": "time", 476 | "name": null, 477 | "show": true, 478 | "values": [] 479 | }, 480 | "yaxes": [ 481 | { 482 | "format": "percent", 483 | "label": null, 484 | "logBase": 1, 485 | "max": "100", 486 | "min": null, 487 | "show": true 488 | }, 489 | { 490 | "format": "short", 491 | "label": null, 492 | "logBase": 1, 493 | "max": null, 494 | "min": null, 495 | "show": true 496 | } 497 | ], 498 | "yaxis": { 499 | "align": false, 500 | "alignLevel": null 501 | } 502 | }, 503 | { 504 | "aliasColors": {}, 505 | "bars": false, 506 | "dashLength": 10, 507 | "dashes": false, 508 | "datasource": null, 509 | "fieldConfig": { 510 | "defaults": { 511 | "custom": {}, 512 | "links": [] 513 | }, 514 | "overrides": [] 515 | }, 516 | "fill": 1, 517 | "fillGradient": 0, 518 | "gridPos": { 519 | "h": 12, 520 | "w": 12, 521 | "x": 0, 522 | "y": 19 523 | }, 524 | "hiddenSeries": false, 525 | "id": 5, 526 | "interval": "1m", 527 | "legend": { 528 | "avg": false, 529 | "current": false, 530 | "max": false, 531 | "min": false, 532 | "show": true, 533 | "total": false, 534 | "values": false 535 | }, 536 | "lines": true, 537 | "linewidth": 1, 538 | "nullPointMode": "null", 539 | "options": { 540 | "alertThreshold": true 541 | }, 542 | "percentage": false, 543 | "pluginVersion": "7.2.0", 544 | "pointradius": 2, 545 | "points": false, 546 | "renderer": "flot", 547 | "seriesOverrides": [], 548 | "spaceLength": 10, 549 | "stack": false, 550 | "steppedLine": false, 551 | "targets": [ 552 | { 553 | "expr": "scaleway_database_disk_usage_percent{name=\"$instance\"}", 554 | "instant": false, 555 | "interval": "1m", 556 | "legendFormat": "{{node}}", 557 | "refId": "A" 558 | } 559 | ], 560 | "thresholds": [ 561 | { 562 | "colorMode": "warning", 563 | "fill": true, 564 | "line": true, 565 | "op": "gt", 566 | "value": 80, 567 | "yaxis": "left" 568 | }, 569 | { 570 | "colorMode": "critical", 571 | "fill": true, 572 | "line": true, 573 | "op": "gt", 574 | "value": 90, 575 | "yaxis": "left" 576 | } 577 | ], 578 | "timeFrom": null, 579 | "timeRegions": [], 580 | "timeShift": null, 581 | "title": "Disk Usage", 582 | "tooltip": { 583 | "shared": true, 584 | "sort": 0, 585 | "value_type": "individual" 586 | }, 587 | "type": "graph", 588 | "xaxis": { 589 | "buckets": null, 590 | "mode": "time", 591 | "name": null, 592 | "show": true, 593 | "values": [] 594 | }, 595 | "yaxes": [ 596 | { 597 | "format": "percent", 598 | "label": null, 599 | "logBase": 1, 600 | "max": "100", 601 | "min": null, 602 | "show": true 603 | }, 604 | { 605 | "format": "short", 606 | "label": null, 607 | "logBase": 1, 608 | "max": null, 609 | "min": null, 610 | "show": true 611 | } 612 | ], 613 | "yaxis": { 614 | "align": false, 615 | "alignLevel": null 616 | } 617 | }, 618 | { 619 | "aliasColors": {}, 620 | "bars": false, 621 | "dashLength": 10, 622 | "dashes": false, 623 | "datasource": null, 624 | "fieldConfig": { 625 | "defaults": { 626 | "custom": {}, 627 | "links": [] 628 | }, 629 | "overrides": [] 630 | }, 631 | "fill": 1, 632 | "fillGradient": 0, 633 | "gridPos": { 634 | "h": 12, 635 | "w": 12, 636 | "x": 12, 637 | "y": 19 638 | }, 639 | "hiddenSeries": false, 640 | "id": 4, 641 | "interval": "1m", 642 | "legend": { 643 | "avg": false, 644 | "current": false, 645 | "max": false, 646 | "min": false, 647 | "show": true, 648 | "total": false, 649 | "values": false 650 | }, 651 | "lines": true, 652 | "linewidth": 1, 653 | "nullPointMode": "null", 654 | "options": { 655 | "alertThreshold": true 656 | }, 657 | "percentage": false, 658 | "pluginVersion": "7.2.0", 659 | "pointradius": 2, 660 | "points": false, 661 | "renderer": "flot", 662 | "seriesOverrides": [], 663 | "spaceLength": 10, 664 | "stack": false, 665 | "steppedLine": false, 666 | "targets": [ 667 | { 668 | "expr": "scaleway_database_total_connections{name=\"$instance\"}", 669 | "instant": false, 670 | "interval": "1m", 671 | "legendFormat": "{{node}}", 672 | "refId": "A" 673 | } 674 | ], 675 | "thresholds": [], 676 | "timeFrom": null, 677 | "timeRegions": [], 678 | "timeShift": null, 679 | "title": "Connections", 680 | "tooltip": { 681 | "shared": true, 682 | "sort": 0, 683 | "value_type": "individual" 684 | }, 685 | "type": "graph", 686 | "xaxis": { 687 | "buckets": null, 688 | "mode": "time", 689 | "name": null, 690 | "show": true, 691 | "values": [] 692 | }, 693 | "yaxes": [ 694 | { 695 | "format": "short", 696 | "label": null, 697 | "logBase": 1, 698 | "max": null, 699 | "min": null, 700 | "show": true 701 | }, 702 | { 703 | "format": "short", 704 | "label": null, 705 | "logBase": 1, 706 | "max": null, 707 | "min": null, 708 | "show": true 709 | } 710 | ], 711 | "yaxis": { 712 | "align": false, 713 | "alignLevel": null 714 | } 715 | } 716 | ], 717 | "refresh": false, 718 | "schemaVersion": 26, 719 | "style": "dark", 720 | "tags": [], 721 | "templating": { 722 | "list": [ 723 | { 724 | "allValue": ".*", 725 | "datasource": "Prometheus", 726 | "definition": "label_values(scaleway_database_up, name)", 727 | "hide": 0, 728 | "includeAll": false, 729 | "label": "Instance", 730 | "multi": false, 731 | "name": "instance", 732 | "options": [], 733 | "query": "label_values(scaleway_database_up, name)", 734 | "refresh": 1, 735 | "regex": "", 736 | "skipUrlSync": false, 737 | "sort": 1, 738 | "tagValuesQuery": "", 739 | "tags": [], 740 | "tagsQuery": "", 741 | "type": "query", 742 | "useTags": false 743 | } 744 | ] 745 | }, 746 | "time": { 747 | "from": "now-6h", 748 | "to": "now" 749 | }, 750 | "timepicker": { 751 | "refresh_intervals": [ 752 | "5s", 753 | "10s", 754 | "30s", 755 | "1m", 756 | "5m", 757 | "15m", 758 | "30m", 759 | "1h", 760 | "2h", 761 | "1d" 762 | ] 763 | }, 764 | "timezone": "", 765 | "title": "Scaleway Database", 766 | "uid": "h3RjfrKMk", 767 | "version": 5 768 | } -------------------------------------------------------------------------------- /hack/grafana/provisioning/dashboards/db-overview.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 2, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "colors": [ 23 | "#299c46", 24 | "rgba(237, 129, 40, 0.89)", 25 | "#d44a3a", 26 | "#4040a0" 27 | ], 28 | "datasource": null, 29 | "fieldConfig": { 30 | "defaults": { 31 | "custom": {} 32 | }, 33 | "overrides": [] 34 | }, 35 | "gridPos": { 36 | "h": 16, 37 | "w": 12, 38 | "x": 0, 39 | "y": 0 40 | }, 41 | "id": 2, 42 | "mappingType": 1, 43 | "mappingTypes": [ 44 | { 45 | "name": "value to text", 46 | "value": 1 47 | }, 48 | { 49 | "name": "range to text", 50 | "value": 2 51 | } 52 | ], 53 | "pluginVersion": "7.2.0", 54 | "polystat": { 55 | "animationSpeed": 2500, 56 | "columnAutoSize": true, 57 | "columns": "", 58 | "defaultClickThrough": "d/h3RjfrKMk/scaleway-database?var-instance=${__cell_name}", 59 | "defaultClickThroughNewTab": true, 60 | "defaultClickThroughSanitize": false, 61 | "displayLimit": 100, 62 | "fontAutoColor": true, 63 | "fontAutoScale": true, 64 | "fontSize": 12, 65 | "fontType": "Roboto", 66 | "globalDecimals": 2, 67 | "globalDisplayMode": "all", 68 | "globalDisplayTextTriggeredEmpty": "OK", 69 | "globalOperatorName": "current", 70 | "globalThresholds": [ 71 | { 72 | "color": "#299c46", 73 | "state": 0, 74 | "value": 0 75 | }, 76 | { 77 | "color": "rgba(237, 129, 40, 0.89)", 78 | "state": 1, 79 | "value": 30 80 | }, 81 | { 82 | "color": "#d44a3a", 83 | "state": 2, 84 | "value": 60 85 | } 86 | ], 87 | "globalUnitFormat": "percent", 88 | "gradientEnabled": true, 89 | "hexagonSortByDirection": 1, 90 | "hexagonSortByField": "name", 91 | "maxMetrics": 0, 92 | "polygonBorderColor": "black", 93 | "polygonBorderSize": 2, 94 | "polygonGlobalFillColor": "#0a50a1", 95 | "radius": "", 96 | "radiusAutoSize": true, 97 | "rowAutoSize": true, 98 | "rows": "", 99 | "shape": "hexagon_pointed_top", 100 | "tooltipDisplayMode": "all", 101 | "tooltipDisplayTextTriggeredEmpty": "OK", 102 | "tooltipFontSize": 12, 103 | "tooltipFontType": "Roboto", 104 | "tooltipPrimarySortDirection": 2, 105 | "tooltipPrimarySortField": "thresholdLevel", 106 | "tooltipSecondarySortDirection": 2, 107 | "tooltipSecondarySortField": "value", 108 | "tooltipTimestampEnabled": true, 109 | "valueEnabled": true 110 | }, 111 | "rangeMaps": [ 112 | { 113 | "from": "null", 114 | "text": "N/A", 115 | "to": "null" 116 | } 117 | ], 118 | "repeat": null, 119 | "savedComposites": [], 120 | "savedOverrides": [], 121 | "targets": [ 122 | { 123 | "expr": "max by (name) (scaleway_database_cpu_usage_percent)", 124 | "format": "time_series", 125 | "instant": true, 126 | "interval": "1m", 127 | "legendFormat": "{{name}}", 128 | "refId": "A" 129 | } 130 | ], 131 | "timeFrom": null, 132 | "timeShift": null, 133 | "title": "CPU Usage", 134 | "transformations": [], 135 | "type": "grafana-polystat-panel", 136 | "valueMaps": [ 137 | { 138 | "op": "=", 139 | "text": "N/A", 140 | "value": "null" 141 | } 142 | ] 143 | }, 144 | { 145 | "colors": [ 146 | "#299c46", 147 | "rgba(237, 129, 40, 0.89)", 148 | "#d44a3a", 149 | "#4040a0" 150 | ], 151 | "datasource": null, 152 | "fieldConfig": { 153 | "defaults": { 154 | "custom": {} 155 | }, 156 | "overrides": [] 157 | }, 158 | "gridPos": { 159 | "h": 16, 160 | "w": 12, 161 | "x": 12, 162 | "y": 0 163 | }, 164 | "id": 5, 165 | "mappingType": 1, 166 | "mappingTypes": [ 167 | { 168 | "name": "value to text", 169 | "value": 1 170 | }, 171 | { 172 | "name": "range to text", 173 | "value": 2 174 | } 175 | ], 176 | "pluginVersion": "7.2.0", 177 | "polystat": { 178 | "animationSpeed": 2500, 179 | "columnAutoSize": true, 180 | "columns": "", 181 | "defaultClickThrough": "d/h3RjfrKMk/scaleway-database?var-instance=${__cell_name}", 182 | "defaultClickThroughNewTab": true, 183 | "defaultClickThroughSanitize": false, 184 | "displayLimit": 100, 185 | "fontAutoColor": true, 186 | "fontAutoScale": true, 187 | "fontSize": 12, 188 | "fontType": "Roboto", 189 | "globalDecimals": 2, 190 | "globalDisplayMode": "all", 191 | "globalDisplayTextTriggeredEmpty": "OK", 192 | "globalOperatorName": "current", 193 | "globalThresholds": [ 194 | { 195 | "color": "#299c46", 196 | "state": 0, 197 | "value": 0 198 | }, 199 | { 200 | "color": "rgba(237, 129, 40, 0.89)", 201 | "state": 1, 202 | "value": 30 203 | }, 204 | { 205 | "color": "#d44a3a", 206 | "state": 2, 207 | "value": 60 208 | } 209 | ], 210 | "globalUnitFormat": "percent", 211 | "gradientEnabled": true, 212 | "hexagonSortByDirection": 1, 213 | "hexagonSortByField": "name", 214 | "maxMetrics": 0, 215 | "polygonBorderColor": "black", 216 | "polygonBorderSize": 2, 217 | "polygonGlobalFillColor": "#0a50a1", 218 | "radius": "", 219 | "radiusAutoSize": true, 220 | "rowAutoSize": true, 221 | "rows": "", 222 | "shape": "hexagon_pointed_top", 223 | "tooltipDisplayMode": "all", 224 | "tooltipDisplayTextTriggeredEmpty": "OK", 225 | "tooltipFontSize": 12, 226 | "tooltipFontType": "Roboto", 227 | "tooltipPrimarySortDirection": 2, 228 | "tooltipPrimarySortField": "thresholdLevel", 229 | "tooltipSecondarySortDirection": 2, 230 | "tooltipSecondarySortField": "value", 231 | "tooltipTimestampEnabled": true, 232 | "valueEnabled": true 233 | }, 234 | "rangeMaps": [ 235 | { 236 | "from": "null", 237 | "text": "N/A", 238 | "to": "null" 239 | } 240 | ], 241 | "savedComposites": [], 242 | "savedOverrides": [], 243 | "targets": [ 244 | { 245 | "expr": "max by (name) (scaleway_database_memory_usage_percent)", 246 | "instant": true, 247 | "interval": "1m", 248 | "legendFormat": "{{name}}", 249 | "refId": "A" 250 | } 251 | ], 252 | "timeFrom": null, 253 | "timeShift": null, 254 | "title": "Memory Usage", 255 | "type": "grafana-polystat-panel", 256 | "valueMaps": [ 257 | { 258 | "op": "=", 259 | "text": "N/A", 260 | "value": "null" 261 | } 262 | ] 263 | }, 264 | { 265 | "colors": [ 266 | "#299c46", 267 | "rgba(237, 129, 40, 0.89)", 268 | "#d44a3a", 269 | "#4040a0" 270 | ], 271 | "datasource": null, 272 | "fieldConfig": { 273 | "defaults": { 274 | "custom": {} 275 | }, 276 | "overrides": [] 277 | }, 278 | "gridPos": { 279 | "h": 16, 280 | "w": 12, 281 | "x": 0, 282 | "y": 16 283 | }, 284 | "id": 6, 285 | "mappingType": 1, 286 | "mappingTypes": [ 287 | { 288 | "name": "value to text", 289 | "value": 1 290 | }, 291 | { 292 | "name": "range to text", 293 | "value": 2 294 | } 295 | ], 296 | "pluginVersion": "7.2.0", 297 | "polystat": { 298 | "animationSpeed": 2500, 299 | "columnAutoSize": true, 300 | "columns": "", 301 | "defaultClickThrough": "d/h3RjfrKMk/scaleway-database?var-instance=${__cell_name}", 302 | "defaultClickThroughNewTab": true, 303 | "defaultClickThroughSanitize": false, 304 | "displayLimit": 100, 305 | "fontAutoColor": true, 306 | "fontAutoScale": true, 307 | "fontSize": 12, 308 | "fontType": "Roboto", 309 | "globalDecimals": 2, 310 | "globalDisplayMode": "all", 311 | "globalDisplayTextTriggeredEmpty": "OK", 312 | "globalOperatorName": "current", 313 | "globalThresholds": [ 314 | { 315 | "color": "#299c46", 316 | "state": 0, 317 | "value": 0 318 | }, 319 | { 320 | "color": "rgba(237, 129, 40, 0.89)", 321 | "state": 1, 322 | "value": 30 323 | }, 324 | { 325 | "color": "#d44a3a", 326 | "state": 2, 327 | "value": 60 328 | } 329 | ], 330 | "globalUnitFormat": "percent", 331 | "gradientEnabled": true, 332 | "hexagonSortByDirection": 1, 333 | "hexagonSortByField": "name", 334 | "maxMetrics": 0, 335 | "polygonBorderColor": "black", 336 | "polygonBorderSize": 2, 337 | "polygonGlobalFillColor": "#0a50a1", 338 | "radius": "", 339 | "radiusAutoSize": true, 340 | "rowAutoSize": true, 341 | "rows": "", 342 | "shape": "hexagon_pointed_top", 343 | "tooltipDisplayMode": "all", 344 | "tooltipDisplayTextTriggeredEmpty": "OK", 345 | "tooltipFontSize": 12, 346 | "tooltipFontType": "Roboto", 347 | "tooltipPrimarySortDirection": 2, 348 | "tooltipPrimarySortField": "thresholdLevel", 349 | "tooltipSecondarySortDirection": 2, 350 | "tooltipSecondarySortField": "value", 351 | "tooltipTimestampEnabled": true, 352 | "valueEnabled": true 353 | }, 354 | "rangeMaps": [ 355 | { 356 | "from": "null", 357 | "text": "N/A", 358 | "to": "null" 359 | } 360 | ], 361 | "savedComposites": [], 362 | "savedOverrides": [], 363 | "targets": [ 364 | { 365 | "expr": "max by (name) (scaleway_database_disk_usage_percent)", 366 | "instant": true, 367 | "interval": "1m", 368 | "legendFormat": "{{name}}", 369 | "refId": "A" 370 | } 371 | ], 372 | "timeFrom": null, 373 | "timeShift": null, 374 | "title": "Disk Usage", 375 | "type": "grafana-polystat-panel", 376 | "valueMaps": [ 377 | { 378 | "op": "=", 379 | "text": "N/A", 380 | "value": "null" 381 | } 382 | ] 383 | }, 384 | { 385 | "colors": [ 386 | "#299c46", 387 | "rgba(237, 129, 40, 0.89)", 388 | "#d44a3a", 389 | "#4040a0" 390 | ], 391 | "datasource": null, 392 | "fieldConfig": { 393 | "defaults": { 394 | "custom": {} 395 | }, 396 | "overrides": [] 397 | }, 398 | "gridPos": { 399 | "h": 16, 400 | "w": 12, 401 | "x": 12, 402 | "y": 16 403 | }, 404 | "id": 7, 405 | "mappingType": 1, 406 | "mappingTypes": [ 407 | { 408 | "name": "value to text", 409 | "value": 1 410 | }, 411 | { 412 | "name": "range to text", 413 | "value": 2 414 | } 415 | ], 416 | "pluginVersion": "7.2.0", 417 | "polystat": { 418 | "animationSpeed": 2500, 419 | "columnAutoSize": true, 420 | "columns": "", 421 | "defaultClickThrough": "d/h3RjfrKMk/scaleway-database?var-instance=${__cell_name}", 422 | "defaultClickThroughNewTab": true, 423 | "defaultClickThroughSanitize": false, 424 | "displayLimit": 100, 425 | "fontAutoColor": true, 426 | "fontAutoScale": true, 427 | "fontSize": 12, 428 | "fontType": "Roboto", 429 | "globalDecimals": 0, 430 | "globalDisplayMode": "all", 431 | "globalDisplayTextTriggeredEmpty": "OK", 432 | "globalOperatorName": "current", 433 | "globalThresholds": [ 434 | { 435 | "color": "#299c46", 436 | "state": 0, 437 | "value": 0 438 | }, 439 | { 440 | "color": "rgba(237, 129, 40, 0.89)", 441 | "state": 1, 442 | "value": 30 443 | }, 444 | { 445 | "color": "#d44a3a", 446 | "state": 2, 447 | "value": 60 448 | } 449 | ], 450 | "globalUnitFormat": "short", 451 | "gradientEnabled": true, 452 | "hexagonSortByDirection": 1, 453 | "hexagonSortByField": "name", 454 | "maxMetrics": 0, 455 | "polygonBorderColor": "black", 456 | "polygonBorderSize": 2, 457 | "polygonGlobalFillColor": "#0a50a1", 458 | "radius": "", 459 | "radiusAutoSize": true, 460 | "rowAutoSize": true, 461 | "rows": "", 462 | "shape": "hexagon_pointed_top", 463 | "tooltipDisplayMode": "all", 464 | "tooltipDisplayTextTriggeredEmpty": "OK", 465 | "tooltipFontSize": 12, 466 | "tooltipFontType": "Roboto", 467 | "tooltipPrimarySortDirection": 2, 468 | "tooltipPrimarySortField": "thresholdLevel", 469 | "tooltipSecondarySortDirection": 2, 470 | "tooltipSecondarySortField": "value", 471 | "tooltipTimestampEnabled": true, 472 | "valueEnabled": true 473 | }, 474 | "rangeMaps": [ 475 | { 476 | "from": "null", 477 | "text": "N/A", 478 | "to": "null" 479 | } 480 | ], 481 | "savedComposites": [], 482 | "savedOverrides": [], 483 | "targets": [ 484 | { 485 | "expr": "max by (name) (scaleway_database_total_connections)", 486 | "instant": true, 487 | "interval": "1m", 488 | "legendFormat": "{{name}}", 489 | "refId": "A" 490 | } 491 | ], 492 | "timeFrom": null, 493 | "timeShift": null, 494 | "title": "Connections", 495 | "type": "grafana-polystat-panel", 496 | "valueMaps": [ 497 | { 498 | "op": "=", 499 | "text": "N/A", 500 | "value": "null" 501 | } 502 | ] 503 | } 504 | ], 505 | "schemaVersion": 26, 506 | "style": "dark", 507 | "tags": [], 508 | "templating": { 509 | "list": [] 510 | }, 511 | "time": { 512 | "from": "now-30m", 513 | "to": "now" 514 | }, 515 | "timepicker": {}, 516 | "timezone": "", 517 | "title": "Scaleway Database overview", 518 | "uid": "rbTUfKcGz", 519 | "version": 2 520 | } -------------------------------------------------------------------------------- /hack/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | # list of datasources that should be deleted from the database 5 | deleteDatasources: 6 | - name: Prometheus 7 | orgId: 1 8 | 9 | # list of datasources to insert/update depending 10 | # whats available in the database 11 | datasources: 12 | # name of the datasource. Required 13 | - name: Prometheus 14 | # datasource type. Required 15 | type: prometheus 16 | # access mode. direct or proxy. Required 17 | access: proxy 18 | # org id. will default to orgId 1 if not specified 19 | orgId: 1 20 | # url 21 | url: http://prometheus:9090 22 | # database password, if used 23 | password: 24 | # database user, if used 25 | user: 26 | # database name, if used 27 | database: 28 | # enable/disable basic auth 29 | basicAuth: false 30 | # basic auth username, if used 31 | basicAuthUser: 32 | # basic auth password, if used 33 | basicAuthPassword: 34 | # enable/disable with credentials headers 35 | withCredentials: 36 | # mark as default datasource. Max one per org 37 | isDefault: true 38 | # fields that will be converted to json and stored in json_data 39 | jsonData: 40 | graphiteVersion: "1.1" 41 | tlsAuth: false 42 | tlsAuthWithCACert: false 43 | # json object of data that will be encrypted. 44 | secureJsonData: 45 | tlsCACert: "..." 46 | tlsClientCert: "..." 47 | tlsClientKey: "..." 48 | version: 1 49 | # allow users to edit datasources from the UI. 50 | editable: true -------------------------------------------------------------------------------- /hack/prometheus/alert.rules: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: example 3 | rules: 4 | 5 | # Alert for any instance that is unreachable for >2 minutes. 6 | - alert: service_down 7 | expr: up == 0 8 | for: 2m 9 | labels: 10 | severity: page 11 | annotations: 12 | summary: "Instance {{ $labels.instance }} down" 13 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 2 minutes." 14 | 15 | - alert: high_load 16 | expr: node_load1 > 0.5 17 | for: 2m 18 | labels: 19 | severity: page 20 | annotations: 21 | summary: "Instance {{ $labels.instance }} under high load" 22 | description: "{{ $labels.instance }} of job {{ $labels.job }} is under high load." -------------------------------------------------------------------------------- /hack/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 4 | evaluation_interval: 15s # By default, scrape targets every 15 seconds. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Attach these labels to any time series or alerts when communicating with 8 | # external systems (federation, remote storage, Alertmanager). 9 | external_labels: 10 | monitor: 'scaleway_exporter' 11 | 12 | # Load and evaluate rules in this file every 'evaluation_interval' seconds. 13 | rule_files: 14 | - 'alert.rules' 15 | 16 | # alert 17 | alerting: 18 | alertmanagers: 19 | - scheme: http 20 | static_configs: 21 | - targets: 22 | - "alertmanager:9093" 23 | 24 | # A scrape configuration containing exactly one endpoint to scrape: 25 | # Here it's Prometheus itself. 26 | scrape_configs: 27 | # The job name is added as a label `job=` to any timeseries scraped from this config. 28 | 29 | - job_name: 'prometheus' 30 | 31 | # Override the global default and scrape targets from this job every 5 seconds. 32 | scrape_interval: 5s 33 | static_configs: 34 | - targets: ['localhost:9090'] 35 | 36 | 37 | - job_name: 'scaleway' 38 | 39 | # Override the global default and scrape targets from this job every 5 seconds. 40 | scrape_interval: 60s 41 | static_configs: 42 | - targets: ['exporter:9503'] -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "runtime" 7 | "time" 8 | 9 | arg "github.com/alexflint/go-arg" 10 | "github.com/go-kit/log" 11 | "github.com/go-kit/log/level" 12 | "github.com/joho/godotenv" 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/collectors" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | "github.com/scaleway/scaleway-sdk-go/scw" 17 | "github.com/yoannma/scaleway_exporter/collector" 18 | ) 19 | 20 | var ( 21 | // Version of this binary. 22 | Version string //nolint:gochecknoglobals // LDFlags 23 | 24 | // Revision or Commit this binary was built from. 25 | Revision string //nolint: gochecknoglobals // LDFlags 26 | 27 | // BuildDate this binary was built. 28 | BuildDate string //nolint:gochecknoglobals // LDFlags 29 | 30 | // GoVersion running this binary. 31 | GoVersion = runtime.Version() //nolint:gochecknoglobals // LDFlags 32 | 33 | // StartTime has the time this was started. 34 | StartTime = time.Now() //nolint:gochecknoglobals // LDFlags 35 | ) 36 | 37 | // Config gets its content from env and passes it on to different packages. 38 | type Config struct { 39 | Debug bool `arg:"env:DEBUG"` 40 | ScalewayAccessKey string `arg:"env:SCALEWAY_ACCESS_KEY"` 41 | ScalewaySecretKey string `arg:"env:SCALEWAY_SECRET_KEY"` 42 | ScalewayRegion scw.Region `arg:"env:SCALEWAY_REGION"` 43 | ScalewayZone scw.Zone `arg:"env:SCALEWAY_ZONE"` 44 | ScalewayOrganizationID string `arg:"env:SCALEWAY_ORGANIZATION_ID"` 45 | HTTPTimeout int `arg:"env:HTTP_TIMEOUT"` 46 | WebAddr string `arg:"env:WEB_ADDR"` 47 | WebPath string `arg:"env:WEB_PATH"` 48 | DisableBillingCollector bool `arg:"--disable-billing-collector"` 49 | DisableBucketCollector bool `arg:"--disable-bucket-collector"` 50 | DisableDatabaseCollector bool `arg:"--disable-database-collector"` 51 | DisableLoadBalancerCollector bool `arg:"--disable-loadbalancer-collector"` 52 | DisableRedisCollector bool `arg:"--disable-redis-collector"` 53 | } 54 | 55 | func main() { 56 | _ = godotenv.Load() 57 | 58 | c := Config{ 59 | HTTPTimeout: 5000, 60 | WebPath: "/metrics", 61 | WebAddr: ":9503", 62 | DisableBillingCollector: false, 63 | DisableBucketCollector: false, 64 | DisableDatabaseCollector: false, 65 | DisableLoadBalancerCollector: false, 66 | } 67 | arg.MustParse(&c) 68 | 69 | filterOption := level.AllowInfo() 70 | if c.Debug { 71 | filterOption = level.AllowDebug() 72 | } 73 | 74 | logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 75 | logger = level.NewFilter(logger, filterOption) 76 | logger = log.With(logger, 77 | "ts", log.DefaultTimestampUTC, 78 | "caller", log.DefaultCaller, 79 | ) 80 | 81 | if c.ScalewayAccessKey == "" { 82 | _ = level.Error(logger).Log("msg", "Scaleway Access Key is required") 83 | os.Exit(1) 84 | } 85 | 86 | if c.ScalewaySecretKey == "" { 87 | _ = level.Error(logger).Log("msg", "Scaleway Secret Key is required") 88 | os.Exit(1) 89 | } 90 | 91 | var regions []scw.Region 92 | if c.ScalewayRegion == "" { 93 | _ = level.Info(logger).Log("msg", "Scaleway Region is set to ALL") 94 | regions = scw.AllRegions 95 | } else { 96 | regions = []scw.Region{c.ScalewayRegion} 97 | } 98 | 99 | var zones []scw.Zone 100 | if c.ScalewayZone == "" { 101 | _ = level.Info(logger).Log("msg", "Scaleway Zone is set to ALL") 102 | zones = scw.AllZones 103 | } else { 104 | zones = []scw.Zone{c.ScalewayZone} 105 | } 106 | 107 | _ = level.Info(logger).Log( 108 | "msg", "starting scaleway_exporter", 109 | "version", Version, 110 | "revision", Revision, 111 | "buildDate", BuildDate, 112 | "goVersion", GoVersion, 113 | ) 114 | 115 | client, err := scw.NewClient( 116 | // Get your credentials at https://console.scaleway.com/account/credentials 117 | scw.WithDefaultRegion(regions[0]), 118 | scw.WithAuth(c.ScalewayAccessKey, c.ScalewaySecretKey), 119 | ) 120 | 121 | if err != nil { 122 | _ = level.Error(logger).Log("msg", "Scaleway client initialization error", "err", err) 123 | os.Exit(1) 124 | } 125 | 126 | timeout := time.Duration(c.HTTPTimeout) * time.Millisecond 127 | 128 | errors := prometheus.NewCounterVec(prometheus.CounterOpts{ 129 | Name: "scaleway_errors_total", 130 | Help: "The total number of errors per collector", 131 | }, []string{"collector"}) 132 | 133 | r := prometheus.NewRegistry() 134 | r.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) 135 | r.MustRegister(collectors.NewGoCollector()) 136 | r.MustRegister(errors) 137 | r.MustRegister(collector.NewExporterCollector(logger, Version, Revision, BuildDate, GoVersion, StartTime)) 138 | 139 | if !c.DisableBillingCollector && c.ScalewayOrganizationID != "" { 140 | r.MustRegister(collector.NewBillingCollector(logger, errors, client, timeout, c.ScalewayOrganizationID)) 141 | } 142 | 143 | if !c.DisableBucketCollector { 144 | r.MustRegister(collector.NewBucketCollector(logger, errors, client, timeout, regions)) 145 | } 146 | 147 | if !c.DisableDatabaseCollector { 148 | r.MustRegister(collector.NewDatabaseCollector(logger, errors, client, timeout, regions)) 149 | } 150 | 151 | if !c.DisableLoadBalancerCollector { 152 | r.MustRegister(collector.NewLoadBalancerCollector(logger, errors, client, timeout, zones)) 153 | } 154 | 155 | if !c.DisableRedisCollector { 156 | r.MustRegister(collector.NewRedisCollector(logger, errors, client, timeout, zones)) 157 | } 158 | 159 | http.Handle(c.WebPath, promhttp.HandlerFor(r, promhttp.HandlerOpts{})) 160 | 161 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 162 | _, _ = w.Write([]byte(` 163 | Scaleway Exporter 164 | 165 |

Scaleway Exporter

166 |

Metrics

167 | 168 | `)) 169 | }) 170 | 171 | _ = level.Info(logger).Log("msg", "listening", "addr", c.WebAddr) 172 | 173 | server := &http.Server{ 174 | Addr: c.WebAddr, 175 | ReadHeaderTimeout: 5 * time.Second, 176 | } 177 | 178 | err = server.ListenAndServe() 179 | 180 | if err != nil { 181 | _ = level.Error(logger).Log("msg", "http ListenAndServe error", "err", err) 182 | 183 | os.Exit(1) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /scaleway.env.default: -------------------------------------------------------------------------------- 1 | SCALEWAY_ACCESS_KEY= 2 | SCALEWAY_SECRET_KEY= 3 | SCALEWAY_REGION= 4 | SCALEWAY_ZONE= --------------------------------------------------------------------------------