Monitoring and health: Prometheus metrics, health probes, Grafana, OTLP and SNMPv3
-
Written for 6.3.4. hMailServer 6.3.6 (6 October 2026) publishes the first container image,
registry.gitlab.com/hmailserver/hmailserver:6.3.6, and a monitor that matches "The response carried no HTTP headers." needs the new sentence, which says who ended the connection, after how many bytes and seconds. See the 6.3.6 announcement. hMailServer 6.3.7 (17 October 2026) publishesregistry.gitlab.com/hmailserver/hmailserver:6.3.7, tagged6.3andlatestuntil 6.3.8; renames the Control Panel page that holds the metrics listener's settings from API & monitoring to Monitoring & alerts, with the alerts' settings, the outbound webhooks and the SNMP agent on it (Ctrl+K takes the old title to it); and refusesGET /api/v1/metrics/history, and the same history over COM, to a credential confined to named domains, such as a domain administrator, the auditor role given a list of domains or an API key issued for named domains. With IMAP and POP3 now taking the client's address from a PROXY protocol header as SMTP does, the High Availability Runbook says that a balancer's/readyzcheck must not carry that header: the metrics listener reads none and answers a request behind one 404. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) publishesregistry.gitlab.com/hmailserver/hmailserver:6.3.8, also tagged6.3andlatest. See the 6.3.8 announcement.This topic covers what hMailServer 6.3.4 offers a monitoring system: the metrics listener's Prometheus exposition and health probes, the repository's Grafana dashboard, the OpenTelemetry push, the server's own metric history, the new SNMPv3 agent and, briefly, the daily block-list check of the server's own addresses. The one thing to know first: the metrics listener is off until you give it a port, and once it is on, the health check to configure is
/readyz, because it is proved by a real database round trip rather than by a flag.The metrics listener
One small HTTP listener serves both the exposition and the probes. Its seven settings are
[Settings]keys, set on the Control Panel's Monitoring & troubleshooting → API & monitoring page (Monitoring & alerts since 6.3.7; Windows only), withPUT /api/v1/settings/ini/{name}, withhmctl ini set, or, on a Linux server whose REST listener is not yet on, withhmailserver --set-setting NAME=VALUE; the topic on settings explains where they are stored and when a save applies. All seven are taken when the listener starts, the three credential settings included, so a saved change waits for a reinitialise (POST /api/v1/server/reinitialize, or Apply now in the Control Panel or the Deck) or a service restart. A token that has been saved and not applied leaves/metricsanswering as it did before.Setting Default Effect When it is wrong MetricsServerPort00means the listener is never createdNo port, no probes, no metrics, and no complaint anywhere MetricsServerBindAddress127.0.0.1An IP literal only, parsed with inet_pton:0.0.0.0,127.0.0.1,::or a specific IPv6 addressA host name or localhostis rejected: the log saysMetricsServer: Invalid bind address, the listener does not start and the probes go with itMetricsServerAuthTokenempty Sent by the scraper as Authorization: Bearer <token>Absent on a non-loopback bind: /metricsanswers 503, not 401, and the body names the settings that would open it. The probes keep answeringMetricsServerAuthUsernameandMetricsServerAuthPasswordempty HTTP Basic; with both schemes configured, either satisfies the check As above MetricsServerCertificateFileandMetricsServerPrivateKeyFileempty Both set makes the whole port HTTPS, probes included Set but unusable: the listener still serves the probes in the clear and /metricsanswers 503::serves both address families; a specific IPv6 literal serves IPv6 only. "Loopback" means the whole of127.0.0.0/8and::1.The probes are answered before any branch that can refuse. A Kubernetes
httpGetprobe, an ELB or ALB health check and a DockerHEALTHCHECKhave nowhere to keep a bearer token. Refused credentials on/metricsare counted inhmailserver_metrics_unauthorized_requests_totalrather than logged.Nothing on the request path touches the database or the disk. A refresher thread probes the database every 5 s (every 30 s after a failure), refreshes the delivery-queue aggregates at most every 10 s and only when a scrape has asked for them, and re-reads the certificate files every 5 minutes. One exception since 6.3.4: the two message-store gauges are cached for an hour and recomputed by whichever caller finds them stale, so about once an hour a scrape can run
select sum(messagesize), count(*) from hm_messageson the listener thread itself: on a large store, a slow answer to that scrape and to any probe queued behind it.It is a single accept loop serving one connection at a time. A request's header read is bounded at 5 s and a response write at 15 s. For a load balancer's check the High Availability Runbook gives an interval of 5 s and a timeout of 2 s; never point a sub-second health check at it.
The three health probes
No credential in any configuration; HTTP/1.0 responses that close the connection.
Path 200 when Body Use it for /livezAlways, if the listener answered; never 503 aliveLiveness restarts. It checks nothing on purpose: a database outage must not get a healthy process killed /readyzState is Runningand the database has proved within the last 20 s that it can answer; 503 otherwiseready, ornot ready: <reason>Load-balancer and VIP routing /healthzThe same conditions {"status":"ok\|unavailable","state":"...","database":"up\|down","uptime_seconds":N}Dashboards and human debugging Readiness is four conditions in order, each with its own reason:
not ready: server not in running state;not ready: database not connected;not ready: database did not answer the last readiness probe; andnot ready: database readiness probe has not completed recently. The last exists because a probe against a database that has stopped answering can hang for as long asDBConnectionAcquireTimeoutallows, 60 s by default. Since 6.3.4 a server in maintenance mode answers503 not ready: draining:POST /api/v1/server/drainenters it,GETreports{"draining","since","sessions","deliveries","drained"}, andDELETEleaves it.Start-up is a refused connection, not a 503: the listener is brought up only after the state has gone to
Running. Shutdown is a 503 first, then a refusal: the state goes toStoppingbefore anything is torn down, so/readyzreports 503 whileShutdownDrainSecondsruns.The High Availability Runbook builds its active/passive design on this: both nodes have the listener on, with
MetricsServerBindAddress=0.0.0.0and aMetricsServerAuthToken, and the VIP is gated strictly on/readyz. The passive node's service is stopped, so its/readyzis refused and it is never routed to. Since the probes need no credential, a missing token shows as missing dashboards, not as a failed failover.Every metric family
Thirty-eight metric families in 6.3.4 (thirty in 6.3.3). Counters are monotonic since server start; a restart resets them, which is what
hmailserver_start_time_secondsis for.Mail flow
Metric Type Meaning, and the trap in it hmailserver_processed_messages_totalcounter One per delivery pass started, not per message received: a message deferred three times and then delivered counts four hmailserver_messages_delivered_totalcounter Delivery passes that completed with no failed recipient hmailserver_messages_deferred_totalcounter Passes rescheduled: a temporary failure, greylisting, or a message the virus scanner could not examine ( AVFailAction=1)hmailserver_messages_bounced_totalcounter Delivery-failure notifications queued to the sender; one can cover several recipients hmailserver_delivery_queue_messagesgauge Rows in hm_messagesin the delivering statehmailserver_delivery_queue_oldest_message_age_secondsgauge Age of the oldest queued message; 0when the queue is empty. Alert on this, not on depthhmailserver_spam_messages_total,hmailserver_viruses_removed_totalcounter Messages classified as spam; viruses removed during delivery preprocessing hmailserver_store_bytes,hmailserver_store_messagesgauge Since 6.3.4. The size and the number of the messages in hm_messages, recomputed at most once an hourhmailserver_domain_messages_received_total{domain},hmailserver_domain_messages_sent_total{domain}counter Only present when MetricsPerDomainEnabled=1; only hosted domains are ever labelledAdded in 6.3.4.
hmailserver_country_blocked_totalandhmailserver_country_blocked_by_country_total{country}count connections and sign-ins refused by a country policy, andhmailserver_spam_trap_hits_totalcounts hits on a declared spam trap. Three gauges cover the feedback loops' seven-day window:hmailserver_domain_complaints{domain},hmailserver_domain_complaint_window_messages{domain}andhmailserver_domain_complaint_rate{domain}. They are present only whenMetricsPerDomainEnabledis set and complaint processing is on. The rate gauge is a ratio, not parts per million: Complaints.md alerts onhmailserver_domain_complaint_rate > 0.001. Parts per million is the unit of thecomplaints.rate.highalert rule's threshold, so an expression written in parts per million against the gauge would never fire.Sessions, authentication and TLS
Metric Type Meaning hmailserver_sessions{protocol}gauge Concurrent sessions; protocolissmtp,imaporpop3hmailserver_auth_success_total,hmailserver_auth_failures_totalcounter A rising failure rate against a flat success rate is a password-guessing run hmailserver_tls_handshakes_total,hmailserver_tls_handshake_failures_totalcounter The mail protocols only; a failed handshake on the metrics port is never counted here hmailserver_tls_certificate_expiry_seconds{certificate}gauge notAfteras a Unix timestamp per configured certificate, re-read every 5 minutes; at most 128 series. Absent entirely when no certificate is configured. With no certificate row, the ACME certificate is labelledACME (automatic)hmailserver_metrics_unauthorized_requests_totalcounter Always 0when no credential is configuredThe server and the database
Metric Type Meaning hmailserver_start_time_secondsgauge Uptime is time() - hmailserver_start_time_secondshmailserver_build_info{version,architecture,database_schema_version}gauge Always 1; the information is in the labelshmailserver_state{state}gauge One series per state ( unknown,stopped,starting,running,stoppingand, since 6.3.4,draining), exactly one of which is1hmailserver_workqueue_depth{queue},hmailserver_workqueue_blocking_tasks_waiting{queue}gauge Seven fixed queuevalues:Server queue,Maintenance queue,Asynchronous task queue,Name lookup queue,IOCPQueue,SMTP delivery queue,External fetch queue.Asynchronous task queuesends the final250, so depth there is mail waiting to be acknowledgedhmailserver_messagestore_missing_filesgauge Always 0unlessMessageStoreConsistencyCheck=1hmailserver_command_processing_seconds,hmailserver_db_query_secondshistogram Bucket bounds from 0.0001 s to 1.0 s, and from 0.001 s to 10.0 s; both emit the full _bucket,_sumand_countsethmailserver_database_connectedgauge 1when the database answered a real round trip recentlyhmailserver_database_probe_success_timestamp_seconds,hmailserver_database_probe_age_secondsgauge When it last answered, and how long ago on the server's own clock; /readyzreports 503 once the age passes 20 shmailserver_db_connections{state}gauge busyandavailable;availablepinned at zero is saturationhmailserver_db_slow_queries_totalcounter Statements slower than SlowQueryLogMilliseconds;0while that setting is0Scraping it
scrape_configs: - job_name: hmailserver scrape_interval: 30s scrape_timeout: 10s metrics_path: /metrics authorization: credentials: "<the value of MetricsServerAuthToken>" static_configs: - targets: ["mail.example.com:9100"]Use
scheme: httpswhen the certificate pair is set, and keepscrape_timeoutwell underscrape_interval. The wiki gives nine alerting rules against these metrics, among themhmailserver_database_connected == 0for 2m,hmailserver_delivery_queue_oldest_message_age_seconds > 3600for 15m andhmailserver_tls_certificate_expiry_seconds - time() < 14 * 86400.The Grafana dashboard
The repository ships
hmailserver/docs/grafana-dashboard.json. Import it as it is and pick your Prometheus data source; theinstancevariable, fromlabel_values(hmailserver_build_info, instance), selects the server. Its uid ishmailserver-overview; it refreshes every1mand opens onnow-24h. Itspanelsarray has 19 entries: four row headers, five stat panels and ten time series. Every rate is over[5m]and every query is filtered byinstance=~"$instance".- Health: server state (
hmailserver_state == 1), database, uptime, missing message files, and TLS certificate expiry, amber below 21 days and red below 7. - Mail flow: delivery outcomes (delivered, deferred and bounced, stacked), queue depth with the oldest message's age on a right-hand axis, messages processed with spam and viruses removed, and active sessions by protocol.
- Authentication and TLS: authentication outcomes and TLS handshakes, as rates.
- Latency and database: command processing and database query latency (p50, p95 and p99 by
histogram_quantileover the two histograms'_bucketseries), the connection pool by state, and slow queries.
What the dashboard does not draw: the two database probe gauges, the two work-queue gauges,
hmailserver_metrics_unauthorized_requests_total, the per-domain counters, the two store gauges and the six families added in 6.3.4.hmailserver_build_infois read by theinstancevariable and by no panel.The metric history inside the server
The server keeps a short history of its own, so the Control Panel's dashboard can show 24 hours, 7 days and 30 days without Prometheus.
MetricsHistoryDays(default7;0switches it off) is the days kept inhm_metricsamples: one row per metric per minute, plus a sample at service start. Fourteen metrics are stored in 6.3.3, named as the exposition's without thehmailserver_prefix and with the label folded in, such assessions_smtpandprocessed_messages_total. 6.3.4 addsstore_bytesandstore_messages, whose history begins the day the server first samples them, andcountry_blocked_totalandspam_trap_hits_total. Read it withGET /api/v1/metrics/history?metric=…&range=24h|7d|30d, or on Windows over COM withUtilities.GetMetricHistory. It has no labels and no histograms and is not a replacement for Prometheus, but it survives a restart.Pushing to an OpenTelemetry collector
Three OTLP/HTTP signals, each a no-op until its own endpoint is set. The pushed metric names are the Prometheus names verbatim.
Setting Default Effect OtelMetricsEndpointempty OTLP metrics; the default path is /v1/metricsOtelMetricsInterval60Seconds between exports, clamped to 5–3600 OtelEndpointempty OTLP traces only: one span per delivery attempt, joined to the trace named by the message's own traceparent; default path/v1/tracesOtelLogsEndpointempty OTLP logs: the same lines, categories and mask the log files get; default path /v1/logsWhat is pushed: the twelve counters that predate 6.3.4 and, since 6.3.4,
hmailserver_spam_trap_hits_total, sessions as one gauge with aprotocolattribute,hmailserver_messagestore_missing_files, and the per-domain counters when they are enabled. The histograms, the database and queue gauges and certificate expiry are scrape-only. On Linux, in every release from 6.3.0 to 6.3.3, a collector that accepted the connection and never answered held the exporter, and the stop that waits for it, for good; 6.3.4 fixes that.The SNMPv3 agent (since 6.3.4)
For a monitoring system that speaks SNMP and not Prometheus or OpenTelemetry, 6.3.4 adds a read-only agent answering the mail MIB (RFC 2789, MTA-MIB) and the standard SNMP groups from the same counters
/metricsserves; an object/metricshas no counter for is answerednoSuchObject, not made up. It speaks SNMPv3 with the User-based Security Model for one user at authPriv only (HMAC-SHA-256 and AES-128), and SNMPv2c as an option that stays off even when the agent is on. SNMPv1 is not answered, a Set is answerednoAccess, and nothing is sent unasked: no traps, no informs. It is off as shipped, and an empty allow list answers nobody.Setting ( hm_settings)REST key Default Meaning SnmpEnabledsnmp_enabledoff The whole agent; off, no socket is opened SnmpPortsnmp_port161The UDP port SnmpBindAddresssnmp_bind_addressempty, every address 127.0.0.1for a collector on the same machine; otherwise an address on a management networkSnmpAllowedAddressessnmp_allowed_addressesempty Addresses and CIDR ranges, at most 64. Name the managers one by one: UDP does not prove a source address SnmpV3Usersnmp_v3_userempty The one SNMPv3 user SnmpV3AuthPassword,SnmpV3PrivPasswordsnmp_v3_auth_password,snmp_v3_priv_password(write-only)empty 8 to 64 ASCII characters each SnmpV2cEnabled,SnmpV2cCommunitysnmp_v2c_enabled,snmp_v2c_community(write-only)off, empty The community crosses the network in the clear; an answer is never more than eight times its request SnmpContact,SnmpLocationsnmp_contact,snmp_locationempty sysContactandsysLocationThey are on
/api/v1/settings(the server group), on the Control Deck's Settings page under Server and inhmctl settings. There is no COM property for any of them, and in 6.3.4 no Control Panel page, so Windows and Linux are configured the same way; since 6.3.7 they are the SNMP tab of the Control Panel's Monitoring & alerts page on Windows, which 6.3.5 and 6.3.6 had on Logging. They can also be given asHM_PROPERTY_SnmpEnabledand so on in the environment, which reaches the database only whenhmailserver --apply-environmentis run (hMailServer.exe --apply-environmenton Windows): the service does not read the environment itself. Each setting is read when the agent starts, at the next service start or Reinitialize. The two passwords and the community are stored protected, never read back, and not carried by a backup: enter them again after a restore.Snmp.md's example keeps every password off the command line. On the server, for a manager at 192.0.2.10:
$ umask 077 $ cat > snmp.json {"snmp_allowed_addresses": "192.0.2.10", "snmp_v3_user": "monitor", "snmp_v3_auth_password": "...", "snmp_v3_priv_password": "...", "snmp_contact": "[email protected]", "snmp_location": "Rack 4, London", "snmp_enabled": true} ^D $ hmctl settings set --from-json snmp.json $ rm snmp.json $ hmctl api POST /api/v1/server/reinitializeOn the manager, with Net-SNMP 5.9 or later,
defVersion 3, the user (defSecurityName), both passphrases,defSecurityLevel authPriv,defAuthType SHA-256anddefPrivType AESgo in~/.snmp/snmp.conf, readable by you alone (chmod 600);snmpwalk mail.example.com 1.3.6.1.2.1.28then walks the mail MIB.What it serves. In the MTA-MIB
mtaGroupTablehas one row per protocol the server runs: 1 SMTP, 2 POP3, 3 IMAP. What has a counter is answered:mtaStoredMessagesis the delivery queue's depth,mtaTransmittedMessagesishmailserver_messages_delivered_total,mtaGroupOldestMessageStoredis the oldest queued message's age, andmtaGroupInboundAssociationsishmailserver_sessionsfor the protocol.mtaReceivedMessagesisnoSuchObject, because no counter of messages received exists, and so are the volume, recipient and conversion columns. Sixteen scalars at1.3.6.1.4.1.32473.1.2.<n>.0carry/metricsseries directly, among themhmailserver_stateas a number at 14 (3 is running, 5 draining) and the two delivery-queue gauges at 15 and 16.The enterprise number is a placeholder. 1.3.6.1.4.1.32473 is the number RFC 5612 reserves for documentation and examples; it is not registered to this project with IANA. When a number is registered,
sysObjectID, the scalars and the engine ID's prefix move to it in one release, and a manager configured for these OIDs will need them changed. There is no MIB module file yet, for the same reason.When it does not start. Error
7420: the port could not be bound, because another program holds it (the operating system's own SNMP service, on Windows or Linux, listens on 161 when it is installed) or, on Linux outside the packaged unit, the process lacksCAP_NET_BIND_SERVICE; the server runs on without the agent.7421: the engine's seed or boot count inhm_settingscould not be read or written.7422: an exception left the agent's thread; a Reinitialize starts it again. On Linux the packagedhmailserver.servicehasCAP_NET_BIND_SERVICEfor port 25, which is what lets it bind 161. No container image of 6.3.4 is published; a container built from the repository's Dockerfile publishes the port as UDP (-p 161:161/udp) and needs a host name of its own, since two started with one fixed hostname are one SNMP engine.The daily block-list check, briefly
Since 6.3.4 the server looks the addresses it sends from up on DNS block lists once a day and raises the
reputation.listedalert when one is listed. The watch is on in a new installation and off after an upgrade, and is read atGET /api/v1/reputation; the topic on DKIM rotation, reports and the reputation watch has the lists, the addresses checked and what a failed lookup means.Where to read more
- Wiki: Monitoring and Health and High Availability Runbook
- Documents at v6.3.4: Snmp.md, HighAvailabilityRunbook.md, ReputationWatch.md, Complaints.md and the dashboard itself, grafana-dashboard.json
- The public documentation: 16.3 Prometheus metrics and health probes
- On this forum: Alerts, the audit trail, syslog and the journal, and per-domain reports, Reading the logs, and Warm standby and high availability for the pair the probes gate. See also The Control Deck and hMailServer on Linux: what you get and what you do not
- Health: server state (
-
C chris pinned this topic on
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login