├── .dockerignore ├── .github ├── RELEASE ├── renovate.json ├── semantic.yml ├── workflows │ ├── kustomize.yml │ ├── docs.yml │ ├── automerge.yml │ ├── flake.yml │ ├── general.yml │ ├── release.yml │ ├── binaries.yml │ ├── tools.yml │ ├── changes.yml │ └── docker.yml └── settings.yml ├── cmd └── prometheus-hcloud-sd │ ├── README.md │ └── main.go ├── docs ├── .gitignore ├── layouts │ ├── shortcodes │ │ └── partial.html │ ├── index.html │ └── partials │ │ └── style.html ├── static │ ├── service-discovery.png │ └── syntax.css ├── archetypes │ └── default.md ├── content │ ├── license.md │ ├── building.md │ ├── kubernetes.md │ └── usage.md ├── partials │ ├── labels.md │ └── envvars.md └── hugo.toml ├── packaging ├── systemd │ ├── server.env │ └── server.service ├── scripts │ ├── preremove.sh │ ├── postremove.sh │ ├── preinstall.sh │ └── postinstall.sh └── config │ └── config.yaml ├── .codacy.yml ├── artifacthub-repo.yml ├── .gitignore ├── pkg ├── middleware │ ├── realip.go │ ├── cache.go │ ├── timeout.go │ └── recoverer.go ├── command │ ├── setup_test.go │ ├── setup.go │ ├── command.go │ ├── health.go │ └── server.go ├── version │ ├── version.go │ └── collector.go ├── action │ ├── metrics.go │ ├── server.go │ └── discoverer.go ├── config │ └── config.go └── adapter │ └── adapter.go ├── .editorconfig ├── deploy └── kubernetes │ ├── service.yml │ ├── servicemonitor.yml │ ├── kustomization.yml │ └── deployment.yml ├── config ├── example.yaml └── example.json ├── revive.toml ├── hack ├── generate-labels-docs.go └── generate-envvars-docs.go ├── .devcontainer └── devcontainer.json ├── Dockerfile ├── DCO ├── Taskfile.yml ├── flake.nix ├── .goreleaser.yaml ├── README.md ├── .releaserc ├── CONTRIBUTING.md ├── flake.lock ├── LICENSE └── go.mod /.dockerignore: -------------------------------------------------------------------------------- 1 | .devenv/ 2 | .direnv/ 3 | -------------------------------------------------------------------------------- /.github/RELEASE: -------------------------------------------------------------------------------- 1 | Mon Dec 22 08:20:06 UTC 2025 2 | -------------------------------------------------------------------------------- /cmd/prometheus-hcloud-sd/README.md: -------------------------------------------------------------------------------- 1 | # prometheus-hcloud-sd 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | public/ 2 | resources/ 3 | 4 | .hugo_build.lock 5 | -------------------------------------------------------------------------------- /docs/layouts/shortcodes/partial.html: -------------------------------------------------------------------------------- 1 | {{- $file := printf "/partials/%s" (.Get 0) -}}{{- $file | readFile | markdownify -}} -------------------------------------------------------------------------------- /packaging/systemd/server.env: -------------------------------------------------------------------------------- 1 | PROMETHEUS_HCLOUD_CONFIG="/etc/prometheus-hcloud-sd/config.yaml" 2 | PROMETHEUS_HCLOUD_ARGS= 3 | -------------------------------------------------------------------------------- /docs/static/service-discovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/promhippie/prometheus-hcloud-sd/HEAD/docs/static/service-discovery.png -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - .github/** 4 | - .bingo/** 5 | - changelog/** 6 | - docs/** 7 | 8 | - CHANGELOG.md 9 | 10 | ... 11 | -------------------------------------------------------------------------------- /artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | repositoryID: 9e64e9c6-9507-40a7-a085-d54ce0aced64 3 | owners: 4 | - name: tboerger 5 | email: thomas@webhippie.de 6 | 7 | ... 8 | -------------------------------------------------------------------------------- /packaging/scripts/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | systemctl stop prometheus-hcloud-sd.service || true 5 | systemctl disable prometheus-hcloud-sd.service || true 6 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>promhippie/.github//renovate/preset" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv/ 2 | .devenv/ 3 | .task/ 4 | 5 | .envrc 6 | coverage.out 7 | 8 | /bin 9 | /dist 10 | 11 | .pre-commit-config.yaml 12 | 13 | hcloud.json 14 | config.json 15 | -------------------------------------------------------------------------------- /docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .TranslationBaseName "-" " " | title }}" 3 | date: {{ .Date }} 4 | anchor: "{{ replace .TranslationBaseName "-" " " | title | urlize }}" 5 | weight: 6 | --- 7 | -------------------------------------------------------------------------------- /packaging/scripts/postremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ ! -d /var/lib/prometheus-hcloud-sd ] && [ ! -d /etc/prometheus-hcloud-sd ]; then 5 | userdel prometheus-hcloud-sd 2>/dev/null || true 6 | groupdel prometheus-hcloud-sd 2>/dev/null || true 7 | fi 8 | -------------------------------------------------------------------------------- /pkg/middleware/realip.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5/middleware" 7 | ) 8 | 9 | // RealIP just wraps the go-chi realip middleware. 10 | func RealIP(next http.Handler) http.Handler { 11 | return middleware.RealIP(next) 12 | } 13 | -------------------------------------------------------------------------------- /packaging/config/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | server: 3 | addr: 0.0.0.0:9000 4 | path: /metrics 5 | web_config: ~ 6 | 7 | logs: 8 | level: error 9 | pretty: false 10 | 11 | target: 12 | engine: http 13 | file: /var/lib/prometheus-hcloud-sd/output.json 14 | refresh: 30 15 | credentials: [] 16 | 17 | ... 18 | -------------------------------------------------------------------------------- /pkg/command/setup_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSetupLogger(t *testing.T) { 11 | logger := setupLogger(config.Load()) 12 | assert.NotNil(t, logger) 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [Makefile] 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | [*.go] 15 | indent_style = tab 16 | indent_size = 4 17 | 18 | [*.md] 19 | trim_trailing_whitespace = true 20 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | commitsOnly: true 3 | anyCommit: true 4 | allowMergeCommits: true 5 | allowRevertCommits: true 6 | 7 | types: 8 | - feat 9 | - fix 10 | - docs 11 | - style 12 | - refactor 13 | - perf 14 | - test 15 | - build 16 | - ci 17 | - chore 18 | - revert 19 | - major 20 | - minor 21 | - patch 22 | - deps 23 | 24 | ... 25 | -------------------------------------------------------------------------------- /docs/content/license.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "License" 3 | date: 2022-07-20T00:00:00+00:00 4 | anchor: "license" 5 | weight: 40 6 | --- 7 | 8 | This project is licensed under the [Apache 2.0][license] license. For the 9 | license of the used libraries you have to check the respective sources. 10 | 11 | [license]: https://github.com/promhippie/prometheus-hcloud-sd/blob/master/LICENSE 12 | -------------------------------------------------------------------------------- /cmd/prometheus-hcloud-sd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/joho/godotenv" 7 | "github.com/promhippie/prometheus-hcloud-sd/pkg/command" 8 | ) 9 | 10 | func main() { 11 | if env := os.Getenv("PROMETHEUS_HCLOUD_ENV_FILE"); env != "" { 12 | _ = godotenv.Load(env) 13 | } 14 | 15 | if err := command.Run(); err != nil { 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packaging/scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if ! getent group prometheus-hcloud-sd >/dev/null 2>&1; then 5 | groupadd --system prometheus-hcloud-sd 6 | fi 7 | 8 | if ! getent passwd prometheus-hcloud-sd >/dev/null 2>&1; then 9 | useradd --system --create-home --home-dir /var/lib/prometheus-hcloud-sd --shell /bin/bash -g prometheus-hcloud-sd prometheus-hcloud-sd 10 | fi 11 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | var ( 8 | // String gets defined by the build system. 9 | String = "0.0.0-dev" 10 | 11 | // Revision indicates the commit this binary was built from. 12 | Revision string 13 | 14 | // Date indicates the date this binary was built. 15 | Date string 16 | 17 | // Go running this binary. 18 | Go = runtime.Version() 19 | ) 20 | -------------------------------------------------------------------------------- /deploy/kubernetes/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | 5 | metadata: 6 | name: prometheus-hcloud-sd 7 | labels: 8 | app.kubernetes.io/name: prometheus-hcloud-sd 9 | app.kubernetes.io/component: server 10 | 11 | spec: 12 | selector: 13 | app.kubernetes.io/name: prometheus-hcloud-sd 14 | app.kubernetes.io/component: server 15 | 16 | ports: 17 | - name: http 18 | port: 9000 19 | targetPort: http 20 | protocol: TCP 21 | 22 | ... 23 | -------------------------------------------------------------------------------- /packaging/scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | chown -R prometheus-hcloud-sd:prometheus-hcloud-sd /etc/prometheus-hcloud-sd 5 | chown -R prometheus-hcloud-sd:prometheus-hcloud-sd /var/lib/prometheus-hcloud-sd 6 | chmod 750 /var/lib/prometheus-hcloud-sd 7 | 8 | if [ -d /run/systemd/system ]; then 9 | systemctl daemon-reload 10 | 11 | if systemctl is-enabled --quiet prometheus-hcloud-sd.service; then 12 | systemctl restart prometheus-hcloud-sd.service 13 | fi 14 | fi 15 | -------------------------------------------------------------------------------- /deploy/kubernetes/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | 5 | metadata: 6 | name: prometheus-hcloud-sd 7 | labels: 8 | app.kubernetes.io/name: prometheus-hcloud-sd 9 | app.kubernetes.io/component: server 10 | 11 | spec: 12 | endpoints: 13 | - interval: 60s 14 | port: http 15 | scheme: http 16 | path: /metrics 17 | 18 | selector: 19 | matchLabels: 20 | app.kubernetes.io/name: prometheus-hcloud-sd 21 | app.kubernetes.io/component: server 22 | 23 | ... 24 | -------------------------------------------------------------------------------- /pkg/middleware/cache.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // Cache writes required cache headers to all requests. 9 | func Cache(next http.Handler) http.Handler { 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") 12 | w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") 13 | w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 14 | 15 | next.ServeHTTP(w, r) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /config/example.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | server: 3 | addr: 0.0.0.0:9000 4 | path: /metrics 5 | web_config: 6 | 7 | logs: 8 | level: error 9 | pretty: false 10 | 11 | target: 12 | engine: file 13 | file: /etc/prometheus/hcloud.json 14 | refresh: 30 15 | credentials: 16 | - project: example1 17 | token: uAgX6TkZVGx7c94jPAff5cfJdym9MLekiveDgN7Oq5dyOXxl4Uu9qkpcC1muILGW 18 | - project: example2 19 | token: UhgX6TkZVGx7c94jPAff5cfJdyc9MLekiveDgN7Oq5dyOXxl4Uu9qkpcD1muILGW 20 | - project: example3 21 | token: aBgX6TkZVGx7c94jPAff5cfJdym9MLekivdDgN7Oq5dyOXxl4Uu9qkpcU1muILGW 22 | 23 | ... 24 | -------------------------------------------------------------------------------- /pkg/middleware/timeout.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // Timeout just copies the go-chi timeout middleware. 10 | func Timeout(next http.Handler) http.Handler { 11 | fn := func(w http.ResponseWriter, r *http.Request) { 12 | ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) 13 | 14 | defer func() { 15 | cancel() 16 | if ctx.Err() == context.DeadlineExceeded { 17 | w.WriteHeader(http.StatusGatewayTimeout) 18 | } 19 | }() 20 | 21 | next.ServeHTTP(w, r.WithContext(ctx)) 22 | } 23 | 24 | return http.HandlerFunc(fn) 25 | } 26 | -------------------------------------------------------------------------------- /packaging/systemd/server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus Hetzner Cloud SD 3 | Documentation=https://promhippie.github.io/prometheus-hcloud-sd/ 4 | 5 | Requires=network.target 6 | After=network.target 7 | 8 | [Service] 9 | Type=simple 10 | User=prometheus-hcloud-sd 11 | Group=prometheus-hcloud-sd 12 | EnvironmentFile=-/etc/default/prometheus-hcloud-sd 13 | EnvironmentFile=-/etc/sysconfig/prometheus-hcloud-sd 14 | ExecStart=/usr/bin/prometheus-hcloud-sd server $PROMETHEUS_HCLOUD_ARGS 15 | WorkingDirectory=/var/lib/prometheus-hcloud-sd 16 | Restart=always 17 | LimitNOFILE=65536 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /pkg/version/collector.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | // Collector simply exports the version information for Prometheus. 8 | func Collector(ns string) *prometheus.GaugeVec { 9 | info := prometheus.NewGaugeVec( 10 | prometheus.GaugeOpts{ 11 | Namespace: ns, 12 | Name: "build_info", 13 | Help: "A metric with a constant '1' value labeled by version, revision and goversion from which it was built.", 14 | }, 15 | []string{"version", "revision", "goversion"}, 16 | ) 17 | 18 | info.WithLabelValues(String, Revision, Go).Set(1) 19 | return info 20 | } 21 | -------------------------------------------------------------------------------- /docs/partials/labels.md: -------------------------------------------------------------------------------- 1 | * `__address__` 2 | * `__meta_hcloud_city` 3 | * `__meta_hcloud_cores` 4 | * `__meta_hcloud_country` 5 | * `__meta_hcloud_cpu` 6 | * `__meta_hcloud_disk` 7 | * `__meta_hcloud_image_name` 8 | * `__meta_hcloud_image_type` 9 | * `__meta_hcloud_ipv4_` 10 | * `__meta_hcloud_label_` 11 | * `__meta_hcloud_location` 12 | * `__meta_hcloud_memory` 13 | * `__meta_hcloud_name` 14 | * `__meta_hcloud_os_flavor` 15 | * `__meta_hcloud_os_version` 16 | * `__meta_hcloud_project` 17 | * `__meta_hcloud_public_ipv4` 18 | * `__meta_hcloud_public_ipv6` 19 | * `__meta_hcloud_status` 20 | * `__meta_hcloud_storage` 21 | * `__meta_hcloud_type` 22 | -------------------------------------------------------------------------------- /docs/hugo.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://promhippie.github.io/prometheus-hcloud-sd/" 2 | languageCode = "en-us" 3 | title = "Prometheus Hetzner Cloud SD" 4 | pygmentsUseClasses = true 5 | 6 | disableKinds = ["RSS", "sitemap", "taxonomy", "term", "section"] 7 | enableRobotsTXT = true 8 | 9 | [markup.goldmark.renderer] 10 | unsafe = true 11 | 12 | [blackfriday] 13 | angledQuotes = true 14 | fractions = false 15 | plainIDAnchors = true 16 | smartlists = true 17 | extensions = ["hardLineBreak"] 18 | 19 | [params] 20 | author = "Thomas Boerger" 21 | description = "Prometheus service discovery for Hetzner Cloud" 22 | keywords = "prometheus, sd, service, discovery, hetzner, cloud" 23 | -------------------------------------------------------------------------------- /deploy/kubernetes/kustomization.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | resources: 6 | - servicemonitor.yml 7 | - service.yml 8 | - deployment.yml 9 | 10 | configMapGenerator: 11 | - name: prometheus-hcloud-sd 12 | literals: 13 | - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=http 14 | - PROMETHEUS_HCLOUD_OUTPUT_FILE=/etc/prometheus/hcloud.json 15 | 16 | secretGenerator: 17 | - name: prometheus-hcloud-sd 18 | literals: [] 19 | - name: prometheus-hcloud-files 20 | literals: [] 21 | 22 | images: 23 | - name: prometheus-hcloud-sd 24 | newName: quay.io/promhippie/prometheus-hcloud-sd 25 | newTag: 2.5.0 26 | 27 | ... 28 | -------------------------------------------------------------------------------- /revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.range] 20 | [rule.receiver-naming] 21 | [rule.time-naming] 22 | [rule.unexported-return] 23 | [rule.indent-error-flow] 24 | [rule.errorf] 25 | [rule.empty-block] 26 | [rule.superfluous-else] 27 | [rule.unused-parameter] 28 | [rule.unreachable-code] 29 | [rule.redefines-builtin-id] 30 | 31 | [rule.package-comments] 32 | Disabled = true 33 | 34 | -------------------------------------------------------------------------------- /pkg/middleware/recoverer.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "runtime/debug" 7 | ) 8 | 9 | // Recoverer initializes a recoverer middleware. 10 | func Recoverer(logger *slog.Logger) func(next http.Handler) http.Handler { 11 | return func(next http.Handler) http.Handler { 12 | fn := func(w http.ResponseWriter, r *http.Request) { 13 | defer func() { 14 | if rvr := recover(); rvr != nil { 15 | logger.Error(rvr.(string), 16 | "trace", string(debug.Stack()), 17 | ) 18 | 19 | http.Error( 20 | w, 21 | http.StatusText(http.StatusInternalServerError), 22 | http.StatusInternalServerError, 23 | ) 24 | } 25 | }() 26 | 27 | next.ServeHTTP(w, r) 28 | } 29 | 30 | return http.HandlerFunc(fn) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/kustomize.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: kustomize 4 | 5 | "on": 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - master 10 | push: 11 | branches: 12 | - master 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | generate: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout source 23 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 24 | 25 | - name: Generate manifest 26 | uses: actionhippie/kustomize@13bdb1f276d257f30bbf227ced665a66eac9fabb # v3 27 | with: 28 | version: 5.7.0 29 | path: deploy/kubernetes/ 30 | target: deploy/kubernetes/bundle.yml 31 | 32 | ... 33 | -------------------------------------------------------------------------------- /hack/generate-labels-docs.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/promhippie/prometheus-hcloud-sd/pkg/action" 13 | ) 14 | 15 | func main() { 16 | labels := []string{} 17 | 18 | for _, label := range action.Labels { 19 | labels = append(labels, label) 20 | } 21 | 22 | sort.Strings(labels) 23 | 24 | f, err := os.Create("docs/partials/labels.md") 25 | 26 | if err != nil { 27 | fmt.Printf("failed to create file") 28 | os.Exit(1) 29 | } 30 | 31 | defer f.Close() 32 | 33 | f.WriteString( 34 | "* `__address__`\n", 35 | ) 36 | 37 | for _, row := range labels { 38 | if strings.HasSuffix(row, "_") { 39 | row = row + "" 40 | } 41 | 42 | f.WriteString(fmt.Sprintf( 43 | "* `%s`\n", 44 | row, 45 | )) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/content/building.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Building" 3 | date: 2022-07-20T00:00:00+00:00 4 | anchor: "building" 5 | weight: 20 6 | --- 7 | 8 | As this project is built with Go you need to install Go first. If you are not 9 | familiar with [Nix][nix] it is up to you to have a working environment for Go 10 | (>= 1.24.0) as the setup won't we covered within this guide. Please follow the 11 | official install instructions for [Go][golang]. Beside that we are using 12 | [go-task][gotask] to define all commands to build this project. 13 | 14 | {{< highlight txt >}} 15 | git clone https://github.com/promhippie/prometheus-hcloud-sd.git 16 | cd prometheus-hcloud-sd 17 | 18 | task generate build 19 | ./bin/prometheus-hcloud-sd -h 20 | {{< / highlight >}} 21 | 22 | [nix]: https://nixos.org/ 23 | [golang]: http://golang.org/doc/install.html 24 | [gotask]: https://taskfile.dev/installation/ 25 | -------------------------------------------------------------------------------- /config/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "addr": "0.0.0.0:9000", 4 | "path": "/metrics", 5 | "web_config": "" 6 | }, 7 | "logs": { 8 | "level": "error", 9 | "pretty": false 10 | }, 11 | "target": { 12 | "engine": "file", 13 | "file": "/etc/prometheus/hcloud.json", 14 | "refresh": 30, 15 | "credentials": [ 16 | { 17 | "project": "example1", 18 | "token": "uAgX6TkZVGx7c94jPAff5cfJdym9MLekiveDgN7Oq5dyOXxl4Uu9qkpcC1muILGW" 19 | }, 20 | { 21 | "project": "example2", 22 | "token": "UhgX6TkZVGx7c94jPAff5cfJdyc9MLekiveDgN7Oq5dyOXxl4Uu9qkpcD1muILGW" 23 | }, 24 | { 25 | "project": "example3", 26 | "token": "aBgX6TkZVGx7c94jPAff5cfJdym9MLekivdDgN7Oq5dyOXxl4Uu9qkpcU1muILGW" 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/partials/envvars.md: -------------------------------------------------------------------------------- 1 | PROMETHEUS_HCLOUD_LOG_LEVEL 2 | : Only log messages with given severity, defaults to `info` 3 | 4 | PROMETHEUS_HCLOUD_LOG_PRETTY 5 | : Enable pretty messages for logging, defaults to `false` 6 | 7 | PROMETHEUS_HCLOUD_WEB_ADDRESS 8 | : Address to bind the metrics server, defaults to `0.0.0.0:9000` 9 | 10 | PROMETHEUS_HCLOUD_WEB_PATH 11 | : Path to bind the metrics server, defaults to `/metrics` 12 | 13 | PROMETHEUS_HCLOUD_WEB_CONFIG 14 | : Path to web-config file 15 | 16 | PROMETHEUS_HCLOUD_OUTPUT_ENGINE 17 | : Enabled engine like file or http, defaults to `file` 18 | 19 | PROMETHEUS_HCLOUD_OUTPUT_FILE 20 | : Path to write the file_sd config, defaults to `/etc/prometheus/hcloud.json` 21 | 22 | PROMETHEUS_HCLOUD_OUTPUT_REFRESH 23 | : Discovery refresh interval in seconds, defaults to `30` 24 | 25 | PROMETHEUS_HCLOUD_TOKEN 26 | : Access token for the HetznerCloud API 27 | 28 | PROMETHEUS_HCLOUD_CONFIG 29 | : Path to HetznerCloud configuration file 30 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json", 3 | 4 | "name": "Ubuntu", 5 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", 6 | 7 | "features": { 8 | "ghcr.io/devcontainers/features/common-utils:2": { 9 | "configureZshAsDefaultShell": true 10 | }, 11 | "ghcr.io/devcontainers/features/nix:1": { 12 | "extraNixConfig": "experimental-features = nix-command flakes" 13 | }, 14 | "ghcr.io/christophermacgown/devcontainer-features/direnv:1": { 15 | "version": "latest" 16 | } 17 | }, 18 | 19 | "customizations": { 20 | "vscode": { 21 | "settings": {}, 22 | "extensions": [] 23 | } 24 | }, 25 | 26 | "portsAttributes": { 27 | "9000": { 28 | "label": "Server" 29 | } 30 | }, 31 | 32 | "postCreateCommand": "(test -f .envrc || echo 'use flake . --impure' >> .envrc) && direnv allow" 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: docs 4 | 5 | "on": 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - master 10 | push: 11 | branches: 12 | - master 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | docs: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout source 23 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 24 | 25 | - name: Setup hugo 26 | uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3 27 | with: 28 | hugo-version: latest 29 | extended: true 30 | 31 | - name: Setup task 32 | uses: arduino/setup-task@v2 33 | with: 34 | version: 3.x 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Run docs 38 | run: task docs 39 | 40 | - name: Deploy pages 41 | if: github.event_name != 'pull_request' 42 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4 43 | with: 44 | github_token: ${{ secrets.GITHUB_TOKEN }} 45 | publish_dir: docs/public/ 46 | 47 | ... 48 | -------------------------------------------------------------------------------- /docs/layouts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ .Site.Title }} 10 | 11 | 12 | 13 | 14 | 15 | {{ partial "style.html" . }} 16 | 17 | 18 | 19 | 38 | 39 | {{ range .Data.Pages.ByWeight }} 40 |
41 |

