diff mbox series

[bug#60788,v9] services: Add vnstat-service-type.

Message ID a77e9de7969534568b2481225844eeae7d7ef3f7.1680613664.git.mirai@makinata.eu
State New
Headers show
Series [bug#60788,v9] services: Add vnstat-service-type. | expand

Commit Message

Bruno Victal April 4, 2023, 1:08 p.m. UTC
* gnu/services/monitoring.scm (vnstat-service-type): New variable.
* doc/guix.texi (Monitoring Services): Document it.
---

Changes since v8:
* Forgot to amend commit in v8, v9 corrects this.

 doc/guix.texi               | 239 ++++++++++++++++++
 gnu/services/monitoring.scm | 467 ++++++++++++++++++++++++++++++++++++
 2 files changed, 706 insertions(+)


base-commit: b9c9c23939a40a850a8c78579adaec25d1972bd1

Comments

Ludovic Courtès April 7, 2023, 3:22 p.m. UTC | #1
Hi!

Bruno Victal <mirai@makinata.eu> skribis:

> * gnu/services/monitoring.scm (vnstat-service-type): New variable.
> * doc/guix.texi (Monitoring Services): Document it.
> ---
>
> Changes since v8:
> * Forgot to amend commit in v8, v9 corrects this.

Sorry to chime in after 9 versions (!).

I think a system test would be nice, we generally require it upfront,
but since Maxim wrote it can come later, let’s not let it block this
patch any longer.

One comment:

> +@item @code{database-dir} (default: @code{"/var/lib/vnstat"}) (type: string)

[...]

> +@item @code{create-dirs?} (default: @code{#t}) (type: maybe-boolean)

For consistency, both within this record and with the rest of Guix, I
suggest avoiding abbreviations.  Since this will be part of the API,
better fix it now than later.

> +@item @code{max-bandwidth} (type: maybe-integer)
> +Maximum bandwidth for all interfaces.  If the interface specific traffic
> +exceeds the given value then the data is assumed to be invalid and
> +rejected.  Set to 0 in order to disable the feature.  Value range:
> +@samp{0}..@samp{50000}
> +
> +@item @code{max-bw} (type: maybe-alist)
> +Same as @var{max-bandwidth} but can be used for setting individual
> +limits for selected interfaces.  This is an association list of
> +interfaces as symbols/strings to integer values.  For example,
> +@lisp
> +(max-bw `((eth0 .  15000)
> +          (ppp0 .  10000)))
> +@end lisp

Both the naming and semantics are a bit confusing to me.

How about s/max-bw/per-interface-max-bandwidth/ ?

Side note: I’d represent interfaces as strings because there’s no
guarantee they “fit” in a symbol.

That’s all, thanks!

Ludo’.
Maxim Cournoyer April 7, 2023, 8:04 p.m. UTC | #2
Hi Ludo,

Ludovic Courtès <ludo@gnu.org> writes:

> Hi!
>
> Bruno Victal <mirai@makinata.eu> skribis:
>
>> * gnu/services/monitoring.scm (vnstat-service-type): New variable.
>> * doc/guix.texi (Monitoring Services): Document it.
>> ---
>>
>> Changes since v8:
>> * Forgot to amend commit in v8, v9 corrects this.
>
> Sorry to chime in after 9 versions (!).
>
> I think a system test would be nice, we generally require it upfront,
> but since Maxim wrote it can come later, let’s not let it block this
> patch any longer.

I didn't mean to lower our standards; I wasn't sure if that policy of
ours was strict, since a few system services do not have corresponding
tests, IIRC.  I wary a bit that demanding a system test for each added
service may cause scalability problems in the long run, as each demand a
disk-heavy image to be generated and the test to run in a VM, which
makes it expensive/slow.  On the other hand, it's nice to know about any
regressions when they happen rather than on a reboot...

If we have such a policy, perhaps we should explicit it in
our documented contribution guidelines?
Bruno Victal April 8, 2023, 12:40 p.m. UTC | #3
Hi  Ludo’,

On 2023-04-07 16:22, Ludovic Courtès wrote:
> I think a system test would be nice, we generally require it upfront,
> but since Maxim wrote it can come later, let’s not let it block this
> patch any longer.

Originally I thought it was unfeasible to write a test for this service
since it required network activity within the VM and it seemed to take more
than 10 minutes for it to pick it up.

I did some manual experiments on it much later and I managed to get it down to
approx. between 2 and 3 minutes for a partially automated test.

What's blocking the test from being implemented in Guix is that my (incomplete) test-suite
for this depends on guile 'spawn', which isn't available yet,
For reference, I've attached the test-suite here [1], the plan is to finish it up and
add it to Guix after the 'spawn' issue is resolved. (or perhaps refactor this to use another approach?)

> 
> One comment:
> 
>> +@item @code{database-dir} (default: @code{"/var/lib/vnstat"}) (type: string)
> 
> [...]
> 
>> +@item @code{create-dirs?} (default: @code{#t}) (type: maybe-boolean)
> 
> For consistency, both within this record and with the rest of Guix, I
> suggest avoiding abbreviations.  Since this will be part of the API,
> better fix it now than later.

I should mention that almost all of the field names here are near verbatim
vnstat config-file directives, i.e. a near 1-1 Scheme translation of vnstat config.
This has the benefit that it makes serialization pretty much straightforward.

It's possible to override their names by the use of the custom serializer parameter
but would it be acceptable to leave them as-is?

>> +@item @code{max-bandwidth} (type: maybe-integer)
>> +Maximum bandwidth for all interfaces.  If the interface specific traffic
>> +exceeds the given value then the data is assumed to be invalid and
>> +rejected.  Set to 0 in order to disable the feature.  Value range:
>> +@samp{0}..@samp{50000}
>> +
>> +@item @code{max-bw} (type: maybe-alist)
>> +Same as @var{max-bandwidth} but can be used for setting individual
>> +limits for selected interfaces.  This is an association list of
>> +interfaces as symbols/strings to integer values.  For example,
>> +@lisp
>> +(max-bw `((eth0 .  15000)
>> +          (ppp0 .  10000)))
>> +@end lisp
> 
> Both the naming and semantics are a bit confusing to me.
> 
> How about s/max-bw/per-interface-max-bandwidth/ ?

I found it a bit confusing as well but I'm not too familiar with this part of
the config to comment about it.
I lifted most of the field-names and documentations straight from the manpage.

> Side note: I’d represent interfaces as strings because there’s no
> guarantee they “fit” in a symbol.

Thanks! I'll have this amended in the next revision.


[1]: Listing of vnstat-test.scm

--8<---------------cut here---------------start------------->8---
(define-module (gnu tests vnstat)
  #:use-module (gnu tests)
  #:use-module ((gnu packages networking) #:select (socat vnstat))
  #:use-module (gnu services)
  #:use-module (gnu services networking)
  #:use-module (gnu services monitoring)
  #:use-module (gnu system)
  #:use-module (gnu system vm)
  #:use-module (guix gexp)
  #:use-module (ice-9 format)
  #:export (%test-vnstat))


(define (run-vnstat-test)
  "Run tests in a vm which has vnstat running."

  (define vnstat-config
    (vnstat-configuration
     (max-bandwidth 0)
     (time-sync-wait 0)
     (bandwidth-detection-interval 0)))

  (define os
    (marionette-operating-system
     (simple-operating-system
      (service dhcp-client-service-type)
      (service vnstat-service-type
               vnstat-config)
      (service inetd-service-type
               (inetd-configuration
                (entries
                 (list (inetd-entry
                        (name "discard")
                        (socket-type 'stream)
                        (protocol "tcp")   ;; FIXME: originally this was UDP but port-forwardings hardcodes TCP
                        (wait? #t)
                        (user "nobody")))))))
     #:imported-modules '((gnu services herd))))

  (define forwarded-port 9999)

  (define vm
    (virtual-machine
     (operating-system os)
     ;; Note: port 9 corresponds to "discard" service.
     (port-forwardings `((,forwarded-port . 9)))))   ;; FIXME: Allow UDP forward.

  (define test-timeout (* 60 2))  ; wait for 2 minutes tops.

  (define test
    (with-imported-modules '((gnu build marionette))
      #~(begin
          (use-modules (gnu build marionette)
                       (srfi srfi-64))

          (let ((marionette (make-marionette (list #$vm)))
                (pid-file #$(vnstat-configuration-pid-file vnstat-config)))

            (test-runner-current (system-test-runner #$output))
            (test-begin "vnstat")

            (test-assert "service is running"
              (marionette-eval
               '(begin
                  (use-modules (gnu services herd))
                  (start-service 'vnstatd))
               marionette))

            (test-assert "vnstatd ready"
              (wait-for-file pid-file marionette))

            (test-assert "vnstatd is logging"
              ;; pump garbage into the "discard" service within the vm
              ;; TODO: guile socket client instead? Is it feasible?
              (let* ((socat #$(file-append socat "/bin/socat"))
                     (dest-addr #$(format #f "TCP4:localhost:~d"
                                          forwarded-port))
                     (args `("socat" "-u" "/dev/zero" ,dest-addr))
                     ;; XXX: Guile bug (22/03/2023, Guile 3.0.9)
                     ;;      Fixed in main: <https://issues.guix.gnu.org/61073>
                     #;(output-port (%make-void-port "w"))
                     (garbage-pump-pid (spawn socat args)))

                (let ((retval
                       (marionette-eval
                        '(begin
                           (use-modules (ice-9 popen)
                                        ;(ice-9 rdelim)
                                        (ice-9 match)
                                        (sxml simple)
                                        (sxml xpath))

                           (define selector
                             (let ((xpath '(vnstat interface traffic total)))
                               (compose (node-pos 1) (sxpath xpath))))

                           (let loop ((i 0))
                             (let* ((vnstat #$(file-append vnstat "/bin/vnstat"))
                                    (query-cmd (format #f "~a --xml") vnstat)
                                    (result
                                     #;(call-with-port
                                     (open-input-pipe query-cmd) read-line)
                                     (call-with-port
                                         (open-input-pipe query-cmd) xml->sxml))
                                    (iface-stats (selector result)))
                               (match iface-stats
                                 ((('total ('rx "0") ('tx "0")))
                                  (sleep 1)
                                  (if (< i #$test-timeout)
                                      (loop (+ i 1))
                                      #f))
                                 ((('total ('rx rx) ('tx tx)))
                                  #t)
                                 (_ #f)))))
                        marionette)))
                  ;; shutdown garbage pump
                  (kill garbage-pump-pid SIGTERM)
                  retval)))

            (test-end)))))

  (gexp->derivation "vnstat-test" test))

(define %test-vnstat
  (system-test
   (name "vnstat")
   (description "Basic tests for vnstat service.")
   (value (run-vnstat-test))))
--8<---------------cut here---------------end--------------->8---


Cheers,
Bruno
Ludovic Courtès April 20, 2023, 10:03 a.m. UTC | #4
Hi,

Maxim Cournoyer <maxim.cournoyer@gmail.com> skribis:

> Ludovic Courtès <ludo@gnu.org> writes:

[...]

>> I think a system test would be nice, we generally require it upfront,
>> but since Maxim wrote it can come later, let’s not let it block this
>> patch any longer.
>
> I didn't mean to lower our standards; I wasn't sure if that policy of
> ours was strict, since a few system services do not have corresponding
> tests, IIRC.  I wary a bit that demanding a system test for each added
> service may cause scalability problems in the long run, as each demand a
> disk-heavy image to be generated and the test to run in a VM, which
> makes it expensive/slow.  On the other hand, it's nice to know about any
> regressions when they happen rather than on a reboot...

Yeah, it’s an unwritten policy; I think we’ve consistently required it
for some time now.  It’s useful because otherwise it’s hard to tell what
the status is for a service.

(Speaking of which, we do *not* have that policy for Home services,
because we don’t even have a test strategy, and that’s something we
should fix before it’s too late.)

> If we have such a policy, perhaps we should explicit it in
> our documented contribution guidelines?

Yes!

Also, we should split the submission guidelines into different
categories: packages, services, doc, core, etc.

Ludo’.
Ludovic Courtès April 20, 2023, 10:09 a.m. UTC | #5
Hello!

Bruno Victal <mirai@makinata.eu> skribis:

>>> +@item @code{database-dir} (default: @code{"/var/lib/vnstat"}) (type: string)
>> 
>> [...]
>> 
>>> +@item @code{create-dirs?} (default: @code{#t}) (type: maybe-boolean)
>> 
>> For consistency, both within this record and with the rest of Guix, I
>> suggest avoiding abbreviations.  Since this will be part of the API,
>> better fix it now than later.
>
> I should mention that almost all of the field names here are near verbatim
> vnstat config-file directives, i.e. a near 1-1 Scheme translation of vnstat config.
> This has the benefit that it makes serialization pretty much straightforward.
>
> It's possible to override their names by the use of the custom serializer parameter
> but would it be acceptable to leave them as-is?

Hmm, I’d say that if the cost of using “nice names” is “really high”,
then yes.  But perhaps we can make that cost low by having a map for the
few cases where we use a name different from upstream?

  (define field-name-mapping
    '((database-directory . "database_dir") …))

> (define-module (gnu tests vnstat)

Woohoo, you rock!

>             (test-assert "vnstatd is logging"
>               ;; pump garbage into the "discard" service within the vm
>               ;; TODO: guile socket client instead? Is it feasible?
>               (let* ((socat #$(file-append socat "/bin/socat"))
>                      (dest-addr #$(format #f "TCP4:localhost:~d"
>                                           forwarded-port))
>                      (args `("socat" "-u" "/dev/zero" ,dest-addr))
>                      ;; XXX: Guile bug (22/03/2023, Guile 3.0.9)
>                      ;;      Fixed in main: <https://issues.guix.gnu.org/61073>
>                      #;(output-port (%make-void-port "w"))
>                      (garbage-pump-pid (spawn socat args)))

You can probably connect directly to DEST-ADDR from Guile instead of
going through ‘socat’?

If not, you can either pass ‘#:guile guile-3.0-latest’ to
‘gexp->derivation’ so you get ‘spawn’ (3.0.9 is the default in
‘core-updates’ anyway), or use ‘primitive-fork’.

HTH!

Ludo’.
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index 4f72e2f34a..7e69098267 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -28569,6 +28569,245 @@  Monitoring Services
 @end table
 @end deftp
 
+@anchor{vnstat}
+@subsubheading vnStat Network Traffic Monitor
+@cindex vnstat
+
+vnStat is a network traffic monitor that uses interface statistics provided
+by the kernel rather than traffic sniffing.  This makes it a light resource
+monitor, regardless of network traffic rate.
+
+@defvar vnstat-service-type
+This is the service type for the @uref{https://humdi.net/vnstat/,vnStat} daemon
+and accepts a @code{vnstat-configuration} value.
+
+The following example will configure the service with default values:
+
+@lisp
+(service vnstat-service-type)
+@end lisp
+@end defvar
+
+@c %start of fragment
+@deftp {Data Type} vnstat-configuration
+Available @code{vnstat-configuration} fields are:
+
+@table @asis
+@item @code{package} (default: @code{vnstat}) (type: file-like)
+The vnstat package.
+
+@item @code{database-dir} (default: @code{"/var/lib/vnstat"}) (type: string)
+Specifies the directory where the database is to be stored.  A full path
+must be given and a leading '/' isn't required.
+
+@item @code{5-minute-hours} (default: @code{48}) (type: maybe-integer)
+Data retention duration for the 5 minute resolution entries.  The
+configuration defines for how many past hours entries will be stored.
+Set to @code{-1} for unlimited entries or to @code{0} to disable the
+data collection of this resolution.
+
+@item @code{64bit-interface-counters} (default: @code{-2}) (type: maybe-integer)
+Select interface counter handling.  Set to @code{1} for defining that
+all interfaces use 64-bit counters on the kernel side and @code{0} for
+defining 32-bit counter.  Set to @code{-1} for using the old style logic
+used in earlier versions where counter values within 32-bits are assumed
+to be 32-bit and anything larger is assumed to be a 64-bit counter.  This
+may produce false results if a 64-bit counter is reset within the
+32-bits.  Set to @code{-2} for using automatic detection based on
+available kernel datastructures.
+
+@item @code{always-add-new-interfaces?} (default: @code{#t}) (type: maybe-boolean)
+Enable or disable automatic creation of new database entries for
+interfaces not currently in the database even if the database file
+already exists when the daemon is started.  New database entries will
+also get created for new interfaces seen while the daemon is running.
+Pseudo interfaces @samp{lo}, @samp{lo0} and @samp{sit0} are always excluded from getting
+added.
+
+@item @code{bandwidth-detection?} (default: @code{#t}) (type: maybe-boolean)
+Try to automatically detect @var{max-bandwidth} value for each monitored
+interface.  Mostly only ethernet interfaces support this feature.
+@var{max-bandwidth} will be used as fallback value if detection fails.
+Any interface specific @var{max-BW} configuration will disable the
+detection for the specified interface.  In Linux, the detection is
+disabled for tun interfaces due to the Linux kernel always reporting 10
+Mbit regardless of the used real interface.
+
+@item @code{bandwidth-detection-interval} (default: @code{5}) (type: maybe-integer)
+How often in minutes interface specific detection of @var{max-bandwidth}
+is done for detecting possible changes when @var{bandwidth-detection} is
+enabled.  Can be disabled by setting to @code{0}.  Value range:
+@samp{0}..@samp{30}
+
+@item @code{boot-variation} (default: @code{15}) (type: maybe-integer)
+Time in seconds how much the boot time reported by system kernel can
+variate between updates.  Value range: @samp{0}..@samp{300}
+
+@item @code{check-disk-space?} (default: @code{#t}) (type: maybe-boolean)
+Enable or disable the availability check of at least some free disk
+space before a database write.
+
+@item @code{create-dirs?} (default: @code{#t}) (type: maybe-boolean)
+Enable or disable the creation of directories when a configured path
+doesn't exist.  This includes @var{database-dir}.
+
+@item @code{daemon-group} (type: maybe-user-group)
+Specify the group to which the daemon process should switch during
+startup.  Set to @code{%unset-value} to disable group switching.
+
+@item @code{daemon-user} (type: maybe-user-account)
+Specify the user to which the daemon process should switch during
+startup.  Set to @code{%unset-value} to disable user switching.
+
+@item @code{daily-days} (default: @code{62}) (type: maybe-integer)
+Data retention duration for the one day resolution entries.  The
+configuration defines for how many past days entries will be stored.  Set
+to @code{-1} for unlimited entries or to @code{0} to disable the data
+collection of this resolution.
+
+@item @code{database-synchronous} (default: @code{-1}) (type: maybe-integer)
+Change the setting of the SQLite "synchronous" flag which controls how
+much care is taken to ensure disk writes have fully completed when
+writing data to the database before continuing other actions.  Higher
+values take extra steps to ensure data safety at the cost of slower
+performance.  A value of @code{0} will result in all handling being left
+to the filesystem itself.  Set to @code{-1} to select the default value
+according to database mode controlled by
+@var{database-write-ahead-logging} setting.  See SQLite documentation
+for more details regarding values from @code{1} to @code{3}.  Value
+range: @samp{-1}..@samp{3}
+
+@item @code{database-write-ahead-logging?} (default: @code{#f}) (type: maybe-boolean)
+Enable or disable SQLite Write-Ahead Logging mode for the database.  See
+SQLite documentation for more details and note that support for
+read-only operations isn't available in older SQLite versions.
+
+@item @code{hourly-days} (default: @code{4}) (type: maybe-integer)
+Data retention duration for the one hour resolution entries.  The
+configuration defines for how many past days entries will be stored.  Set
+to @code{-1} for unlimited entries or to @code{0} to disable the data
+collection of this resolution.
+
+@item @code{log-file} (type: maybe-string)
+Specify log file path and name to be used if @var{use-logging} is set to
+@code{1}.
+
+@item @code{max-bandwidth} (type: maybe-integer)
+Maximum bandwidth for all interfaces.  If the interface specific traffic
+exceeds the given value then the data is assumed to be invalid and
+rejected.  Set to 0 in order to disable the feature.  Value range:
+@samp{0}..@samp{50000}
+
+@item @code{max-bw} (type: maybe-alist)
+Same as @var{max-bandwidth} but can be used for setting individual
+limits for selected interfaces.  This is an association list of
+interfaces as symbols/strings to integer values.  For example,
+@lisp
+(max-bw `((eth0 .  15000)
+          (ppp0 .  10000)))
+@end lisp
+@var{bandwidth-detection} is disabled on an interface specific level for
+each @var{max-bw} configuration.  Value range: @samp{0}..@samp{50000}
+
+@item @code{monthly-months} (default: @code{25}) (type: maybe-integer)
+Data retention duration for the one month resolution entries.  The
+configuration defines for how many past months entries will be stored.
+Set to @code{-1} for unlimited entries or to @code{0} to disable the
+data collection of this resolution.
+
+@item @code{month-rotate} (default: @code{1}) (type: maybe-integer)
+Day of month that months are expected to change.  Usually set to 1 but
+can be set to alternative values for example for tracking monthly billed
+traffic where the billing period doesn't start on the first day.  For
+example, if set to 7, days of February up to and including the 6th will
+count for January.  Changing this option will not cause existing data to
+be recalculated.  Value range: @samp{1}..@samp{28}
+
+@item @code{month-rotate-affects-years?} (default: @code{#f}) (type: maybe-boolean)
+Enable or disable @var{month-rotate} also affecting yearly data.
+Applicable only when @var{month-rotate} has a value greater than one.
+
+@item @code{offline-save-interval} (default: @code{30}) (type: maybe-integer)
+How often in minutes cached interface data is saved to file when all
+monitored interfaces are offline.  Value range:
+@var{save-interval}..@samp{60}
+
+@item @code{pid-file} (default: @code{"/var/run/vnstatd.pid"}) (type: maybe-string)
+Specify pid file path and name to be used.
+
+@item @code{poll-interval} (default: @code{5}) (type: maybe-integer)
+How often in seconds interfaces are checked for status changes.  Value
+range: @samp{2}..@samp{60}
+
+@item @code{rescan-database-on-save?} (type: maybe-boolean)
+Automatically discover added interfaces from the database and start
+monitoring.  The rescan is done every @var{save-interval} or
+@var{offline-save-interval} minutes depending on the current activity
+state.
+
+@item @code{save-interval} (default: @code{5}) (type: maybe-integer)
+How often in minutes cached interface data is saved to file.  Value
+range: ( @var{update-interval} / 60 )..@samp{60}
+
+@item @code{save-on-status-change?} (default: @code{#t}) (type: maybe-boolean)
+Enable or disable the additional saving to file of cached interface data
+when the availability of an interface changes, i.e., when an interface
+goes offline or comes online.
+
+@item @code{time-sync-wait} (default: @code{5}) (type: maybe-integer)
+How many minutes to wait during daemon startup for system clock to sync
+if most recent database update appears to be in the future.  This may be
+needed in systems without a real-time clock (RTC) which require some
+time after boot to query and set the correct time.  @code{0} = wait
+disabled.  Value range: @samp{0}..@samp{60}
+
+@item @code{top-day-entries} (default: @code{20}) (type: maybe-integer)
+Data retention duration for the top day entries.  The configuration
+defines how many of the past top day entries will be stored.  Set to
+@code{-1} for unlimited entries or to @code{0} to disable the data
+collection of this resolution.
+
+@item @code{trafficless-entries?} (default: @code{#t}) (type: maybe-boolean)
+Create database entries even when there is no traffic during the entry's
+time period.
+
+@item @code{update-file-owner?} (default: @code{#t}) (type: maybe-boolean)
+Enable or disable the update of file ownership during daemon process
+startup.  During daemon startup, only database, log and pid files will
+be modified if the user or group change feature ( @var{daemon-user} or
+@var{daemon-group} ) is enabled and the files don't match the requested
+user or group.  During manual database creation, this option will cause
+file ownership to be inherited from the database directory if the
+directory already exists.  This option only has effect when the process
+is started as root or via sudo.
+
+@item @code{update-interval} (default: @code{20}) (type: maybe-integer)
+How often in seconds the interface data is updated.  Value range:
+@var{poll-interval}..@samp{300}
+
+@item @code{use-logging} (default: @code{2}) (type: maybe-integer)
+Enable or disable logging.  This option is ignored when the daemon is
+started with .B "-n, --nodaemon" which results in all log output being
+shown in terminal the daemon process is using.  @code{0} = disabled,
+@code{1} = logfile and @code{2} = syslog.
+
+@item @code{use-utc?} (type: maybe-boolean)
+Enable or disable using UTC as timezone in the database for all entries.
+When enabled, all entries added to the database will use UTC regardless
+of the configured system timezone.  When disabled, the configured system
+timezone will be used.  Changing this setting will not result in already
+existing data to be modified.
+
+@item @code{yearly-years} (default: @code{-1}) (type: maybe-integer)
+Data retention duration for the one year resolution entries.  The
+configuration defines for how many past years entries will be stored.
+Set to @code{-1} for unlimited entries or to @code{0} to disable the
+data collection of this resolution.
+
+@end table
+@end deftp
+@c %end of fragment
+
 @subsubheading Zabbix server
 @cindex zabbix zabbix-server
 Zabbix is a high performance monitoring system that can collect data from a
diff --git a/gnu/services/monitoring.scm b/gnu/services/monitoring.scm
index bbf8b10f8b..09de7807c0 100644
--- a/gnu/services/monitoring.scm
+++ b/gnu/services/monitoring.scm
@@ -3,6 +3,7 @@ 
 ;;; Copyright © 2018, 2019 Gábor Boskovits <boskovits@gmail.com>
 ;;; Copyright © 2018, 2019, 2020 Oleg Pykhalov <go.wigust@gmail.com>
 ;;; Copyright © 2022 Marius Bakke <marius@gnu.org>
+;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -26,6 +27,7 @@  (define-module (gnu services monitoring)
   #:use-module (gnu services web)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages monitoring)
+  #:use-module (gnu packages networking)
   #:use-module (gnu system shadow)
   #:use-module (guix gexp)
   #:use-module (guix packages)
@@ -34,6 +36,7 @@  (define-module (gnu services monitoring)
   #:use-module ((guix ui) #:select (display-hint G_))
   #:use-module (ice-9 match)
   #:use-module (ice-9 rdelim)
+  #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-35)
   #:export (darkstat-configuration
@@ -45,6 +48,46 @@  (define-module (gnu services monitoring)
             prometheus-node-exporter-web-listen-address
             prometheus-node-exporter-service-type
 
+            vnstat-configuration
+            vnstat-configuration?
+            vnstat-service-type
+            vnstat-configuration-package
+            vnstat-configuration-database-dir
+            vnstat-configuration-5-minute-hours
+            vnstat-configuration-64bit-interface-counters
+            vnstat-configuration-always-add-new-interfaces?
+            vnstat-configuration-bandwidth-detection?
+            vnstat-configuration-bandwidth-detection-interval
+            vnstat-configuration-boot-variation
+            vnstat-configuration-check-disk-space?
+            vnstat-configuration-create-dirs?
+            vnstat-configuration-daemon-group
+            vnstat-configuration-daemon-user
+            vnstat-configuration-daily-days
+            vnstat-configuration-database-synchronous
+            vnstat-configuration-database-write-ahead-logging?
+            vnstat-configuration-hourly-days
+            vnstat-configuration-log-file
+            vnstat-configuration-max-bandwidth
+            vnstat-configuration-max-bw
+            vnstat-configuration-monthly-months
+            vnstat-configuration-month-rotate
+            vnstat-configuration-month-rotate-affects-years?
+            vnstat-configuration-offline-save-interval
+            vnstat-configuration-pid-file
+            vnstat-configuration-poll-interval
+            vnstat-configuration-rescan-database-on-save?
+            vnstat-configuration-save-interval
+            vnstat-configuration-save-on-status-change?
+            vnstat-configuration-time-sync-wait
+            vnstat-configuration-top-day-entries
+            vnstat-configuration-trafficless-entries?
+            vnstat-configuration-update-file-owner?
+            vnstat-configuration-update-interval
+            vnstat-configuration-use-logging
+            vnstat-configuration-use-utc?
+            vnstat-configuration-yearly-years
+
             zabbix-server-configuration
             zabbix-server-service-type
             zabbix-agent-configuration
@@ -196,6 +239,430 @@  (define prometheus-node-exporter-service-type
                         prometheus-node-exporter-shepherd-service)))
    (default-value (prometheus-node-exporter-configuration))))
 
+
+;;;
+;;; vnstat daemon
+;;;
+
+(define* (camelfy-field-name field-name #:key (dromedary? #f))
+  (match (string-split (symbol->string field-name) #\-)
+    ((head tail ...)
+     (string-join (cons (if dromedary? head (string-upcase head 0 1))
+                        (map (cut string-upcase <> 0 1) tail)) ""))))
+
+(define (strip-trailing-?-character field-name)
+  "Drop rightmost '?' character"
+  (let ((str (symbol->string field-name)))
+    (if (string-suffix? "?" str)
+        (string->symbol (string-drop-right str 1))
+        field-name)))
+
+(define (vnstat-serialize-string field-name value)
+  #~(format #f "~a ~s~%"
+            #$(camelfy-field-name field-name)
+            #$value))
+
+(define vnstat-serialize-integer vnstat-serialize-string)
+
+(define (vnstat-serialize-boolean field-name value)
+  #~(format #f "~a ~a~%"
+            #$(camelfy-field-name (strip-trailing-?-character field-name))
+            #$(if value 1 0)))
+
+(define (vnstat-serialize-alist field-name value)
+  (generic-serialize-alist string-append
+                           (lambda (iface val)
+                             (vnstat-serialize-integer
+                              (format #f "MaxBW~a" iface) val))
+                           value))
+
+(define (vnstat-serialize-user-account field-name value)
+  (vnstat-serialize-string field-name (user-account-name value)))
+
+(define (vnstat-serialize-user-group field-name value)
+  (vnstat-serialize-string field-name (user-group-name value)))
+
+(define-maybe string  (prefix vnstat-))
+(define-maybe integer (prefix vnstat-))
+(define-maybe boolean (prefix vnstat-))
+(define-maybe alist   (prefix vnstat-))
+(define-maybe user-account (prefix vnstat-))
+(define-maybe user-group (prefix vnstat-))
+
+(define %vnstat-user
+  (user-account
+   (name "vnstat")
+   (group "vnstat")
+   (system? #t)
+   (home-directory "/var/empty")
+   (shell (file-append shadow "/sbin/nologin"))))
+
+(define %vnstat-group
+  (user-group
+   (name "vnstat")
+   (system? #t)))
+
+;; Documentation strings from vnstat.conf manpage adapted to texinfo.
+;; vnstat checkout: v2.10, commit b3408af1c609aa6265d296cab7bfe59a61d7cf70
+;; Do not reflow these strings or drop the initial \ escape as it makes it
+;; harder to diff against the manpage.
+(define-configuration vnstat-configuration
+  (package
+    (file-like vnstat)
+    "The vnstat package."
+    empty-serializer)
+
+  (database-dir
+   (string "/var/lib/vnstat")
+   "\
+Specifies the directory where the database is to be stored.
+A full path must be given and a leading '/' isn't required.")  
+
+  (5-minute-hours
+   (maybe-integer 48)
+   "\
+Data retention duration for the 5 minute resolution entries. The configuration
+defines for how many past hours entries will be stored. Set to @code{-1} for
+unlimited entries or to @code{0} to disable the data collection of this
+resolution.")
+
+  (64bit-interface-counters
+   (maybe-integer -2)
+   "\
+Select interface counter handling. Set to @code{1} for defining that all interfaces
+use 64-bit counters on the kernel side and @code{0} for defining 32-bit counter. Set
+to @code{-1} for using the old style logic used in earlier versions where counter
+values within 32-bits are assumed to be 32-bit and anything larger is assumed to
+be a 64-bit counter. This may produce false results if a 64-bit counter is
+reset within the 32-bits. Set to @code{-2} for using automatic detection based on
+available kernel datastructures.")
+
+  (always-add-new-interfaces?
+   (maybe-boolean #t)
+   "\
+Enable or disable automatic creation of new database entries for interfaces not
+currently in the database even if the database file already exists when the
+daemon is started. New database entries will also get created for new interfaces
+seen while the daemon is running. Pseudo interfaces @samp{lo}, @samp{lo0} and @samp{sit0} are always
+excluded from getting added.")
+
+  (bandwidth-detection?
+   (maybe-boolean #t)
+   "\
+Try to automatically detect
+@var{max-bandwidth}
+value for each monitored interface. Mostly only ethernet interfaces support
+this feature.
+@var{max-bandwidth}
+will be used as fallback value if detection fails. Any interface specific
+@var{max-BW}
+configuration will disable the detection for the specified interface.
+In Linux, the detection is disabled for tun interfaces due to the
+Linux kernel always reporting 10 Mbit regardless of the used real interface.")
+
+  (bandwidth-detection-interval
+   (maybe-integer 5)
+   "\
+How often in minutes interface specific detection of
+@var{max-bandwidth}
+is done for detecting possible changes when
+@var{bandwidth-detection}
+is enabled. Can be disabled by setting to @code{0}. Value range: @samp{0}..@samp{30}")
+
+  (boot-variation
+   (maybe-integer 15)
+   "\
+Time in seconds how much the boot time reported by system kernel can variate
+between updates. Value range: @samp{0}..@samp{300}")
+
+  (check-disk-space?
+   (maybe-boolean #t)
+   "\
+Enable or disable the availability check of at least some free disk space before
+a database write.")
+
+  (create-dirs?
+   (maybe-boolean #t)
+   "\
+Enable or disable the creation of directories when a configured path doesn't
+exist. This includes @var{database-dir}.")
+
+  ;; Note: Documentation for daemon-group and daemon-user adapted
+  ;; for user-group and user-account record-types.
+  (daemon-group
+   (maybe-user-group %vnstat-group)
+   "\
+Specify the group to which the daemon process should switch during startup.
+Set to @code{%unset-value} to disable group switching.")
+
+  (daemon-user
+   (maybe-user-account %vnstat-user)
+   "\
+Specify the user to which the daemon process should switch during startup.
+Set to @code{%unset-value} to disable user switching.")
+
+  (daily-days
+   (maybe-integer 62)
+   "\
+Data retention duration for the one day resolution entries. The configuration
+defines for how many past days entries will be stored. Set to @code{-1} for
+unlimited entries or to @code{0} to disable the data collection of this
+resolution.")
+
+  (database-synchronous
+   (maybe-integer -1)
+   "\
+Change the setting of the SQLite \"synchronous\" flag which controls how much
+care is taken to ensure disk writes have fully completed when writing data to
+the database before continuing other actions. Higher values take extra steps
+to ensure data safety at the cost of slower performance. A value of @code{0} will
+result in all handling being left to the filesystem itself. Set to @code{-1} to
+select the default value according to database mode controlled by
+@var{database-write-ahead-logging}
+setting. See SQLite documentation for more details regarding values from @code{1}
+to @code{3}. Value range: @samp{-1}..@samp{3}")
+
+  (database-write-ahead-logging?
+   (maybe-boolean #f)
+   "\
+Enable or disable SQLite Write-Ahead Logging mode for the database. See SQLite
+documentation for more details and note that support for read-only operations
+isn't available in older SQLite versions.")
+
+  (hourly-days
+   (maybe-integer 4)
+   "\
+Data retention duration for the one hour resolution entries. The configuration
+defines for how many past days entries will be stored. Set to @code{-1} for
+unlimited entries or to @code{0} to disable the data collection of this
+resolution.")
+
+  (log-file
+   maybe-string
+   "\
+Specify log file path and name to be used if @var{use-logging} is set to @code{1}.")
+
+  (max-bandwidth
+   maybe-integer
+   "\
+Maximum bandwidth for all interfaces. If the interface specific traffic
+exceeds the given value then the data is assumed to be invalid and rejected.
+Set to 0 in order to disable the feature. Value range: @samp{0}..@samp{50000}")
+
+  ;; documentation adapted for alist type
+  (max-bw
+   maybe-alist
+   "\
+Same as
+@var{max-bandwidth}
+but can be used for setting individual limits
+for selected interfaces. This is an association list of interfaces
+as symbols/strings to integer values. For example,
+@lisp
+(max-bw
+ `((eth0 . 15000)
+   (ppp0 . 10000)))
+@end lisp
+@var{bandwidth-detection}
+is disabled on an interface specific level for each
+@var{max-bw}
+configuration. Value range: @samp{0}..@samp{50000}"
+   (serializer
+    (lambda (field-name value)
+      (if (maybe-value-set? value)
+          (vnstat-serialize-alist field-name value) ""))))
+
+  (monthly-months
+   (maybe-integer 25)
+   "\
+Data retention duration for the one month resolution entries. The configuration
+defines for how many past months entries will be stored. Set to @code{-1} for
+unlimited entries or to @code{0} to disable the data collection of this
+resolution.")
+
+  (month-rotate
+   (maybe-integer 1)
+   "\
+Day of month that months are expected to change. Usually set to
+1 but can be set to alternative values for example for tracking
+monthly billed traffic where the billing period doesn't start on
+the first day. For example, if set to 7, days of February up to and
+including the 6th will count for January. Changing this option will
+not cause existing data to be recalculated. Value range: @samp{1}..@samp{28}")
+
+  (month-rotate-affects-years?
+   (maybe-boolean #f)
+   "\
+Enable or disable
+@var{month-rotate}
+also affecting yearly data. Applicable only when
+@var{month-rotate}
+has a value greater than one.")
+
+  (offline-save-interval
+   (maybe-integer 30)
+   "\
+How often in minutes cached interface data is saved to file when all monitored
+interfaces are offline. Value range:
+@var{save-interval}..@samp{60}")
+
+  (pid-file
+   (maybe-string "/var/run/vnstat/vnstatd.pid")
+   "\
+Specify pid file path and name to be used.")
+
+  (poll-interval
+   (maybe-integer 5)
+   "\
+How often in seconds interfaces are checked for status changes.
+Value range: @samp{2}..@samp{60}")
+
+  (rescan-database-on-save?
+   maybe-boolean
+   "\
+Automatically discover added interfaces from the database and start monitoring.
+The rescan is done every
+@var{save-interval}
+or
+@var{offline-save-interval}
+minutes depending on the current activity state.")
+
+  (save-interval
+   (maybe-integer 5)
+   "\
+How often in minutes cached interface data is saved to file.
+Value range: (
+@var{update-interval} / 60 )..@samp{60}")
+
+  (save-on-status-change?
+   (maybe-boolean #t)
+   "\
+Enable or disable the additional saving to file of cached interface data
+when the availability of an interface changes, i.e., when an interface goes
+offline or comes online.")
+
+  (time-sync-wait
+   (maybe-integer 5)
+   "\
+How many minutes to wait during daemon startup for system clock to sync if
+most recent database update appears to be in the future. This may be needed
+in systems without a real-time clock (RTC) which require some time after boot
+to query and set the correct time. @code{0} = wait disabled.
+Value range: @samp{0}..@samp{60}")
+
+  (top-day-entries
+   (maybe-integer 20)
+   "\
+Data retention duration for the top day entries. The configuration
+defines how many of the past top day entries will be stored. Set to @code{-1} for
+unlimited entries or to @code{0} to disable the data collection of this
+resolution.")
+
+  (trafficless-entries?
+   (maybe-boolean #t)
+   "\
+Create database entries even when there is no traffic during the entry's time
+period.")
+
+  (update-file-owner?
+   (maybe-boolean #t)
+   "\
+Enable or disable the update of file ownership during daemon process startup.
+During daemon startup, only database, log and pid files will be modified if the
+user or group change feature (
+@var{daemon-user}
+or
+@var{daemon-group}
+) is enabled and the files don't match the requested user or group. During manual
+database creation, this option will cause file ownership to be inherited from the
+database directory if the directory already exists. This option only has effect
+when the process is started as root or via sudo.")
+
+  (update-interval
+   (maybe-integer 20)
+   "\
+How often in seconds the interface data is updated. Value range:
+@var{poll-interval}..@samp{300}")
+
+  (use-logging
+   (maybe-integer 2)
+   "\
+Enable or disable logging. This option is ignored when the daemon is started with
+.B \"-n, --nodaemon\"
+which results in all log output being shown in terminal the daemon process is using.
+@code{0} = disabled, @code{1} = logfile and @code{2} = syslog.")
+
+  (use-utc?
+   maybe-boolean
+   "\
+Enable or disable using UTC as timezone in the database for all entries. When
+enabled, all entries added to the database will use UTC regardless of the
+configured system timezone. When disabled, the configured system timezone
+will be used. Changing this setting will not result in already existing
+data to be modified."
+   (serializer
+    (lambda (_ value)
+      (if (maybe-value-set? value)
+          (vnstat-serialize-boolean 'use-UTC value) ""))))
+
+  (yearly-years
+   (maybe-integer -1)
+   "\
+Data retention duration for the one year resolution entries. The configuration
+defines for how many past years entries will be stored. Set to @code{-1} for
+unlimited entries or to @code{0} to disable the data collection of this
+resolution.")
+
+  (prefix vnstat-))
+
+(define (vnstat-serialize-configuration config)
+  (mixed-text-file
+   "vnstat.conf"
+   (serialize-configuration config vnstat-configuration-fields)))
+
+(define (vnstat-shepherd-service config)
+  (let ((config-file (vnstat-serialize-configuration config)))
+    (match-record config <vnstat-configuration> (package pid-file)
+      (shepherd-service
+       (documentation "Run vnstatd.")
+       (requirement `(networking file-systems))
+       (provision '(vnstatd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/sbin/vnstatd")
+                       "--daemon"
+                       "--config" #$config-file)
+                 #:pid-file #$pid-file))
+       (stop #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reload)
+               (documentation "Reload vnstatd.")
+               (procedure
+                #~(lambda (pid)
+                    (if pid
+                        (begin
+                          (kill pid SIGHUP)
+                          (format #t
+                                  "Issued SIGHUP to vnstatd (PID ~a)."
+                                  pid))
+                        (format #t "vnstatd is not running.")))))))))))
+
+(define (vnstat-account-service config)
+  (match-record config <vnstat-configuration> (daemon-group daemon-user)
+    (filter-map maybe-value (list daemon-group daemon-user))))
+
+(define vnstat-service-type
+  (service-type
+   (name 'vnstat)
+   (description "vnStat network-traffic monitor service.")
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             (compose list vnstat-shepherd-service))
+          (service-extension account-service-type
+                             vnstat-account-service)))
+   (default-value (vnstat-configuration))))
+
 
 ;;;
 ;;; Zabbix server