diff --git a/README.md b/README.md index 030f9a1..7a78ecc 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Supported parameters include: - `--chrony.address`: the address/port (UDP) or path to Unix socket used to connect to chrony (default: `"[::1]:323"`) - `--collector.sources`: Enable/disable the collection of `chronyc sources` metrics. (Default: Disabled) - `--collector.tracking`: Enable/disable the collection of `chronyc tracking` metrics. (Default: Enabled) +- `--collector.serverstats`: Enable/disable the collection of `chronyc serverstats` metrics. This collector only works when chrony is accessed through the Unix socket. (Default: Disabled) - `--collector.dns-lookups`: Enable/disable reverse DNS Lookups. (Default: Enabled) To disable a collector, use `--no-`. (i.e. `--no-collector.tracking`) diff --git a/collector/collector.go b/collector/collector.go index 10f7cae..88677aa 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -49,10 +49,11 @@ type Exporter struct { address string timeout time.Duration - collectSources bool - collectTracking bool - chmodSocket bool - dnsLookups bool + collectSources bool + collectTracking bool + collectServerstats bool + chmodSocket bool + dnsLookups bool logger log.Logger } @@ -82,6 +83,8 @@ type ChronyCollectorConfig struct { CollectSources bool // CollectTracking will configure the exporter to collect `chronyc tracking`. CollectTracking bool + // CollectServerstats will configure the exporter to collect `chronyc serverstats`. + CollectServerstats bool } func NewExporter(conf ChronyCollectorConfig, logger log.Logger) Exporter { @@ -89,10 +92,11 @@ func NewExporter(conf ChronyCollectorConfig, logger log.Logger) Exporter { address: conf.Address, timeout: conf.Timeout, - collectSources: conf.CollectSources, - collectTracking: conf.CollectTracking, - chmodSocket: conf.ChmodSocket, - dnsLookups: conf.DNSLookups, + collectSources: conf.CollectSources, + collectTracking: conf.CollectTracking, + collectServerstats: conf.CollectServerstats, + chmodSocket: conf.ChmodSocket, + dnsLookups: conf.DNSLookups, logger: logger, } @@ -162,4 +166,12 @@ func (e Exporter) Collect(ch chan<- prometheus.Metric) { up = 0 } } + + if e.collectServerstats { + err = e.getServerstatsMetrics(ch, client) + if err != nil { + level.Debug(e.logger).Log("msg", "Couldn't get serverstats", "err", err) + up = 0 + } + } } diff --git a/collector/serverstats.go b/collector/serverstats.go new file mode 100644 index 0000000..a7a8175 --- /dev/null +++ b/collector/serverstats.go @@ -0,0 +1,186 @@ +// Copyright 2024 Ben Kochie +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "fmt" + + "github.com/facebook/time/ntp/chrony" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + serverstatsSubsystem = "serverstats" +) + +var ( + serverstatsNTPHits = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "ntp_packets_received_total"), + "The number of valid NTP requests received by the server.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsNKEHits = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "nts_ke_connections_accepted_total"), + "The number of NTS-KE connections accepted by the server.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsCMDHits = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "command_packets_received_total"), + "The number of command requests received by the server.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsNTPDrops = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "ntp_packets_dropped_total"), + "The number of NTP requests dropped by the server due to rate limiting.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsNKEDrops = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "nts_ke_connections_dropped_total"), + "The number of NTS-KE connections dropped by the server due to rate limiting.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsCMDDrops = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "command_packets_dropped_total"), + "The number of command requests dropped by the server due to rate limiting.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsLogDrops = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "client_log_records_dropped_total"), + "The number of client log records dropped by the server to limit the memory use.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsNTPAuthHits = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "auhtenticated_ntp_packets_total"), + "The number of received NTP requests that were authenticated (with a symmetric key or NTS).", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsNTPInterleavedHits = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "interleaved_ntp_packets_total"), + "The number of received NTP requests that were detected to be in the interleaved mode.", + nil, + nil, + ), + prometheus.CounterValue, + } + + serverstatsNTPTimestamps = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "ntp_timestamps_held"), + "The number of pairs of receive and transmit timestamps that the server is currently holding in memory for clients using the interleaved mode.", + nil, + nil, + ), + prometheus.GaugeValue, + } + + serverstatsNTPSpanSeconds = typedDesc{ + prometheus.NewDesc( + prometheus.BuildFQName(namespace, serverstatsSubsystem, "ntp_timestamp_span_seconds"), + "The interval (in seconds) covered by the currently held NTP timestamps.", + nil, + nil, + ), + prometheus.GaugeValue, + } +) + +func (e Exporter) getServerstatsMetrics(ch chan<- prometheus.Metric, client chrony.Client) error { + packet, err := client.Communicate(chrony.NewServerStatsPacket()) + if err != nil { + return err + } + level.Debug(e.logger).Log("msg", "Got 'serverstats' response", "serverstats_packet", packet.GetStatus()) + + serverstats, ok := packet.(*chrony.ReplyServerStats3) + if !ok { + return fmt.Errorf("got wrong 'serverstats' response: %q", packet) + } + + ch <- serverstatsNTPHits.mustNewConstMetric(float64(serverstats.NTPHits)) + level.Debug(e.logger).Log("msg", "Serverstats NTP Hits", "ntp_hits", serverstats.NTPHits) + + ch <- serverstatsNKEHits.mustNewConstMetric(float64(serverstats.NKEHits)) + level.Debug(e.logger).Log("msg", "Serverstats NKE Hits", "nke_hits", serverstats.NKEHits) + + ch <- serverstatsCMDHits.mustNewConstMetric(float64(serverstats.CMDHits)) + level.Debug(e.logger).Log("msg", "Serverstats CMD Hits", "cmd_hits", serverstats.CMDHits) + + ch <- serverstatsNTPDrops.mustNewConstMetric(float64(serverstats.NTPDrops)) + level.Debug(e.logger).Log("msg", "Serverstats NTP Drops", "ntp_drops", serverstats.NTPDrops) + + ch <- serverstatsNKEDrops.mustNewConstMetric(float64(serverstats.NKEDrops)) + level.Debug(e.logger).Log("msg", "Serverstats NKE Drops", "nke_drops", serverstats.NKEDrops) + + ch <- serverstatsCMDDrops.mustNewConstMetric(float64(serverstats.CMDDrops)) + level.Debug(e.logger).Log("msg", "Serverstats CMD Drops", "cmd_drops", serverstats.CMDDrops) + + ch <- serverstatsLogDrops.mustNewConstMetric(float64(serverstats.LogDrops)) + level.Debug(e.logger).Log("msg", "Serverstats Log Drops", "log_drops", serverstats.LogDrops) + + ch <- serverstatsNTPAuthHits.mustNewConstMetric(float64(serverstats.NTPAuthHits)) + level.Debug(e.logger).Log("msg", "Serverstats Authenticated Packets", "auth_hits", serverstats.NTPAuthHits) + + ch <- serverstatsNTPInterleavedHits.mustNewConstMetric(float64(serverstats.NTPInterleavedHits)) + level.Debug(e.logger).Log("msg", "Serverstats Interleaved Packets", "interleaved_hits", serverstats.NTPInterleavedHits) + + ch <- serverstatsNTPTimestamps.mustNewConstMetric(float64(serverstats.NTPTimestamps)) + level.Debug(e.logger).Log("msg", "Serverstats Timestamps Held", "ntp_timestamps_held", serverstats.NTPTimestamps) + + ch <- serverstatsNTPSpanSeconds.mustNewConstMetric(float64(serverstats.NTPSpanSeconds)) + level.Debug(e.logger).Log("msg", "Serverstats Timestamps Span", "ntp_timestamps_span", serverstats.NTPSpanSeconds) + + return nil +} diff --git a/main.go b/main.go index 57f52e8..3687306 100644 --- a/main.go +++ b/main.go @@ -57,6 +57,11 @@ func main() { "Collect sources metrics", ).Default("false").BoolVar(&conf.CollectSources) + kingpin.Flag( + "collector.serverstats", + "Collect serverstats metrics", + ).Default("false").BoolVar(&conf.CollectServerstats) + kingpin.Flag( "collector.chmod-socket", "Chmod 0666 the receiving unix datagram socket",