42 | 43 | {{ .Title }} 44 | 45 | 46 | 47 | 48 | Back to Top 49 | 50 | 51 |

52 | 53 | {{ .Content | markdownify }} 54 |
55 | {{ end }} 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: automerge 4 | 5 | "on": 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | 15 | jobs: 16 | dependabot: 17 | runs-on: ubuntu-latest 18 | if: github.actor == 'dependabot[bot]' 19 | 20 | steps: 21 | - name: Generate token 22 | id: token 23 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 24 | with: 25 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 26 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 27 | permission-contents: write 28 | permission-pull-requests: write 29 | permission-issues: write 30 | 31 | - name: Fetch metadata 32 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Approve request 37 | run: gh pr review --approve "${{github.event.pull_request.html_url}}" 38 | env: 39 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Enable automerge 42 | run: gh pr merge --rebase --auto "${{github.event.pull_request.html_url}}" 43 | env: 44 | GH_TOKEN: ${{ steps.token.outputs.token }} 45 | 46 | ... 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.25.5-alpine3.21@sha256:b4dbd292a0852331c89dfd64e84d16811f3e3aae4c73c13d026c4d200715aff6 AS builder 2 | 3 | RUN apk add --no-cache -U git curl 4 | RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin 5 | 6 | WORKDIR /go/src/prometheus-hcloud-sd 7 | COPY . /go/src/prometheus-hcloud-sd/ 8 | 9 | RUN --mount=type=cache,target=/go/pkg \ 10 | go mod download -x 11 | 12 | ARG TARGETOS 13 | ARG TARGETARCH 14 | 15 | RUN --mount=type=cache,target=/go/pkg \ 16 | --mount=type=cache,target=/root/.cache/go-build \ 17 | task generate build GOOS=${TARGETOS} GOARCH=${TARGETARCH} 18 | 19 | FROM alpine:3.23@sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62 20 | 21 | RUN apk add --no-cache ca-certificates mailcap && \ 22 | addgroup -g 1337 prometheus-hcloud-sd && \ 23 | adduser -D -u 1337 -h /var/lib/prometheus-hcloud-sd -G prometheus-hcloud-sd prometheus-hcloud-sd 24 | 25 | EXPOSE 9000 26 | VOLUME ["/var/lib/prometheus-hcloud-sd"] 27 | ENTRYPOINT ["/usr/bin/prometheus-hcloud-sd"] 28 | CMD ["server"] 29 | HEALTHCHECK CMD ["/usr/bin/prometheus-hcloud-sd", "health"] 30 | 31 | ENV PROMETHEUS_HCLOUD_OUTPUT_ENGINE="http" 32 | ENV PROMETHEUS_HCLOUD_OUTPUT_FILE="/var/lib/prometheus-hcloud-sd/output.json" 33 | 34 | COPY --from=builder /go/src/prometheus-hcloud-sd/bin/prometheus-hcloud-sd /usr/bin/prometheus-hcloud-sd 35 | WORKDIR /var/lib/prometheus-hcloud-sd 36 | USER prometheus-hcloud-sd 37 | -------------------------------------------------------------------------------- /.github/workflows/flake.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: flake 4 | 5 | "on": 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * 1" 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | flake: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Generate token 19 | id: token 20 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 21 | with: 22 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 23 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 24 | permission-contents: write 25 | 26 | - name: Checkout source 27 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 28 | with: 29 | token: ${{ steps.token.outputs.token }} 30 | 31 | - name: Install nix 32 | uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31 33 | 34 | - name: Update flake 35 | run: nix flake update 36 | 37 | - name: Source rebase 38 | run: git pull --autostash --rebase 39 | 40 | - name: Commit changes 41 | uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9 42 | with: 43 | author_name: GitHub Actions 44 | author_email: github@webhippie.de 45 | add: flake.lock 46 | message: "chore(flake): updated lockfile [skip ci]" 47 | push: true 48 | commit: --signoff 49 | 50 | ... 51 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /pkg/action/metrics.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/collectors" 9 | "github.com/promhippie/prometheus-hcloud-sd/pkg/version" 10 | ) 11 | 12 | var ( 13 | registry = prometheus.NewRegistry() 14 | namespace = "prometheus_hcloud_sd" 15 | ) 16 | 17 | var ( 18 | requestDuration = prometheus.NewHistogramVec( 19 | prometheus.HistogramOpts{ 20 | Namespace: namespace, 21 | Name: "request_duration_seconds", 22 | Help: "Histogram of latencies for requests to the HetznerCloud API.", 23 | Buckets: []float64{0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0}, 24 | }, 25 | []string{"project"}, 26 | ) 27 | 28 | requestFailures = prometheus.NewCounterVec( 29 | prometheus.CounterOpts{ 30 | Namespace: namespace, 31 | Name: "request_failures_total", 32 | Help: "Total number of failed requests to the HetznerCloud API.", 33 | }, 34 | []string{"project"}, 35 | ) 36 | ) 37 | 38 | func init() { 39 | registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{ 40 | Namespace: namespace, 41 | })) 42 | 43 | registry.MustRegister(collectors.NewGoCollector()) 44 | registry.MustRegister(version.Collector(namespace)) 45 | 46 | registry.MustRegister(requestDuration) 47 | registry.MustRegister(requestFailures) 48 | } 49 | 50 | type promLogger struct { 51 | logger *slog.Logger 52 | } 53 | 54 | func (pl promLogger) Println(v ...interface{}) { 55 | pl.logger.Error(fmt.Sprintln(v...)) 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: general 4 | 5 | "on": 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - master 10 | push: 11 | branches: 12 | - master 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | testing: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout source 23 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 24 | 25 | - name: Setup golang 26 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 27 | with: 28 | go-version-file: go.mod 29 | 30 | - name: Setup task 31 | uses: arduino/setup-task@v2 32 | with: 33 | version: 3.x 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Run generate 37 | run: task generate 38 | 39 | - name: Run vet 40 | run: task vet 41 | 42 | - name: Server golangci 43 | run: task golangci 44 | 45 | - name: Run lint 46 | run: task lint 47 | 48 | - name: Run test 49 | run: task test 50 | 51 | - name: Run build 52 | run: task build 53 | 54 | - name: Coverage report 55 | if: github.event_name != 'pull_request' 56 | uses: codacy/codacy-coverage-reporter-action@89d6c85cfafaec52c72b6c5e8b2878d33104c699 # v1 57 | with: 58 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 59 | coverage-reports: coverage.out 60 | force-coverage-parser: go 61 | 62 | ... 63 | -------------------------------------------------------------------------------- /pkg/command/setup.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log/slog" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | var ( 16 | // ErrConfigFormatInvalid defines the error if ext is unsupported. 17 | ErrConfigFormatInvalid = errors.New("config extension is not supported") 18 | ) 19 | 20 | func setupLogger(cfg *config.Config) *slog.Logger { 21 | if cfg.Logs.Pretty { 22 | return slog.New( 23 | slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 24 | Level: loggerLevel(cfg), 25 | }), 26 | ) 27 | } 28 | 29 | return slog.New( 30 | slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 31 | Level: loggerLevel(cfg), 32 | }), 33 | ) 34 | } 35 | 36 | func loggerLevel(cfg *config.Config) slog.Leveler { 37 | switch strings.ToLower(cfg.Logs.Level) { 38 | case "error": 39 | return slog.LevelError 40 | case "warn": 41 | return slog.LevelWarn 42 | case "info": 43 | return slog.LevelInfo 44 | case "debug": 45 | return slog.LevelDebug 46 | } 47 | 48 | return slog.LevelInfo 49 | } 50 | 51 | func readConfig(file string, cfg *config.Config) error { 52 | if file == "" { 53 | return nil 54 | } 55 | 56 | content, err := os.ReadFile(file) 57 | 58 | if err != nil { 59 | return err 60 | } 61 | 62 | switch strings.ToLower(filepath.Ext(file)) { 63 | case ".yaml", ".yml": 64 | if err = yaml.Unmarshal(content, cfg); err != nil { 65 | return err 66 | } 67 | case ".json": 68 | if err = json.Unmarshal(content, cfg); err != nil { 69 | return err 70 | } 71 | default: 72 | return ErrConfigFormatInvalid 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /deploy/kubernetes/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | 5 | metadata: 6 | name: prometheus-hcloud-sd 7 | labels: 8 | app.kubernetes.io/name: prometheus-hcloud-sd 9 | app.kubernetes.io/component: exporter 10 | 11 | spec: 12 | replicas: 1 13 | 14 | revisionHistoryLimit: 3 15 | progressDeadlineSeconds: 600 16 | 17 | strategy: 18 | type: Recreate 19 | 20 | selector: 21 | matchLabels: 22 | app.kubernetes.io/name: prometheus-hcloud-sd 23 | app.kubernetes.io/component: server 24 | 25 | template: 26 | metadata: 27 | labels: 28 | app.kubernetes.io/name: prometheus-hcloud-sd 29 | app.kubernetes.io/component: server 30 | 31 | spec: 32 | restartPolicy: Always 33 | terminationGracePeriodSeconds: 30 34 | 35 | containers: 36 | - name: server 37 | image: prometheus-hcloud-sd 38 | imagePullPolicy: Always 39 | 40 | envFrom: 41 | - configMapRef: 42 | name: prometheus-hcloud-sd 43 | - secretRef: 44 | name: prometheus-hcloud-sd 45 | 46 | ports: 47 | - name: http 48 | containerPort: 9000 49 | protocol: TCP 50 | 51 | livenessProbe: 52 | httpGet: 53 | path: /healthz 54 | port: http 55 | 56 | readinessProbe: 57 | httpGet: 58 | path: /readyz 59 | port: http 60 | 61 | volumeMounts: 62 | - name: files 63 | mountPath: /etc/prometheus-hcloud-sd 64 | 65 | volumes: 66 | - name: files 67 | configMap: 68 | name: prometheus-hcloud-files 69 | 70 | ... 71 | -------------------------------------------------------------------------------- /pkg/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 8 | "github.com/promhippie/prometheus-hcloud-sd/pkg/version" 9 | "github.com/urfave/cli/v3" 10 | ) 11 | 12 | // Run parses the command line arguments and executes the program. 13 | func Run() error { 14 | cfg := config.Load() 15 | 16 | app := &cli.Command{ 17 | Name: "prometheus-hcloud-sd", 18 | Version: version.String, 19 | Usage: "Prometheus HetznerCloud SD", 20 | Authors: []any{ 21 | "Thomas Boerger ", 22 | }, 23 | Flags: RootFlags(cfg), 24 | Commands: []*cli.Command{ 25 | Health(cfg), 26 | Server(cfg), 27 | }, 28 | } 29 | 30 | cli.HelpFlag = &cli.BoolFlag{ 31 | Name: "help", 32 | Aliases: []string{"h"}, 33 | Usage: "Show the help, so what you see now", 34 | } 35 | 36 | cli.VersionFlag = &cli.BoolFlag{ 37 | Name: "version", 38 | Aliases: []string{"v"}, 39 | Usage: "Print the current version of that tool", 40 | } 41 | 42 | return app.Run(context.Background(), os.Args) 43 | } 44 | 45 | // RootFlags defines the available root flags. 46 | func RootFlags(cfg *config.Config) []cli.Flag { 47 | return []cli.Flag{ 48 | &cli.StringFlag{ 49 | Name: "log.level", 50 | Value: "info", 51 | Usage: "Only log messages with given severity", 52 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_LOG_LEVEL"), 53 | Destination: &cfg.Logs.Level, 54 | }, 55 | &cli.BoolFlag{ 56 | Name: "log.pretty", 57 | Value: false, 58 | Usage: "Enable pretty messages for logging", 59 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_LOG_PRETTY"), 60 | Destination: &cfg.Logs.Pretty, 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: release 4 | 5 | "on": 6 | schedule: 7 | - cron: "0 8 * * 1" 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Generate token 19 | id: token 20 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 21 | with: 22 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 23 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 24 | permission-contents: write 25 | permission-pull-requests: write 26 | permission-issues: write 27 | 28 | - name: Checkout source 29 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 30 | with: 31 | token: ${{ steps.token.outputs.token }} 32 | 33 | - name: Write buildtime 34 | run: date >| .github/RELEASE 35 | 36 | - name: Install releaser 37 | run: | 38 | npm install -g \ 39 | conventional-changelog-conventionalcommits@6.1.0 \ 40 | semantic-release@23.1.1 \ 41 | @semantic-release/changelog \ 42 | @semantic-release/git \ 43 | @semantic-release/github \ 44 | semantic-release-replace-plugin 45 | 46 | - name: Run releaser 47 | env: 48 | GITHUB_TOKEN: ${{ steps.token.outputs.token }} 49 | run: semantic-release 50 | 51 | - name: Source rebase 52 | run: git pull --autostash --rebase 53 | 54 | - name: Commit changes 55 | uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9 56 | with: 57 | author_name: GitHub Actions 58 | author_email: github@webhippie.de 59 | add: .github/RELEASE 60 | message: "docs: automated release update [skip ci]" 61 | push: true 62 | commit: --signoff 63 | 64 | ... 65 | -------------------------------------------------------------------------------- /pkg/command/health.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 9 | "github.com/urfave/cli/v3" 10 | ) 11 | 12 | // Health provides the sub-command to perform a health check. 13 | func Health(cfg *config.Config) *cli.Command { 14 | return &cli.Command{ 15 | Name: "health", 16 | Usage: "Perform health checks", 17 | Flags: HealthFlags(cfg), 18 | Action: func(_ context.Context, cmd *cli.Command) error { 19 | logger := setupLogger(cfg) 20 | 21 | if cmd.IsSet("hcloud.config") { 22 | if err := readConfig(cmd.String("hcloud.config"), cfg); err != nil { 23 | logger.Error("Failed to read config", 24 | "err", err, 25 | ) 26 | 27 | return err 28 | } 29 | } 30 | 31 | resp, err := http.Get( 32 | fmt.Sprintf( 33 | "http://%s/healthz", 34 | cfg.Server.Addr, 35 | ), 36 | ) 37 | 38 | if err != nil { 39 | logger.Error("Failed to request health check", 40 | "err", err, 41 | ) 42 | 43 | return err 44 | } 45 | 46 | defer func() { _ = resp.Body.Close() }() 47 | 48 | if resp.StatusCode != 200 { 49 | logger.Error("Health check seems to be in bad state", 50 | "err", err, 51 | "code", resp.StatusCode, 52 | ) 53 | 54 | return err 55 | } 56 | 57 | logger.Debug("Health check seems to be fine", 58 | "code", resp.StatusCode, 59 | ) 60 | 61 | return nil 62 | }, 63 | } 64 | } 65 | 66 | // HealthFlags defines the available health flags. 67 | func HealthFlags(cfg *config.Config) []cli.Flag { 68 | return []cli.Flag{ 69 | &cli.StringFlag{ 70 | Name: "web.address", 71 | Value: "0.0.0.0:9000", 72 | Usage: "Address to bind the metrics server", 73 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_WEB_ADDRESS"), 74 | Destination: &cfg.Server.Addr, 75 | }, 76 | &cli.StringFlag{ 77 | Name: "hcloud.config", 78 | Value: "", 79 | Usage: "Path to HetznerCloud configuration file", 80 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_CONFIG"), 81 | Destination: nil, 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // Credential defines a single project credential. 11 | type Credential struct { 12 | Project string `json:"project" yaml:"project"` 13 | Token string `json:"token" yaml:"token"` 14 | } 15 | 16 | // Server defines the general server configuration. 17 | type Server struct { 18 | Addr string `json:"addr" yaml:"addr"` 19 | Path string `json:"path" yaml:"path"` 20 | Web string `json:"web_config" yaml:"web_config"` 21 | } 22 | 23 | // Logs defines the level and color for log configuration. 24 | type Logs struct { 25 | Level string `json:"level" yaml:"level"` 26 | Pretty bool `json:"pretty" yaml:"pretty"` 27 | } 28 | 29 | // Target defines the target specific configuration. 30 | type Target struct { 31 | Engine string `json:"engine" yaml:"engine"` 32 | File string `json:"file" yaml:"file"` 33 | Refresh int `json:"refresh" yaml:"refresh"` 34 | Credentials []Credential `json:"credentials" yaml:"credentials"` 35 | } 36 | 37 | // Config is a combination of all available configurations. 38 | type Config struct { 39 | Server Server `json:"server" yaml:"server"` 40 | Logs Logs `json:"logs" yaml:"logs"` 41 | Target Target `json:"target" yaml:"target"` 42 | } 43 | 44 | // Load initializes a default configuration struct. 45 | func Load() *Config { 46 | return &Config{ 47 | Target: Target{ 48 | Credentials: make([]Credential, 0), 49 | }, 50 | } 51 | } 52 | 53 | // Value returns the config value based on a DSN. 54 | func Value(val string) (string, error) { 55 | if strings.HasPrefix(val, "file://") { 56 | content, err := os.ReadFile( 57 | strings.TrimPrefix(val, "file://"), 58 | ) 59 | 60 | if err != nil { 61 | return "", fmt.Errorf("failed to parse secret file: %w", err) 62 | } 63 | 64 | return string(content), nil 65 | } 66 | 67 | if strings.HasPrefix(val, "base64://") { 68 | content, err := base64.StdEncoding.DecodeString( 69 | strings.TrimPrefix(val, "base64://"), 70 | ) 71 | 72 | if err != nil { 73 | return "", fmt.Errorf("failed to parse base64 value: %w", err) 74 | } 75 | 76 | return string(content), nil 77 | } 78 | 79 | return val, nil 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/binaries.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: binaries 4 | 5 | "on": 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - master 10 | push: 11 | branches: 12 | - master 13 | tags: 14 | - v* 15 | 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | binaries: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Generate token 25 | id: token 26 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 27 | with: 28 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 29 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 30 | permission-contents: write 31 | permission-packages: write 32 | 33 | - name: Checkout source 34 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 35 | 36 | - name: Setup golang 37 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 38 | with: 39 | go-version-file: go.mod 40 | 41 | - name: Setup task 42 | uses: arduino/setup-task@v2 43 | with: 44 | version: 3.x 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | - name: Setup goreleaser 48 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 49 | with: 50 | install-only: true 51 | 52 | - name: Setup signing 53 | if: github.event_name != 'pull_request' 54 | uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6 55 | with: 56 | gpg_private_key: ${{ secrets.GNUPG_KEY }} 57 | passphrase: ${{ secrets.GNUPG_PASSWORD }} 58 | 59 | - name: Run release 60 | env: 61 | GITHUB_TOKEN: ${{ steps.token.outputs.token }} 62 | run: | 63 | if [[ $GITHUB_REF == refs/tags/* ]]; then 64 | task build:release 65 | else 66 | task build:snapshot 67 | fi 68 | 69 | - name: Install cloudsmith 70 | if: startsWith(github.ref, 'refs/tags/') 71 | uses: cloudsmith-io/cloudsmith-cli-action@7de77876f92caf8db21887182d83d086fc8f31f3 # v2.0.0 72 | with: 73 | api-key: ${{ secrets.CLOUDSMITH_API_KEY }} 74 | 75 | - name: Upload packages 76 | if: startsWith(github.ref, 'refs/tags/') 77 | run: | 78 | for PACKAGE in dist/prometheus-hcloud-sd-*.deb; do 79 | cloudsmith push deb webhippie/promhippie/any-distro/any-version "${PACKAGE}" 80 | done 81 | 82 | for PACKAGE in dist/prometheus-hcloud-sd-*.rpm; do 83 | cloudsmith push rpm webhippie/promhippie/any-distro/any-version "${PACKAGE}" 84 | done 85 | 86 | ... 87 | -------------------------------------------------------------------------------- /hack/generate-envvars-docs.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/promhippie/prometheus-hcloud-sd/pkg/command" 13 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 14 | "github.com/urfave/cli/v3" 15 | ) 16 | 17 | type flag struct { 18 | Flag string 19 | Default string 20 | Envs []string 21 | Help string 22 | List bool 23 | } 24 | 25 | func main() { 26 | flags := make([]flag, 0) 27 | 28 | for _, f := range append(command.RootFlags(config.Load()), command.ServerFlags(config.Load())...) { 29 | switch v := f.(type) { 30 | case *cli.StringFlag: 31 | flags = append(flags, flag{ 32 | Flag: v.Name, 33 | Default: v.Value, 34 | Envs: v.Sources.EnvKeys(), 35 | Help: v.Usage, 36 | List: false, 37 | }) 38 | case *cli.IntFlag: 39 | flags = append(flags, flag{ 40 | Flag: v.Name, 41 | Default: strconv.Itoa(v.Value), 42 | Envs: v.Sources.EnvKeys(), 43 | Help: v.Usage, 44 | List: false, 45 | }) 46 | case *cli.Int64Flag: 47 | flags = append(flags, flag{ 48 | Flag: v.Name, 49 | Default: strconv.FormatInt(v.Value, 10), 50 | Envs: v.Sources.EnvKeys(), 51 | Help: v.Usage, 52 | List: false, 53 | }) 54 | case *cli.BoolFlag: 55 | flags = append(flags, flag{ 56 | Flag: v.Name, 57 | Default: fmt.Sprintf("%+v", v.Value), 58 | Envs: v.Sources.EnvKeys(), 59 | Help: v.Usage, 60 | List: false, 61 | }) 62 | case *cli.DurationFlag: 63 | flags = append(flags, flag{ 64 | Flag: v.Name, 65 | Default: v.Value.String(), 66 | Envs: v.Sources.EnvKeys(), 67 | Help: v.Usage, 68 | List: false, 69 | }) 70 | case *cli.StringSliceFlag: 71 | flags = append(flags, flag{ 72 | Flag: v.Name, 73 | Default: strings.Join(v.Value, ", "), 74 | Envs: v.Sources.EnvKeys(), 75 | Help: v.Usage, 76 | List: true, 77 | }) 78 | default: 79 | fmt.Printf("unknown type: %s\n", v) 80 | os.Exit(1) 81 | } 82 | } 83 | 84 | f, err := os.Create("docs/partials/envvars.md") 85 | 86 | if err != nil { 87 | fmt.Printf("failed to create file") 88 | os.Exit(1) 89 | } 90 | 91 | defer f.Close() 92 | 93 | last := flags[len(flags)-1] 94 | for _, row := range flags { 95 | f.WriteString( 96 | strings.Join( 97 | row.Envs, 98 | ", ", 99 | ) + "\n", 100 | ) 101 | 102 | f.WriteString(fmt.Sprintf( 103 | ": %s", 104 | row.Help, 105 | )) 106 | 107 | if row.List { 108 | f.WriteString( 109 | ", comma-separated list", 110 | ) 111 | } 112 | 113 | if row.Default != "" { 114 | f.WriteString(fmt.Sprintf( 115 | ", defaults to `%s`", 116 | row.Default, 117 | )) 118 | } 119 | 120 | f.WriteString("\n") 121 | 122 | if row.Flag != last.Flag { 123 | f.WriteString("\n") 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /.github/workflows/tools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tools 3 | 4 | "on": 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "0 8 * * 1" 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | tools: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Generate token 19 | id: token 20 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 21 | with: 22 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 23 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 24 | permission-contents: write 25 | permission-pull-requests: write 26 | permission-issues: write 27 | 28 | - name: Checkout source 29 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 30 | with: 31 | token: ${{ steps.token.outputs.token }} 32 | fetch-depth: 0 33 | 34 | - name: Setup golang 35 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 36 | with: 37 | go-version-file: go.mod 38 | 39 | - name: Update golangci 40 | run: | 41 | TOOL="github.com/golangci/golangci-lint/v2/cmd/golangci-lint" 42 | MOD="$(go list -mod=readonly -f '{{.Module.Path}}' ${TOOL})" 43 | VERSION="$(go list -mod=readonly -u -json -m ${MOD} | jq -r .Update.Version)" 44 | if [[ "${VERSION}" != "null" ]]; then 45 | go get -tool "${TOOL}@${VERSION}" && go mod tidy 46 | fi 47 | 48 | - name: Update revive 49 | run: | 50 | TOOL="github.com/mgechev/revive" 51 | MOD="$(go list -mod=readonly -f '{{.Module.Path}}' ${TOOL})" 52 | VERSION="$(go list -mod=readonly -u -json -m ${MOD} | jq -r .Update.Version)" 53 | if [[ "${VERSION}" != "null" ]]; then 54 | go get -tool "${TOOL}@${VERSION}" && go mod tidy 55 | fi 56 | 57 | - name: Create request 58 | id: request 59 | uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8 60 | with: 61 | branch: update/tools 62 | delete-branch: true 63 | committer: "GitHub Actions " 64 | commit-message: "ci: automated tool updates" 65 | signoff: true 66 | title: "ci: automated tool updates" 67 | body: "New versions updated, automerge should handle that!" 68 | labels: tools 69 | token: ${{ steps.token.outputs.token }} 70 | 71 | - name: Approve request 72 | if: steps.request.outputs.pull-request-operation == 'created' 73 | run: gh pr review --approve "${{ steps.request.outputs.pull-request-number }}" 74 | env: 75 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | 77 | - name: Enable automerge 78 | if: steps.request.outputs.pull-request-operation == 'created' 79 | run: gh pr merge --rebase --auto "${{ steps.request.outputs.pull-request-number }}" 80 | env: 81 | GH_TOKEN: ${{ steps.token.outputs.token }} 82 | 83 | ... 84 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.40" 3 | 4 | vars: 5 | SOURCES: 6 | sh: find . -name "*.go" -type f -not -iname mock.go -not -path "./.devenv/*" -not -path "./.direnv/*" | xargs echo 7 | PACKAGES: 8 | sh: go list ./... | xargs echo 9 | 10 | tasks: 11 | clean: 12 | desc: Remove all temporary build artifacts 13 | cmds: 14 | - go clean -i ./... 15 | - rm -rf bin/ dist/ 16 | 17 | generate: 18 | desc: Generate code for server 19 | cmds: 20 | - go generate {{ .PACKAGES }} 21 | 22 | fmt: 23 | desc: Run standard formatter for server 24 | cmds: 25 | - gofmt -s -w {{ .SOURCES }} 26 | 27 | vet: 28 | desc: Run vet linting for server 29 | cmds: 30 | - go vet {{ .PACKAGES }} 31 | 32 | lint: 33 | desc: Run revive linting for server 34 | cmds: 35 | - for PKG in {{ .PACKAGES }}; do go tool github.com/mgechev/revive -config revive.toml -set_exit_status $PKG || exit 1; done; 36 | 37 | golangci: 38 | desc: Run golangci linter for server 39 | cmds: 40 | - go tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint run ./... 41 | 42 | test: 43 | desc: Run tests for server 44 | cmds: 45 | - go test -coverprofile coverage.out {{ .PACKAGES }} 46 | 47 | build: 48 | desc: Build all required binary artifacts 49 | deps: 50 | - build:server 51 | 52 | build:release: 53 | desc: Generate a release with goreleaser 54 | cmds: 55 | - goreleaser release --clean 56 | 57 | build:snapshot: 58 | desc: Generate a snapshot with goreleaser 59 | cmds: 60 | - goreleaser release --clean --snapshot --skip=announce,publish,validate,sign 61 | 62 | build:server: 63 | desc: Build server component 64 | cmds: 65 | - go build -v 66 | -tags 'netgo' 67 | -ldflags '-s -w -extldflags "-static" -X "{{ .IMPORT }}/pkg/version.String={{ .VERSION }}" -X "{{ .IMPORT }}/pkg/version.Revision={{ .REVISION }}" -X "{{ .IMPORT }}/pkg/version.Date={{ now | date "20060102" }}"' 68 | -o bin/prometheus-hcloud-sd{{if eq OS "windows"}}.exe{{end}} 69 | ./cmd/prometheus-hcloud-sd 70 | env: 71 | CGO_ENABLED: "0" 72 | GOOS: "{{ .GOOS }}" 73 | GOARCH: "{{ .GOARCH }}" 74 | vars: 75 | IMPORT: github.com/promhippie/prometheus-hcloud-sd 76 | VERSION: 77 | sh: if [[ -z "${CI_COMMIT_TAG}" ]]; then git rev-parse --short HEAD; else echo "${CI_COMMIT_TAG#v}"; fi 78 | REVISION: 79 | sh: git rev-parse --short HEAD 80 | 81 | watch: 82 | desc: Run reloading development server 83 | cmds: 84 | - task: build:server 85 | - bin/prometheus-hcloud-sd server 86 | watch: true 87 | method: none 88 | sources: 89 | - 'cmd/**/*.go' 90 | - 'pkg/**/*.go' 91 | 92 | docs: 93 | desc: Generate documentation with hugo 94 | cmds: 95 | - hugo -s docs/ 96 | 97 | envvars: 98 | desc: Generate envvar partial for docs 99 | cmds: 100 | - go run hack/generate-envvars-docs.go 101 | 102 | labels: 103 | desc: Generate labels partial for docs 104 | cmds: 105 | - go run hack/generate-labels-docs.go 106 | 107 | ... 108 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Nix flake for development"; 3 | 4 | inputs = { 5 | nixpkgs = { 6 | url = "github:nixos/nixpkgs/nixpkgs-unstable"; 7 | }; 8 | 9 | devenv = { 10 | url = "github:cachix/devenv"; 11 | }; 12 | 13 | flake-parts = { 14 | url = "github:hercules-ci/flake-parts"; 15 | }; 16 | 17 | git-hooks = { 18 | url = "github:cachix/git-hooks.nix"; 19 | }; 20 | }; 21 | 22 | outputs = 23 | inputs@{ flake-parts, ... }: 24 | flake-parts.lib.mkFlake { inherit inputs; } { 25 | imports = [ 26 | inputs.devenv.flakeModule 27 | inputs.git-hooks.flakeModule 28 | ]; 29 | 30 | systems = [ 31 | "x86_64-linux" 32 | "aarch64-linux" 33 | "x86_64-darwin" 34 | "aarch64-darwin" 35 | ]; 36 | 37 | perSystem = 38 | { 39 | config, 40 | self', 41 | inputs', 42 | pkgs, 43 | system, 44 | ... 45 | }: 46 | { 47 | imports = [ 48 | { 49 | _module.args.pkgs = import inputs.nixpkgs { 50 | inherit system; 51 | config.allowUnfree = true; 52 | }; 53 | } 54 | ]; 55 | 56 | devenv = { 57 | shells = { 58 | default = { 59 | git-hooks = { 60 | hooks = { 61 | nixfmt-rfc-style = { 62 | enable = true; 63 | }; 64 | 65 | gofmt = { 66 | enable = true; 67 | }; 68 | 69 | golangci-lint = { 70 | enable = true; 71 | entry = "go tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint run ./..."; 72 | pass_filenames = false; 73 | }; 74 | }; 75 | }; 76 | 77 | languages = { 78 | go = { 79 | enable = true; 80 | package = pkgs.go_1_25; 81 | }; 82 | }; 83 | 84 | packages = with pkgs; [ 85 | go-task 86 | goreleaser 87 | hugo 88 | nixfmt-rfc-style 89 | ]; 90 | 91 | env = { 92 | CGO_ENABLED = "0"; 93 | }; 94 | 95 | processes = { 96 | current-server = { 97 | exec = "task watch"; 98 | 99 | process-compose = { 100 | readiness_probe = { 101 | exec.command = "${pkgs.curl}/bin/curl -sSf http://localhost:9000/readyz"; 102 | initial_delay_seconds = 2; 103 | period_seconds = 10; 104 | timeout_seconds = 4; 105 | success_threshold = 1; 106 | failure_threshold = 5; 107 | }; 108 | 109 | availability = { 110 | restart = "on_failure"; 111 | }; 112 | }; 113 | }; 114 | }; 115 | }; 116 | }; 117 | }; 118 | }; 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | version: 2 3 | 4 | builds: 5 | - id: server 6 | main: ./cmd/prometheus-hcloud-sd 7 | binary: prometheus-hcloud-sd 8 | env: 9 | - CGO_ENABLED=0 10 | ldflags: 11 | - -s -w -extldflags "-static" -X "github.com/promhippie/prometheus-hcloud-sd/pkg/version.String={{.Version}}" -X "github.com/promhippie/prometheus-hcloud-sd/pkg/version.Revision={{.Commit}}" -X "github.com/promhippie/prometheus-hcloud-sd/pkg/version.Date={{.Date}}" 12 | tags: 13 | - netgo 14 | goos: 15 | - linux 16 | - windows 17 | - darwin 18 | goarch: 19 | - amd64 20 | - "386" 21 | - arm64 22 | - arm 23 | ignore: 24 | - goos: darwin 25 | goarch: "386" 26 | - goos: windows 27 | goarch: arm 28 | 29 | archives: 30 | - id: server 31 | ids: 32 | - server 33 | name_template: "prometheus-hcloud-sd-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}-{{ . }}{{ end }}" 34 | format_overrides: 35 | - goos: windows 36 | formats: 37 | - zip 38 | files: 39 | - LICENSE 40 | - CHANGELOG.md 41 | - src: cmd/prometheus-hcloud-sd/README.md 42 | dst: README.md 43 | 44 | nfpms: 45 | - id: server 46 | ids: 47 | - server 48 | package_name: prometheus-hcloud-sd 49 | file_name_template: "prometheus-hcloud-sd-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}-{{ . }}{{ end }}" 50 | vendor: Webhippie 51 | homepage: https://promhippie.github.io/prometheus-hcloud-sd/ 52 | maintainer: Thomas Boerger 53 | description: |- 54 | Prometheus service discovery for Hetzner Cloud 55 | license: Apache 2.0 56 | formats: 57 | - deb 58 | - rpm 59 | contents: 60 | - src: packaging/systemd/server.service 61 | dst: /usr/lib/systemd/system/prometheus-hcloud-sd.service 62 | - src: packaging/systemd/server.env 63 | dst: /etc/sysconfig/prometheus-hcloud-sd 64 | packager: rpm 65 | - src: packaging/systemd/server.env 66 | dst: /etc/default/prometheus-hcloud-sd 67 | packager: deb 68 | - src: packaging/config/config.yaml 69 | dst: /etc/prometheus-hcloud-sd/config.yaml 70 | type: config|noreplace 71 | - dst: /var/lib/prometheus-hcloud-sd 72 | type: dir 73 | scripts: 74 | preinstall: packaging/scripts/preinstall.sh 75 | postinstall: packaging/scripts/postinstall.sh 76 | preremove: packaging/scripts/preremove.sh 77 | postremove: packaging/scripts/postremove.sh 78 | 79 | signs: 80 | - id: archives 81 | signature: "${artifact}.asc" 82 | cmd: gpg2 83 | artifacts: archive 84 | args: 85 | - --batch 86 | - --armor 87 | - --local-user 88 | - B8BB213D9E131E46D2EBE22E44E93172C6FDE7E6 89 | - --output 90 | - ${signature} 91 | - --detach-sign 92 | - ${artifact} 93 | - id: packages 94 | signature: "${artifact}.asc" 95 | cmd: gpg2 96 | artifacts: package 97 | args: 98 | - --batch 99 | - --armor 100 | - --local-user 101 | - B8BB213D9E131E46D2EBE22E44E93172C6FDE7E6 102 | - --output 103 | - ${signature} 104 | - --detach-sign 105 | - ${artifact} 106 | 107 | snapshot: 108 | version_template: testing 109 | 110 | changelog: 111 | disable: true 112 | 113 | checksum: 114 | disable: false 115 | split: true 116 | -------------------------------------------------------------------------------- /pkg/command/server.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/promhippie/prometheus-hcloud-sd/pkg/action" 8 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 9 | "github.com/urfave/cli/v3" 10 | ) 11 | 12 | // Server provides the sub-command to start the server. 13 | func Server(cfg *config.Config) *cli.Command { 14 | return &cli.Command{ 15 | Name: "server", 16 | Usage: "Start integrated server", 17 | Flags: ServerFlags(cfg), 18 | Action: func(_ context.Context, cmd *cli.Command) error { 19 | logger := setupLogger(cfg) 20 | 21 | if cmd.IsSet("hcloud.config") { 22 | if err := readConfig(cmd.String("hcloud.config"), cfg); err != nil { 23 | logger.Error("Failed to read config", 24 | "err", err, 25 | ) 26 | 27 | return err 28 | } 29 | } 30 | 31 | if cfg.Target.File == "" { 32 | logger.Error("Missing path for output.file") 33 | return errors.New("missing path for output.file") 34 | } 35 | 36 | if cmd.IsSet("hcloud.token") { 37 | credentials := config.Credential{ 38 | Project: "default", 39 | Token: cmd.String("hcloud.token"), 40 | } 41 | 42 | cfg.Target.Credentials = append( 43 | cfg.Target.Credentials, 44 | credentials, 45 | ) 46 | 47 | if credentials.Token == "" { 48 | logger.Error("Missing required hcloud.token") 49 | return errors.New("missing required hcloud.token") 50 | } 51 | } 52 | 53 | if len(cfg.Target.Credentials) == 0 { 54 | logger.Error("Missing any credentials") 55 | return errors.New("missing any credentials") 56 | } 57 | 58 | return action.Server(cfg, logger) 59 | }, 60 | } 61 | } 62 | 63 | // ServerFlags defines the available server flags. 64 | func ServerFlags(cfg *config.Config) []cli.Flag { 65 | return []cli.Flag{ 66 | &cli.StringFlag{ 67 | Name: "web.address", 68 | Value: "0.0.0.0:9000", 69 | Usage: "Address to bind the metrics server", 70 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_WEB_ADDRESS"), 71 | Destination: &cfg.Server.Addr, 72 | }, 73 | &cli.StringFlag{ 74 | Name: "web.path", 75 | Value: "/metrics", 76 | Usage: "Path to bind the metrics server", 77 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_WEB_PATH"), 78 | Destination: &cfg.Server.Path, 79 | }, 80 | &cli.StringFlag{ 81 | Name: "web.config", 82 | Value: "", 83 | Usage: "Path to web-config file", 84 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_WEB_CONFIG"), 85 | Destination: &cfg.Server.Web, 86 | }, 87 | &cli.StringFlag{ 88 | Name: "output.engine", 89 | Value: "file", 90 | Usage: "Enabled engine like file or http", 91 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_OUTPUT_ENGINE"), 92 | Destination: &cfg.Target.Engine, 93 | }, 94 | &cli.StringFlag{ 95 | Name: "output.file", 96 | Value: "/etc/prometheus/hcloud.json", 97 | Usage: "Path to write the file_sd config", 98 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_OUTPUT_FILE"), 99 | Destination: &cfg.Target.File, 100 | }, 101 | &cli.IntFlag{ 102 | Name: "output.refresh", 103 | Value: 30, 104 | Usage: "Discovery refresh interval in seconds", 105 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_OUTPUT_REFRESH"), 106 | Destination: &cfg.Target.Refresh, 107 | }, 108 | &cli.StringFlag{ 109 | Name: "hcloud.token", 110 | Value: "", 111 | Usage: "Access token for the HetznerCloud API", 112 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_TOKEN"), 113 | }, 114 | &cli.StringFlag{ 115 | Name: "hcloud.config", 116 | Value: "", 117 | Usage: "Path to HetznerCloud configuration file", 118 | Sources: cli.EnvVars("PROMETHEUS_HCLOUD_CONFIG"), 119 | }, 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/changes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: changes 4 | 5 | "on": 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - master 10 | push: 11 | branches: 12 | - master 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | envvars: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Generate token 23 | if: github.event_name != 'pull_request' 24 | id: token 25 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 26 | with: 27 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 28 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 29 | permission-contents: write 30 | 31 | - name: Checkout source 32 | if: github.event_name != 'pull_request' 33 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 34 | with: 35 | token: ${{ steps.token.outputs.token }} 36 | 37 | - name: PR checkout 38 | if: github.event_name == 'pull_request' 39 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 40 | 41 | - name: Setup golang 42 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 43 | with: 44 | go-version-file: go.mod 45 | 46 | - name: Setup task 47 | uses: arduino/setup-task@v2 48 | with: 49 | version: 3.x 50 | repo-token: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Generate envvars 53 | run: task envvars 54 | 55 | - name: Source rebase 56 | if: github.event_name != 'pull_request' 57 | run: git pull --autostash --rebase 58 | 59 | - name: Commit changes 60 | if: github.event_name != 'pull_request' 61 | uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9 62 | with: 63 | author_name: GitHub Actions 64 | author_email: github@webhippie.de 65 | add: docs/partials/envvars.md 66 | message: "docs: automated envvars update" 67 | push: true 68 | commit: --signoff 69 | 70 | labels: 71 | runs-on: ubuntu-latest 72 | 73 | steps: 74 | - name: Generate token 75 | if: github.event_name != 'pull_request' 76 | id: token 77 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 78 | with: 79 | app-id: ${{ secrets.TOKEN_EXCHANGE_APP }} 80 | private-key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 81 | permission-contents: write 82 | 83 | - name: Checkout source 84 | if: github.event_name != 'pull_request' 85 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 86 | with: 87 | token: ${{ steps.token.outputs.token }} 88 | 89 | - name: PR checkout 90 | if: github.event_name == 'pull_request' 91 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 92 | 93 | - name: Setup golang 94 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 95 | with: 96 | go-version-file: go.mod 97 | 98 | - name: Setup task 99 | uses: arduino/setup-task@v2 100 | with: 101 | version: 3.x 102 | repo-token: ${{ secrets.GITHUB_TOKEN }} 103 | 104 | - name: Generate labels 105 | run: task labels 106 | 107 | - name: Source rebase 108 | if: github.event_name != 'pull_request' 109 | run: git pull --autostash --rebase 110 | 111 | - name: Commit changes 112 | if: github.event_name != 'pull_request' 113 | uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9 114 | with: 115 | author_name: GitHub Actions 116 | author_email: github@webhippie.de 117 | add: docs/partials/labels.md 118 | message: "docs: automated labels update" 119 | push: true 120 | commit: --signoff 121 | 122 | ... 123 | -------------------------------------------------------------------------------- /docs/content/kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Kubernetes" 3 | date: 2022-07-22T00:00:00+00:00 4 | anchor: "kubernetes" 5 | weight: 20 6 | --- 7 | 8 | ## Kubernetes 9 | 10 | Currently we are covering the most famous installation methods on Kubernetes, 11 | you can choose between [Kustomize][kustomize] and [Helm][helm]. 12 | 13 | ### Kustomize 14 | 15 | We won't cover the installation of [Kustomize][kustomize] within this guide, to 16 | get it installed and working please read the upstream documentation. After the 17 | installation of [Kustomize][kustomize] you just need to prepare a 18 | `kustomization.yml` wherever you like similar to this: 19 | 20 | {{< highlight yaml >}} 21 | apiVersion: kustomize.config.k8s.io/v1beta1 22 | kind: Kustomization 23 | namespace: prometheus-hcloud-sd 24 | 25 | resources: 26 | - github.com/promhippie/prometheus-hcloud-sd//deploy/kubernetes?ref=master 27 | 28 | configMapGenerator: 29 | - name: prometheus-hcloud-sd 30 | behavior: merge 31 | literals: [] 32 | 33 | secretGenerator: 34 | - name: prometheus-hcloud-sd 35 | behavior: merge 36 | literals: [] 37 | {{< / highlight >}} 38 | 39 | After that you can simply execute `kustomize build | kubectl apply -f -` to get 40 | the manifest applied. Generally it's best to use fixed versions of the container 41 | images, this can be done quite easy, you just need to append this block to your 42 | `kustomization.yml` to use this specific version: 43 | 44 | {{< highlight yaml >}} 45 | images: 46 | - name: quay.io/promhippie/prometheus-hcloud-sd 47 | newTag: 1.1.0 48 | {{< / highlight >}} 49 | 50 | After applying this manifest the exporter should be directly visible within your 51 | Prometheus instance if you are using the Prometheus Operator as these manifests 52 | are providing a ServiceMonitor. 53 | 54 | To consume the service discovery within Prometheus you got to configre matching 55 | scrape targets using the HTTP engine, just add a block similar to this one to 56 | your Prometheus configuration: 57 | 58 | {{< highlight yaml >}} 59 | scrape_configs: 60 | - job_name: node 61 | http_sd_configs: 62 | - url: http://hcloud-sd.prometheus-hcloud-sd.svc.cluster.local:9000/sd 63 | relabel_configs: 64 | - source_labels: [__meta_hcloud_public_ipv4] 65 | replacement: "${1}:9100" 66 | target_label: __address__ 67 | - source_labels: [__meta_hcloud_location] 68 | target_label: location 69 | - source_labels: [__meta_hcloud_name] 70 | target_label: instance 71 | {{< / highlight >}} 72 | 73 | ### Helm 74 | 75 | We won't cover the installation of [Helm][helm] within this guide, to get it 76 | installed and working please read the upstream documentation. After the 77 | installation of [Helm][helm] you just need to execute the following commands: 78 | 79 | {{< highlight console >}} 80 | helm repo add promhippie https://promhippie.github.io/charts 81 | helm show values promhippie/prometheus-hcloud-sd 82 | helm install prometheus-hcloud-sd promhippie/prometheus-hcloud-sd 83 | {{< / highlight >}} 84 | 85 | You can also watch that available values and generally the details of the chart 86 | provided by us within our [chart][chart] repository. 87 | 88 | After applying this manifest the exporter should be directly visible within your 89 | Prometheus instance depending on your installation if you enabled the 90 | annotations or the service monitor. 91 | 92 | To consume the service discovery within Prometheus you got to configre matching 93 | scrape targets using the HTTP engine, just add a block similar to this one to 94 | your Prometheus configuration: 95 | 96 | {{< highlight yaml >}} 97 | scrape_configs: 98 | - job_name: node 99 | http_sd_configs: 100 | - url: http://hcloud-sd.prometheus-hcloud-sd.svc.cluster.local:9000/sd 101 | relabel_configs: 102 | - source_labels: [__meta_hcloud_public_ipv4] 103 | replacement: "${1}:9100" 104 | target_label: __address__ 105 | - source_labels: [__meta_hcloud_location] 106 | target_label: location 107 | - source_labels: [__meta_hcloud_name] 108 | target_label: instance 109 | {{< / highlight >}} 110 | 111 | [kustomize]: https://github.com/kubernetes-sigs/kustomize 112 | [helm]: https://helm.sh 113 | [chart]: https://github.com/promhippie/charts/tree/master/charts/prometheus-hcloud-sd 114 | -------------------------------------------------------------------------------- /pkg/adapter/adapter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package adapter 16 | 17 | // NOTE: you do not need to edit this file when implementing a custom sd. 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "log/slog" 23 | "os" 24 | "path/filepath" 25 | "reflect" 26 | 27 | "github.com/prometheus/prometheus/discovery" 28 | "github.com/prometheus/prometheus/discovery/targetgroup" 29 | ) 30 | 31 | type customSD struct { 32 | Targets []string `json:"targets"` 33 | Labels map[string]string `json:"labels"` 34 | } 35 | 36 | // Adapter runs an unknown service discovery implementation and converts its target groups 37 | // to JSON and writes to a file for file_sd. 38 | type Adapter struct { 39 | ctx context.Context 40 | disc discovery.Discoverer 41 | groups map[string]*customSD 42 | manager *discovery.Manager 43 | output string 44 | name string 45 | logger *slog.Logger 46 | } 47 | 48 | func mapToArray(m map[string]*customSD) []customSD { 49 | arr := make([]customSD, 0, len(m)) 50 | for _, v := range m { 51 | arr = append(arr, *v) 52 | } 53 | return arr 54 | } 55 | 56 | // Parses incoming target groups updates. If the update contains changes to the target groups 57 | // Adapter already knows about, or new target groups, we Marshal to JSON and write to file. 58 | func (a *Adapter) generateTargetGroups(allTargetGroups map[string][]*targetgroup.Group) { 59 | tempGroups := make(map[string]*customSD) 60 | for k, sdTargetGroups := range allTargetGroups { 61 | for i, group := range sdTargetGroups { 62 | newTargets := make([]string, 0) 63 | newLabels := make(map[string]string) 64 | 65 | for _, targets := range group.Targets { 66 | for _, target := range targets { 67 | newTargets = append(newTargets, string(target)) 68 | } 69 | } 70 | 71 | for name, value := range group.Labels { 72 | newLabels[string(name)] = string(value) 73 | } 74 | // Make a unique key, including the current index, in case the sd_type (map key) and group.Source is not unique. 75 | key := fmt.Sprintf("%s:%s:%d", k, group.Source, i) 76 | tempGroups[key] = &customSD{ 77 | Targets: newTargets, 78 | Labels: newLabels, 79 | } 80 | } 81 | } 82 | if !reflect.DeepEqual(a.groups, tempGroups) { 83 | a.groups = tempGroups 84 | err := a.writeOutput() 85 | if err != nil { 86 | a.logger.With("component", "sd-adapter").Error("", "err", err) 87 | } 88 | } 89 | 90 | } 91 | 92 | // Writes JSON formatted targets to output file. 93 | func (a *Adapter) writeOutput() error { 94 | arr := mapToArray(a.groups) 95 | b, _ := json.MarshalIndent(arr, "", " ") 96 | 97 | dir, _ := filepath.Split(a.output) 98 | tmpfile, err := os.CreateTemp(dir, "sd-adapter") 99 | if err != nil { 100 | return err 101 | } 102 | //nolint:errcheck 103 | defer tmpfile.Close() 104 | 105 | _, err = tmpfile.Write(b) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | err = os.Rename(tmpfile.Name(), a.output) 111 | if err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func (a *Adapter) runCustomSD(ctx context.Context) { 118 | updates := a.manager.SyncCh() 119 | for { 120 | select { 121 | case <-ctx.Done(): 122 | case allTargetGroups, ok := <-updates: 123 | // Handle the case that a target provider exits and closes the channel 124 | // before the context is done. 125 | if !ok { 126 | return 127 | } 128 | a.generateTargetGroups(allTargetGroups) 129 | } 130 | } 131 | } 132 | 133 | // Run starts a Discovery Manager and the custom service discovery implementation. 134 | func (a *Adapter) Run() { 135 | //nolint:errcheck 136 | go a.manager.Run() 137 | a.manager.StartCustomProvider(a.ctx, a.name, a.disc) 138 | go a.runCustomSD(a.ctx) 139 | } 140 | 141 | // NewAdapter creates a new instance of Adapter. 142 | func NewAdapter(ctx context.Context, file string, name string, d discovery.Discoverer, logger *slog.Logger) *Adapter { 143 | return &Adapter{ 144 | ctx: ctx, 145 | disc: d, 146 | groups: make(map[string]*customSD), 147 | manager: discovery.NewManager(ctx, nil), 148 | output: file, 149 | name: name, 150 | logger: logger, 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus Hetzner Cloud SD 2 | 3 | [![Current Tag](https://img.shields.io/github/v/tag/promhippie/prometheus-hcloud-sd?sort=semver)](https://github.com/promhippie/prometheus-hcloud-sd) [![General Build](https://github.com/promhippie/prometheus-hcloud-sd/actions/workflows/general.yml/badge.svg)](https://github.com/promhippie/prometheus-hcloud-sd/actions/workflows/general.yaml) [![Join the Matrix chat at https://matrix.to/#/#webhippie:matrix.org](https://img.shields.io/badge/matrix-%23webhippie-7bc9a4.svg)](https://matrix.to/#/#webhippie:matrix.org) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d7900c4c246740edb77cf29a4b1d85ee)](https://www.codacy.com/gh/promhippie/prometheus-hcloud-sd/dashboard?utm_source=github.com&utm_medium=referral&utm_content=promhippie/prometheus-hcloud-sd&utm_campaign=Badge_Grade) [![Go Doc](https://godoc.org/github.com/promhippie/prometheus-hcloud-sd?status.svg)](http://godoc.org/github.com/promhippie/prometheus-hcloud-sd) [![Go Report](http://goreportcard.com/badge/github.com/promhippie/prometheus-hcloud-sd)](http://goreportcard.com/report/github.com/promhippie/prometheus-hcloud-sd) [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) 4 | 5 | This project provides a server to automatically discover nodes within your 6 | Hetzner Cloud account in a Prometheus SD compatible format. 7 | 8 | ## Install 9 | 10 | You can download prebuilt binaries from our [GitHub releases][releases]. Besides 11 | that we also prepared repositories for DEB and RPM packages which can be found 12 | at [Cloudsmith][pkgrepo]. If you prefer to use containers you could use our 13 | images published on [GHCR][ghcr], [Docker Hub][dockerhub] or [Quay][quayio]. If 14 | you need further guidance how to install this take a look at our [docs][docs]. 15 | 16 | Package repository hosting is graciously provided by [Cloudsmith][cloudsmith]. 17 | Cloudsmith is the only fully hosted, cloud-native, universal package management 18 | solution, that enables your organization to create, store and share packages in 19 | any format, to any place, with total confidence. 20 | 21 | ## Development 22 | 23 | If you are not familiar with [Nix][nix] it is up to you to have a working 24 | environment for Go (>= 1.24.0) as the setup won't we covered within this guide. 25 | Please follow the official install instructions for [Go][golang]. Beside that 26 | we are using [go-task][gotask] to define all commands to build this project. 27 | 28 | ```console 29 | git clone https://github.com/promhippie/prometheus-hcloud-sd.git 30 | cd prometheus-hcloud-sd 31 | 32 | task generate build 33 | ./bin/prometheus-hcloud-sd -h 34 | ``` 35 | 36 | If you got [Nix][nix] and [Direnv][direnv] configured you can simply execute 37 | the following commands to get al dependencies including [go-task][gotask] and 38 | the required runtimes installed. You are also able to directly use the process 39 | manager of [devenv][devenv]: 40 | 41 | ```console 42 | cat << EOF > .envrc 43 | use flake . --impure --extra-experimental-features nix-command 44 | EOF 45 | 46 | direnv allow 47 | ``` 48 | 49 | To start developing on this project you have to execute only a few commands: 50 | 51 | ```console 52 | task watch 53 | ``` 54 | 55 | The development server should be running on 56 | [http://localhost:9000](http://localhost:9000). Generally it supports 57 | hot reloading which means the services are automatically restarted/reloaded on 58 | code changes. 59 | 60 | If you got [Nix][nix] configured you can simply execute the [devenv][devenv] 61 | command to start: 62 | 63 | ```console 64 | devenv up 65 | ``` 66 | 67 | ## Security 68 | 69 | If you find a security issue please contact 70 | [thomas@webhippie.de](mailto:thomas@webhippie.de) first. 71 | 72 | ## Contributing 73 | 74 | Fork -> Patch -> Push -> Pull Request 75 | 76 | ## Authors 77 | 78 | - [Thomas Boerger](https://github.com/tboerger) 79 | 80 | ## License 81 | 82 | Apache-2.0 83 | 84 | ## Copyright 85 | 86 | ```console 87 | Copyright (c) 2018 Thomas Boerger 88 | ``` 89 | 90 | [releases]: https://github.com/promhippie/prometheus-hcloud-sd/releases 91 | [pkgrepo]: https://cloudsmith.io/~webhippie/repos/promhippie/groups/ 92 | [cloudsmith]: https://cloudsmith.com/ 93 | [ghcr]: https://github.com/promhippie/prometheus-hcloud-sd/pkgs/container/prometheus-hcloud-sd 94 | [dockerhub]: https://hub.docker.com/r/promhippie/prometheus-hcloud-sd/tags/ 95 | [quayio]: https://quay.io/repository/promhippie/prometheus-hcloud-sd?tab=tags 96 | [docs]: https://promhippie.github.io/prometheus-hcloud-sd/#getting-started 97 | [nix]: https://nixos.org/ 98 | [golang]: http://golang.org/doc/install.html 99 | [gotask]: https://taskfile.dev/installation/ 100 | [direnv]: https://direnv.net/ 101 | [devenv]: https://devenv.sh/ 102 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json 3 | name: docker 4 | 5 | "on": 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - master 10 | tags: 11 | - v* 12 | pull_request: 13 | branches: 14 | - master 15 | 16 | permissions: 17 | contents: write 18 | packages: write 19 | 20 | jobs: 21 | docker: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout source 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 27 | 28 | - name: Docker meta 29 | id: meta 30 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | images: | 34 | promhippie/prometheus-hcloud-sd 35 | quay.io/promhippie/prometheus-hcloud-sd 36 | ghcr.io/promhippie/prometheus-hcloud-sd 37 | labels: | 38 | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/promhippie/prometheus-hcloud-sd/master/README.md 39 | org.opencontainers.image.vendor=Webhippie 40 | maintainer=Thomas Boerger 41 | tags: | 42 | type=ref,event=pr 43 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} 44 | type=semver,pattern={{version}} 45 | type=semver,pattern={{major}}.{{minor}} 46 | type=semver,pattern={{major}} 47 | 48 | - name: Setup qemu 49 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 50 | 51 | - name: Setup buildx 52 | id: buildx 53 | uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 54 | 55 | - name: Setup cosign 56 | if: github.event_name != 'pull_request' 57 | uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 58 | 59 | - name: Hub login 60 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 61 | if: github.event_name != 'pull_request' 62 | with: 63 | username: ${{ secrets.DOCKER_USERNAME }} 64 | password: ${{ secrets.DOCKER_PASSWORD }} 65 | 66 | - name: Quay login 67 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 68 | if: github.event_name != 'pull_request' 69 | with: 70 | registry: quay.io 71 | username: ${{ secrets.QUAY_USERNAME }} 72 | password: ${{ secrets.QUAY_PASSWORD }} 73 | 74 | - name: Ghcr login 75 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 76 | if: github.event_name != 'pull_request' 77 | with: 78 | registry: ghcr.io 79 | username: ${{ github.actor }} 80 | password: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | - name: Build image 83 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 84 | with: 85 | builder: ${{ steps.buildx.outputs.name }} 86 | context: . 87 | file: Dockerfile 88 | platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6 89 | push: ${{ github.event_name != 'pull_request' }} 90 | labels: ${{ steps.meta.outputs.labels }} 91 | tags: ${{ steps.meta.outputs.tags }} 92 | 93 | - name: Sign images 94 | if: github.event_name != 'pull_request' 95 | env: 96 | COSIGN_KEY: ${{ secrets.COSIGN_KEY }} 97 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 98 | run: | 99 | echo "${{ steps.meta.outputs.tags }}" | while read -r TAG; do 100 | cosign sign --yes --key env://COSIGN_KEY ${TAG} 101 | done 102 | 103 | readme: 104 | runs-on: ubuntu-latest 105 | needs: docker 106 | if: github.event_name != 'pull_request' 107 | 108 | steps: 109 | - name: Checkout source 110 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 111 | 112 | - name: Hub readme 113 | uses: actionhippie/pushrm@db8835668f770a1b8be17d19b5e6b36450c6766f # v1 114 | with: 115 | provider: dockerhub 116 | target: promhippie/prometheus-hcloud-sd 117 | username: ${{ secrets.DOCKER_USERNAME }} 118 | password: ${{ secrets.DOCKER_PASSWORD }} 119 | description: Prometheus Hetzner Cloud SD 120 | readme: README.md 121 | 122 | - name: Quay readme 123 | uses: actionhippie/pushrm@db8835668f770a1b8be17d19b5e6b36450c6766f # v1 124 | with: 125 | provider: quay 126 | target: quay.io/promhippie/prometheus-hcloud-sd 127 | apikey: ${{ secrets.QUAY_APIKEY }} 128 | readme: README.md 129 | 130 | ... 131 | -------------------------------------------------------------------------------- /pkg/action/server.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "time" 12 | 13 | "github.com/go-chi/chi/v5" 14 | "github.com/hetznercloud/hcloud-go/v2/hcloud" 15 | "github.com/oklog/run" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | "github.com/prometheus/exporter-toolkit/web" 18 | "github.com/promhippie/prometheus-hcloud-sd/pkg/adapter" 19 | "github.com/promhippie/prometheus-hcloud-sd/pkg/config" 20 | "github.com/promhippie/prometheus-hcloud-sd/pkg/middleware" 21 | "github.com/promhippie/prometheus-hcloud-sd/pkg/version" 22 | ) 23 | 24 | // Server handles the server sub-command. 25 | func Server(cfg *config.Config, logger *slog.Logger) error { 26 | logger.Info("Launching Prometheus HetznerCloud SD", 27 | "version", version.String, 28 | "revision", version.Revision, 29 | "date", version.Date, 30 | "go", version.Go, 31 | "engine", cfg.Target.Engine, 32 | ) 33 | 34 | var gr run.Group 35 | 36 | { 37 | ctx := context.Background() 38 | clients := make(map[string]*hcloud.Client, len(cfg.Target.Credentials)) 39 | 40 | for _, credential := range cfg.Target.Credentials { 41 | token, err := config.Value(credential.Token) 42 | 43 | if err != nil { 44 | logger.Error("Failed to read token secret", 45 | "project", credential.Project, 46 | "err", err, 47 | ) 48 | 49 | return fmt.Errorf("failed to read token secret for %s", credential.Project) 50 | } 51 | 52 | clients[credential.Project] = hcloud.NewClient( 53 | hcloud.WithToken( 54 | token, 55 | ), 56 | ) 57 | } 58 | 59 | disc := Discoverer{ 60 | clients: clients, 61 | logger: logger, 62 | refresh: cfg.Target.Refresh, 63 | separator: ",", 64 | lasts: make(map[string]struct{}), 65 | } 66 | 67 | a := adapter.NewAdapter(ctx, cfg.Target.File, "hcloud-sd", disc, logger) 68 | a.Run() 69 | } 70 | 71 | { 72 | server := &http.Server{ 73 | Addr: cfg.Server.Addr, 74 | Handler: handler(cfg, logger), 75 | ReadTimeout: 5 * time.Second, 76 | WriteTimeout: 10 * time.Second, 77 | } 78 | 79 | gr.Add(func() error { 80 | logger.Info("Starting metrics server", 81 | "address", cfg.Server.Addr, 82 | ) 83 | 84 | return web.ListenAndServe( 85 | server, 86 | &web.FlagConfig{ 87 | WebListenAddresses: sliceP([]string{cfg.Server.Addr}), 88 | WebSystemdSocket: boolP(false), 89 | WebConfigFile: stringP(cfg.Server.Web), 90 | }, 91 | logger, 92 | ) 93 | }, func(reason error) { 94 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 95 | defer cancel() 96 | 97 | if err := server.Shutdown(ctx); err != nil { 98 | logger.Error("Failed to shutdown metrics gracefully", 99 | "err", err, 100 | ) 101 | 102 | return 103 | } 104 | 105 | logger.Info("Metrics shutdown gracefully", 106 | "reason", reason, 107 | ) 108 | }) 109 | } 110 | 111 | { 112 | stop := make(chan os.Signal, 1) 113 | 114 | gr.Add(func() error { 115 | signal.Notify(stop, os.Interrupt) 116 | 117 | <-stop 118 | 119 | return nil 120 | }, func(_ error) { 121 | close(stop) 122 | }) 123 | } 124 | 125 | return gr.Run() 126 | } 127 | 128 | func handler(cfg *config.Config, logger *slog.Logger) *chi.Mux { 129 | mux := chi.NewRouter() 130 | mux.Use(middleware.Recoverer(logger)) 131 | mux.Use(middleware.RealIP) 132 | mux.Use(middleware.Timeout) 133 | mux.Use(middleware.Cache) 134 | 135 | reg := promhttp.HandlerFor( 136 | registry, 137 | promhttp.HandlerOpts{ 138 | ErrorLog: promLogger{logger}, 139 | }, 140 | ) 141 | 142 | mux.Route("/", func(root chi.Router) { 143 | root.Get(cfg.Server.Path, func(w http.ResponseWriter, r *http.Request) { 144 | reg.ServeHTTP(w, r) 145 | }) 146 | 147 | root.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { 148 | w.Header().Set("Content-Type", "text/plain") 149 | w.WriteHeader(http.StatusOK) 150 | 151 | _, _ = io.WriteString(w, http.StatusText(http.StatusOK)) 152 | }) 153 | 154 | root.Get("/readyz", func(w http.ResponseWriter, _ *http.Request) { 155 | w.Header().Set("Content-Type", "text/plain") 156 | w.WriteHeader(http.StatusOK) 157 | 158 | _, _ = io.WriteString(w, http.StatusText(http.StatusOK)) 159 | }) 160 | 161 | if cfg.Target.Engine == "http" { 162 | root.Get("/sd", func(w http.ResponseWriter, _ *http.Request) { 163 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 164 | 165 | content, err := os.ReadFile(cfg.Target.File) 166 | 167 | if err != nil { 168 | logger.Error("Failed to read service discovery data", 169 | "err", err, 170 | ) 171 | 172 | http.Error( 173 | w, 174 | "Failed to read service discovery data", 175 | http.StatusInternalServerError, 176 | ) 177 | 178 | return 179 | } 180 | 181 | w.WriteHeader(http.StatusOK) 182 | _, _ = w.Write(content) 183 | }) 184 | } 185 | }) 186 | 187 | return mux 188 | } 189 | 190 | func boolP(i bool) *bool { 191 | return &i 192 | } 193 | 194 | func stringP(i string) *string { 195 | return &i 196 | } 197 | 198 | func sliceP(i []string) *[]string { 199 | return &i 200 | } 201 | -------------------------------------------------------------------------------- /docs/static/syntax.css: -------------------------------------------------------------------------------- 1 | /* Background */ 2 | .chroma { 3 | color: #f8f8f2; 4 | background-color: #272822 5 | } 6 | 7 | /* Error */ 8 | .chroma .err { 9 | color: #960050; 10 | background-color: #1e0010 11 | } 12 | 13 | /* LineTableTD */ 14 | .chroma .lntd { 15 | vertical-align: top; 16 | padding: 0; 17 | margin: 0; 18 | border: 0; 19 | } 20 | 21 | /* LineTable */ 22 | .chroma .lntable { 23 | border-spacing: 0; 24 | padding: 0; 25 | margin: 0; 26 | border: 0; 27 | width: auto; 28 | overflow: auto; 29 | display: block; 30 | } 31 | 32 | /* LineHighlight */ 33 | .chroma .hl { 34 | display: block; 35 | width: 100%; 36 | background-color: #ffffcc 37 | } 38 | 39 | /* LineNumbersTable */ 40 | .chroma .lnt { 41 | margin-right: 0.4em; 42 | padding: 0 0.4em 0 0.4em; 43 | } 44 | 45 | /* LineNumbers */ 46 | .chroma .ln { 47 | margin-right: 0.4em; 48 | padding: 0 0.4em 0 0.4em; 49 | } 50 | 51 | /* Keyword */ 52 | .chroma .k { 53 | color: #66d9ef 54 | } 55 | 56 | /* KeywordConstant */ 57 | .chroma .kc { 58 | color: #66d9ef 59 | } 60 | 61 | /* KeywordDeclaration */ 62 | .chroma .kd { 63 | color: #66d9ef 64 | } 65 | 66 | /* KeywordNamespace */ 67 | .chroma .kn { 68 | color: #f92672 69 | } 70 | 71 | /* KeywordPseudo */ 72 | .chroma .kp { 73 | color: #66d9ef 74 | } 75 | 76 | /* KeywordReserved */ 77 | .chroma .kr { 78 | color: #66d9ef 79 | } 80 | 81 | /* KeywordType */ 82 | .chroma .kt { 83 | color: #66d9ef 84 | } 85 | 86 | /* NameAttribute */ 87 | .chroma .na { 88 | color: #a6e22e 89 | } 90 | 91 | /* NameClass */ 92 | .chroma .nc { 93 | color: #a6e22e 94 | } 95 | 96 | /* NameConstant */ 97 | .chroma .no { 98 | color: #66d9ef 99 | } 100 | 101 | /* NameDecorator */ 102 | .chroma .nd { 103 | color: #a6e22e 104 | } 105 | 106 | /* NameException */ 107 | .chroma .ne { 108 | color: #a6e22e 109 | } 110 | 111 | /* NameFunction */ 112 | .chroma .nf { 113 | color: #a6e22e 114 | } 115 | 116 | /* NameOther */ 117 | .chroma .nx { 118 | color: #a6e22e 119 | } 120 | 121 | /* NameTag */ 122 | .chroma .nt { 123 | color: #f92672 124 | } 125 | 126 | /* Literal */ 127 | .chroma .l { 128 | color: #ae81ff 129 | } 130 | 131 | /* LiteralDate */ 132 | .chroma .ld { 133 | color: #e6db74 134 | } 135 | 136 | /* LiteralString */ 137 | .chroma .s { 138 | color: #e6db74 139 | } 140 | 141 | /* LiteralStringAffix */ 142 | .chroma .sa { 143 | color: #e6db74 144 | } 145 | 146 | /* LiteralStringBacktick */ 147 | .chroma .sb { 148 | color: #e6db74 149 | } 150 | 151 | /* LiteralStringChar */ 152 | .chroma .sc { 153 | color: #e6db74 154 | } 155 | 156 | /* LiteralStringDelimiter */ 157 | .chroma .dl { 158 | color: #e6db74 159 | } 160 | 161 | /* LiteralStringDoc */ 162 | .chroma .sd { 163 | color: #e6db74 164 | } 165 | 166 | /* LiteralStringDouble */ 167 | .chroma .s2 { 168 | color: #e6db74 169 | } 170 | 171 | /* LiteralStringEscape */ 172 | .chroma .se { 173 | color: #ae81ff 174 | } 175 | 176 | /* LiteralStringHeredoc */ 177 | .chroma .sh { 178 | color: #e6db74 179 | } 180 | 181 | /* LiteralStringInterpol */ 182 | .chroma .si { 183 | color: #e6db74 184 | } 185 | 186 | /* LiteralStringOther */ 187 | .chroma .sx { 188 | color: #e6db74 189 | } 190 | 191 | /* LiteralStringRegex */ 192 | .chroma .sr { 193 | color: #e6db74 194 | } 195 | 196 | /* LiteralStringSingle */ 197 | .chroma .s1 { 198 | color: #e6db74 199 | } 200 | 201 | /* LiteralStringSymbol */ 202 | .chroma .ss { 203 | color: #e6db74 204 | } 205 | 206 | /* LiteralNumber */ 207 | .chroma .m { 208 | color: #ae81ff 209 | } 210 | 211 | /* LiteralNumberBin */ 212 | .chroma .mb { 213 | color: #ae81ff 214 | } 215 | 216 | /* LiteralNumberFloat */ 217 | .chroma .mf { 218 | color: #ae81ff 219 | } 220 | 221 | /* LiteralNumberHex */ 222 | .chroma .mh { 223 | color: #ae81ff 224 | } 225 | 226 | /* LiteralNumberInteger */ 227 | .chroma .mi { 228 | color: #ae81ff 229 | } 230 | 231 | /* LiteralNumberIntegerLong */ 232 | .chroma .il { 233 | color: #ae81ff 234 | } 235 | 236 | /* LiteralNumberOct */ 237 | .chroma .mo { 238 | color: #ae81ff 239 | } 240 | 241 | /* Operator */ 242 | .chroma .o { 243 | color: #f92672 244 | } 245 | 246 | /* OperatorWord */ 247 | .chroma .ow { 248 | color: #f92672 249 | } 250 | 251 | /* Comment */ 252 | .chroma .c { 253 | color: #75715e 254 | } 255 | 256 | /* CommentHashbang */ 257 | .chroma .ch { 258 | color: #75715e 259 | } 260 | 261 | /* CommentMultiline */ 262 | .chroma .cm { 263 | color: #75715e 264 | } 265 | 266 | /* CommentSingle */ 267 | .chroma .c1 { 268 | color: #75715e 269 | } 270 | 271 | /* CommentSpecial */ 272 | .chroma .cs { 273 | color: #75715e 274 | } 275 | 276 | /* CommentPreproc */ 277 | .chroma .cp { 278 | color: #75715e 279 | } 280 | 281 | /* CommentPreprocFile */ 282 | .chroma .cpf { 283 | color: #75715e 284 | } 285 | 286 | /* GenericDeleted */ 287 | .chroma .gd { 288 | color: #f92672 289 | } 290 | 291 | /* GenericEmph */ 292 | .chroma .ge { 293 | font-style: italic 294 | } 295 | 296 | /* GenericInserted */ 297 | .chroma .gi { 298 | color: #a6e22e 299 | } 300 | 301 | /* GenericStrong */ 302 | .chroma .gs { 303 | font-weight: bold 304 | } 305 | 306 | /* GenericSubheading */ 307 | .chroma .gu { 308 | color: #75715e 309 | } 310 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | repository: 3 | name: prometheus-hcloud-sd 4 | description: Prometheus Service Discovery for Hetzner Cloud 5 | homepage: https://promhippie.github.io/prometheus-hcloud-sd/ 6 | topics: prometheus, service, discovery, sd, service-discovery, prometheus-exporter, hetzner, hcloud 7 | 8 | private: false 9 | has_issues: true 10 | has_wiki: false 11 | has_downloads: false 12 | 13 | default_branch: master 14 | 15 | allow_merge_commit: false 16 | allow_squash_merge: true 17 | allow_rebase_merge: true 18 | 19 | allow_update_branch: true 20 | allow_auto_merge: true 21 | delete_branch_on_merge: true 22 | enable_automated_security_fixes: true 23 | enable_vulnerability_alerts: true 24 | 25 | rulesets: 26 | - name: prevent destruction 27 | target: branch 28 | enforcement: active 29 | conditions: 30 | ref_name: 31 | include: 32 | - "~DEFAULT_BRANCH" 33 | exclude: [] 34 | rules: 35 | - type: required_linear_history 36 | - type: deletion 37 | - type: non_fast_forward 38 | 39 | - name: check verification 40 | target: branch 41 | enforcement: active 42 | conditions: 43 | ref_name: 44 | include: 45 | - "~DEFAULT_BRANCH" 46 | exclude: [] 47 | rules: 48 | - type: required_status_checks 49 | parameters: 50 | strict_required_status_checks_policy: true 51 | required_status_checks: 52 | - context: envvars 53 | integration_id: 15368 54 | - context: labels 55 | integration_id: 15368 56 | - context: docker 57 | integration_id: 15368 58 | - context: binaries 59 | integration_id: 15368 60 | - context: testing 61 | integration_id: 15368 62 | bypass_actors: 63 | - actor_id: 1 64 | actor_type: OrganizationAdmin 65 | bypass_mode: always 66 | - actor_id: 415759 # app 67 | actor_type: Integration 68 | bypass_mode: always 69 | - actor_id: 6359478 # bots 70 | actor_type: Team 71 | bypass_mode: always 72 | 73 | - name: require reviewing 74 | target: branch 75 | enforcement: active 76 | conditions: 77 | ref_name: 78 | include: 79 | - "~DEFAULT_BRANCH" 80 | exclude: [] 81 | rules: 82 | - type: pull_request 83 | parameters: 84 | allowed_merge_methods: 85 | - squash 86 | - rebase 87 | dismiss_stale_reviews_on_push: false 88 | require_code_owner_review: false 89 | require_last_push_approval: false 90 | required_approving_review_count: 0 91 | required_review_thread_resolution: false 92 | bypass_actors: 93 | - actor_id: 1 94 | actor_type: OrganizationAdmin 95 | bypass_mode: always 96 | - actor_id: 415759 # app 97 | actor_type: Integration 98 | bypass_mode: always 99 | - actor_id: 6359478 # bots 100 | actor_type: Team 101 | bypass_mode: always 102 | 103 | teams: 104 | - name: admins 105 | permission: admin 106 | - name: bots 107 | permission: admin 108 | - name: members 109 | permission: maintain 110 | 111 | labels: 112 | - name: bug 113 | color: fc2929 114 | description: Something isn't working 115 | - name: duplicate 116 | color: cccccc 117 | description: This issue or pull request already exists 118 | - name: enhancement 119 | color: 84b6eb 120 | description: New feature or request 121 | - name: good first issue 122 | color: 7057ff 123 | description: Good for newcomers 124 | - name: help wanted 125 | color: 159818 126 | description: Extra attention is needed 127 | - name: invalid 128 | color: e6e6e6 129 | description: This doesn't seem right 130 | - name: question 131 | color: cc317c 132 | description: Further information is requested 133 | - name: renovate 134 | color: 1d76db 135 | description: Automated action from Renovate 136 | - name: wontfix 137 | color: 5319e7 138 | description: This will not be worked on 139 | - name: hacktoberfest 140 | color: d4c5f9 141 | description: Contribution at Hacktoberfest appreciated 142 | - name: ready 143 | color: ededed 144 | description: This is ready to be worked on 145 | - name: in progress 146 | color: ededed 147 | description: This is currently worked on 148 | - name: infra 149 | color: 006b75 150 | description: Related to the infrastructure 151 | - name: lint 152 | color: fbca04 153 | description: Related to linting tools 154 | - name: poc 155 | color: c2e0c6 156 | description: Proof of concept for new feature 157 | - name: rebase 158 | color: ffa8a5 159 | description: Branch requires a rebase 160 | - name: third-party 161 | color: e99695 162 | description: Depends on third-party tool or library 163 | - name: translation 164 | color: b60205 165 | description: Change or issue related to translations 166 | - name: ci 167 | color: b60105 168 | description: Related to Continous Integration 169 | - name: docs 170 | color: b60305 171 | description: Related to documentation 172 | - name: outdated 173 | color: cccccc 174 | description: This is out of scope and outdated 175 | - name: tools 176 | color: 1d76db 177 | description: Tools updates used for CI and other tasks 178 | 179 | ... 180 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master" 4 | ], 5 | "plugins": [ 6 | [ 7 | "@semantic-release/commit-analyzer", 8 | { 9 | "preset": "conventionalcommits", 10 | "releaseRules": [ 11 | { 12 | "type": "major", 13 | "release": "major" 14 | }, 15 | { 16 | "type": "deps", 17 | "scope": "major", 18 | "release": "major" 19 | }, 20 | { 21 | "type": "minor", 22 | "release": "minor" 23 | }, 24 | { 25 | "type": "deps", 26 | "scope": "minor", 27 | "release": "minor" 28 | }, 29 | { 30 | "type": "patch", 31 | "release": "patch" 32 | }, 33 | { 34 | "type": "deps", 35 | "scope": "patch", 36 | "release": "patch" 37 | }, 38 | { 39 | "type": "refactor", 40 | "release": "minor" 41 | }, 42 | { 43 | "scope": "docs", 44 | "release": false 45 | } 46 | ] 47 | } 48 | ], 49 | [ 50 | "@semantic-release/release-notes-generator", 51 | { 52 | "preset": "conventionalcommits", 53 | "presetConfig": { 54 | "types": [ 55 | { 56 | "type": "major", 57 | "section": "Features" 58 | }, 59 | { 60 | "type": "deps", 61 | "scope": "major", 62 | "section": "Features" 63 | }, 64 | { 65 | "type": "minor", 66 | "section": "Features" 67 | }, 68 | { 69 | "type": "deps", 70 | "scope": "minor", 71 | "section": "Features" 72 | }, 73 | { 74 | "type": "patch", 75 | "section": "Bugfixes" 76 | }, 77 | { 78 | "type": "deps", 79 | "scope": "patch", 80 | "section": "Bugfixes" 81 | }, 82 | { 83 | "type": "feat", 84 | "section": "Features" 85 | }, 86 | { 87 | "type": "fix", 88 | "section": "Bugfixes" 89 | }, 90 | { 91 | "type": "chore", 92 | "section": "Miscellaneous", 93 | "hidden": false 94 | }, 95 | { 96 | "type": "docs", 97 | "hidden": true 98 | }, 99 | { 100 | "type": "refactor", 101 | "hidden": true 102 | } 103 | ] 104 | } 105 | } 106 | ], 107 | [ 108 | "@semantic-release/changelog", 109 | { 110 | "changelogTitle": "# Changelog" 111 | } 112 | ], 113 | [ 114 | "semantic-release-replace-plugin", 115 | { 116 | "replacements": [ 117 | { 118 | "files": [ 119 | "deploy/kubernetes/kustomization.yml" 120 | ], 121 | "from": "newTag: .*", 122 | "to": "newTag: ${nextRelease.version}", 123 | "results": [ 124 | { 125 | "file": "deploy/kubernetes/kustomization.yml", 126 | "hasChanged": true, 127 | "numMatches": 1, 128 | "numReplacements": 1 129 | } 130 | ], 131 | "countMatches": true 132 | } 133 | ] 134 | } 135 | ], 136 | [ 137 | "@semantic-release/github", 138 | { 139 | "assets": [] 140 | } 141 | ], 142 | [ 143 | "@semantic-release/git", 144 | { 145 | "message": "chore: release ${nextRelease.version}", 146 | "assets": [ 147 | "CHANGELOG.md", 148 | ".github/RELEASE", 149 | "deploy/kubernetes/kustomization.yml" 150 | ] 151 | } 152 | ] 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Promhippie 2 | 3 | Welcome! Our community focuses on helping others and making this project the 4 | best it can be. We gladly accept contributions and encourage you to get 5 | involved! 6 | 7 | ## Bug reports 8 | 9 | Please search the issues on the issue tracker with a variety of keywords to 10 | ensure your bug is not already reported. 11 | 12 | If unique, [open an issue][issues] and 13 | answer the questions so we can understand and reproduce the problematic 14 | behavior. 15 | 16 | The burden is on you to convince us that it is actually a bug in our project. 17 | This is easiest to do when you write clear, concise instructions so we can 18 | reproduce the behavior (even if it seems obvious). The more detailed and 19 | specific you are, the faster we will be able to help you. Check out 20 | [How to Report Bugs Effectively][bugreport]. 21 | 22 | Please be kind, remember that this project comes at no cost to you, and you're 23 | getting free help. 24 | 25 | ## Check for assigned people 26 | 27 | We are using Github Issues for submitting known issues, e.g. bugs, features, 28 | etc. Some issues will have someone assigned, meaning that there's already 29 | someone that takes responsibility for fixing said issue. This is not done to 30 | discourage contributions, rather to not step in the work that has already been 31 | done by the assignee. If you want to work on a known issue with someone already 32 | assigned to it, please consider contacting the assignee first, e.g. by 33 | mentioning the assignee in a new comment on the specific issue. This way you can 34 | contribute with ideas, or even with code if the assignee decides that you can 35 | step in. 36 | 37 | If you plan to work on a non assigned issue, please add a comment on the issue 38 | to prevent duplicated work. 39 | 40 | ## Minor improvements and new tests 41 | 42 | Submit pull requests at any time for minor changes or new tests. Make sure to 43 | write tests to assert your change is working properly and is thoroughly covered. 44 | We'll ask most pull requests to be squashed, especially with small commits. 45 | 46 | Your pull request may be thoroughly reviewed. This is because if we accept the 47 | PR, we also assume responsibility for it, although we would prefer you to help 48 | maintain your code after it gets merged. 49 | 50 | ## Mind the Style 51 | 52 | We believe that in order to have a healthy codebase we need to abide to a 53 | certain code style. We use `gofmt` with Go and `eslint` with Javascript for this 54 | matter, which are tools that has proved to be useful. So, before submitting your 55 | pull request, make sure that `gofmt` and if viable `eslint` are passing for you. 56 | 57 | Finally, note that `gofmt` and if viable `eslint` are called on the CI system. 58 | This means that your pull request will not be merged until the changes are 59 | approved. 60 | 61 | ## Update the Changelog 62 | 63 | We keep a changelog in the `CHANGELOG.md` file. This is useful to understand 64 | what has changed between each version. When you implement a new feature, or a 65 | fix for an issue, please also update the `CHANGELOG.md` file accordingly. We 66 | don't follow a strict style for the changelog, just try to be consistent with 67 | the rest of the file. 68 | 69 | ## Sign your work 70 | 71 | The sign-off is a simple line at the end of the explanation for the patch. Your 72 | signature certifies that you wrote the patch or otherwise have the right to pass 73 | it on as an open-source patch. The rules are pretty simple: If you can certify 74 | [DCO](./DCO), then you just add a line to every git commit message: 75 | 76 | ```console 77 | Signed-off-by: Joe Smith 78 | ``` 79 | 80 | Please use your real name, we really dislike pseudonyms or anonymous 81 | contributions. We are in the opensource world without secrets. If you set your 82 | `user.name` and `user.email` git configs, you can sign your commit automatically 83 | with `git commit -s`. 84 | 85 | ## Collaborator status 86 | 87 | If your pull request is merged, congratulations! You're technically a 88 | collaborator. We may also grant you "collaborator status" which means you can 89 | push to the repository and merge other pull requests. We hope that you will stay 90 | involved by reviewing pull requests, submitting more of your own, and resolving 91 | issues as you are able to. Thanks for making this project amazing! 92 | 93 | We ask that collaborators will conduct thorough code reviews and be nice to new 94 | contributors. Before merging a PR, it's best to get the approval of at least one 95 | or two other collaborators and/or the project owner. We prefer squashed commits 96 | instead of many little, semantically-unimportant commits. Also, CI and other 97 | post-commit hooks must pass before being merged except in certain unusual 98 | circumstances. 99 | 100 | Collaborator status may be removed for inactive users from time to time as we 101 | see fit; this is not an insult, just a basic security precaution in case the 102 | account becomes inactive or abandoned. Privileges can always be restored later. 103 | 104 | **Reviewing pull requests:** Please help submit and review pull requests as you 105 | are able! We would ask that every pull request be reviewed by at least one 106 | collaborator who did not open the pull request before merging. This will help 107 | ensure high code quality as new collaborators are added to the project. 108 | 109 | ## Vulnerabilities 110 | 111 | If you've found a vulnerability that is serious, please email to 112 | thomas@webhippie.de. If it's not a big deal, a pull request will probably be 113 | faster. 114 | 115 | ## Thank you 116 | 117 | Thanks for your help! This project would not be what it is today without your 118 | contributions. 119 | 120 | [issues]: https://github.com/promhippie/prometheus-hcloud-sd/issues 121 | [bugreport]: http://www.chiark.greenend.org.uk/~sgtatham/bugs.html 122 | -------------------------------------------------------------------------------- /docs/layouts/partials/style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 344 | -------------------------------------------------------------------------------- /pkg/action/discoverer.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hetznercloud/hcloud-go/v2/hcloud" 12 | "github.com/prometheus/common/model" 13 | "github.com/prometheus/prometheus/discovery/targetgroup" 14 | ) 15 | 16 | var ( 17 | // providerPrefix defines the general prefix for all labels. 18 | providerPrefix = model.MetaLabelPrefix + "hcloud_" 19 | 20 | // Labels defines all available labels for this provider. 21 | Labels = map[string]string{ 22 | "imageName": providerPrefix + "image_name", 23 | "imageType": providerPrefix + "image_type", 24 | "labelPrefix": providerPrefix + "label_", 25 | "locationCity": providerPrefix + "city", 26 | "locationCountry": providerPrefix + "country", 27 | "locationName": providerPrefix + "location", 28 | "name": providerPrefix + "name", 29 | "osFlavor": providerPrefix + "os_flavor", 30 | "osVersion": providerPrefix + "os_version", 31 | "project": providerPrefix + "project", 32 | "privateIPv4": providerPrefix + "ipv4_", 33 | "publicIPv4": providerPrefix + "public_ipv4", 34 | "publicIPv6": providerPrefix + "public_ipv6", 35 | "serverTypeCores": providerPrefix + "cores", 36 | "serverTypeCPU": providerPrefix + "cpu", 37 | "serverTypeDisk": providerPrefix + "disk", 38 | "serverTypeMemory": providerPrefix + "memory", 39 | "serverTypeName": providerPrefix + "type", 40 | "serverTypeStorage": providerPrefix + "storage", 41 | "status": providerPrefix + "status", 42 | } 43 | 44 | // replacer defines a list of characters that gets replaced. 45 | replacer = strings.NewReplacer( 46 | ".", "_", 47 | "-", "_", 48 | ) 49 | ) 50 | 51 | // Discoverer implements the Prometheus discoverer interface. 52 | type Discoverer struct { 53 | clients map[string]*hcloud.Client 54 | logger *slog.Logger 55 | refresh int 56 | separator string 57 | lasts map[string]struct{} 58 | } 59 | 60 | // Run initializes fetching the targets for service discovery. 61 | func (d Discoverer) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { 62 | ticker := time.NewTicker(time.Duration(d.refresh) * time.Second) 63 | 64 | for { 65 | targets, err := d.getTargets(ctx) 66 | 67 | if err == nil { 68 | ch <- targets 69 | } 70 | 71 | select { 72 | case <-ticker.C: 73 | continue 74 | case <-ctx.Done(): 75 | return 76 | } 77 | } 78 | } 79 | 80 | func (d *Discoverer) getTargets(ctx context.Context) ([]*targetgroup.Group, error) { 81 | current := make(map[string]struct{}) 82 | targets := make([]*targetgroup.Group, 0) 83 | 84 | for project, client := range d.clients { 85 | 86 | now := time.Now() 87 | 88 | networks, err := client.Network.All(ctx) 89 | 90 | if err != nil { 91 | d.logger.Warn("Failed to fetch networks", 92 | "project", project, 93 | "err", err, 94 | ) 95 | 96 | requestFailures.WithLabelValues(project).Inc() 97 | continue 98 | } 99 | 100 | servers, err := client.Server.All(ctx) 101 | 102 | if err != nil { 103 | d.logger.Warn("Failed to fetch servers", 104 | "project", project, 105 | "err", err, 106 | ) 107 | 108 | requestFailures.WithLabelValues(project).Inc() 109 | continue 110 | } 111 | 112 | requestDuration.WithLabelValues(project).Observe(time.Since(now).Seconds()) 113 | 114 | d.logger.Debug("Requested servers", 115 | "project", project, 116 | "count", len(servers), 117 | ) 118 | 119 | for _, server := range servers { 120 | var ( 121 | imageType string 122 | imageName string 123 | osFlavor string 124 | osVersion string 125 | ) 126 | 127 | if server.Image != nil { 128 | imageType = string(server.Image.Type) 129 | imageName = server.Image.Name 130 | osFlavor = server.Image.OSFlavor 131 | osVersion = server.Image.OSVersion 132 | } 133 | 134 | target := &targetgroup.Group{ 135 | Source: fmt.Sprintf("hcloud/%d", server.ID), 136 | Targets: []model.LabelSet{ 137 | { 138 | model.AddressLabel: model.LabelValue(server.PublicNet.IPv4.IP.String()), 139 | }, 140 | }, 141 | Labels: model.LabelSet{ 142 | model.AddressLabel: model.LabelValue(server.PublicNet.IPv4.IP.String()), 143 | model.LabelName(Labels["project"]): model.LabelValue(project), 144 | model.LabelName(Labels["name"]): model.LabelValue(server.Name), 145 | model.LabelName(Labels["status"]): model.LabelValue(server.Status), 146 | model.LabelName(Labels["publicIPv4"]): model.LabelValue(server.PublicNet.IPv4.IP.String()), 147 | model.LabelName(Labels["publicIPv6"]): model.LabelValue(server.PublicNet.IPv6.IP.String()), 148 | model.LabelName(Labels["serverTypeName"]): model.LabelValue(server.ServerType.Name), 149 | model.LabelName(Labels["serverTypeCores"]): model.LabelValue(strconv.Itoa(int(server.ServerType.Cores))), 150 | model.LabelName(Labels["serverTypeMemory"]): model.LabelValue(strconv.Itoa(int(server.ServerType.Memory))), 151 | model.LabelName(Labels["serverTypeDisk"]): model.LabelValue(strconv.Itoa(int(server.ServerType.Disk))), 152 | model.LabelName(Labels["serverTypeStorage"]): model.LabelValue(server.ServerType.StorageType), 153 | model.LabelName(Labels["serverTypeCPU"]): model.LabelValue(server.ServerType.CPUType), 154 | model.LabelName(Labels["locationName"]): model.LabelValue(server.Location.Name), 155 | model.LabelName(Labels["locationCity"]): model.LabelValue(server.Location.City), 156 | model.LabelName(Labels["locationCountry"]): model.LabelValue(server.Location.Country), 157 | model.LabelName(Labels["imageType"]): model.LabelValue(imageType), 158 | model.LabelName(Labels["imageName"]): model.LabelValue(imageName), 159 | model.LabelName(Labels["osFlavor"]): model.LabelValue(osFlavor), 160 | model.LabelName(Labels["osVersion"]): model.LabelValue(osVersion), 161 | }, 162 | } 163 | 164 | for key, value := range server.Labels { 165 | target.Labels[model.LabelName(normalizeLabel(Labels["labelPrefix"]+key))] = model.LabelValue(value) 166 | } 167 | 168 | for _, priv := range server.PrivateNet { 169 | for _, network := range networks { 170 | if network.ID == priv.Network.ID { 171 | target.Labels[model.LabelName(normalizeNetwork(Labels["privateIPv4"]+network.Name))] = model.LabelValue(priv.IP.String()) 172 | break 173 | } 174 | } 175 | } 176 | 177 | d.logger.Debug("Server added", 178 | "project", project, 179 | "source", target.Source, 180 | ) 181 | 182 | current[target.Source] = struct{}{} 183 | targets = append(targets, target) 184 | } 185 | 186 | } 187 | 188 | for k := range d.lasts { 189 | if _, ok := current[k]; !ok { 190 | d.logger.Debug("Server deleted", 191 | "source", k, 192 | ) 193 | 194 | targets = append( 195 | targets, 196 | &targetgroup.Group{ 197 | Source: k, 198 | }, 199 | ) 200 | } 201 | } 202 | 203 | d.lasts = current 204 | return targets, nil 205 | } 206 | 207 | func normalizeLabel(val string) string { 208 | return replacer.Replace(val) 209 | } 210 | 211 | func normalizeNetwork(val string) string { 212 | return replacer.Replace(val) 213 | } 214 | -------------------------------------------------------------------------------- /docs/content/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Usage" 3 | date: 2022-07-21T00:00:00+00:00 4 | anchor: "getting-started" 5 | weight: 10 6 | --- 7 | 8 | ## Installation 9 | 10 | We won't cover further details how to properly setup [Prometheus][prometheus] 11 | itself, we will only cover some basic setup based on [docker-compose][compose]. 12 | But if you want to run this service discovery without [docker-compose][compose] 13 | you should be able to adopt that to your needs. 14 | 15 | First of all we need to prepare a configuration for [Prometheus][prometheus] 16 | that includes the service discovery which simply maps to a node exporter. 17 | 18 | {{< highlight yaml >}} 19 | global: 20 | scrape_interval: 1m 21 | scrape_timeout: 10s 22 | evaluation_interval: 1m 23 | 24 | scrape_configs: 25 | - job_name: node 26 | file_sd_configs: 27 | - files: [ "/etc/sd/hcloud.json" ] 28 | relabel_configs: 29 | - source_labels: [__meta_hcloud_public_ipv4] 30 | replacement: "${1}:9100" 31 | target_label: __address__ 32 | - source_labels: [__meta_hcloud_location] 33 | target_label: location 34 | - source_labels: [__meta_hcloud_name] 35 | target_label: instance 36 | - job_name: hcloud-sd 37 | static_configs: 38 | - targets: 39 | - hcloud-sd:9000 40 | {{< / highlight >}} 41 | 42 | After preparing the configuration we need to create the `docker-compose.yml` 43 | within the same folder, this `docker-compose.yml` starts a simple 44 | [Prometheus][prometheus] instance together with the service discovery. Don't 45 | forget to update the environment variables with the required credentials. If you 46 | are using a different volume for the service discovery you have to make sure 47 | that the container user is allowed to write to this volume. 48 | 49 | {{< highlight yaml >}} 50 | version: '2' 51 | 52 | volumes: 53 | prometheus: 54 | 55 | services: 56 | prometheus: 57 | image: prom/prometheus:latest 58 | restart: always 59 | ports: 60 | - 9090:9090 61 | volumes: 62 | - prometheus:/prometheus 63 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 64 | - ./service-discovery:/etc/sd 65 | 66 | hcloud-sd: 67 | image: promhippie/prometheus-hcloud-sd:latest 68 | restart: always 69 | environment: 70 | - PROMETHEUS_HCLOUD_LOG_PRETTY=true 71 | - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=file 72 | - PROMETHEUS_HCLOUD_OUTPUT_FILE=/etc/sd/hcloud.json 73 | - PROMETHEUS_HCLOUD_TOKEN=your-token 74 | volumes: 75 | - ./service-discovery:/etc/sd 76 | {{< / highlight >}} 77 | 78 | Since our `latest` tag always refers to the `master` branch of the Git 79 | repository you should always use some fixed version. You can see all available 80 | tags at [DockerHub][dockerhub] or [Quay][quayio], there you will see that we 81 | also provide a manifest, you can easily start the exporter on various 82 | architectures without any change to the image name. You should apply a change 83 | like this to the `docker-compose.yml` file: 84 | 85 | {{< highlight diff >}} 86 | hcloud-sd: 87 | - image: promhippie/prometheus-hcloud-sd:latest 88 | + image: promhippie/prometheus-hcloud-sd:x.x.x 89 | restart: always 90 | environment: 91 | - PROMETHEUS_HCLOUD_LOG_PRETTY=true 92 | - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=file 93 | - PROMETHEUS_HCLOUD_OUTPUT_FILE=/etc/sd/hcloud.json 94 | - PROMETHEUS_HCLOUD_TOKEN=your-token 95 | volumes: 96 | - ./service-discovery:/etc/sd 97 | {{< / highlight >}} 98 | 99 | Depending on how you have launched and configured [Prometheus][prometheus] it's 100 | possible that it's running as user `nobody`, in that case you should run the 101 | service discovery as this user as well, otherwise [Prometheus][prometheus] won't 102 | be able to read the generated JSON file: 103 | 104 | {{< highlight diff >}} 105 | hcloud-sd: 106 | image: promhippie/prometheus-hcloud-sd:latest 107 | restart: always 108 | + user: '65534' 109 | environment: 110 | - PROMETHEUS_HCLOUD_LOG_PRETTY=true 111 | - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=file 112 | - PROMETHEUS_HCLOUD_OUTPUT_FILE=/etc/sd/hcloud.json 113 | - PROMETHEUS_HCLOUD_TOKEN=your-token 114 | volumes: 115 | - ./service-discovery:/etc/sd 116 | {{< / highlight >}} 117 | 118 | If you want to secure the access to the exporter or also the HTTP service 119 | discovery endpoint you can provide a web config. You just need to provide a path 120 | to the config file in order to enable the support for it, for details about the 121 | config format look at the [documentation](#web-configuration) section: 122 | 123 | {{< highlight diff >}} 124 | hcloud-sd: 125 | image: promhippie/prometheus-hcloud-sd:latest 126 | restart: always 127 | environment: 128 | + - PROMETHEUS_HCLOUD_WEB_CONFIG=path/to/web-config.json 129 | - PROMETHEUS_HCLOUD_LOG_PRETTY=true 130 | - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=file 131 | - PROMETHEUS_HCLOUD_OUTPUT_FILE=/etc/sd/hcloud.json 132 | - PROMETHEUS_HCLOUD_TOKEN=your-token 133 | volumes: 134 | - ./service-discovery:/etc/sd 135 | {{< / highlight >}} 136 | 137 | To avoid the dependency on a shared filesystem between this service discovery 138 | and the [Prometheus][prometheus] configuration directory, you are able to use 139 | the new [HTTP service discovery][httpsd] starting with 140 | [Prometheus][prometheus] >= v2.28, you just need to switch the engine for this 141 | service discovery: 142 | 143 | {{< highlight diff >}} 144 | hcloud-sd: 145 | image: promhippie/prometheus-hcloud-sd:latest 146 | restart: always 147 | environment: 148 | - PROMETHEUS_HCLOUD_LOG_PRETTY=true 149 | - - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=file 150 | + - PROMETHEUS_HCLOUD_OUTPUT_ENGINE=http 151 | - PROMETHEUS_HCLOUD_OUTPUT_FILE=/etc/sd/hcloud.json 152 | - PROMETHEUS_HCLOUD_TOKEN=your-token 153 | volumes: 154 | - ./service-discovery:/etc/sd 155 | {{< / highlight >}} 156 | 157 | To use the HTTP service discovery you just need to change the 158 | [Prometheus][prometheus] configuration mentioned above a little bit: 159 | 160 | {{< highlight yaml >}} 161 | scrape_configs: 162 | - job_name: node 163 | http_sd_configs: 164 | - url: http://hcloud-sd:9000/sd 165 | relabel_configs: 166 | - source_labels: [__meta_hcloud_public_ipv4] 167 | replacement: "${1}:9100" 168 | target_label: __address__ 169 | - source_labels: [__meta_hcloud_location] 170 | target_label: location 171 | - source_labels: [__meta_hcloud_name] 172 | target_label: instance 173 | {{< / highlight >}} 174 | 175 | Finally the service discovery should be configured fine, let's start this stack 176 | with [docker-compose][compose]], you just need to execute `docker-compose up` 177 | within the directory where you have stored `prometheus.yml` and 178 | `docker-compose.yml`. That's all, the service discovery should be up and 179 | running. You can access [Prometheus][prometheus] at 180 | [http://localhost:9090](http://localhost:9090). 181 | 182 | {{< figure src="service-discovery.png" title="Prometheus service discovery for Hetzner Cloud" >}} 183 | 184 | ## Configuration 185 | 186 | ### Environment variables 187 | 188 | If you prefer to configure the service with environment variables you can see 189 | the available variables below, in case you want to configure multiple accounts 190 | with a single service you are forced to use the configuration file as the 191 | environment variables are limited to a single account. As the service is pretty 192 | lightweight you can even start an instance per account and configure it entirely 193 | by the variables, it's up to you. 194 | 195 | {{< partial "envvars.md" >}} 196 | 197 | ### Web Configuration 198 | 199 | If you want to secure the service by TLS or by some basic authentication you can 200 | provide a `YAML` configuration file which follows the [Prometheus][prometheus] 201 | toolkit format. You can see a full configuration example within the 202 | [toolkit documentation][toolkit]. 203 | 204 | ### Configuration file 205 | 206 | Especially if you want to configure multiple accounts within a single service 207 | discovery you got to use the configuration file. So far we support the file 208 | formats `JSON` and `YAML`, if you want to get a full example configuration just 209 | take a look at [our repository][configs], there you can always see the latest 210 | configuration format. These example configurations include all available 211 | options, they also include the default values. 212 | 213 | ## Labels 214 | 215 | {{< partial "labels.md" >}} 216 | 217 | ## Metrics 218 | 219 | prometheus_hcloud_sd_request_duration_seconds{project} 220 | : Histogram of latencies for requests to the Hetzner Cloud API 221 | 222 | prometheus_hcloud_sd_request_failures_total{project} 223 | : Total number of failed requests to the Hetzner Cloud API 224 | 225 | [prometheus]: https://prometheus.io 226 | [compose]: https://docs.docker.com/compose/ 227 | [dockerhub]: https://hub.docker.com/r/promhippie/prometheus-hcloud-sd/tags/ 228 | [quayio]: https://quay.io/repository/promhippie/prometheus-hcloud-sd?tab=tags 229 | [httpsd]: https://prometheus.io/docs/prometheus/2.28/configuration/configuration/#http_sd_config 230 | [toolkit]: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md 231 | [configs]: https://github.com/promhippie/prometheus-hcloud-sd/tree/master/config 232 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "cachix": { 4 | "inputs": { 5 | "devenv": [ 6 | "devenv" 7 | ], 8 | "flake-compat": [ 9 | "devenv", 10 | "flake-compat" 11 | ], 12 | "git-hooks": [ 13 | "devenv", 14 | "git-hooks" 15 | ], 16 | "nixpkgs": [ 17 | "devenv", 18 | "nixpkgs" 19 | ] 20 | }, 21 | "locked": { 22 | "lastModified": 1760971495, 23 | "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=", 24 | "owner": "cachix", 25 | "repo": "cachix", 26 | "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "cachix", 31 | "ref": "latest", 32 | "repo": "cachix", 33 | "type": "github" 34 | } 35 | }, 36 | "devenv": { 37 | "inputs": { 38 | "cachix": "cachix", 39 | "flake-compat": "flake-compat", 40 | "flake-parts": "flake-parts", 41 | "git-hooks": "git-hooks", 42 | "nix": "nix", 43 | "nixpkgs": "nixpkgs" 44 | }, 45 | "locked": { 46 | "lastModified": 1766087669, 47 | "narHash": "sha256-1+LJXcOaeX5YCFCCCY+bh6nSQBS5fPVcudQs5/G2+P4=", 48 | "owner": "cachix", 49 | "repo": "devenv", 50 | "rev": "c03eed645ea94da7afbee29da76436b7ce33a5cb", 51 | "type": "github" 52 | }, 53 | "original": { 54 | "owner": "cachix", 55 | "repo": "devenv", 56 | "type": "github" 57 | } 58 | }, 59 | "flake-compat": { 60 | "flake": false, 61 | "locked": { 62 | "lastModified": 1761588595, 63 | "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", 64 | "owner": "edolstra", 65 | "repo": "flake-compat", 66 | "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "edolstra", 71 | "repo": "flake-compat", 72 | "type": "github" 73 | } 74 | }, 75 | "flake-compat_2": { 76 | "flake": false, 77 | "locked": { 78 | "lastModified": 1761588595, 79 | "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", 80 | "owner": "edolstra", 81 | "repo": "flake-compat", 82 | "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", 83 | "type": "github" 84 | }, 85 | "original": { 86 | "owner": "edolstra", 87 | "repo": "flake-compat", 88 | "type": "github" 89 | } 90 | }, 91 | "flake-parts": { 92 | "inputs": { 93 | "nixpkgs-lib": [ 94 | "devenv", 95 | "nixpkgs" 96 | ] 97 | }, 98 | "locked": { 99 | "lastModified": 1760948891, 100 | "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", 101 | "owner": "hercules-ci", 102 | "repo": "flake-parts", 103 | "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "hercules-ci", 108 | "repo": "flake-parts", 109 | "type": "github" 110 | } 111 | }, 112 | "flake-parts_2": { 113 | "inputs": { 114 | "nixpkgs-lib": "nixpkgs-lib" 115 | }, 116 | "locked": { 117 | "lastModified": 1765835352, 118 | "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", 119 | "owner": "hercules-ci", 120 | "repo": "flake-parts", 121 | "rev": "a34fae9c08a15ad73f295041fec82323541400a9", 122 | "type": "github" 123 | }, 124 | "original": { 125 | "owner": "hercules-ci", 126 | "repo": "flake-parts", 127 | "type": "github" 128 | } 129 | }, 130 | "git-hooks": { 131 | "inputs": { 132 | "flake-compat": [ 133 | "devenv", 134 | "flake-compat" 135 | ], 136 | "gitignore": "gitignore", 137 | "nixpkgs": [ 138 | "devenv", 139 | "nixpkgs" 140 | ] 141 | }, 142 | "locked": { 143 | "lastModified": 1760663237, 144 | "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", 145 | "owner": "cachix", 146 | "repo": "git-hooks.nix", 147 | "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", 148 | "type": "github" 149 | }, 150 | "original": { 151 | "owner": "cachix", 152 | "repo": "git-hooks.nix", 153 | "type": "github" 154 | } 155 | }, 156 | "git-hooks_2": { 157 | "inputs": { 158 | "flake-compat": "flake-compat_2", 159 | "gitignore": "gitignore_2", 160 | "nixpkgs": "nixpkgs_2" 161 | }, 162 | "locked": { 163 | "lastModified": 1765911976, 164 | "narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=", 165 | "owner": "cachix", 166 | "repo": "git-hooks.nix", 167 | "rev": "b68b780b69702a090c8bb1b973bab13756cc7a27", 168 | "type": "github" 169 | }, 170 | "original": { 171 | "owner": "cachix", 172 | "repo": "git-hooks.nix", 173 | "type": "github" 174 | } 175 | }, 176 | "gitignore": { 177 | "inputs": { 178 | "nixpkgs": [ 179 | "devenv", 180 | "git-hooks", 181 | "nixpkgs" 182 | ] 183 | }, 184 | "locked": { 185 | "lastModified": 1709087332, 186 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 187 | "owner": "hercules-ci", 188 | "repo": "gitignore.nix", 189 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 190 | "type": "github" 191 | }, 192 | "original": { 193 | "owner": "hercules-ci", 194 | "repo": "gitignore.nix", 195 | "type": "github" 196 | } 197 | }, 198 | "gitignore_2": { 199 | "inputs": { 200 | "nixpkgs": [ 201 | "git-hooks", 202 | "nixpkgs" 203 | ] 204 | }, 205 | "locked": { 206 | "lastModified": 1709087332, 207 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 208 | "owner": "hercules-ci", 209 | "repo": "gitignore.nix", 210 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 211 | "type": "github" 212 | }, 213 | "original": { 214 | "owner": "hercules-ci", 215 | "repo": "gitignore.nix", 216 | "type": "github" 217 | } 218 | }, 219 | "nix": { 220 | "inputs": { 221 | "flake-compat": [ 222 | "devenv", 223 | "flake-compat" 224 | ], 225 | "flake-parts": [ 226 | "devenv", 227 | "flake-parts" 228 | ], 229 | "git-hooks-nix": [ 230 | "devenv", 231 | "git-hooks" 232 | ], 233 | "nixpkgs": [ 234 | "devenv", 235 | "nixpkgs" 236 | ], 237 | "nixpkgs-23-11": [ 238 | "devenv" 239 | ], 240 | "nixpkgs-regression": [ 241 | "devenv" 242 | ] 243 | }, 244 | "locked": { 245 | "lastModified": 1761648602, 246 | "narHash": "sha256-H97KSB/luq/aGobKRuHahOvT1r7C03BgB6D5HBZsbN8=", 247 | "owner": "cachix", 248 | "repo": "nix", 249 | "rev": "3e5644da6830ef65f0a2f7ec22830c46285bfff6", 250 | "type": "github" 251 | }, 252 | "original": { 253 | "owner": "cachix", 254 | "ref": "devenv-2.30.6", 255 | "repo": "nix", 256 | "type": "github" 257 | } 258 | }, 259 | "nixpkgs": { 260 | "locked": { 261 | "lastModified": 1761313199, 262 | "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=", 263 | "owner": "cachix", 264 | "repo": "devenv-nixpkgs", 265 | "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", 266 | "type": "github" 267 | }, 268 | "original": { 269 | "owner": "cachix", 270 | "ref": "rolling", 271 | "repo": "devenv-nixpkgs", 272 | "type": "github" 273 | } 274 | }, 275 | "nixpkgs-lib": { 276 | "locked": { 277 | "lastModified": 1765674936, 278 | "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", 279 | "owner": "nix-community", 280 | "repo": "nixpkgs.lib", 281 | "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", 282 | "type": "github" 283 | }, 284 | "original": { 285 | "owner": "nix-community", 286 | "repo": "nixpkgs.lib", 287 | "type": "github" 288 | } 289 | }, 290 | "nixpkgs_2": { 291 | "locked": { 292 | "lastModified": 1764947035, 293 | "narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=", 294 | "owner": "NixOS", 295 | "repo": "nixpkgs", 296 | "rev": "a672be65651c80d3f592a89b3945466584a22069", 297 | "type": "github" 298 | }, 299 | "original": { 300 | "owner": "NixOS", 301 | "ref": "nixpkgs-unstable", 302 | "repo": "nixpkgs", 303 | "type": "github" 304 | } 305 | }, 306 | "nixpkgs_3": { 307 | "locked": { 308 | "lastModified": 1766125104, 309 | "narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=", 310 | "owner": "nixos", 311 | "repo": "nixpkgs", 312 | "rev": "7d853e518814cca2a657b72eeba67ae20ebf7059", 313 | "type": "github" 314 | }, 315 | "original": { 316 | "owner": "nixos", 317 | "ref": "nixpkgs-unstable", 318 | "repo": "nixpkgs", 319 | "type": "github" 320 | } 321 | }, 322 | "root": { 323 | "inputs": { 324 | "devenv": "devenv", 325 | "flake-parts": "flake-parts_2", 326 | "git-hooks": "git-hooks_2", 327 | "nixpkgs": "nixpkgs_3" 328 | } 329 | } 330 | }, 331 | "root": "root", 332 | "version": 7 333 | } 334 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/promhippie/prometheus-hcloud-sd 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.2.3 7 | github.com/hetznercloud/hcloud-go/v2 v2.33.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/oklog/run v1.2.0 10 | github.com/prometheus/client_golang v1.23.2 11 | github.com/prometheus/common v0.67.4 12 | github.com/prometheus/exporter-toolkit v0.15.0 13 | github.com/prometheus/prometheus v1.8.2-0.20210220213500-8c8de46003d1 14 | github.com/stretchr/testify v1.11.1 15 | github.com/urfave/cli/v3 v3.6.1 16 | gopkg.in/yaml.v3 v3.0.1 17 | ) 18 | 19 | require ( 20 | 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 21 | 4d63.com/gochecknoglobals v0.2.2 // indirect 22 | codeberg.org/chavacava/garif v0.2.0 // indirect 23 | dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect 24 | dev.gaijin.team/go/golib v0.6.0 // indirect 25 | github.com/4meepo/tagalign v1.4.3 // indirect 26 | github.com/Abirdcfly/dupword v0.1.7 // indirect 27 | github.com/AdminBenni/iota-mixing v1.0.0 // indirect 28 | github.com/AlwxSin/noinlineerr v1.0.5 // indirect 29 | github.com/Antonboom/errname v1.1.1 // indirect 30 | github.com/Antonboom/nilnil v1.1.1 // indirect 31 | github.com/Antonboom/testifylint v1.6.4 // indirect 32 | github.com/BurntSushi/toml v1.5.0 // indirect 33 | github.com/Djarvur/go-err113 v0.1.1 // indirect 34 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 35 | github.com/MirrexOne/unqueryvet v1.3.0 // indirect 36 | github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect 37 | github.com/alecthomas/chroma/v2 v2.20.0 // indirect 38 | github.com/alecthomas/go-check-sumtype v0.3.1 // indirect 39 | github.com/alexkohler/nakedret/v2 v2.0.6 // indirect 40 | github.com/alexkohler/prealloc v1.0.0 // indirect 41 | github.com/alfatraining/structtag v1.0.0 // indirect 42 | github.com/alingse/asasalint v0.0.11 // indirect 43 | github.com/alingse/nilnesserr v0.2.0 // indirect 44 | github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect 45 | github.com/ashanbrown/makezero/v2 v2.1.0 // indirect 46 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 47 | github.com/beorn7/perks v1.0.1 // indirect 48 | github.com/bkielbasa/cyclop v1.2.3 // indirect 49 | github.com/blizzy78/varnamelen v0.8.0 // indirect 50 | github.com/bombsimon/wsl/v4 v4.7.0 // indirect 51 | github.com/bombsimon/wsl/v5 v5.3.0 // indirect 52 | github.com/breml/bidichk v0.3.3 // indirect 53 | github.com/breml/errchkjson v0.4.1 // indirect 54 | github.com/butuzov/ireturn v0.4.0 // indirect 55 | github.com/butuzov/mirror v1.3.0 // indirect 56 | github.com/catenacyber/perfsprint v0.10.1 // indirect 57 | github.com/ccojocar/zxcvbn-go v1.0.4 // indirect 58 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 59 | github.com/charithe/durationcheck v0.0.11 // indirect 60 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 61 | github.com/charmbracelet/lipgloss v1.1.0 // indirect 62 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 63 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 64 | github.com/charmbracelet/x/term v0.2.1 // indirect 65 | github.com/ckaznocha/intrange v0.3.1 // indirect 66 | github.com/coreos/go-systemd/v22 v22.6.0 // indirect 67 | github.com/curioswitch/go-reassign v0.3.0 // indirect 68 | github.com/daixiang0/gci v0.13.7 // indirect 69 | github.com/dave/dst v0.27.3 // indirect 70 | github.com/davecgh/go-spew v1.1.1 // indirect 71 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 72 | github.com/dlclark/regexp2 v1.11.5 // indirect 73 | github.com/ettle/strcase v0.2.0 // indirect 74 | github.com/fatih/color v1.18.0 // indirect 75 | github.com/fatih/structtag v1.2.0 // indirect 76 | github.com/firefart/nonamedreturns v1.0.6 // indirect 77 | github.com/fsnotify/fsnotify v1.5.4 // indirect 78 | github.com/fzipp/gocyclo v0.6.0 // indirect 79 | github.com/ghostiam/protogetter v0.3.17 // indirect 80 | github.com/go-critic/go-critic v0.14.2 // indirect 81 | github.com/go-kit/kit v0.13.0 // indirect 82 | github.com/go-kit/log v0.2.1 // indirect 83 | github.com/go-logfmt/logfmt v0.6.0 // indirect 84 | github.com/go-toolsmith/astcast v1.1.0 // indirect 85 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 86 | github.com/go-toolsmith/astequal v1.2.0 // indirect 87 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 88 | github.com/go-toolsmith/astp v1.1.0 // indirect 89 | github.com/go-toolsmith/strparse v1.1.0 // indirect 90 | github.com/go-toolsmith/typep v1.1.0 // indirect 91 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 92 | github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect 93 | github.com/gobwas/glob v0.2.3 // indirect 94 | github.com/godoc-lint/godoc-lint v0.10.2 // indirect 95 | github.com/gofrs/flock v0.13.0 // indirect 96 | github.com/golang-jwt/jwt/v5 v5.3.0 // indirect 97 | github.com/golangci/asciicheck v0.5.0 // indirect 98 | github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect 99 | github.com/golangci/go-printf-func-name v0.1.1 // indirect 100 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect 101 | github.com/golangci/golangci-lint/v2 v2.7.2 // indirect 102 | github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect 103 | github.com/golangci/misspell v0.7.0 // indirect 104 | github.com/golangci/plugin-module-register v0.1.2 // indirect 105 | github.com/golangci/revgrep v0.8.0 // indirect 106 | github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect 107 | github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect 108 | github.com/google/go-cmp v0.7.0 // indirect 109 | github.com/google/uuid v1.6.0 // indirect 110 | github.com/gordonklaus/ineffassign v0.2.0 // indirect 111 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 112 | github.com/gostaticanalysis/comment v1.5.0 // indirect 113 | github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect 114 | github.com/gostaticanalysis/nilerr v0.1.2 // indirect 115 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect 116 | github.com/hashicorp/go-version v1.8.0 // indirect 117 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 118 | github.com/hashicorp/hcl v1.0.0 // indirect 119 | github.com/hexops/gotextdiff v1.0.3 // indirect 120 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 121 | github.com/jgautheron/goconst v1.8.2 // indirect 122 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 123 | github.com/jjti/go-spancheck v0.6.5 // indirect 124 | github.com/jpillora/backoff v1.0.0 // indirect 125 | github.com/julz/importas v0.2.0 // indirect 126 | github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect 127 | github.com/kisielk/errcheck v1.9.0 // indirect 128 | github.com/kkHAIKE/contextcheck v1.1.6 // indirect 129 | github.com/kulti/thelper v0.7.1 // indirect 130 | github.com/kunwardeep/paralleltest v1.0.15 // indirect 131 | github.com/lasiar/canonicalheader v1.1.2 // indirect 132 | github.com/ldez/exptostd v0.4.5 // indirect 133 | github.com/ldez/gomoddirectives v0.7.1 // indirect 134 | github.com/ldez/grignotin v0.10.1 // indirect 135 | github.com/ldez/tagliatelle v0.7.2 // indirect 136 | github.com/ldez/usetesting v0.5.0 // indirect 137 | github.com/leonklingele/grouper v1.1.2 // indirect 138 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 139 | github.com/macabu/inamedparam v0.2.0 // indirect 140 | github.com/magiconair/properties v1.8.6 // indirect 141 | github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect 142 | github.com/manuelarte/funcorder v0.5.0 // indirect 143 | github.com/maratori/testableexamples v1.0.1 // indirect 144 | github.com/maratori/testpackage v1.1.2 // indirect 145 | github.com/matoous/godox v1.1.0 // indirect 146 | github.com/mattn/go-colorable v0.1.14 // indirect 147 | github.com/mattn/go-isatty v0.0.20 // indirect 148 | github.com/mattn/go-runewidth v0.0.16 // indirect 149 | github.com/mdlayher/socket v0.4.1 // indirect 150 | github.com/mdlayher/vsock v1.2.1 // indirect 151 | github.com/mgechev/dots v1.0.0 // indirect 152 | github.com/mgechev/revive v1.13.0 // indirect 153 | github.com/mitchellh/go-homedir v1.1.0 // indirect 154 | github.com/mitchellh/mapstructure v1.5.0 // indirect 155 | github.com/moricho/tparallel v0.3.2 // indirect 156 | github.com/muesli/termenv v0.16.0 // indirect 157 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 158 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 159 | github.com/nakabonne/nestif v0.3.1 // indirect 160 | github.com/nishanths/exhaustive v0.12.0 // indirect 161 | github.com/nishanths/predeclared v0.2.2 // indirect 162 | github.com/nunnatsa/ginkgolinter v0.21.2 // indirect 163 | github.com/pelletier/go-toml v1.9.5 // indirect 164 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 165 | github.com/pmezard/go-difflib v1.0.0 // indirect 166 | github.com/polyfloyd/go-errorlint v1.8.0 // indirect 167 | github.com/prometheus/client_model v0.6.2 // indirect 168 | github.com/prometheus/procfs v0.16.1 // indirect 169 | github.com/quasilyte/go-ruleguard v0.4.5 // indirect 170 | github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect 171 | github.com/quasilyte/gogrep v0.5.0 // indirect 172 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 173 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 174 | github.com/raeperd/recvcheck v0.2.0 // indirect 175 | github.com/rivo/uniseg v0.4.7 // indirect 176 | github.com/rogpeppe/go-internal v1.14.1 // indirect 177 | github.com/ryancurrah/gomodguard v1.4.1 // indirect 178 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 179 | github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect 180 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect 181 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 182 | github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect 183 | github.com/securego/gosec/v2 v2.22.11-0.20251204091113-daccba6b93d7 // indirect 184 | github.com/sirupsen/logrus v1.9.3 // indirect 185 | github.com/sivchari/containedctx v1.0.3 // indirect 186 | github.com/sonatard/noctx v0.4.0 // indirect 187 | github.com/sourcegraph/go-diff v0.7.0 // indirect 188 | github.com/spf13/afero v1.15.0 // indirect 189 | github.com/spf13/cast v1.5.0 // indirect 190 | github.com/spf13/cobra v1.10.2 // indirect 191 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 192 | github.com/spf13/pflag v1.0.10 // indirect 193 | github.com/spf13/viper v1.12.0 // indirect 194 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 195 | github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect 196 | github.com/stretchr/objx v0.5.2 // indirect 197 | github.com/subosito/gotenv v1.4.1 // indirect 198 | github.com/tetafro/godot v1.5.4 // indirect 199 | github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect 200 | github.com/timonwong/loggercheck v0.11.0 // indirect 201 | github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect 202 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 203 | github.com/ultraware/funlen v0.2.0 // indirect 204 | github.com/ultraware/whitespace v0.2.0 // indirect 205 | github.com/uudashr/gocognit v1.2.0 // indirect 206 | github.com/uudashr/iface v1.4.1 // indirect 207 | github.com/xen0n/gosmopolitan v1.3.0 // indirect 208 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 209 | github.com/yagipy/maintidx v1.0.0 // indirect 210 | github.com/yeya24/promlinter v0.3.0 // indirect 211 | github.com/ykadowak/zerologlint v0.1.5 // indirect 212 | gitlab.com/bosi/decorder v0.4.2 // indirect 213 | go-simpler.org/musttag v0.14.0 // indirect 214 | go-simpler.org/sloglint v0.11.1 // indirect 215 | go.augendre.info/arangolint v0.3.1 // indirect 216 | go.augendre.info/fatcontext v0.9.0 // indirect 217 | go.uber.org/automaxprocs v1.6.0 // indirect 218 | go.uber.org/multierr v1.10.0 // indirect 219 | go.uber.org/zap v1.27.0 // indirect 220 | go.yaml.in/yaml/v2 v2.4.3 // indirect 221 | go.yaml.in/yaml/v3 v3.0.4 // indirect 222 | golang.org/x/crypto v0.46.0 // indirect 223 | golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect 224 | golang.org/x/mod v0.30.0 // indirect 225 | golang.org/x/net v0.48.0 // indirect 226 | golang.org/x/oauth2 v0.32.0 // indirect 227 | golang.org/x/sync v0.19.0 // indirect 228 | golang.org/x/sys v0.39.0 // indirect 229 | golang.org/x/text v0.32.0 // indirect 230 | golang.org/x/time v0.13.0 // indirect 231 | golang.org/x/tools v0.39.0 // indirect 232 | google.golang.org/protobuf v1.36.10 // indirect 233 | gopkg.in/ini.v1 v1.67.0 // indirect 234 | gopkg.in/yaml.v2 v2.4.0 // indirect 235 | honnef.co/go/tools v0.6.1 // indirect 236 | mvdan.cc/gofumpt v0.9.2 // indirect 237 | mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect 238 | ) 239 | 240 | tool ( 241 | github.com/golangci/golangci-lint/v2/cmd/golangci-lint 242 | github.com/mgechev/revive 243 | ) 244 | --------------------------------------------------------------------------------