diff --git a/collectors.go b/collectors.go index 54fffe4..f11b58d 100644 --- a/collectors.go +++ b/collectors.go @@ -34,6 +34,7 @@ import ( "github.com/czerwonk/junos_exporter/pkg/features/mplslsp" "github.com/czerwonk/junos_exporter/pkg/features/nat" "github.com/czerwonk/junos_exporter/pkg/features/nat2" + "github.com/czerwonk/junos_exporter/pkg/features/ntp" "github.com/czerwonk/junos_exporter/pkg/features/ospf" "github.com/czerwonk/junos_exporter/pkg/features/power" "github.com/czerwonk/junos_exporter/pkg/features/route" @@ -83,6 +84,9 @@ func (c *collectors) initCollectorsForDevices(device *connector.Device, descRe * c.addCollectorIfEnabledForDevice(device, "alarm", f.Alarm, func() collector.RPCCollector { return alarm.NewCollector(*alarmFilter) }) + c.addCollectorIfEnabledForDevice(device, "ntp", f.NTP, func() collector.RPCCollector { + return ntp.NewCollector() + }) c.addCollectorIfEnabledForDevice(device, "bfd", f.BFD, bfd.NewCollector) c.addCollectorIfEnabledForDevice(device, "bgp", f.BGP, func() collector.RPCCollector { return bgp.NewCollector(c.logicalSystem, descRe) diff --git a/internal/config/config.go b/internal/config/config.go index 3f16119..f1ccdf2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -62,6 +62,7 @@ type DeviceConfig struct { // FeatureConfig is the list of collectors enabled or disabled type FeatureConfig struct { Alarm bool `yaml:"alarm,omitempty"` + NTP bool `yaml:"ntp,omitempty"` Environment bool `yaml:"environment,omitempty"` BFD bool `yaml:"bfd,omitempty"` BGP bool `yaml:"bgp,omitempty"` @@ -151,6 +152,7 @@ func setDefaultValues(c *Config) { c.LSEnabled = false f := &c.Features f.Alarm = true + f.NTP = false f.BGP = true f.Environment = true f.Interfaces = true diff --git a/main.go b/main.go index d4f7654..06b68eb 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,8 @@ var ( sshKeepAliveTimeout = flag.Duration("ssh.keep-alive-timeout", 15*time.Second, "Duration to wait for keep alive message response") sshExpireTimeout = flag.Duration("ssh.expire-timeout", 15*time.Minute, "Duration after an connection is terminated when it is not used") debug = flag.Bool("debug", false, "Show verbose debug output in log") - alarmEnabled = flag.Bool("alarm.enabled", true, "Scrape Alarm metrics") + alarmEnabled = flag.Bool("alarm.enabled", false, "Scrape Alarm metrics") + ntpEnabled = flag.Bool("ntp.enabled", false, "Scrape NTP metrics") bgpEnabled = flag.Bool("bgp.enabled", true, "Scrape BGP metrics") ospfEnabled = flag.Bool("ospf.enabled", true, "Scrape OSPFv3 metrics") isisEnabled = flag.Bool("isis.enabled", false, "Scrape ISIS metrics") @@ -229,6 +230,7 @@ func loadConfigFromFlags() *config.Config { f := &c.Features f.Alarm = *alarmEnabled + f.NTP = *ntpEnabled f.BGP = *bgpEnabled f.Environment = *environmentEnabled f.Firewall = *firewallEnabled diff --git a/pkg/features/ntp/collector.go b/pkg/features/ntp/collector.go new file mode 100644 index 0000000..2455f17 --- /dev/null +++ b/pkg/features/ntp/collector.go @@ -0,0 +1,148 @@ +package ntp + +import ( + "log" + "math" + "strconv" + "strings" + + "github.com/czerwonk/junos_exporter/pkg/collector" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" +) + +const prefix = "junos_ntp_" + +var ( + ntpStratumDesc *prometheus.Desc + ntpOffsetDesc *prometheus.Desc + ntpSysJitterDesc *prometheus.Desc + ntpClkJitterDesc *prometheus.Desc + ntpRootDelayDesc *prometheus.Desc + ntpLeapDesc *prometheus.Desc + ntpPrecisionDesc *prometheus.Desc + ntpPollDesc *prometheus.Desc +) + +func init() { + l := []string{"target", "server"} + ntpStratumDesc = prometheus.NewDesc(prefix+"stratum", "NTP stratum level (0: reference clock, 1-15: hops to refernce clock, 16: not syncronized)", l, nil) + ntpOffsetDesc = prometheus.NewDesc(prefix+"offset", "Time offset in msec", l, nil) + ntpSysJitterDesc = prometheus.NewDesc(prefix+"system_jitter", "System jitter in msec", l, nil) + ntpClkJitterDesc = prometheus.NewDesc(prefix+"clock_jitter", "Clock jitter in msec", l, nil) + ntpRootDelayDesc = prometheus.NewDesc(prefix+"root_delay", "Root delay in msec", l, nil) + ntpLeapDesc = prometheus.NewDesc(prefix+"leap", "Leap indicator (00=ok, 01: last minute with 61 seconds, 10: last minute with 59 seconds, 11: not syncronized)", l, nil) + ntpPrecisionDesc = prometheus.NewDesc(prefix+"precision", "Clock precision (should be -20 to -22)", l, nil) + ntpPollDesc = prometheus.NewDesc(prefix+"poll_interval", "Poll interval in seconds", l, nil) +} + +type ntpCollector struct{} + +func NewCollector() collector.RPCCollector { + return &ntpCollector{} +} + +func (c *ntpCollector) Name() string { + return "ntp" +} + +func (c *ntpCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- ntpStratumDesc + ch <- ntpOffsetDesc + ch <- ntpSysJitterDesc + ch <- ntpClkJitterDesc + ch <- ntpRootDelayDesc + ch <- ntpLeapDesc + ch <- ntpPrecisionDesc + ch <- ntpPollDesc +} + +func (c *ntpCollector) Collect(client collector.Client, ch chan<- prometheus.Metric, labelValues []string) error { + var reply rpcReply + + err := client.RunCommandAndParse("show ntp status | display xml", &reply) + if err != nil { + return errors.Wrap(err, "failed to execute NTP command") + } + + // Hier wird das parseResult direkt aus den Metriken erzeugt + metrics := parseNTPOutput(reply.Output.Text) + if len(metrics) == 0 { + return errors.New("no NTP metrics parsed") + } + + tc := mustParseFloat(metrics["tc"]) + if tc == 0 { + tc = 10 + } + + // Konvertierung der Metriken in parseResult + result := &parseResult{ + AssocID: metrics["associd"], + Stratum: mustParseFloat(metrics["stratum"]), + RefID: metrics["refid"], + Offset: mustParseFloat(metrics["offset"]), + SysJitter: mustParseFloat(metrics["sys_jitter"]), + ClkJitter: mustParseFloat(metrics["clk_jitter"]), + RootDelay: mustParseFloat(metrics["rootdelay"]), + Leap: metrics["leap"], + Precision: mustParseFloat(metrics["precision"]), + PollInterval: math.Pow(2, tc), // 2^10 = 1024 + } + + server := result.RefID + if server == "" { + server = "unknown" + } + + labels := append(labelValues, server) + + exportMetric(ch, ntpStratumDesc, result.Stratum, labels) + exportMetric(ch, ntpOffsetDesc, result.Offset, labels) + exportMetric(ch, ntpSysJitterDesc, result.SysJitter, labels) + exportMetric(ch, ntpClkJitterDesc, result.ClkJitter, labels) + exportMetric(ch, ntpRootDelayDesc, result.RootDelay, labels) + exportMetric(ch, ntpLeapDesc, parseLeap(result.Leap), labels) + exportMetric(ch, ntpPrecisionDesc, result.Precision, labels) + exportMetric(ch, ntpPollDesc, result.PollInterval, labels) + + return nil +} + +func exportMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value float64, labels []string) { + ch <- prometheus.MustNewConstMetric( + desc, + prometheus.GaugeValue, + value, + labels..., + ) +} + +func parseLeap(leap string) float64 { + leap = strings.TrimSpace(leap) + switch leap { + case "00": + return 0 + case "01": + return 1 + case "10": + return 2 + case "11": + return 3 + default: + return -1 + } +} + +func mustParseFloat(s string) float64 { + s = strings.Trim(s, "+,\" ") // Kommas entfernen + if s == "" || s == "-" { + return 0 + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + log.Printf("Parse error for '%s': %v", s, err) + return 0 + } + return f +} diff --git a/pkg/features/ntp/rpc.go b/pkg/features/ntp/rpc.go new file mode 100644 index 0000000..ab37fd0 --- /dev/null +++ b/pkg/features/ntp/rpc.go @@ -0,0 +1,41 @@ +// In rpc.go NUR folgendes belassen: +package ntp + +import ( + "encoding/xml" + "regexp" + "strings" +) + +type rpcReply struct { + XMLName xml.Name `xml:"rpc-reply"` + Output struct { + Text string `xml:",chardata"` + } `xml:"output"` +} + +type parseResult struct { + AssocID string + Stratum float64 + RefID string + Offset float64 + SysJitter float64 + ClkJitter float64 + RootDelay float64 + Leap string + Precision float64 + PollInterval float64 +} + +func parseNTPOutput(output string) map[string]string { + re := regexp.MustCompile(`(\w+)=("[^"]+"|\S+)`) + matches := re.FindAllStringSubmatch(output, -1) + + metrics := make(map[string]string) + for _, m := range matches { + key := m[1] + value := strings.Trim(m[2], "\", ") + metrics[key] = value + } + return metrics +}