├── .gitattributes ├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── requirements.txt └── xen-exporter.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | permissions: 8 | contents: read 9 | packages: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Build and push Docker image 17 | uses: pmorelli92/github-container-registry-build-push@2.0.0 18 | with: 19 | # Token such as GITHUB_TOKEN that has `write:packages` scope to authenticate against GCHR. 20 | github-push-secret: ${{ secrets.GITHUB_TOKEN }} 21 | # Docker Image name 22 | docker-image-name: xen-exporter 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | 3 | COPY . /app 4 | 5 | RUN python3 -m pip install -r /app/requirements.txt 6 | 7 | EXPOSE 9100/tcp 8 | 9 | STOPSIGNAL SIGINT 10 | ENTRYPOINT [ "python3", "/app/xen-exporter.py" ] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Michael Dombrowski 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xen-exporter 2 | XCP-ng (XenServer) Prometheus Exporter 3 | 4 | Automatically exports _all_ statistics from the [RRD metrics database](https://xapi-project.github.io/xen-api/metrics.html) from Xen. 5 | 6 | # Usage 7 | 8 | ```cmd 9 | docker run -e XEN_USER=root -e XEN_PASSWORD= -e XEN_HOST= -e XEN_SSL_VERIFY=true -p 9100:9100 --rm ghcr.io/mikedombo/xen-exporter:latest 10 | ``` 11 | 12 | > HALT_ON_NO_UUID - optional, false by default. Ignores metrics with no UUID 13 | 14 | > XEN_MODE - optional, "host" by default. "pool" if you want to parse all hosts from the pool 15 | 16 | # Grafana 17 | A Grafana dashboard is [available here](https://grafana.com/grafana/dashboards/16588) (id 16588), which graphs most of the critical metrics 18 | gathered by this exporter. 19 | 20 | ![Grafana dashboard sample 1](https://grafana.com/api/dashboards/16588/images/12479/image) 21 | ![Grafana dashboard sample 2](https://grafana.com/api/dashboards/16588/images/12482/image) 22 | 23 | 24 | 25 | # Example setup for a XenServer cluster 26 | 27 | docker-compose.yml 28 | 29 | ``` 30 | version: '2.4' 31 | services: 32 | xen01: 33 | container_name: xen01 34 | image: ghcr.io/mikedombo/xen-exporter:latest 35 | environment: 36 | - XEN_HOST=10.10.10.101 37 | - XEN_USER=root 38 | - XEN_PASSWORD=s0m3f4ncyp4ssw0rd 39 | - XEN_SSL_VERIFY=false 40 | 41 | xen02: 42 | container_name: xen02 43 | image: ghcr.io/mikedombo/xen-exporter:latest 44 | environment: 45 | - XEN_HOST=10.10.10.102 46 | - XEN_USER=root 47 | - XEN_PASSWORD=s0m3f4ncyp4ssw0rd 48 | - XEN_SSL_VERIFY=false 49 | ``` 50 | 51 | prometheus.yml 52 | 53 | ``` 54 | - job_name: xenserver 55 | scrape_interval: 60s 56 | scrape_timeout: 50s 57 | static_configs: 58 | - targets: 59 | - xen01:9100 60 | - xen02:9100 61 | ``` 62 | 63 | # Limitations 64 | 65 | No Prometheus help (comments) or types are currently emitted since all the metrics are being formatted almost entirely automatically. 66 | Meaning that there is no list in the code of what metrics will be emitted, nor is there a list of nice descriptions for each metric type. 67 | When using a cluster, assumes that the username and password of the poolmaster and hosts are the same. 68 | 69 | If you use XEN_MODE=pool, you must have the same credentials for all hosts in your pool 70 | 71 | # TODO 72 | - Proper Prometheus help and types for known metrics 73 | - Additional metrics beyond what RRD provides? Perhaps like https://github.com/lovoo/xenstats_exporter 74 | # List of all statistics 75 |
76 | 77 | - xen_host_avgqu_sz 78 | - xen_host_cpu 79 | - xen_host_cpu_avg 80 | - xen_host_cpu_avg_freq 81 | - xen_host_cpu_c0 82 | - xen_host_cpu_c1 83 | - xen_host_cpu_p0 84 | - xen_host_cpu_p1 85 | - xen_host_cpu_p2 86 | - xen_host_inflight 87 | - xen_host_io_throughput_read 88 | - xen_host_io_throughput_total 89 | - xen_host_io_throughput_write 90 | - xen_host_iops_read 91 | - xen_host_iops_total 92 | - xen_host_iops_write 93 | - xen_host_iowait 94 | - xen_host_latency 95 | - xen_host_loadavg 96 | - xen_host_memory_free_kib 97 | - xen_host_memory_reclaimed 98 | - xen_host_memory_reclaimed_max 99 | - xen_host_memory_total_kib 100 | - xen_host_pif_rx 101 | - xen_host_pif_tx 102 | - xen_host_pool_session_count 103 | - xen_host_pool_task_count 104 | - xen_host_read 105 | - xen_host_read_latency 106 | - xen_host_sr_cache_hits 107 | - xen_host_sr_cache_misses 108 | - xen_host_sr_cache_size 109 | - xen_host_tapdisks_in_low_memory_mode 110 | - xen_host_write 111 | - xen_host_write_latency 112 | - xen_host_xapi_allocation_kib 113 | - xen_host_xapi_free_memory_kib 114 | - xen_host_xapi_live_memory_kib 115 | - xen_host_xapi_memory_usage_kib 116 | - xen_host_xapi_open_fds 117 | - xen_vm_cpu 118 | - xen_vm_memory 119 | - xen_vm_memory_internal_free 120 | - xen_vm_memory_target 121 | - xen_vm_vbd_avgqu_sz 122 | - xen_vm_vbd_inflight 123 | - xen_vm_vbd_io_throughput_read 124 | - xen_vm_vbd_io_throughput_total 125 | - xen_vm_vbd_io_throughput_write 126 | - xen_vm_vbd_iops_read 127 | - xen_vm_vbd_iops_total 128 | - xen_vm_vbd_iops_write 129 | - xen_vm_vbd_iowait 130 | - xen_vm_vbd_latency 131 | - xen_vm_vbd_read 132 | - xen_vm_vbd_read_latency 133 | - xen_vm_vbd_write 134 | - xen_vm_vbd_write_latency 135 | - xen_vm_vif_rx 136 | - xen_vm_vif_tx 137 | - xen_collector_duration_seconds 138 |
139 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyjson5 2 | XenAPI 3 | -------------------------------------------------------------------------------- /xen-exporter.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import http.server 3 | import urllib.request 4 | import time 5 | import traceback 6 | import ssl 7 | import os 8 | import re 9 | 10 | import pyjson5 11 | import XenAPI 12 | 13 | 14 | # We aggressively cache the SRs, VMs, and hosts to avoid calling XAPI which can double the runtime (~0.8s to ~1.5s) 15 | # Mapping from UUID to human readable name 16 | srs = dict() 17 | vms = dict() 18 | hosts = dict() 19 | all_srs = set() 20 | 21 | def get_all_hosts_in_pool(session): 22 | list = [] 23 | xen_hosts = session.xenapi.host.get_all() 24 | for host in xen_hosts: 25 | list.append(session.xenapi.PIF.get_record(session.xenapi.host.get_management_interface(host))['IP']) 26 | return list 27 | 28 | def lookup_vm_name(vm_uuid, session): 29 | return session.xenapi.VM.get_name_label(session.xenapi.VM.get_by_uuid(vm_uuid)) 30 | 31 | 32 | def lookup_sr_name_by_uuid(sr_uuid, session): 33 | try: 34 | return session.xenapi.SR.get_name_label(session.xenapi.SR.get_by_uuid(sr_uuid)) 35 | except XenAPI.XenAPI.Failure: 36 | return sr_uuid 37 | 38 | 39 | def lookup_host_name(host_uuid, session): 40 | return session.xenapi.host.get_name_label( 41 | session.xenapi.host.get_by_uuid(host_uuid) 42 | ) 43 | 44 | 45 | def lookup_sr_uuid_by_ref(sr_ref, session): 46 | return session.xenapi.SR.get_uuid(sr_ref) 47 | 48 | 49 | def find_full_sr_uuid(beginning_uuid, xen, halt_on_no_uuid): 50 | for i in range(0, 2): 51 | uuid = list(filter(lambda x: x.startswith(beginning_uuid), all_srs)) 52 | if len(uuid) == 0: 53 | all_srs.update( 54 | set( 55 | map( 56 | lambda x: lookup_sr_uuid_by_ref(x, xen), 57 | xen.xenapi.SR.get_all(), 58 | ) 59 | ) 60 | ) 61 | continue # skip the rest of the loop and try the search again 62 | elif len(uuid) > 1: 63 | raise Exception(f"Found multiple SRs starting with UUID {beginning_uuid}") 64 | uuid = uuid[0] 65 | return uuid 66 | if halt_on_no_uuid: 67 | raise Exception(f"Found no SRs starting with UUID {beginning_uuid}") 68 | 69 | 70 | def get_or_set(d, key, func, *args): 71 | if key not in d: 72 | d[key] = func(key, *args) 73 | return d[key] 74 | 75 | 76 | def collect_poolmaster( 77 | xen_user: str, xen_password: str, xen_host: str, verify_ssl: bool 78 | ): 79 | try: 80 | with Xen("https://" + xen_host, xen_user, xen_password, verify_ssl) as xen: 81 | poolmaster = xen_host 82 | except XenAPI.XenAPI.Failure as e: 83 | ipPattern = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}") 84 | poolmaster = re.findall(ipPattern, str(e))[0] 85 | return poolmaster 86 | 87 | def collect_sr_usage(session: XenAPI.Session): 88 | sr_records = session.xenapi.SR.get_all_records() 89 | output = "" 90 | for sr_record in sr_records.values(): 91 | sr_name_label = sr_record["name_label"] 92 | sr_uuid = sr_record["uuid"] 93 | if "physical_size" in sr_record: 94 | output += f'xen_sr_physical_size{{sr_uuid="{sr_uuid}", sr="{sr_name_label}", type="{sr_record["type"]}", content_type="{sr_record["content_type"]}"}} {str(sr_record["physical_size"])}\n' 95 | 96 | if "physical_utilisation" in sr_record: 97 | output += f'xen_sr_physical_utilization{{sr_uuid="{sr_uuid}", sr="{sr_name_label}", type="{sr_record["type"]}", content_type="{sr_record["content_type"]}"}} {str(sr_record["physical_utilisation"])}\n' 98 | 99 | if "virtual_allocation" in sr_record: 100 | output += f'xen_sr_virtual_allocation{{sr_uuid="{sr_uuid}", sr="{sr_name_label}", type="{sr_record["type"]}", content_type="{sr_record["content_type"]}"}} {str(sr_record["virtual_allocation"])}\n' 101 | return output 102 | 103 | 104 | class Xen: 105 | def __init__(self, url, username, password, verify_ssl): 106 | self.session = XenAPI.Session(url, ignore_ssl=not verify_ssl) 107 | self.session.xenapi.login_with_password( 108 | username, password, "1.0", "xen-exporter" 109 | ) 110 | 111 | def __enter__(self): 112 | return self.session 113 | 114 | def __exit__(self, exc_type, exc_value, traceback): 115 | self.session.xenapi.session.logout() 116 | return False 117 | 118 | 119 | # Known SR metrics whose legends include the beginning of the UUID, rather than the full UUID 120 | sr_metrics = set( 121 | [ 122 | "io_throughput_total", 123 | "avgqu_sz", 124 | "inflight", 125 | "iops_write", 126 | "iops_total", 127 | "io_throughput_read", 128 | "read", 129 | "latency", 130 | "write_latency", 131 | "write", 132 | "io_throughput_write", 133 | "iowait", 134 | "read_latency", 135 | "iops_read", 136 | ] 137 | ) 138 | 139 | def collect_metrics(): 140 | xen_user = os.getenv("XEN_USER", "root") 141 | xen_password = os.getenv("XEN_PASSWORD", "") 142 | xen_host = os.getenv("XEN_HOST", "localhost") 143 | xen_mode = os.getenv("XEN_MODE", "host") 144 | verify_ssl = os.getenv("XEN_SSL_VERIFY", "true") 145 | verify_ssl = True if verify_ssl.lower() == "true" else False 146 | 147 | halt_on_no_uuid = os.getenv("HALT_ON_NO_UUID", "false") 148 | halt_on_no_uuid = True if halt_on_no_uuid.lower() == "true" else False 149 | 150 | collector_start_time = time.perf_counter() 151 | xen_poolmaster = collect_poolmaster( 152 | xen_user=xen_user, 153 | xen_password=xen_password, 154 | xen_host=xen_host, 155 | verify_ssl=verify_ssl, 156 | ) 157 | 158 | with Xen("https://" + xen_poolmaster, xen_user, xen_password, verify_ssl) as xen: 159 | if xen_mode == "host": 160 | xen_hosts =[xen_host] 161 | else: 162 | xen_hosts = get_all_hosts_in_pool(xen) 163 | 164 | output = "" 165 | for xen_host in xen_hosts: 166 | url = f"https://{xen_host}/rrd_updates?start={int(time.time()-10)}&json=true&host=true&cf=AVERAGE" 167 | 168 | req = urllib.request.Request(url) 169 | req.add_header( 170 | "Authorization", 171 | "Basic " 172 | + base64.b64encode((xen_user + ":" + xen_password).encode("utf-8")).decode( 173 | "utf-8" 174 | ), 175 | ) 176 | res = urllib.request.urlopen( 177 | req, context=None if verify_ssl else ssl._create_unverified_context() 178 | ) 179 | metrics = pyjson5.decode_io(res) 180 | 181 | for i, metric_name in enumerate(metrics["meta"]["legend"]): 182 | metric_legend = metric_name.split(":")[1:] 183 | collector_type = metric_legend[0] 184 | collector = metric_legend[1] 185 | metric_type = metric_legend[2] 186 | extra_tags = {f"{collector_type}": collector} 187 | 188 | if collector_type == "vm": 189 | vm = get_or_set(vms, collector, lookup_vm_name, xen) 190 | extra_tags["vm"] = vm 191 | extra_tags["vm_uuid"] = collector 192 | elif collector_type == "host": 193 | host = get_or_set(hosts, collector, lookup_host_name, xen) 194 | extra_tags["host"] = host 195 | extra_tags["host_uuid"] = collector 196 | 197 | if collector_type == "host" and "sr_" in metric_type: 198 | x = metric_type.split("sr_")[1] 199 | sr = get_or_set(srs, x.split("_")[0], lookup_sr_name_by_uuid, xen) 200 | extra_tags["sr"] = sr 201 | extra_tags["sr_uuid"] = x.split("_")[0] 202 | metric_type = "sr_" + "_".join(x.split("_")[1:]) 203 | 204 | # Handle SR metrics which don't have a full UUID (and don't have sr_) 205 | if ( 206 | collector_type == "host" 207 | and len(metric_type.split("_")[-1]) == 8 208 | and "_".join(metric_type.split("_")[0:-1]) in sr_metrics 209 | ): 210 | short_sr = metric_type.split("_")[-1] 211 | long_sr = find_full_sr_uuid(short_sr, xen, halt_on_no_uuid) 212 | if long_sr is not None: 213 | sr = get_or_set(srs, long_sr, lookup_sr_name_by_uuid, xen) 214 | extra_tags["sr"] = sr 215 | extra_tags["sr_uuid"] = long_sr 216 | metric_type = "_".join(metric_type.split("_")[0:-1]) 217 | 218 | if collector_type == "vm" and "vbd_" in metric_type: 219 | x = metric_type.split("vbd_")[1] 220 | extra_tags["vbd"] = x.split("_")[0] 221 | metric_type = "vbd_" + "_".join(x.split("_")[1:]) 222 | 223 | if collector_type == "vm" and "vif_" in metric_type: 224 | x = metric_type.split("vif_")[1] 225 | extra_tags["vif"] = x.split("_")[0] 226 | metric_type = "vif_" + "_".join(x.split("_")[1:]) 227 | 228 | if collector_type == "host" and "pif_" in metric_type: 229 | x = metric_type.split("pif_")[1] 230 | extra_tags["pif"] = x.split("_")[0] 231 | metric_type = "pif_" + "_".join(x.split("_")[1:]) 232 | 233 | if "cpu" in metric_type: 234 | x = metric_type.split("cpu")[1] 235 | if x.isnumeric(): 236 | extra_tags["cpu"] = x 237 | metric_type = "cpu" 238 | elif "-" in x: 239 | extra_tags["cpu"] = x.split("-")[0] 240 | metric_type = "cpu_" + x.split("-")[1] 241 | if "CPU" in metric_type: 242 | x = metric_type.split("CPU")[1] 243 | extra_tags["cpu"] = x.split("-")[0] 244 | metric_type = "cpu_" + "_".join(x.split("-")[1:]) 245 | 246 | # Normalize metric names to lowercase and underscores 247 | metric_type = metric_type.lower().replace("-", "_") 248 | 249 | tags = {f'{k}="{v}"' for k, v in extra_tags.items()} 250 | output += f"xen_{collector_type}_{metric_type}{{{', '.join(tags)}}} {metrics['data'][0]['values'][i]}\n" 251 | 252 | output += collect_sr_usage(xen) 253 | collector_end_time = time.perf_counter() 254 | output += f"xen_collector_duration_seconds {collector_end_time - collector_start_time}\n" 255 | return output 256 | 257 | class Handler(http.server.BaseHTTPRequestHandler): 258 | def __init__(self, request: bytes, client_address: tuple[str, int], server) -> None: 259 | super().__init__(request, client_address, server) 260 | 261 | def do_GET(self): 262 | try: 263 | metric_output = collect_metrics().encode("utf-8") 264 | self.send_response(200) 265 | self.send_header("Content-type", "text/plain") 266 | self.end_headers() 267 | self.wfile.write(metric_output) 268 | except BaseException: 269 | print(traceback.format_exc(), flush=True) 270 | self.send_response(500) 271 | 272 | 273 | if __name__ == "__main__": 274 | port = os.getenv("PORT", "9100") 275 | bind = os.getenv("BIND", "0.0.0.0") 276 | 277 | if os.getenv("XEN_MODE"): 278 | if os.getenv("XEN_MODE", "host") != "host" and os.getenv("XEN_MODE", "host") != "pool": 279 | raise Exception(f"Incorrect Mode: host or pool is required") 280 | http.server.HTTPServer( 281 | ( 282 | bind, 283 | int(port), 284 | ), 285 | Handler, 286 | ).serve_forever() --------------------------------------------------------------------------------