├── .github └── workflows │ ├── docker-image.yml │ └── release-chart.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── code ├── exporter.py └── requirements.txt ├── cr.yaml ├── deploy ├── docker-compose │ └── docker-compose.yml ├── helm-chart │ ├── .helmignore │ ├── Chart.yaml │ ├── src │ │ └── dashboards │ │ │ └── dashboard.json │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── clusterrolebinding.yaml │ │ ├── dashboard.yaml │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ ├── servicemonitor.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ └── values.yaml └── kubernetes-manifest │ └── kubernetes.yaml ├── example └── grafana-dashboard │ └── hetzner-load-balancer.json └── img ├── api_token.png ├── exporter_metrics.png ├── grafana_metrics.png └── hetzner_lb_metrics.png /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out the repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@4c0219f9ac95b02789c1075625400b2acbff50b1 21 | 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | 28 | - name: Extract metadata (tags, labels) for Docker 29 | id: meta 30 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 31 | with: 32 | images: wacken/hetzner-load-balancer-prometheus-exporter 33 | 34 | - name: Build and push Docker image 35 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 36 | with: 37 | context: . 38 | push: true 39 | tags: ${{ steps.meta.outputs.tags }} 40 | labels: ${{ steps.meta.outputs.labels }} 41 | platforms: linux/amd64,linux/arm64 42 | -------------------------------------------------------------------------------- /.github/workflows/release-chart.yaml: -------------------------------------------------------------------------------- 1 | name: Release Charts 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | permissions: {} 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write # to push chart release and create a release (helm/chart-releaser-action) 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Configure Git 23 | run: | 24 | git config user.name "$GITHUB_ACTOR" 25 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 26 | 27 | - name: Install Helm 28 | uses: azure/setup-helm@v1 29 | with: 30 | version: v3.5.2 31 | 32 | - name: Run chart-releaser 33 | uses: helm/chart-releaser-action@v1.5.0 34 | with: 35 | charts_dir: deploy 36 | config: cr.yaml 37 | env: 38 | CR_TOKEN: "${{ secrets.CR_TOKEN }}" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | RUN addgroup --gid 11000 app && \ 4 | adduser -uid 11001 --disabled-login -gid 11000 --home /code app 5 | 6 | COPY code /code 7 | RUN pip install --no-cache-dir -r /code/requirements.txt 8 | 9 | WORKDIR /code 10 | ENV PYTHONPATH '/code/' 11 | 12 | EXPOSE 8000 13 | 14 | USER 11001 15 | 16 | CMD ["python" , "-u", "/code/exporter.py"] 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hetzner Load Balancer Prometheus Exporter 2 | 3 | Exports metrics from Hetzner Load Balancer for consumption by Prometheus 4 | 5 | ## Preparing 6 | 7 | ### API TOKEN 8 | 9 | Go to [Hetzner Console](console.hetzner.cloud). Open project where you have running Load Balancer and create `API TOKEN` in Security section 10 | 11 | ![api token](img/api_token.png "API TOKEN") 12 | 13 | ### Load Balancer ID 14 | 15 | Next we sholud get `ID` of our Load Balancer. This information we will get from `Hetzner API`, everything about `API` you find in [official API documentation](https://docs.hetzner.cloud/#load-balancers-get-all-load-balancers) 16 | 17 | Example `curl` 18 | 19 | ```bash 20 | curl \ 21 | -H "Authorization: Bearer $API_TOKEN" \ 22 | 'https://api.hetzner.cloud/v1/load_balancers' 23 | ``` 24 | 25 | Response sample 26 | 27 | ```json 28 | { 29 | "load_balancers": [ 30 | { 31 | "id": 4711, 32 | "name": "Web Frontend", 33 | "public_net": { 34 | "enabled": false, 35 | "ipv4": { 36 | "ip": "1.2.3.4" 37 | }, 38 | ... 39 | } 40 | } 41 | ``` 42 | 43 | ### Configuring 44 | 45 | The exporter can be configured using environment variables. Instead of providing the values directly, you can also use the variables suffixed with `_FILE` to provide a file containing the value, which is useful with mounted secrets, for example. 46 | 47 | | Enviroment | Description | 48 | | ------- | ------ | 49 | | `LOAD_BALANCER_IDS` | Supported string with specific id `11,22,33` or `all` for scraping metrics from all load balancers in the project | 50 | | `LOAD_BALANCER_IDS_PATH` | Path to a file containing the load balancer IDs | 51 | | `ACCESS_TOKEN` | Hetzner API token | 52 | | `ACCESS_TOKEN_PATH` | Path to a file containing the Hetzner API token | 53 | | Optional `SCRAPE_INTERVAL` | value in seconds, default value is `30 seconds` | 54 | | Optional `SCRAPE_INTERVAL_PATH` | Path to a file containing the scrape interval | 55 | 56 | #### Kubernetes usage 57 | 58 | In `deploy/kubernetes.yaml` add in `env` section id which we got from `API` and `API TOKEN` 59 | 60 | ```yaml 61 | env: 62 | - name: LOAD_BALANCER_IDS 63 | value: "11,22,33,44" 64 | - name: ACCESS_TOKEN 65 | value: "ewsfds43r*****132" 66 | ## Optional 67 | - name: SCRAPE_INTERVAL 68 | value: '60' 69 | ``` 70 | 71 | Deploy it to Kubernetes cluster 72 | 73 | ```bash 74 | kubectl apply -f deploy/kubernetes.yaml 75 | ``` 76 | 77 | Or use [helm](https://helm.sh/docs/) to deploy the exporter: 78 | In `deploy/helm-chart/values.yaml` add in `env` section id which we got from `API` and `API TOKEN` 79 | 80 | ```bash 81 | # Add repo 82 | helm repo add wacken89 https://wacken89.github.io/hetzner-load-balancer-prometheus-exporter 83 | helm repo update 84 | # Install chart 85 | helm install hcloud-lb-exporter wacken89/hetzner-load-balancer-exporter -f values.yml 86 | ``` 87 | 88 | ### Check metrics page 89 | 90 | ```bash 91 | kubectl port-forward 8000:8000 92 | ``` 93 | 94 | Open in your browser `localhost:8000`: 95 | 96 | ![exporter metrics](img/exporter_metrics.png) 97 | 98 | 99 | ## Grafana 100 | 101 | Grafana Dashboard you can find [here](example/grafana-dashboard/hetzner-load-balancer.json) 102 | 103 | Metrics in Hetzner console 104 | ![Hetzner console](img/hetzner_lb_metrics.png) 105 | 106 | Metrics in Grafana 107 | ![exporter metrics](img/grafana_metrics.png) 108 | -------------------------------------------------------------------------------- /code/exporter.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import json 4 | import os 5 | import sys 6 | from typing import Optional 7 | from prometheus_client import start_http_server, Gauge, Info 8 | import requests 9 | from pathlib import Path 10 | 11 | 12 | def load_env(var: str, default: Optional[any] = None) -> Optional[str]: 13 | if var in os.environ: 14 | return os.environ[var] 15 | 16 | path_var = f"{var}_PATH" 17 | 18 | if path_var not in os.environ: 19 | if default is None: 20 | raise KeyError(f"Neither variable '{var}' nor '{path_var}' are defined") 21 | 22 | print(f"Variable '{var}' is not defined, using default '{default}'") 23 | return default 24 | 25 | path = Path(os.environ[path_var]) 26 | try: 27 | with path.open("r", encoding="utf-8") as file: 28 | return file.read().strip() 29 | except FileNotFoundError as error: 30 | if default is None: 31 | raise KeyError(f"Missing secret file '{path}' specified for '{path_var}'") from error 32 | 33 | print(f"Missing secret file for '{path_var}', using default '{default}'") 34 | return default 35 | 36 | 37 | try: 38 | load_balancer_ids = load_env('LOAD_BALANCER_IDS') 39 | access_token = load_env('ACCESS_TOKEN') 40 | SCRAPE_INTERVAL = load_env('SCRAPE_INTERVAL', 30) 41 | except KeyError as error: 42 | print(str(error)[1:-1]) 43 | exit(1) 44 | 45 | HETZNER_CLOUD_API_URL_BASE = 'https://api.hetzner.cloud/v1' 46 | HETZNER_CLOUD_API_URL_LB = f'{HETZNER_CLOUD_API_URL_BASE}/load_balancers/' 47 | HETZNER_CLOUD_API_URL_SERVER = f'{HETZNER_CLOUD_API_URL_BASE}/servers/' 48 | 49 | 50 | def get_all_load_balancers_ids() -> dict: 51 | url = f'{HETZNER_CLOUD_API_URL_LB}' 52 | headers = { 53 | 'Content-type': "application/json", 54 | 'Authorization': f"Bearer {access_token}" 55 | } 56 | 57 | get = requests.get(url, headers=headers) 58 | return get.json()['load_balancers'] 59 | 60 | 61 | def get_load_balancer_info(lbid) -> dict: 62 | url = f'{HETZNER_CLOUD_API_URL_LB}{lbid}' 63 | 64 | headers = { 65 | 'Content-type': "application/json", 66 | 'Authorization': f"Bearer {access_token}" 67 | } 68 | 69 | get = requests.get(url, headers=headers) 70 | return get.json() 71 | 72 | 73 | def get_all_server_names() -> dict: 74 | url = f'{HETZNER_CLOUD_API_URL_SERVER}' 75 | 76 | headers = { 77 | 'Content-type': "application/json", 78 | 'Authorization': f"Bearer {access_token}" 79 | } 80 | 81 | get = requests.get(url, headers=headers) 82 | return {x['id']: x['name'] for x in get.json()['servers']} 83 | 84 | 85 | def get_server_info(server_id) -> dict: 86 | url = f'{HETZNER_CLOUD_API_URL_SERVER}{server_id}' 87 | 88 | headers = { 89 | 'Content-type': "application/json", 90 | 'Authorization': f"Bearer {access_token}" 91 | } 92 | 93 | get = requests.get(url, headers=headers) 94 | return get.json() 95 | 96 | 97 | def get_server_name_from_cache(server_id: str) -> str: 98 | global server_name_cache 99 | if server_id in server_name_cache: 100 | return server_name_cache[server_id] 101 | else: 102 | # Refresh cache 103 | server_name_cache = get_all_server_names() 104 | # Check again 105 | if server_id in server_name_cache: 106 | return server_name_cache[server_id] 107 | else: 108 | # If still not found, return id as name 109 | return server_id 110 | 111 | 112 | def get_metrics(metrics_type, lbid): 113 | utc_offset_sec = time.altzone if time.localtime().tm_isdst else time.timezone 114 | utc_offset = datetime.timedelta(seconds=-utc_offset_sec) 115 | hetzner_date = datetime.datetime.now().replace(tzinfo=datetime.timezone(offset=utc_offset)).isoformat() 116 | 117 | url = f"{HETZNER_CLOUD_API_URL_LB}{lbid}/metrics" 118 | 119 | headers = { 120 | 'Content-type': "application/json", 121 | 'Authorization': f"Bearer {access_token}" 122 | } 123 | 124 | data = { 125 | "type": metrics_type, 126 | "start": hetzner_date, 127 | "end": hetzner_date, 128 | "step": 60 129 | } 130 | 131 | get = requests.get(url, headers=headers, data=json.dumps(data)) 132 | return get.json() 133 | 134 | 135 | if __name__ == '__main__': 136 | 137 | print('Hetzner Load Balancer Exporter is starting ...') 138 | 139 | if load_balancer_ids.lower() == 'all': 140 | load_balancer_ids_list = [] 141 | for key in get_all_load_balancers_ids(): 142 | load_balancer_ids_list.append(key['id']) 143 | else: 144 | load_balancer_ids_list = list(load_balancer_ids.split(",")) 145 | 146 | print('Getting Info from Hetzner ...\n') 147 | print(f"Found Load Balancer{'s' if len(load_balancer_ids_list) > 1 else ''}") 148 | 149 | load_balancer_full_list = [] 150 | 151 | for load_balancer_id in load_balancer_ids_list: 152 | load_balancer = get_load_balancer_info(load_balancer_id).get('load_balancer') 153 | try: 154 | load_balancer_name = load_balancer['name'] 155 | except Exception as e: 156 | print("Couldn't get field", e ) 157 | sys.exit(1) 158 | 159 | 160 | for services in load_balancer['services']: 161 | if services['protocol'] in ['http', 'https']: 162 | LOAD_BALANCER_TYPE = 'http' 163 | else: 164 | LOAD_BALANCER_TYPE = 'tcp' 165 | 166 | load_balancer_full_list.append([load_balancer_id, load_balancer_name, LOAD_BALANCER_TYPE]) 167 | 168 | print(f'\n\tName:\t{load_balancer_name}\n\tId:\t{load_balancer_id}\n\tType:\t{LOAD_BALANCER_TYPE}') 169 | 170 | print(f'\nScrape interval: {SCRAPE_INTERVAL} seconds') 171 | 172 | print('\nBuilding server name cache from Hetzner for labeling ...') 173 | server_name_cache = get_all_server_names() 174 | print(f'Retrieved {len(server_name_cache.keys())} server names from Hetzner ...\n') 175 | 176 | id_name_list = ['hetzner_load_balancer_id', 'hetzner_load_balancer_name'] 177 | hetzner_load_balancer_info = Info('hetzner_load_balancer', 'Hetzner Load Balancer Exporter build info') 178 | hetzner_openconnections = Gauge('hetzner_load_balancer_open_connections', 'Open Connections on Hetzner Load Balancer', id_name_list) 179 | hetzner_connections_per_second = Gauge('hetzner_load_balancer_connections_per_second', 'Connections per Second on Hetzner Load Balancer', id_name_list) 180 | hetzner_requests_per_second = Gauge('hetzner_load_balancer_requests_per_second', 'Requests per Second on Hetzner Load Balancer', id_name_list) 181 | hetzner_bandwidth_in = Gauge('hetzner_load_balancer_bandwidth_in', 'Bandwidth in on Hetzner Load Balancer', id_name_list) 182 | hetzner_bandwidth_out = Gauge('hetzner_load_balancer_bandwidth_out', 'Bandwidth out on Hetzner Load Balancer', id_name_list) 183 | id_name_service_list = id_name_list + ['hetzner_target_id', 'hetzner_target_name', 'hetzner_target_port'] 184 | hetzner_service_state = Gauge('hetzner_load_balancer_service_state', 'Health status of Load Balancer\'s services', id_name_service_list) 185 | 186 | start_http_server(8000) 187 | print('\nHetzner Load Balancer Exporter started') 188 | print('Visit http://localhost:8000/ to view the metrics') 189 | hetzner_load_balancer_info.info({'version': '1.2.0', 'buildhost': 'drake0103@gmail.com'}) 190 | 191 | while True: 192 | for load_balancer_id, lb_name, load_balancer_type in load_balancer_full_list: 193 | hetzner_openconnections.labels(hetzner_load_balancer_id=load_balancer_id, 194 | hetzner_load_balancer_name=lb_name).set(get_metrics('open_connections',load_balancer_id)["metrics"]["time_series"]["open_connections"]["values"][0][1]) 195 | hetzner_connections_per_second.labels(hetzner_load_balancer_id=load_balancer_id, 196 | hetzner_load_balancer_name=lb_name).set(get_metrics('connections_per_second',load_balancer_id)["metrics"]["time_series"]["connections_per_second"]["values"][0][1]) 197 | if load_balancer_type == 'http': 198 | hetzner_requests_per_second.labels(hetzner_load_balancer_id=load_balancer_id, 199 | hetzner_load_balancer_name=lb_name).set(get_metrics('requests_per_second',load_balancer_id)["metrics"]["time_series"]["requests_per_second"]["values"][0][1]) 200 | hetzner_bandwidth_in.labels(hetzner_load_balancer_id=load_balancer_id, 201 | hetzner_load_balancer_name=lb_name).set(get_metrics('bandwidth',load_balancer_id)["metrics"]["time_series"]["bandwidth.in"]["values"][0][1]) 202 | hetzner_bandwidth_out.labels(hetzner_load_balancer_id=load_balancer_id, 203 | hetzner_load_balancer_name=lb_name).set(get_metrics('bandwidth',load_balancer_id)["metrics"]["time_series"]["bandwidth.out"]["values"][0][1]) 204 | 205 | lb_info = get_load_balancer_info(load_balancer_id)['load_balancer'] 206 | 207 | targets = [] 208 | for x in lb_info['targets']: 209 | if x['type'] == 'server': 210 | targets.append(x) 211 | elif x['type'] == 'label_selector': 212 | targets.extend(x['targets']) 213 | 214 | for target in targets: 215 | for health_status in target['health_status']: 216 | hetzner_service_state.labels(hetzner_load_balancer_id=load_balancer_id, 217 | hetzner_load_balancer_name=lb_name, 218 | hetzner_target_id=target['server']['id'], 219 | hetzner_target_name=get_server_name_from_cache(target['server']['id']), 220 | hetzner_target_port=health_status['listen_port'])\ 221 | .set((1 if health_status['status'] == 'healthy' else 0)) 222 | time.sleep(int(SCRAPE_INTERVAL)) 223 | -------------------------------------------------------------------------------- /code/requirements.txt: -------------------------------------------------------------------------------- 1 | prometheus-client==0.13.1 2 | datetime==4.3 3 | requests==2.24.0 -------------------------------------------------------------------------------- /cr.yaml: -------------------------------------------------------------------------------- 1 | sign: false -------------------------------------------------------------------------------- /deploy/docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | hetzner-load-balancer-prometheus-exporter: 4 | image: test 5 | container_name: hetzner-lb 6 | ports: 7 | - 8000 8 | environment: 9 | LOAD_BALANCER_IDS: 'all' 10 | ACCESS_TOKEN: '' 11 | SCRAPE_INTERVAL: '30' 12 | -------------------------------------------------------------------------------- /deploy/helm-chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/helm-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: hetzner-load-balancer-exporter 3 | description: Hetzner Load Balancer Prometheus Exporter 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 1.5.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: 1.5.0 24 | -------------------------------------------------------------------------------- /deploy/helm-chart/src/dashboards/dashboard.json: -------------------------------------------------------------------------------- 1 | ../../../../example/grafana-dashboard/hetzner-load-balancer.json -------------------------------------------------------------------------------- /deploy/helm-chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if contains "NodePort" .Values.service.type }} 3 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "exporter.fullname" . }}) 4 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 5 | echo http://$NODE_IP:$NODE_PORT 6 | {{- else if contains "LoadBalancer" .Values.service.type }} 7 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 8 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "exporter.fullname" . }}' 9 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "exporter.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 10 | echo http://$SERVICE_IP:{{ .Values.service.port }} 11 | {{- else if contains "ClusterIP" .Values.service.type }} 12 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "exporter.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 13 | echo "Visit http://127.0.0.1:8080 to use your application" 14 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "exporter.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "exporter.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "exporter.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "exporter.labels" -}} 37 | helm.sh/chart: {{ include "exporter.chart" . }} 38 | {{ include "exporter.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "exporter.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "exporter.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "exporter.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "exporter.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.enabled (not (eq .Values.rbac.name "")) }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: {{ .Values.rbac.type }}Binding 4 | metadata: 5 | labels: 6 | {{- include "exporter.labels" . | nindent 4 }} 7 | name: {{ include "exporter.fullname" . }} 8 | {{- if eq .Values.rbac.type "Role" }} 9 | namespace: {{ .Release.Namespace }} 10 | {{- end }} 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: {{ .Values.rbac.type }} 14 | name: {{ .Values.rbac.name }} 15 | subjects: 16 | - kind: ServiceAccount 17 | name: {{ include "exporter.serviceAccountName" . }} 18 | namespace: {{ .Release.Namespace }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/dashboard.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.dashboard.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | labels: 6 | {{- include "exporter.labels" . | nindent 4 }} 7 | {{- toYaml .Values.dashboard.labels | nindent 4 }} 8 | name: {{ include "exporter.fullname" . }} 9 | namespace: {{ .Values.dashboard.namespace | default .Release.Namespace }} 10 | data: 11 | dashboard.yaml: |- 12 | {{ $.Files.Get "src/dashboards/dashboard.json" | fromJson | toJson }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "exporter.fullname" . }} 5 | labels: 6 | {{- include "exporter.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "exporter.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "exporter.selectorLabels" . | nindent 8 }} 22 | {{- with .Values.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "exporter.serviceAccountName" . }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }} 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | env: 39 | {{- toYaml .Values.env | nindent 12 }} 40 | imagePullPolicy: {{ .Values.image.pullPolicy }} 41 | ports: 42 | - name: http 43 | containerPort: 8000 44 | protocol: TCP 45 | livenessProbe: 46 | httpGet: 47 | path: / 48 | port: http 49 | readinessProbe: 50 | httpGet: 51 | path: / 52 | port: http 53 | resources: 54 | {{- toYaml .Values.resources | nindent 12 }} 55 | {{- with .Values.nodeSelector }} 56 | nodeSelector: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.affinity }} 60 | affinity: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.tolerations }} 64 | tolerations: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "exporter.fullname" . }} 6 | labels: 7 | {{- include "exporter.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "exporter.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "exporter.fullname" . }} 5 | labels: 6 | {{- include "exporter.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "exporter.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "exporter.serviceAccountName" . }} 6 | labels: 7 | {{- include "exporter.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "exporter.fullname" . }} 6 | namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }} 7 | labels: 8 | {{- include "exporter.labels" . | nindent 4 }} 9 | {{- if .Values.serviceMonitor.labels }} 10 | {{- toYaml .Values.serviceMonitor.labels | nindent 4 }} 11 | {{- end }} 12 | {{- if .Values.serviceMonitor.annotations }} 13 | annotations: 14 | {{- toYaml .Values.serviceMonitor.annotations | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | jobLabel: {{ .Values.serviceMonitor.jobLabel | quote }} 18 | selector: 19 | matchLabels: 20 | {{- include "exporter.selectorLabels" . | nindent 6 }} 21 | endpoints: 22 | - port: http 23 | scheme: "http" 24 | {{- if .Values.serviceMonitor.interval }} 25 | interval: {{ .Values.serviceMonitor.interval }} 26 | {{- end }} 27 | {{- if .Values.serviceMonitor.scrapeTimeout }} 28 | scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} 29 | {{- end }} 30 | {{- if .Values.serviceMonitor.honorLabels }} 31 | honorLabels: {{ .Values.serviceMonitor.honorLabels }} 32 | {{- end }} 33 | {{- if .Values.serviceMonitor.metricRelabelings }} 34 | metricRelabelings: {{ .Values.serviceMonitor.metricRelabelings }} 35 | {{- end }} 36 | {{- if .Values.serviceMonitor.relabelings }} 37 | relabelings: {{ .Values.serviceMonitor.relabelings }} 38 | {{- end }} 39 | namespaceSelector: 40 | matchNames: 41 | - {{ .Release.Namespace }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /deploy/helm-chart/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "exporter.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "exporter.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "exporter.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /deploy/helm-chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for exporter. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: wacken/hetzner-load-balancer-prometheus-exporter 9 | pullPolicy: Always 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: latest 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | rbac: 27 | enabled: false 28 | type: ClusterRole # ClusterRole or Role 29 | name: "" # example: system:auth-delegator 30 | 31 | podAnnotations: 32 | prometheus.io/scrape: 'true' 33 | prometheus.io/port: '8000' 34 | prometheus.io/path: '/' 35 | 36 | podLabels: {} 37 | 38 | env: 39 | # Set envs like in the kubernetes pod spec 40 | - name: LOAD_BALANCER_IDS 41 | value: "all" 42 | - name: SCRAPE_INTERVAL 43 | value: "60" 44 | - name: ACCESS_TOKEN 45 | value: "" 46 | # Or set token via secret ref 47 | # - name: ACCESS_TOKEN 48 | # valueFrom: 49 | # secretKeyRef: 50 | # key: token 51 | # name: hcloud 52 | 53 | 54 | podSecurityContext: {} 55 | # fsGroup: 2000 56 | 57 | securityContext: 58 | capabilities: 59 | drop: 60 | - ALL 61 | readOnlyRootFilesystem: true 62 | runAsNonRoot: true 63 | runAsUser: 11001 64 | seccompProfile: 65 | type: RuntimeDefault 66 | 67 | service: 68 | type: ClusterIP 69 | port: 8000 70 | 71 | resources: {} 72 | # We usually recommend not to specify default resources and to leave this as a conscious 73 | # choice for the user. This also increases chances charts run on environments with little 74 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 75 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 76 | # limits: 77 | # cpu: 100m 78 | # memory: 128Mi 79 | # requests: 80 | # cpu: 100m 81 | # memory: 128Mi 82 | 83 | autoscaling: 84 | enabled: false 85 | minReplicas: 1 86 | maxReplicas: 100 87 | targetCPUUtilizationPercentage: 80 88 | # targetMemoryUtilizationPercentage: 80 89 | 90 | nodeSelector: {} 91 | 92 | tolerations: [] 93 | 94 | affinity: {} 95 | 96 | dashboard: 97 | enabled: false 98 | namespace: "" # will be set to .Release.Namespace if empty 99 | labels: 100 | grafana_dashboard: "1" 101 | 102 | serviceMonitor: 103 | enabled: false 104 | namespace: "" 105 | annotations: {} 106 | labels: {} 107 | jobLabel: "" 108 | honorLabels: false 109 | interval: "" 110 | scrapeTimeout: "" 111 | metricRelabelings: [] 112 | relabelings: [] 113 | selector: {} 114 | -------------------------------------------------------------------------------- /deploy/kubernetes-manifest/kubernetes.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hetzner-load-balancer-prometheus-exporter 5 | labels: 6 | app: hetzner-load-balancer-prometheus-exporter 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hetzner-load-balancer-prometheus-exporter 12 | template: 13 | metadata: 14 | labels: 15 | app: hetzner-load-balancer-prometheus-exporter 16 | annotations: 17 | prometheus.io/scrape: 'true' 18 | prometheus.io/port: '8000' 19 | prometheus.io/path: '/' 20 | spec: 21 | containers: 22 | - name: exporter 23 | image: wacken/hetzner-load-balancer-prometheus-exporter:latest 24 | env: 25 | - name: LOAD_BALANCER_IDS 26 | value: "" 27 | # Uncoment this part if want to use read IDs from file 28 | # - name: LOAD_BALANCER_IDS_PATH 29 | # value: "" 30 | 31 | - name: ACCESS_TOKEN 32 | value: "" 33 | # Uncoment this part if want to use read Token from file 34 | # - name: ACCESS_TOKEN_FILE 35 | # value: "" 36 | 37 | # Optional 38 | # - name: SCRAPE_INTERVAL 39 | # value: "" 40 | # - name: SCRAPE_INTERVAL_PATH 41 | # value: "" 42 | resources: 43 | requests: 44 | memory: "128Mi" 45 | cpu: "250m" 46 | limits: 47 | memory: "128Mi" 48 | cpu: "250m" 49 | securityContext: 50 | capabilities: 51 | drop: 52 | - ALL 53 | readOnlyRootFilesystem: true 54 | runAsNonRoot: true 55 | runAsUser: 11001 56 | seccompProfile: 57 | type: RuntimeDefault 58 | ports: 59 | - containerPort: 8000 -------------------------------------------------------------------------------- /example/grafana-dashboard/hetzner-load-balancer.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [], 3 | "__requires": [ 4 | { 5 | "type": "grafana", 6 | "id": "grafana", 7 | "name": "Grafana", 8 | "version": "7.1.1" 9 | }, 10 | { 11 | "type": "panel", 12 | "id": "graph", 13 | "name": "Graph", 14 | "version": "" 15 | }, 16 | { 17 | "type": "datasource", 18 | "id": "prometheus", 19 | "name": "Prometheus", 20 | "version": "1.0.0" 21 | } 22 | ], 23 | "annotations": { 24 | "list": [ 25 | { 26 | "builtIn": 1, 27 | "datasource": "-- Grafana --", 28 | "enable": true, 29 | "hide": true, 30 | "iconColor": "rgba(0, 211, 255, 1)", 31 | "name": "Annotations & Alerts", 32 | "type": "dashboard" 33 | } 34 | ] 35 | }, 36 | "editable": true, 37 | "gnetId": null, 38 | "graphTooltip": 0, 39 | "id": null, 40 | "iteration": 1603287400201, 41 | "links": [], 42 | "panels": [ 43 | { 44 | "aliasColors": {}, 45 | "bars": false, 46 | "dashLength": 10, 47 | "dashes": false, 48 | "datasource": "$datasource", 49 | "fieldConfig": { 50 | "defaults": { 51 | "custom": {} 52 | }, 53 | "overrides": [] 54 | }, 55 | "fill": 1, 56 | "fillGradient": 0, 57 | "gridPos": { 58 | "h": 9, 59 | "w": 24, 60 | "x": 0, 61 | "y": 0 62 | }, 63 | "hiddenSeries": false, 64 | "id": 2, 65 | "legend": { 66 | "alignAsTable": true, 67 | "avg": true, 68 | "current": true, 69 | "max": true, 70 | "min": true, 71 | "show": true, 72 | "total": false, 73 | "values": true 74 | }, 75 | "lines": true, 76 | "linewidth": 1, 77 | "nullPointMode": "null", 78 | "percentage": false, 79 | "pluginVersion": "7.1.1", 80 | "pointradius": 2, 81 | "points": false, 82 | "renderer": "flot", 83 | "seriesOverrides": [], 84 | "spaceLength": 10, 85 | "stack": false, 86 | "steppedLine": false, 87 | "targets": [ 88 | { 89 | "expr": "hetzner_load_balancer_open_connections{hetzner_load_balancer_name=\"$loadbalancer\"}", 90 | "interval": "", 91 | "legendFormat": "{{ hetzner_load_balancer_name }}", 92 | "refId": "A" 93 | } 94 | ], 95 | "thresholds": [], 96 | "timeFrom": null, 97 | "timeRegions": [], 98 | "timeShift": null, 99 | "title": "Open Connections", 100 | "tooltip": { 101 | "shared": true, 102 | "sort": 0, 103 | "value_type": "individual" 104 | }, 105 | "type": "graph", 106 | "xaxis": { 107 | "buckets": null, 108 | "mode": "time", 109 | "name": null, 110 | "show": true, 111 | "values": [] 112 | }, 113 | "yaxes": [ 114 | { 115 | "format": "short", 116 | "label": null, 117 | "logBase": 1, 118 | "max": null, 119 | "min": null, 120 | "show": true 121 | }, 122 | { 123 | "format": "short", 124 | "label": null, 125 | "logBase": 1, 126 | "max": null, 127 | "min": null, 128 | "show": true 129 | } 130 | ], 131 | "yaxis": { 132 | "align": false, 133 | "alignLevel": null 134 | } 135 | }, 136 | { 137 | "aliasColors": {}, 138 | "bars": false, 139 | "dashLength": 10, 140 | "dashes": false, 141 | "datasource": "$datasource", 142 | "fieldConfig": { 143 | "defaults": { 144 | "custom": {} 145 | }, 146 | "overrides": [] 147 | }, 148 | "fill": 1, 149 | "fillGradient": 0, 150 | "gridPos": { 151 | "h": 8, 152 | "w": 24, 153 | "x": 0, 154 | "y": 9 155 | }, 156 | "hiddenSeries": false, 157 | "id": 4, 158 | "legend": { 159 | "alignAsTable": true, 160 | "avg": false, 161 | "current": true, 162 | "max": true, 163 | "min": true, 164 | "show": true, 165 | "total": false, 166 | "values": true 167 | }, 168 | "lines": true, 169 | "linewidth": 1, 170 | "nullPointMode": "null", 171 | "percentage": false, 172 | "pluginVersion": "7.1.1", 173 | "pointradius": 2, 174 | "points": false, 175 | "renderer": "flot", 176 | "seriesOverrides": [], 177 | "spaceLength": 10, 178 | "stack": false, 179 | "steppedLine": false, 180 | "targets": [ 181 | { 182 | "expr": "hetzner_load_balancer_connections_per_second{hetzner_load_balancer_name=\"$loadbalancer\"}", 183 | "interval": "", 184 | "legendFormat": "{{ hetzner_load_balancer_name }} cps", 185 | "refId": "A" 186 | } 187 | ], 188 | "thresholds": [], 189 | "timeFrom": null, 190 | "timeRegions": [], 191 | "timeShift": null, 192 | "title": "Connections per Second", 193 | "tooltip": { 194 | "shared": true, 195 | "sort": 0, 196 | "value_type": "individual" 197 | }, 198 | "type": "graph", 199 | "xaxis": { 200 | "buckets": null, 201 | "mode": "time", 202 | "name": null, 203 | "show": true, 204 | "values": [] 205 | }, 206 | "yaxes": [ 207 | { 208 | "format": "short", 209 | "label": null, 210 | "logBase": 1, 211 | "max": null, 212 | "min": null, 213 | "show": true 214 | }, 215 | { 216 | "format": "short", 217 | "label": null, 218 | "logBase": 1, 219 | "max": null, 220 | "min": null, 221 | "show": true 222 | } 223 | ], 224 | "yaxis": { 225 | "align": false, 226 | "alignLevel": null 227 | } 228 | }, 229 | { 230 | "aliasColors": {}, 231 | "bars": false, 232 | "dashLength": 10, 233 | "dashes": false, 234 | "datasource": "$datasource", 235 | "fieldConfig": { 236 | "defaults": { 237 | "custom": {} 238 | }, 239 | "overrides": [] 240 | }, 241 | "fill": 1, 242 | "fillGradient": 0, 243 | "gridPos": { 244 | "h": 8, 245 | "w": 24, 246 | "x": 0, 247 | "y": 17 248 | }, 249 | "hiddenSeries": false, 250 | "id": 6, 251 | "legend": { 252 | "alignAsTable": true, 253 | "avg": true, 254 | "current": true, 255 | "max": true, 256 | "min": true, 257 | "rightSide": false, 258 | "show": true, 259 | "total": false, 260 | "values": true 261 | }, 262 | "lines": true, 263 | "linewidth": 1, 264 | "nullPointMode": "null", 265 | "percentage": false, 266 | "pluginVersion": "7.1.1", 267 | "pointradius": 2, 268 | "points": false, 269 | "renderer": "flot", 270 | "seriesOverrides": [ 271 | { 272 | "alias": "/.*out.*/", 273 | "transform": "negative-Y" 274 | } 275 | ], 276 | "spaceLength": 10, 277 | "stack": false, 278 | "steppedLine": false, 279 | "targets": [ 280 | { 281 | "expr": "hetzner_load_balancer_bandwidth_in{hetzner_load_balancer_name=\"$loadbalancer\"}", 282 | "interval": "", 283 | "legendFormat": "{{ hetzner_load_balancer_name }}in", 284 | "refId": "A" 285 | }, 286 | { 287 | "expr": "hetzner_load_balancer_bandwidth_out{hetzner_load_balancer_name=\"$loadbalancer\"}", 288 | "instant": false, 289 | "interval": "", 290 | "legendFormat": "{{ hetzner_load_balancer_name }}out", 291 | "refId": "B" 292 | } 293 | ], 294 | "thresholds": [], 295 | "timeFrom": null, 296 | "timeRegions": [], 297 | "timeShift": null, 298 | "title": "Bandwidth", 299 | "tooltip": { 300 | "shared": true, 301 | "sort": 0, 302 | "value_type": "individual" 303 | }, 304 | "type": "graph", 305 | "xaxis": { 306 | "buckets": null, 307 | "mode": "time", 308 | "name": null, 309 | "show": true, 310 | "values": [] 311 | }, 312 | "yaxes": [ 313 | { 314 | "format": "bits", 315 | "label": "- out / + in", 316 | "logBase": 1, 317 | "max": null, 318 | "min": null, 319 | "show": true 320 | }, 321 | { 322 | "format": "short", 323 | "label": null, 324 | "logBase": 1, 325 | "max": null, 326 | "min": null, 327 | "show": false 328 | } 329 | ], 330 | "yaxis": { 331 | "align": false, 332 | "alignLevel": null 333 | } 334 | } 335 | ], 336 | "schemaVersion": 26, 337 | "style": "dark", 338 | "tags": [], 339 | "templating": { 340 | "list": [ 341 | { 342 | "current": { 343 | "selected": true, 344 | "text": "default", 345 | "value": "default" 346 | }, 347 | "hide": 0, 348 | "includeAll": false, 349 | "label": "Prometheus", 350 | "multi": false, 351 | "name": "datasource", 352 | "options": [], 353 | "query": "prometheus", 354 | "queryValue": "", 355 | "refresh": 1, 356 | "regex": "", 357 | "skipUrlSync": false, 358 | "type": "datasource" 359 | }, 360 | { 361 | "allValue": null, 362 | "current": {}, 363 | "datasource": "$datasource", 364 | "definition": "label_values(hetzner_load_balancer_info,hetzner_load_balancer_name)", 365 | "hide": 0, 366 | "includeAll": false, 367 | "label": "Load Balancer", 368 | "multi": false, 369 | "name": "loadbalancer", 370 | "options": [], 371 | "query": "label_values(hetzner_load_balancer_info,hetzner_load_balancer_name)", 372 | "refresh": 1, 373 | "regex": "", 374 | "skipUrlSync": false, 375 | "sort": 0, 376 | "tagValuesQuery": "", 377 | "tags": [], 378 | "tagsQuery": "", 379 | "type": "query", 380 | "useTags": false 381 | } 382 | ] 383 | }, 384 | "time": { 385 | "from": "now-30m", 386 | "to": "now" 387 | }, 388 | "timepicker": { 389 | "refresh_intervals": [ 390 | "10s", 391 | "30s", 392 | "1m", 393 | "5m", 394 | "15m", 395 | "30m", 396 | "1h", 397 | "2h", 398 | "1d" 399 | ] 400 | }, 401 | "timezone": "", 402 | "title": "Hetzner Load Balancer", 403 | "uid": "QW3gNNpGz", 404 | "version": 1 405 | } -------------------------------------------------------------------------------- /img/api_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wacken89/hetzner-load-balancer-prometheus-exporter/cdc83181b43401fee5e9b7c2316926fabb358039/img/api_token.png -------------------------------------------------------------------------------- /img/exporter_metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wacken89/hetzner-load-balancer-prometheus-exporter/cdc83181b43401fee5e9b7c2316926fabb358039/img/exporter_metrics.png -------------------------------------------------------------------------------- /img/grafana_metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wacken89/hetzner-load-balancer-prometheus-exporter/cdc83181b43401fee5e9b7c2316926fabb358039/img/grafana_metrics.png -------------------------------------------------------------------------------- /img/hetzner_lb_metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wacken89/hetzner-load-balancer-prometheus-exporter/cdc83181b43401fee5e9b7c2316926fabb358039/img/hetzner_lb_metrics.png --------------------------------------------------------------------------------