From b9e08f394562cb7854af47fc8271100818a7a919 Mon Sep 17 00:00:00 2001 From: shpokas Date: Fri, 27 Mar 2026 12:57:10 +0200 Subject: [PATCH 1/3] Add Dovecot application monitoring via SNMP extend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polls the dovecot SNMP extend (nsExtendOutputFull) which returns JSON statistics from doveadm. Tracks connections, auth rates and durations, IMAP/SMTP command rates and latencies, mail delivery, and Sieve actions. Creates 7 RRD categories: connections, auth, auth_duration, imap, smtp, delivery, sieve — with corresponding graph files and a device app page. Co-Authored-By: Claude Sonnet 4.6 --- .../graphs/application/dovecot_auth.inc.php | 29 ++++ .../application/dovecot_auth_duration.inc.php | 33 ++++ .../application/dovecot_connections.inc.php | 29 ++++ .../application/dovecot_delivery.inc.php | 31 ++++ .../graphs/application/dovecot_imap.inc.php | 31 ++++ .../graphs/application/dovecot_sieve.inc.php | 31 ++++ .../graphs/application/dovecot_smtp.inc.php | 31 ++++ .../html/pages/device/apps/dovecot.inc.php | 29 ++++ includes/polling/applications/dovecot.inc.php | 157 ++++++++++++++++++ 9 files changed, 401 insertions(+) create mode 100644 includes/html/graphs/application/dovecot_auth.inc.php create mode 100644 includes/html/graphs/application/dovecot_auth_duration.inc.php create mode 100644 includes/html/graphs/application/dovecot_connections.inc.php create mode 100644 includes/html/graphs/application/dovecot_delivery.inc.php create mode 100644 includes/html/graphs/application/dovecot_imap.inc.php create mode 100644 includes/html/graphs/application/dovecot_sieve.inc.php create mode 100644 includes/html/graphs/application/dovecot_smtp.inc.php create mode 100644 includes/html/pages/device/apps/dovecot.inc.php create mode 100644 includes/polling/applications/dovecot.inc.php diff --git a/includes/html/graphs/application/dovecot_auth.inc.php b/includes/html/graphs/application/dovecot_auth.inc.php new file mode 100644 index 00000000000..5523b5890a1 --- /dev/null +++ b/includes/html/graphs/application/dovecot_auth.inc.php @@ -0,0 +1,29 @@ +app_id, 'auth']); + +$array = [ + 'success' => 'Auth Success', + 'failures' => 'Auth Failures', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/dovecot_auth_duration.inc.php b/includes/html/graphs/application/dovecot_auth_duration.inc.php new file mode 100644 index 00000000000..baa77b88cf1 --- /dev/null +++ b/includes/html/graphs/application/dovecot_auth_duration.inc.php @@ -0,0 +1,33 @@ +app_id, 'auth_duration']); + +$array = [ + 'succ_avg' => 'Success avg', + 'succ_median' => 'Success median', + 'succ_p95' => 'Success p95', + 'fail_avg' => 'Failure avg', + 'fail_median' => 'Failure median', + 'fail_p95' => 'Failure p95', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/dovecot_connections.inc.php b/includes/html/graphs/application/dovecot_connections.inc.php new file mode 100644 index 00000000000..a651e8e56e9 --- /dev/null +++ b/includes/html/graphs/application/dovecot_connections.inc.php @@ -0,0 +1,29 @@ +app_id, 'connections']); + +$array = [ + 'users' => 'Connected Users', + 'sessions' => 'Connected Sessions', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/dovecot_delivery.inc.php b/includes/html/graphs/application/dovecot_delivery.inc.php new file mode 100644 index 00000000000..4c3ff512db4 --- /dev/null +++ b/includes/html/graphs/application/dovecot_delivery.inc.php @@ -0,0 +1,31 @@ +app_id, 'delivery']); + +$array = [ + 'count' => 'Deliveries/s', + 'avg' => 'Duration avg (µs)', + 'median' => 'Duration median (µs)', + 'p95' => 'Duration p95 (µs)', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/dovecot_imap.inc.php b/includes/html/graphs/application/dovecot_imap.inc.php new file mode 100644 index 00000000000..ece950d699a --- /dev/null +++ b/includes/html/graphs/application/dovecot_imap.inc.php @@ -0,0 +1,31 @@ +app_id, 'imap']); + +$array = [ + 'count' => 'Commands/s', + 'avg' => 'Duration avg (µs)', + 'median' => 'Duration median (µs)', + 'p95' => 'Duration p95 (µs)', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/dovecot_sieve.inc.php b/includes/html/graphs/application/dovecot_sieve.inc.php new file mode 100644 index 00000000000..d5f1eeee875 --- /dev/null +++ b/includes/html/graphs/application/dovecot_sieve.inc.php @@ -0,0 +1,31 @@ +app_id, 'sieve']); + +$array = [ + 'count' => 'Actions/s', + 'avg' => 'Duration avg (µs)', + 'median' => 'Duration median (µs)', + 'p95' => 'Duration p95 (µs)', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/dovecot_smtp.inc.php b/includes/html/graphs/application/dovecot_smtp.inc.php new file mode 100644 index 00000000000..dd82906b494 --- /dev/null +++ b/includes/html/graphs/application/dovecot_smtp.inc.php @@ -0,0 +1,31 @@ +app_id, 'smtp']); + +$array = [ + 'count' => 'Commands/s', + 'avg' => 'Duration avg (µs)', + 'median' => 'Duration median (µs)', + 'p95' => 'Duration p95 (µs)', +]; + +$rrd_list = []; +$i = 0; +foreach ($array as $ds => $descr) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $descr; + $rrd_list[$i]['ds'] = $ds; + $i++; +} + +require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/pages/device/apps/dovecot.inc.php b/includes/html/pages/device/apps/dovecot.inc.php new file mode 100644 index 00000000000..9393e9926dd --- /dev/null +++ b/includes/html/pages/device/apps/dovecot.inc.php @@ -0,0 +1,29 @@ + 'Connected Users & Sessions', + 'dovecot_auth' => 'Authentication (rate)', + 'dovecot_auth_duration' => 'Authentication Duration (µs)', + 'dovecot_imap' => 'IMAP Commands', + 'dovecot_smtp' => 'SMTP Commands', + 'dovecot_delivery' => 'Mail Delivery', + 'dovecot_sieve' => 'Sieve Actions', +]; + +foreach ($graphs as $key => $text) { + $graph_array['height'] = '100'; + $graph_array['width'] = '215'; + $graph_array['to'] = time(); + $graph_array['id'] = $app['app_id']; + $graph_array['type'] = 'application_' . $key; + echo '
+
+

' . $text . '

+
+
+
'; + include 'includes/html/print-graphrow.inc.php'; + echo '
'; + echo '
'; + echo '
'; +} diff --git a/includes/polling/applications/dovecot.inc.php b/includes/polling/applications/dovecot.inc.php new file mode 100644 index 00000000000..c225a8e2743 --- /dev/null +++ b/includes/polling/applications/dovecot.inc.php @@ -0,0 +1,157 @@ + $data['connected_users']['count'] ?? 0, + 'sessions' => $data['connected_sessions']['count'] ?? 0, +]; +$rrd_def = RrdDefinition::make() + ->addDataset('users', 'GAUGE', 0) + ->addDataset('sessions', 'GAUGE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +// --- auth: auth_success and auth_failures counts --- +$category = 'auth'; +$fields = [ + 'success' => $data['auth_success']['count'] ?? 0, + 'failures' => $data['auth_failures']['count'] ?? 0, +]; +$rrd_def = RrdDefinition::make() + ->addDataset('success', 'DERIVE', 0) + ->addDataset('failures', 'DERIVE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +// --- auth_duration: response time stats for auth_success and auth_failures (microseconds) --- +$category = 'auth_duration'; +$fields = [ + 'succ_avg' => (int) ($data['auth_success']['avg'] ?? 0), + 'succ_median' => (int) ($data['auth_success']['median'] ?? 0), + 'succ_p95' => (int) ($data['auth_success']['%95'] ?? 0), + 'fail_avg' => (int) ($data['auth_failures']['avg'] ?? 0), + 'fail_median' => (int) ($data['auth_failures']['median'] ?? 0), + 'fail_p95' => (int) ($data['auth_failures']['%95'] ?? 0), +]; +$rrd_def = RrdDefinition::make() + ->addDataset('succ_avg', 'GAUGE', 0) + ->addDataset('succ_median', 'GAUGE', 0) + ->addDataset('succ_p95', 'GAUGE', 0) + ->addDataset('fail_avg', 'GAUGE', 0) + ->addDataset('fail_median', 'GAUGE', 0) + ->addDataset('fail_p95', 'GAUGE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +// --- imap: IMAP command count and response times --- +$category = 'imap'; +$fields = [ + 'count' => $data['imap_command']['count'] ?? 0, + 'avg' => (int) ($data['imap_command']['avg'] ?? 0), + 'median' => (int) ($data['imap_command']['median'] ?? 0), + 'p95' => (int) ($data['imap_command']['%95'] ?? 0), +]; +$rrd_def = RrdDefinition::make() + ->addDataset('count', 'DERIVE', 0) + ->addDataset('avg', 'GAUGE', 0) + ->addDataset('median', 'GAUGE', 0) + ->addDataset('p95', 'GAUGE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +// --- smtp: SMTP command count and response times --- +$category = 'smtp'; +$fields = [ + 'count' => $data['smtp_command']['count'] ?? 0, + 'avg' => (int) ($data['smtp_command']['avg'] ?? 0), + 'median' => (int) ($data['smtp_command']['median'] ?? 0), + 'p95' => (int) ($data['smtp_command']['%95'] ?? 0), +]; +$rrd_def = RrdDefinition::make() + ->addDataset('count', 'DERIVE', 0) + ->addDataset('avg', 'GAUGE', 0) + ->addDataset('median', 'GAUGE', 0) + ->addDataset('p95', 'GAUGE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +// --- delivery: mail delivery count and response times --- +$category = 'delivery'; +$fields = [ + 'count' => $data['mail_delivery']['count'] ?? 0, + 'avg' => (int) ($data['mail_delivery']['avg'] ?? 0), + 'median' => (int) ($data['mail_delivery']['median'] ?? 0), + 'p95' => (int) ($data['mail_delivery']['%95'] ?? 0), +]; +$rrd_def = RrdDefinition::make() + ->addDataset('count', 'DERIVE', 0) + ->addDataset('avg', 'GAUGE', 0) + ->addDataset('median', 'GAUGE', 0) + ->addDataset('p95', 'GAUGE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +// --- sieve: Sieve action count and response times --- +$category = 'sieve'; +$fields = [ + 'count' => $data['sieve_action']['count'] ?? 0, + 'avg' => (int) ($data['sieve_action']['avg'] ?? 0), + 'median' => (int) ($data['sieve_action']['median'] ?? 0), + 'p95' => (int) ($data['sieve_action']['%95'] ?? 0), +]; +$rrd_def = RrdDefinition::make() + ->addDataset('count', 'DERIVE', 0) + ->addDataset('avg', 'GAUGE', 0) + ->addDataset('median', 'GAUGE', 0) + ->addDataset('p95', 'GAUGE', 0); +$rrd_name = ['app', $name, $app->app_id, $category]; +$metrics[$category] = $fields; +$tags = ['name' => $name, 'app_id' => $app->app_id, 'rrd_def' => $rrd_def, 'rrd_name' => $rrd_name]; +app('Datastore')->put($device, 'app', $tags, $fields); + +update_application($app, 'OK', $metrics); From 30c558994372e4ba61be1eb1b96e37147fd5c5ab Mon Sep 17 00:00:00 2001 From: shpokas Date: Fri, 27 Mar 2026 13:03:14 +0200 Subject: [PATCH 2/3] Add Dovecot application monitoring documentation Covers Dovecot 2.4 metric configuration, SNMP extend setup including sudo requirements, verification steps, and a graph reference table. Co-Authored-By: Claude Sonnet 4.6 --- doc/Extensions/Applications/Dovecot.md | 146 +++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 doc/Extensions/Applications/Dovecot.md diff --git a/doc/Extensions/Applications/Dovecot.md b/doc/Extensions/Applications/Dovecot.md new file mode 100644 index 00000000000..98b1b0f4e2f --- /dev/null +++ b/doc/Extensions/Applications/Dovecot.md @@ -0,0 +1,146 @@ +# Dovecot + +[Dovecot](https://www.dovecot.org/) is an open-source IMAP and POP3 server. + +!!! note + This integration requires **Dovecot 2.4** or later. The statistics + system was redesigned in 2.4 and the metric definitions below are + not compatible with earlier versions. + +## Dovecot configuration + +Dovecot 2.4 requires metrics to be explicitly defined before +`doveadm stats dump` will return data. Add the following to your +Dovecot configuration (e.g. `/etc/dovecot/conf.d/10-stats.conf`): + +``` +## +## Statistics and metrics +## +## In Dovecot 2.4, metrics must be explicitly defined for +## doveadm stats dump to return data. +## + +metric auth_success { + filter = event=auth_request_finished AND success=yes +} + +metric auth_failures { + filter = event=auth_request_finished AND NOT success=yes +} + +metric imap_command { + filter = event=imap_command_finished +} + +metric smtp_command { + filter = event=smtp_server_command_finished +} + +metric sieve_action { + filter = event=sieve_action_finished +} + +metric mail_delivery { + filter = event=mail_delivery_finished +} +``` + +Reload Dovecot after making changes: + +```bash +doveadm reload +``` + +Verify that statistics are being collected: + +```bash +doveadm stats dump +``` + +## SNMP Extend + +=== "SNMP Extend" + + 1. Install the collection script to the target host: + + ```bash + wget https://raw.githubusercontent.com/librenms/librenms-agent/master/snmp/dovecot-stats-snmp.sh \ + -O /usr/local/bin/dovecot-stats-snmp.sh + ``` + + 2. Make the script executable: + + ```bash + chmod +x /usr/local/bin/dovecot-stats-snmp.sh + ``` + + 3. The script calls `doveadm stats dump` and `doveadm who`. The + `snmpd` daemon typically runs as an unprivileged user and may not + have permission to run `doveadm`. Grant access via sudo: + + ```bash + # /etc/sudoers.d/snmpd-dovecot + Debian-snmp ALL = NOPASSWD: /usr/bin/doveadm + ``` + + Adjust the username to match the user `snmpd` runs as on your + system (e.g. `snmpd` on RHEL/Rocky, `Debian-snmp` on + Debian/Ubuntu). Then update the script to use sudo: + + ```bash + # At the top of /usr/local/bin/dovecot-stats-snmp.sh, set: + DOVEADM="sudo /usr/bin/doveadm" + ``` + + 4. Edit your snmpd configuration (usually `/etc/snmp/snmpd.conf`) + and add: + + ```bash + extend dovecot /usr/local/bin/dovecot-stats-snmp.sh + ``` + + 5. Restart snmpd: + + === "Systemd" + + ```bash + sudo systemctl restart snmpd + ``` + + === "Xinetd" + + ```bash + sudo service snmpd restart + ``` + + 6. Verify the extend is working: + + ```bash + snmpget -v2c -c \ + 'NET-SNMP-EXTEND-MIB::nsExtendOutputFull."dovecot"' + ``` + + You should see a JSON array containing the collected metrics. + + The application should be auto-discovered the next time LibreNMS + runs discovery against the host. If it is not, follow the steps + under the *Enable the application(s) to be discovered* section at + the top of the [Applications](../Applications.md) page. + +## Graphs + +The following graphs are available once the application is discovered +and polled: + +| Graph | Description | +|-------|-------------| +| Connected Users & Sessions | Current number of connected users and active sessions | +| Authentication (rate) | Rate of successful and failed authentication attempts | +| Authentication Duration | Auth response times — avg, median, 95th percentile (µs) | +| IMAP Commands | IMAP command throughput and latency | +| SMTP Commands | SMTP command throughput and latency | +| Mail Delivery | Mail delivery throughput and latency | +| Sieve Actions | Sieve script action throughput and latency | + +All duration values are in **microseconds**. From 23da98df4cdaa2f114341ed013d314f524af055b Mon Sep 17 00:00:00 2001 From: shpokas Date: Fri, 27 Mar 2026 13:06:18 +0200 Subject: [PATCH 3/3] Add dovecot to global apps page graph list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Required for the /apps?app=dovecot page to render without an "Undefined array key" error — the default.inc.php handler iterates $graphs[$app->app_type] which must be declared here. Co-Authored-By: Claude Sonnet 4.6 --- includes/html/pages/apps.inc.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/includes/html/pages/apps.inc.php b/includes/html/pages/apps.inc.php index 2522bcc836c..ded4a0e6de3 100644 --- a/includes/html/pages/apps.inc.php +++ b/includes/html/pages/apps.inc.php @@ -646,6 +646,15 @@ 'peers', 'total_bytes', ]; +$graphs['dovecot'] = [ + 'connections', + 'auth', + 'auth_duration', + 'imap', + 'smtp', + 'delivery', + 'sieve', +]; echo '
'; echo '
';