Skip to content

Commit 23ca69e

Browse files
authored
Merge pull request #2 from SuperQ/sources_collector
Add sources collector
2 parents eaf3fcf + 42cb8a2 commit 23ca69e

6 files changed

Lines changed: 233 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.1.0 / 2022-03-20
2+
3+
Initial release.

README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,53 @@
1-
# chrony_exporter
2-
Exporter for Chrony NTP
1+
# Prometheus Chrony Exporter
2+
3+
[![Build Status](https://circleci.com/gh/superq/chrony_exporter.svg?style=svg)](https://circleci.com/gh/superq/chrony_exporter)
4+
5+
This is a [Prometheus Exporter](https://prometheus.io) for [Chrony NTP](https://chrony.tuxfamily.org/).
6+
7+
## Installation
8+
9+
For most use-cases, simply download the [the latest
10+
release](https://github.com/superq/chrony_exporter/releases).
11+
12+
### Building from source
13+
14+
You need a Go development environment. Then, simply run `make` to build the
15+
executable:
16+
17+
make
18+
19+
This uses the common prometheus tooling to build and run some tests.
20+
21+
### Building a Docker container
22+
23+
You can build a Docker container with the included `docker` make target:
24+
25+
make promu
26+
promu crossbuild -p linux/amd64 -p linux/arm64
27+
make docker
28+
29+
This will not even require Go tooling on the host.
30+
31+
## Running
32+
33+
A minimal invocation looks like this:
34+
35+
./chrony_exporter
36+
37+
Supported parameters include:
38+
39+
- `--web.listen-address`: the address/port to listen on (default: `":9290"`)
40+
- `--collector.sources`: Enable/disable the collection of `chronyc sources` metrics. (Default: Disabled)
41+
- `--collector.tracking`: Enable/disable the collection of `chronyc tracking` metrics. (Default: Enabled)
42+
43+
To disable a collector, use `--no-`. (i.e. `--no-collector.tracking`)
44+
45+
By default, the exporter will bind on `:9123`.
46+
47+
## TLS and basic authentication
48+
49+
The IPMI Exporter supports TLS and basic authentication.
50+
51+
To use TLS and/or basic authentication, you need to pass a configuration file
52+
using the `--web.config.file` parameter. The format of the file is described
53+
[in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md).

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.1

collector/collector.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,27 @@ import (
2121
"github.com/go-kit/log"
2222
"github.com/go-kit/log/level"
2323
"github.com/prometheus/client_golang/prometheus"
24+
"gopkg.in/alecthomas/kingpin.v2"
2425
)
2526

2627
const (
2728
namespace = "chrony"
2829
)
2930

31+
var (
32+
collectTracking = kingpin.Flag("collector.tracking", "Collect tracking metrics").Default("true").Bool()
33+
collectSources = kingpin.Flag("collector.sources", "Collect sources metrics").Default("false").Bool()
34+
)
35+
3036
// Exporter collects chrony stats from the given server and exports
3137
// them using the prometheus metrics package.
3238
type Exporter struct {
3339
address string
3440
timeout time.Duration
3541

42+
collectSources bool
43+
collectTracking bool
44+
3645
logger log.Logger
3746
}
3847

@@ -46,10 +55,15 @@ func (d *typedDesc) mustNewConstMetric(value float64, labels ...string) promethe
4655
}
4756

4857
func NewExporter(address string, logger log.Logger) Exporter {
58+
4959
return Exporter{
5060
address: address,
5161
timeout: 5 * time.Second,
52-
logger: logger,
62+
63+
collectSources: *collectSources,
64+
collectTracking: *collectTracking,
65+
66+
logger: logger,
5367
}
5468
}
5569

@@ -67,5 +81,11 @@ func (e Exporter) Collect(ch chan<- prometheus.Metric) {
6781

6882
client := chrony.Client{Sequence: 1, Connection: conn}
6983

70-
e.getTrackingMetrics(ch, client)
84+
if e.collectSources {
85+
e.getSourcesMetrics(ch, client)
86+
}
87+
88+
if e.collectTracking {
89+
e.getTrackingMetrics(ch, client)
90+
}
7191
}

collector/sources.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2022 Ben Kochie
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import (
17+
"fmt"
18+
"math"
19+
"net"
20+
"strings"
21+
22+
"github.com/facebook/time/ntp/chrony"
23+
"github.com/go-kit/log/level"
24+
"github.com/prometheus/client_golang/prometheus"
25+
)
26+
27+
const (
28+
sourcesSubsystem = "sources"
29+
)
30+
31+
var (
32+
sourcesLastRx = typedDesc{
33+
prometheus.NewDesc(
34+
prometheus.BuildFQName(namespace, sourcesSubsystem, "last_sample_age_seconds"),
35+
"Chrony sources last good sample age in seconds",
36+
[]string{"source_address", "source_name"},
37+
nil,
38+
),
39+
prometheus.GaugeValue,
40+
}
41+
42+
sourcesLastSample = typedDesc{
43+
prometheus.NewDesc(
44+
prometheus.BuildFQName(namespace, sourcesSubsystem, "last_sample_offset_seconds"),
45+
"Chrony sources last sample margin of error in seconds",
46+
[]string{"source_address", "source_name"},
47+
nil,
48+
),
49+
prometheus.GaugeValue,
50+
}
51+
52+
sourcesLastSampleErr = typedDesc{
53+
prometheus.NewDesc(
54+
prometheus.BuildFQName(namespace, sourcesSubsystem, "last_sample_error_margin_seconds"),
55+
"Chrony sources last sample offset in seconds",
56+
[]string{"source_address", "source_name"},
57+
nil,
58+
),
59+
prometheus.GaugeValue,
60+
}
61+
62+
sourcesPollInterval = typedDesc{
63+
prometheus.NewDesc(
64+
prometheus.BuildFQName(namespace, sourcesSubsystem, "polling_interval_seconds"),
65+
"Chrony sources polling interval in seconds",
66+
[]string{"source_address", "source_name"},
67+
nil,
68+
),
69+
prometheus.GaugeValue,
70+
}
71+
72+
sourcesStateInfo = typedDesc{
73+
prometheus.NewDesc(
74+
prometheus.BuildFQName(namespace, sourcesSubsystem, "state_info"),
75+
"Chrony sources state info",
76+
[]string{"source_address", "source_name", "source_state", "source_mode"},
77+
nil,
78+
),
79+
prometheus.GaugeValue,
80+
}
81+
82+
sourcesStratum = typedDesc{
83+
prometheus.NewDesc(
84+
prometheus.BuildFQName(namespace, sourcesSubsystem, "stratum"),
85+
"Chrony sources stratum",
86+
[]string{"source_address", "source_name"},
87+
nil,
88+
),
89+
prometheus.GaugeValue,
90+
}
91+
)
92+
93+
func (e Exporter) getSourcesMetrics(ch chan<- prometheus.Metric, client chrony.Client) {
94+
packet, err := client.Communicate(chrony.NewSourcesPacket())
95+
if err != nil {
96+
level.Error(e.logger).Log("msg", "Couldn't get sources", "err", err)
97+
return
98+
}
99+
level.Debug(e.logger).Log("msg", "Got 'sources' response", "sources_packet", packet.GetStatus())
100+
101+
sources, ok := packet.(*chrony.ReplySources)
102+
if !ok {
103+
level.Error(e.logger).Log("msg", "Got wrong 'sources' response", "packet", packet)
104+
return
105+
}
106+
107+
results := make([]chrony.ReplySourceData, sources.NSources)
108+
109+
for i := 0; i < int(sources.NSources); i++ {
110+
level.Debug(e.logger).Log("msg", "Fetching source", "source", i)
111+
packet, err = client.Communicate(chrony.NewSourceDataPacket(int32(i)))
112+
if err != nil {
113+
level.Error(e.logger).Log("msg", "Failed to get sourcedata response", "source", i)
114+
return
115+
}
116+
sourceData, ok := packet.(*chrony.ReplySourceData)
117+
if !ok {
118+
level.Error(e.logger).Log("msg", "Got wrong 'sourcedata' response", "packet", packet)
119+
return
120+
}
121+
results[i] = *sourceData
122+
}
123+
124+
for _, r := range results {
125+
sourceAddress := r.IPAddr.String()
126+
// Ignore reverse lookup errors.
127+
sourceNames, _ := net.LookupAddr(sourceAddress)
128+
sourceName := strings.Join(sourceNames, ",")
129+
130+
ch <- sourcesLastRx.mustNewConstMetric(float64(r.SinceSample), sourceAddress, sourceName)
131+
ch <- sourcesLastSample.mustNewConstMetric(r.LatestMeas, sourceAddress, sourceName)
132+
ch <- sourcesLastSampleErr.mustNewConstMetric(r.LatestMeasErr, sourceAddress, sourceName)
133+
ch <- sourcesPollInterval.mustNewConstMetric(math.Pow(2, float64(r.Poll)), sourceAddress, sourceName)
134+
ch <- sourcesStateInfo.mustNewConstMetric(1.0, sourceAddress, sourceName, r.State.String(), modeTypeString(r.Mode))
135+
ch <- sourcesStratum.mustNewConstMetric(float64(r.Stratum), sourceAddress, sourceName)
136+
}
137+
}
138+
139+
var modeTypeDesc = []string{
140+
"client",
141+
"peer",
142+
"ref",
143+
}
144+
145+
func modeTypeString(m chrony.ModeType) string {
146+
if int(m) >= len(modeTypeDesc) {
147+
return fmt.Sprintf("unknown (%d)", m)
148+
}
149+
return modeTypeDesc[m]
150+
}

main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,19 @@ func main() {
5151
flag.AddFlags(kingpin.CommandLine, promlogConfig)
5252
kingpin.CommandLine.UsageWriter(os.Stdout)
5353
kingpin.HelpFlag.Short('h')
54-
kingpin.Version(version.Print("ipmi_exporter"))
54+
kingpin.Version(version.Print("chrony_exporter"))
5555
kingpin.Parse()
5656

5757
logger = promlog.New(promlogConfig)
58+
level.Info(logger).Log("msg", "Starting chrony_exporter", "version", version.Info())
59+
prometheus.MustRegister(version.NewCollector("chrony_exporter"))
5860

5961
exporter := collector.NewExporter(*address, logger)
6062
prometheus.MustRegister(exporter)
6163

6264
http.Handle("/metrics", promhttp.Handler())
6365

66+
level.Info(logger).Log("msg", "Listening on", "address", *listenAddress)
6467
srv := &http.Server{Addr: *listenAddress}
6568
if err := web.ListenAndServe(srv, *webConfig, logger); err != nil {
6669
level.Error(logger).Log("msg", "HTTP listener stopped", "error", err)

0 commit comments

Comments
 (0)