{"id":166,"date":"2026-08-17T00:55:51","date_gmt":"2026-08-17T00:55:51","guid":{"rendered":"http:\/\/127.0.0.1\/en\/2026\/08\/17\/monitoring-the-openstack-lab-prometheus-grafana-ceph\/"},"modified":"2026-08-17T00:55:51","modified_gmt":"2026-08-17T00:55:51","slug":"monitoring-the-openstack-lab-prometheus-grafana-ceph","status":"publish","type":"post","link":"https:\/\/wp-nv.radut.info\/ro\/2026\/08\/17\/monitoring-the-openstack-lab-prometheus-grafana-ceph\/","title":{"rendered":"Monitoring the OpenStack Lab: Prometheus, Grafana, Ceph"},"content":{"rendered":"<p><strong>Scope:<\/strong> this covers how the OpenStack lab and its Ceph cluster are actually monitored \u2014 the exporter stack, what Ceph exposes natively, the Prometheus recording rules that make the dashboards fast, a tour of the real dashboard panels (with screenshots), day-2 Ceph status commands, and how to load-test the Ceph cluster itself with <code>dd<\/code>\/<code>fio<\/code>. For how the cloud and its network\/BGP are built, see <strong>OpenStack Lab Deployment<\/strong>; for what runs on top of it, see <strong>Kubernetes on OpenStack<\/strong>. This article doesn&#8217;t repeat either \u2014 it&#8217;s monitoring-only.<\/p>\n<h2>1. The exporter stack<\/h2>\n<p>Every metric on these dashboards comes from one of these, laid down by a single Ansible playbook (<code>monitoring.yml<\/code>) that targets the whole OpenStack host group plus one dedicated monitoring VM:<\/p>\n<table>\n<tr>\n<th>Exporter<\/th>\n<th>What it covers<\/th>\n<th>Where it runs<\/th>\n<\/tr>\n<tr>\n<td><code>node_exporter<\/code><\/td>\n<td>Standard host metrics (CPU, memory, disk, network) for each physical OpenStack host<\/td>\n<td>Every OpenStack host, as a pinned-tag Docker container under a long-lived systemd unit<\/td>\n<\/tr>\n<tr>\n<td>cadvisor<\/td>\n<td>Container-level RAM\/CPU for every LXC container OSA creates (one per infra service) and any Ceph containers<\/td>\n<td>Every OpenStack host<\/td>\n<\/tr>\n<tr>\n<td>process-exporter<\/td>\n<td>Per-process RAM\/CPU \u2014 what&#8217;s actually holding memory: <code>ceph-osd<\/code>, <code>mysqld<\/code>, <code>rabbitmq<\/code>\/<code>beam.smp<\/code>, <code>nova-*<\/code>, <code>ovs<\/code>, <code>qemu<\/code> \u2014 beyond what Nova&#8217;s own VM-allocation view shows<\/td>\n<td>Every OpenStack host<\/td>\n<\/tr>\n<tr>\n<td>lvm-exporter (<code>hansmi\/prometheus-lvm-exporter<\/code>)<\/td>\n<td>LVM volume-group\/logical-volume usage \u2014 node_exporter has no native LVM collector, needed for both OpenStack block-storage VGs and Ceph OSD VGs<\/td>\n<td>Every OpenStack host<\/td>\n<\/tr>\n<tr>\n<td>openstack-exporter<\/td>\n<td>Centralized Nova\/Neutron\/Cinder\/Glance\/Keystone API and resource metrics \u2014 queries Keystone <em>once<\/em> for the whole cloud, not per-node<\/td>\n<td>The monitoring VM itself, not the OpenStack cluster (a separate inventory owns it)<\/td>\n<\/tr>\n<tr>\n<td>ceph-mgr Prometheus module<\/td>\n<td>Ceph cluster health, capacity, IOPS\/throughput, per-pool and (optionally) per-daemon perf counters<\/td>\n<td>Every Ceph mon\/mgr node, port <strong>9283<\/strong><\/td>\n<\/tr>\n<\/table>\n<p>Two operational details worth knowing before you go looking for &#8220;missing&#8221; metrics:<\/p>\n<ul>\n<li><strong>node_exporter&#8217;s image tag is pinned inside an existing systemd unit, never recreated.<\/strong> The unit was laid down years ago; bumping the version is a regex replace of the image tag in the unit file followed by a restart \u2014 nothing else about the host is touched. If you&#8217;re chasing a stale node_exporter build info, check the systemd unit file directly rather than assuming a fresh container spec.<\/li>\n<li><strong>ceph-mgr&#8217;s Prometheus endpoint on standby mons returns HTTP 200 with an empty body<\/strong> \u2014 only the currently-active mgr returns a real metrics payload. That&#8217;s expected Ceph behavior, not a broken exporter; don&#8217;t page on it.<\/li>\n<\/ul>\n<h2>2. Ceph-side Prometheus configuration<\/h2>\n<p>Beyond just enabling the module, a few explicit <code>ceph config set<\/code> calls shape what the mgr actually exposes:<\/p>\n<pre><code># Per-pool RBD stats only for the pools that matter (avoids scraping every pool in the cluster)\nceph config set mgr mgr\/prometheus\/rbd_stats_pools glance,cinder,nova,kuberbd\n\n# Per-daemon perf counters (OSD op latency, mon session counts, etc.) \u2014 off by default,\n# turned on deliberately since this cluster is small enough that the extra cardinality is fine\nceph config set mgr mgr\/prometheus\/exclude_perf_counters false\n\nceph mgr module enable dashboard\nceph mgr module enable devicehealth\nceph mgr module disable nfs   # not used, one less module to reason about\n\n# SMART-based device health, scraped periodically\nceph device scrape-health-metrics\nceph device check-health<\/code><\/pre>\n<p>The dashboard module also gets a dedicated read-only user (separate from the <code>admin<\/code> account) so the Ceph web dashboard can be shared without handing out cluster-admin.<\/p>\n<h2>3. Prometheus recording rules<\/h2>\n<p>Recording rules exist for one reason here: pre-computing an expensive, high-cardinality join <em>once<\/em> so every dashboard panel and alert downstream can query a cheap precomputed series instead of repeating the same join boilerplate. The real one worth understanding end-to-end is the per-VM CPU\/memory attribution chain:<\/p>\n<pre><code># process-exporter groups every qemu guest process by groupname=\"qemu-vm-&lt;instance-uuid&gt;\".\n# This rule attaches the human project NAME to each OpenStack server by joining on tenant_id.\n- record: qemu_vm:server_info:by_uuid\n  expr: |\n    openstack_nova_server_status * on(tenant_id) group_left(project_name) (\n      label_replace(\n        label_replace(openstack_identity_project_info, \"tenant_id\", \"$1\", \"id\", \"(.*)\"),\n        \"project_name\", \"$1\", \"name\", \"(.*)\"\n      ) * 0 + 1\n    )\n\n# Real per-VM CPU, pulled out of the qemu-vm-&lt;uuid&gt; process group and joined to\n# name\/hypervisor\/project via the rule above.\n- record: qemu_vm:cpu_cores:by_uuid\n  expr: |\n    label_replace(\n      sum by (node, groupname) (rate(namedprocess_namegroup_cpu_seconds_total{groupname=~\"qemu-vm-.*\"}[5m])),\n      \"uuid\", \"$1\", \"groupname\", \"qemu-vm-(.*)\"\n    ) * on(uuid) group_left(name, hypervisor_hostname, project_name) (qemu_vm:server_info:by_uuid * 0 + 1)\n\n- record: qemu_vm:memory_resident_bytes:by_uuid\n  expr: |\n    label_replace(\n      sum by (node, groupname) (namedprocess_namegroup_memory_bytes{groupname=~\"qemu-vm-.*\", memtype=\"resident\"}),\n      \"uuid\", \"$1\", \"groupname\", \"qemu-vm-(.*)\"\n    ) * on(uuid) group_left(name, hypervisor_hostname, project_name) (qemu_vm:server_info:by_uuid * 0 + 1)\n\n# Same CPU figure, normalized to % of that VM's own hypervisor's real physical core count \u2014\n# lets you compare load across hosts with different core counts on one 0-100 scale.\n- record: qemu_vm:cpu_percent_of_host:by_uuid\n  expr: |\n    qemu_vm:cpu_cores:by_uuid * 100\n    \/ on(node) group_left() (count by (node) (count by (node, cpu) (node_cpu_seconds_total{app=\"openstack\",tier=\"host\"})))<\/code><\/pre>\n<p>The mechanism this depends on: every qemu guest process carries <code>-uuid &lt;instance-uuid&gt;<\/code> in its own command line, and that UUID matches Nova&#8217;s own <code>openstack_nova_server_status{uuid=...}<\/code> series exactly \u2014 so a process-exporter cmdline-regex capture is directly joinable to the real instance name\/project\/hypervisor with no separate lookup service needed.<\/p>\n<p>A second, simpler recording rule (<code>now<\/code>, <code>expr: time()<\/code>) exists purely so alert-rule expressions can reference a stable &#8220;current time&#8221; series without repeating <code>time()<\/code> inline in every rule.<\/p>\n<h2>4. Dashboard tour<\/h2>\n<p>Two real, recent changes to the Hypervisor Overview dashboard show the actual iteration this monitoring stack goes through:<\/p>\n<ul>\n<li><strong>Merged the qemu and ceph-osd CPU\/Memory panels, normalized to %.<\/strong> These hosts are hyperconverged (Ceph OSD + Nova compute on the same physical machines), so &#8220;VM CPU&#8221; and &#8220;Ceph OSD CPU&#8221; used to be two separate panels per host. They&#8217;re now one panel each, with the legend distinguishing <code>qemu<\/code> vs <code>ceph-osd<\/code> so the source is unambiguous \u2014 and every CPU-breakdown panel on that row now shows % of that host&#8217;s real physical core count instead of raw core counts, so hosts with different core counts read on the same 0\u2013100 scale.<\/li>\n<li><strong>Added &#8220;Total Other CPU\/Memory per host&#8221; panels, and bumped the top-N process breakdown from 12 to 15.<\/strong> &#8220;Total Other&#8221; is the sum of everything <em>except<\/em> qemu and ceph-osd \u2014 i.e. real system\/host overhead \u2014 shown as its own panel next to the VM\/OSD ones, separate from the top-N individual-process breakdown table below it.<\/li>\n<\/ul>\n<p>The cluster-level &#8220;traffic light&#8221; panels \u2014 the ones worth checking first during an incident \u2014 are aggregate stat\/gauge panels with Grafana thresholds (green\/yellow\/red), not per-host or per-VM breakdowns:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/wp.radut.info\/wp-content\/uploads\/2026\/08\/ceph-cluster-health.png\" alt=\"Ceph Cluster - Advanced dashboard: cluster state row showing HEALTHY status, 94.4% available capacity, 6\/6 OSDs up, 3\/3 monitors in quorum, 0 firing alerts\" \/><\/p>\n<p>Ceph Cluster \u2014 Advanced, &#8220;Cluster State&#8221; row: health status, available capacity, OSD\/MGR\/monitor counts, and firing-alert counts by severity, all at a glance.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/wp.radut.info\/wp-content\/uploads\/2026\/08\/openstack-cluster-status.png\" alt=\"OpenStack - Cluster Status dashboard: Keystone\/Nova\/Neutron\/Cinder\/Glance service-availability blocks all green, 0 alerts firing, 27 VMs, 5 networks, 4 volumes, 11 images, 20 security groups\" \/><\/p>\n<p>OpenStack \u2014 Cluster Status: per-service availability (green\/gray blocks per API) alongside live resource counts (VMs, networks, volumes, images, floating IPs, security groups).<\/p>\n<h2>5. Ceph day-2: checking cluster status by hand<\/h2>\n<p>Dashboards are for trends; these are the commands actually run against a mon node when something looks off or right after a change:<\/p>\n<pre><code>ceph -s                    # or: ceph status \u2014 health, mon\/mgr\/osd counts, pg states, IO rate, at a glance\nceph health detail         # when health isn't HEALTH_OK, this says exactly which check is failing and why\nceph osd tree               # OSD topology + up\/down\/in\/out state, grouped by host \u2014 first stop for \"which OSD\"\nceph osd df                 # per-OSD usage % and PG count \u2014 spot a lopsided\/nearly-full OSD before it pages you\nceph df                     # cluster-wide and per-pool capacity\/usage\nceph crash ls -f json | jq -r '.[].crash_id' | xargs --no-run-if-empty -I{} ceph crash archive {}\n                             # archive known crashes so `ceph health` stops reporting them as new<\/code><\/pre>\n<p>That crash-archive one-liner runs both right after any cluster-config change and again at the end, bracketing the change so any crash that appears <em>during<\/em> it is caught rather than silently archived away with the pre-existing ones.<\/p>\n<h2>6. Ceph performance testing with dd and fio<\/h2>\n<p><strong>Run these from a Ceph client, never from an OSD node.<\/strong> A test on an OSD host measures local disk I\/O, not the actual replicated write path a real workload takes (network + replication + the OSD&#8217;s own journal\/WAL). Use a VM with an RBD-backed volume, or a scratch pod with an RBD or CephFS PVC mounted (<code>kubectl run --rm -it cephperf --image=... <\/code> with the PVC attached, or <code>kubectl exec<\/code> into an existing pod that already has one \u2014 several of this repo&#8217;s k8s addons already mount CephFS\/RBD PVCs, e.g. WordPress&#8217;s <code>wp-content<\/code> volume, so any pod using <code>csi-cephfs-sc<\/code>\/<code>csi-rbd-sc<\/code> works).<\/p>\n<h3>6.1 Quick sanity check: dd<\/h3>\n<pre><code>dd if=\/dev\/zero of=testfile bs=1M count=1024 oflag=direct\n# 1024 x 1M = 1GiB, oflag=direct bypasses the page cache so you're actually hitting Ceph,\n# not just writing into RAM and reporting a fake number<\/code><\/pre>\n<p><code>dd<\/code> is single-threaded, sequential-only, and gives you exactly one number (MB\/s). It&#8217;s a fine 10-second gut check that the path isn&#8217;t obviously broken, but it says nothing about IOPS, latency, or how the cluster behaves under concurrent\/random access \u2014 which is what almost every real workload actually looks like. Reach for <code>fio<\/code> for anything you&#8217;d actually make a capacity decision from.<\/p>\n<h3>6.2 Realistic testing: fio<\/h3>\n<p><strong>Database-like workload<\/strong> \u2014 small-block random read\/write, the pattern that matters for anything backed by InnoDB or similar (4K matches InnoDB&#8217;s page size, and DB access patterns are overwhelmingly random rather than sequential):<\/p>\n<pre><code>fio --name=db-randrw --rw=randrw --bs=4k --iodepth=32 --numjobs=4 \\\n    --size=1G --runtime=60 --time_based --group_reporting<\/code><\/pre>\n<p>What to look at in the output: <strong>IOPS and latency percentiles<\/strong> (p99\/p99.9 matter more than the average for anything DB-shaped \u2014 a long tail is what actually causes query timeouts), not the throughput number.<\/p>\n<p><strong>Big-file \/ large-sequential workload<\/strong> \u2014 the pattern for backup dumps and media files (relevant here since this is the same Ceph cluster backing CephFS\/RBD volumes like WordPress&#8217;s media\/backup storage elsewhere in this repo&#8217;s k8s addons):<\/p>\n<pre><code>fio --name=bigfile-seq --rw=write --bs=1M --iodepth=16 --numjobs=1 \\\n    --size=10G --runtime=60 --time_based --group_reporting<\/code><\/pre>\n<p>What to look at here: <strong>sustained MB\/s<\/strong> \u2014 this is the number that tells you how long a multi-gigabyte backup dump or media restore will actually take against this cluster.<\/p>\n<p>These are lab-scale numbers, useful for capacity-planning intuition and catching regressions \u2014 not a substitute for Ceph&#8217;s own built-in per-OSD benchmark if you want to isolate a single slow disk: <code>ceph tell osd.&lt;id&gt; bench<\/code> runs a sequential write test directly against that one OSD, bypassing the client-side network\/replication path entirely.<\/p>\n<h2>Summary<\/h2>\n<ol>\n<li>Six exporters cover host, container, per-process, LVM, OpenStack-API, and Ceph metrics (\u00a71\u20132)<\/li>\n<li>Recording rules pre-join qemu process data to real instance\/project\/hypervisor identity once, so every downstream panel\/alert stays cheap (\u00a73)<\/li>\n<li>Cluster-level stat panels with Grafana thresholds are the first place to look during an incident (\u00a74)<\/li>\n<li><code>ceph -s<\/code>\/<code>health detail<\/code>\/<code>osd tree<\/code>\/<code>df<\/code> for day-2 status checks (\u00a75)<\/li>\n<li><code>dd<\/code> for a 10-second sanity check, <code>fio<\/code> (randrw 4K for DB-like, sequential 1M for bulk) for real capacity-planning numbers, <code>ceph tell osd.&lt;id&gt; bench<\/code> to isolate a single OSD (\u00a76)<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>Scope: this covers how the OpenStack lab and its Ceph cluster are actually monitored \u2014 the exporter stack, what Ceph exposes natively, the Prometheus recording&#8230;<\/p>\n<div class=\"more-link-wrapper\"><a class=\"more-link\" href=\"https:\/\/wp-nv.radut.info\/ro\/2026\/08\/17\/monitoring-the-openstack-lab-prometheus-grafana-ceph\/\">Continue reading<span class=\"screen-reader-text\">Monitoring the OpenStack Lab: Prometheus, Grafana, Ceph<\/span><\/a><\/div>","protected":false},"author":0,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"qubely_global_settings":"","qubely_interactions":"","_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","_uag_custom_page_level_css":"","footnotes":""},"categories":[21],"tags":[],"class_list":["post-166","post","type-post","status-publish","format-standard","hentry","category-openstack","entry"],"qubely_featured_image_url":null,"qubely_author":{"display_name":"","author_link":"https:\/\/wp-nv.radut.info\/ro\/author\/"},"qubely_comment":0,"qubely_category":"<a href=\"https:\/\/wp-nv.radut.info\/ro\/category\/openstack\/\" rel=\"category tag\">OpenStack<\/a>","qubely_excerpt":"Scope: this covers how the OpenStack lab and its Ceph cluster are actually monitored \u2014 the exporter stack, what Ceph exposes natively, the Prometheus recording&#8230;Continue readingMonitoring the OpenStack Lab: Prometheus, Grafana, Ceph","uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false,"trp-custom-language-flag":false,"qubely_landscape":false,"qubely_portrait":false,"qubely_thumbnail":false},"uagb_author_info":{"display_name":"","author_link":"https:\/\/wp-nv.radut.info\/ro\/author\/"},"uagb_comment_info":0,"uagb_excerpt":"Scope: this covers how the OpenStack lab and its Ceph cluster are actually monitored \u2014 the exporter stack, what Ceph exposes natively, the Prometheus recording&#8230;Continue readingMonitoring the OpenStack Lab: Prometheus, Grafana, Ceph","_links":{"self":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts\/166","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/comments?post=166"}],"version-history":[{"count":0,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/posts\/166\/revisions"}],"wp:attachment":[{"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/media?parent=166"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/categories?post=166"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wp-nv.radut.info\/ro\/wp-json\/wp\/v2\/tags?post=166"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}