diff --git a/.gitignore b/.gitignore index db0dff8..db26547 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ go.work # vscode Go debugging files __debug_* + +# vscode editor config +.vscode/ diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..ae7849d --- /dev/null +++ b/collector/collector.go @@ -0,0 +1,117 @@ +/* +Copyright 2024 Adora Laura Kalb + +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 ( + "context" + "encoding/json" + "io" + "net/http" + "net/netip" + "time" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" + "golang.adora.codes/ntppool-exporter/models" +) + +type Metrics struct { + NtpppolServerScore prometheus.Gauge +} + +type Collector struct { + ctx context.Context + target netip.Addr + logger log.Logger + metrics Metrics +} + +func New(ctx context.Context, target netip.Addr, logger log.Logger, metrics Metrics) *Collector { + return &Collector{ctx: ctx, target: target, logger: logger, metrics: metrics} +} + +// Describe implements Prometheus.Collector. +func (c Collector) Describe(ch chan<- *prometheus.Desc) { + ch <- prometheus.NewDesc("dummy", "dummy", nil, nil) +} + +// Collect implements Prometheus.Collector. +func (c Collector) Collect(ch chan<- prometheus.Metric) { + logger := log.With(c.logger, "scraper") + level.Debug(logger).Log("msg", "Starting scrape") + start := time.Now() + c.collect(ch, logger) + duration := time.Since(start).Seconds() + level.Debug(logger).Log("msg", "Finished scrape", "duration_seconds", duration) +} + +func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) { + httpError := false + var serverApiScore float64 = 0 + const apiEndpoint = "https://www.ntppool.org/scores/" + const apiQuery = "/json?limit=1&monitor=24" + + url := apiEndpoint + c.target.String() + apiQuery + + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + level.Error(logger).Log("msg", "Error sending HTTP request", "url", url, "message", err) + httpError = true + } + res, err := client.Do(req) + if err != nil { + level.Error(logger).Log("msg", "Error in HTTP response", "status", res.Status) + httpError = true + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + level.Error(logger).Log("msg", "Error reading HTTP response body", "error", err) + httpError = true + } + + var response models.ApiResponse + + err = json.Unmarshal(body, &response) + if err != nil { + level.Error(logger).Log("msg", "Error unmarshaling JSON body", "error", err) + httpError = true + } + + if !httpError { + serverApiScore = response.History[0].Score + } + + // TODO: Test or delete + serverScoreMetric := prometheus.NewDesc("ntppool_score", + "Shows the server score currently assigned at ntppool.org", + nil, prometheus.Labels{"server": c.target.String()}) + + // TODO: Test or delete + //c.metrics.NtpppolServerScore.Add(serverApiScore) + + // TODO: Test or delete + //Write latest value for each metric in the prometheus metric channel. + //Note that you can pass CounterValue, GaugeValue, or UntypedValue types here. + m1 := prometheus.MustNewConstMetric(serverScoreMetric, prometheus.GaugeValue, serverApiScore) + m1 = prometheus.NewMetricWithTimestamp(time.Now(), m1) + ch <- m1 +} diff --git a/go.mod b/go.mod index 6d1db2c..35b3a98 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,28 @@ go 1.22.2 require github.com/prometheus/client_golang v1.19.0 require ( + github.com/alecthomas/kingpin/v2 v2.4.0 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/exporter-toolkit v0.11.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index cb062b6..c39c1d3 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,69 @@ +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/exporter-toolkit v0.11.0 h1:yNTsuZ0aNCNFQ3aFTD2uhPOvr4iD7fdBvKPAEGkNf+g= +github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c109601 --- /dev/null +++ b/main.go @@ -0,0 +1,154 @@ +/* +Copyright 2024 Adora Laura Kalb + +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 main + +import ( + "net/http" + "net/netip" + "os" + + "github.com/alecthomas/kingpin/v2" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/promlog" + "github.com/prometheus/common/promlog/flag" + "github.com/prometheus/common/version" + "github.com/prometheus/exporter-toolkit/web" + webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" + "golang.adora.codes/ntppool-exporter/collector" +) + +const ( + namespace = "ntppool" +) + +var ( + metricsPath = kingpin.Flag( + "web.telemetry-path", + "Path under which to expose metrics.", + ).Default("/metrics").String() + //appListenIP = kingpin.Flag("web.listen-address", "Addresses on which to expose metrics and web interface").Default(":43609").String() + toolkitFlags = webflag.AddFlags(kingpin.CommandLine, ":43609") + + // Metrics about the SNMP exporter itself. + ntppoolRequestErrors = promauto.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "request_errors_total", + Help: "Errors in requests to the SNMP exporter", + }, + ) +) + +func handler(w http.ResponseWriter, r *http.Request, logger log.Logger, exporterMetrics collector.Metrics) { + + // /metrics&target=2003%3Adb8%3A%3A1 + // /metrics&target=1.2.3.4 + query := r.URL.Query() + + target := query.Get("target") + if len(query["target"]) != 1 || target == "" { + http.Error(w, "'target' parameter must be specified once", http.StatusBadRequest) + ntppoolRequestErrors.Inc() + return + } + + targetIP, ipError := netip.ParseAddr(target) + if ipError != nil { + http.Error(w, "'target' parameter must be a valid IPv4/IPv6 address", http.StatusBadRequest) + ntppoolRequestErrors.Inc() + return + } + + logger = log.With(logger, "target", target) + registry := prometheus.NewRegistry() + c := collector.New(r.Context(), targetIP, logger, exporterMetrics) + registry.MustRegister(c) + // Delegate http serving to Prometheus client library, which will call collector.Collect. + h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + h.ServeHTTP(w, r) +} + +func main() { + promlogConfig := &promlog.Config{} + flag.AddFlags(kingpin.CommandLine, promlogConfig) + kingpin.Version(version.Print("ntppool_exporter")) + kingpin.HelpFlag.Short('h') + kingpin.Parse() + logger := promlog.New(promlogConfig) + + level.Info(logger).Log("msg", "Starting ntppool_exporter", "version", version.Info()) + level.Info(logger).Log("build_context", version.BuildContext()) + + exporterMetrics := collector.Metrics{ + NtpppolServerScore: promauto.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "server_score", + Help: "Current ntppool server score", + }, + ), + } + + http.HandleFunc(*metricsPath, func(w http.ResponseWriter, r *http.Request) { + handler(w, r, logger, exporterMetrics) + }) + + metricsPathValue := *metricsPath + + if *metricsPath != "/" && *metricsPath != "" { + landingConfig := web.LandingConfig{ + Name: "ntppool Exporter", + Description: "Prometheus Exporter for SNMP targets", + Version: version.Info(), + Form: web.LandingForm{ + Action: metricsPathValue, + Inputs: []web.LandingFormInput{ + { + Label: "Target", + Type: "text", + Name: "target", + Placeholder: "X.X.X.X/[::X]", + Value: "::1", + }, + }, + }, + Links: []web.LandingLinks{ + { + Address: *metricsPath, + Text: "Metrics", + }, + }, + } + + landingPage, err := web.NewLandingPage(landingConfig) + if err != nil { + level.Error(logger).Log("err", err) + os.Exit(1) + } + http.Handle("/", landingPage) + } + + srv := &http.Server{} + if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { + level.Error(logger).Log("msg", "Error starting HTTP server", "err", err) + os.Exit(1) + } +} diff --git a/models/api_response.go b/models/api_response.go new file mode 100644 index 0000000..3dcd88d --- /dev/null +++ b/models/api_response.go @@ -0,0 +1,41 @@ +/* +Copyright 2024 Adora Laura Kalb + +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 models + +import "time" + +type ApiResponse struct { + History []ApiResponseHistory `json:"history"` + Monitors []struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Timestamp time.Time `json:"ts"` + Score float64 `json:"score"` + Status string `json:"status"` + } `json:"monitors"` + Server struct { + IP string `json:"ip"` + } `json:"server"` +} + +type ApiResponseHistory struct { + Timestamp int `json:"ts"` + Step int `json:"step"` + Score float64 `json:"score"` + MonitorID int `json:"monitor_id"` +}