Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

16 changed files with 34 additions and 308 deletions

3
.gitignore vendored
View file

@ -22,9 +22,6 @@ go.work
# vscode Go debugging files # vscode Go debugging files
__debug_* __debug_*
ntppool-exporter
bin/
# vscode editor config # vscode editor config
.vscode/ .vscode/

View file

@ -1,35 +0,0 @@
when:
- event: [pull_request, tag]
steps:
docker-deploy-pr:
when:
- event: pull_request
image: woodpeckerci/plugin-docker-buildx
settings:
dockerfile: Dockerfile
platforms: linux/arm/v7,linux/arm64/v8,linux/amd64
repo: lauralani/ntppool-exporter
registry: https://index.docker.io/v1/
tag: pr-${CI_COMMIT_PULL_REQUEST}
username: lauralani
password:
from_secret: dockerhub_token
docker-deploy-tag:
when:
event: tag
image: woodpeckerci/plugin-docker-buildx
settings:
dockerfile: Dockerfile
platforms: linux/arm/v7,linux/arm64/v8,linux/amd64
repo: lauralani/ntppool-exporter
registry: https://index.docker.io/v1/
auto_tag: true
username: lauralani
password:
from_secret: dockerhub_token

View file

@ -1,9 +0,0 @@
when:
- event: push
steps:
test:
image: golang:1.22-bullseye
commands:
- go mod download
- go test -v ./...

View file

@ -6,35 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed
- changed docker base image to gcr.io/distroless/static-debian12
## [0.2.0] - 2024-04-19
### ⚠️ Breaking Changes
- renamed the score metric from `ntppool_score` to `ntppool_server_score`
- removed unneccessary `server` label in metrics
### Added
- added in-memory cache to reduce API strain (#4)
### Changed
- fixed some debug logs
- update readme to reflect new repo
## [0.1.1] - 2024-04-17
### Fixed
- Fix nil references when http request fails (#2)
## [0.1.0] - 2024-04-17 ## [0.1.0] - 2024-04-17
### Added ### Added
- This CHANGELOG file to document all significant changes - This CHANGELOG file to document all significant changes
- LICENSE - LICENSE
- CONTRIBUTING guidelines - CONTRIBUTING guidelines
- MAINTAINERS file - MAINTAINERS file
- minimal implementation of the exporter - minimal implementation of the exporter
[unreleased]: https://gitlab.com/adoralaura/ntppool-exporter/compare/0.2.0...HEAD [unreleased]: https://gitlab.com/adoralaura/ntppool-exporter/compare/v0.1.0...HEAD
[0.2.0]: https://gitlab.com/adoralaura/ntppool-exporter/compare/0.1.1...0.2.0 [0.1.0]: https://gitlab.com/adoralaura/ntppool-exporter/releases/tag/v0.1.0
[0.1.1]: https://gitlab.com/adoralaura/ntppool-exporter/compare/0.1.0...0.1.1
[0.1.0]: https://gitlab.com/adoralaura/ntppool-exporter/releases/tag/0.1.0

View file

@ -1,12 +1,12 @@
# Contributing # Contributing
I use my own [Forgejo Instance](https://code.lila.network) to manage issues and pull requests. We use GitLab to manage reviews of pull requests.
* If you have a trivial fix or improvement, go ahead and create a pull request, * If you have a trivial fix or improvement, go ahead and create a pull request,
addressing (with `@...`) the maintainer of this repository (see addressing (with `@...`) the maintainer of this repository (see
[MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request.
* If you plan to do something more involved, first please [send me a mail]( mailto:adora@lila.network?subject=%5Bntppool-exporter%5D). * If you plan to do something more involved, first please create [a new issue](https://gitlab.com/adoralaura/ntppool-exporter/-/issues/new).
# What to contribute # What to contribute

View file

@ -1,9 +1,8 @@
FROM golang:1.22-bullseye AS dev FROM golang:1.22-alpine AS dev
COPY . /var/app COPY . /var/app
WORKDIR /var/app WORKDIR /var/app
ENV GO111MODULE="on" \ ENV GO111MODULE="on" \
CGO_ENABLED=0 \ CGO_ENABLED=0 \
GOOS=linux GOOS=linux
@ -14,13 +13,12 @@ ENTRYPOINT ["sh"]
FROM dev as build FROM dev as build
RUN DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --assume-yes ca-certificates
RUN go mod download && go mod verify RUN go mod download && go mod verify
RUN go build -o ntppool-exporter main.go RUN go build -o ntppool-exporter main.go$
RUN chmod +x ntppool-exporter RUN chmod +x ntppool-exporter
FROM gcr.io/distroless/static-debian12 AS prod FROM scratch AS prod
WORKDIR /app WORKDIR /app
COPY --from=build /var/app/ntppool-exporter /bin/ COPY --from=build /var/app/ntppool-exporter /bin/

View file

@ -1,3 +0,0 @@
test:
go test ./...

View file

@ -2,13 +2,16 @@
This exporter is @adoralaura s try to display server scores from servers in [ntppool.org](https://www.ntppool.org) in a format which [Prometheus](https://prometheus.io/) can ingest. This exporter is @adoralaura s try to display server scores from servers in [ntppool.org](https://www.ntppool.org) in a format which [Prometheus](https://prometheus.io/) can ingest.
# Usage # Usage
## Installation ## Installation
Pre-built docker images can be found at [Docker Hub](https://hub.docker.com/r/lauralani/ntppool-exporter). A sample `docker-compose.yml` can be [found here](docker-compose.yml). Binaries can be downloaded soon from the [GitLab releases page](https://gitlab.com/adoralaura/ntppool-exporter/-/releases) and need no
special installation
Binaries can be built by cloning this repository and run `go build main.go` with [Golang](https://go.dev/) installed.
## Running ## Running

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.0

93
cache/cache.go vendored
View file

@ -1,93 +0,0 @@
/*
Copyright 2024 Adora Laura Kalb <adora@lila.network>
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 cache
import (
"net/netip"
"sync"
"time"
)
var (
GlobalScoreCache *ScoreCache
cacheExpiredAfter = 5 * time.Minute
)
type ServerScore struct {
expiresAt time.Time
Score float64
}
type ScoreCache struct {
stop chan struct{}
mu sync.RWMutex
scores map[netip.Addr]ServerScore
}
type CacheMissError struct{}
func (m *CacheMissError) Error() string {
return "User is not in cache!"
}
func newCacheMissError() *CacheMissError {
return &CacheMissError{}
}
func NewScoreCache() *ScoreCache {
lc := &ScoreCache{
scores: make(map[netip.Addr]ServerScore),
stop: make(chan struct{}),
}
return lc
}
func (sc *ScoreCache) Add(score float64, ip netip.Addr, ts time.Time) {
ssc := ServerScore{Score: score, expiresAt: ts.Add(cacheExpiredAfter)}
sc.mu.Lock()
sc.scores[ip] = ssc
sc.mu.Unlock()
}
func (lc *ScoreCache) Get(ip netip.Addr) (ServerScore, error) {
now := time.Now()
lc.mu.RLock()
cachedScore, ok := lc.scores[ip]
if !ok {
lc.mu.RUnlock()
return ServerScore{}, newCacheMissError()
}
lc.mu.RUnlock()
if now.After(cachedScore.expiresAt) {
lc.delete(ip)
return ServerScore{}, newCacheMissError()
}
return cachedScore, nil
}
func (lc *ScoreCache) delete(ip netip.Addr) {
lc.mu.Lock()
delete(lc.scores, ip)
lc.mu.Unlock()
}

49
cache/cache_test.go vendored
View file

@ -1,49 +0,0 @@
/*
Copyright 2024 Adora Laura Kalb <adora@lila.network>
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 cache
import (
"net/netip"
"testing"
"time"
)
func TestCacheGet(t *testing.T) {
const testFloat = 1.23456
cache := NewScoreCache()
cache.Add(testFloat, netip.MustParseAddr("1.2.3.4"), time.Now().Add(5*time.Minute))
score, _ := cache.Get(netip.MustParseAddr("1.2.3.4"))
if score.Score != testFloat {
t.Fatalf("cache.Get(\"1.2.3.4\") = %f, want %f", score.Score, testFloat)
}
}
func TestCacheMiss(t *testing.T) {
now := time.Now()
const testFloat = 1.23456
expiredTime := now.Add(-(time.Minute * 10))
cache := NewScoreCache()
cache.Add(testFloat, netip.MustParseAddr("1.2.3.4"), expiredTime)
_, err := cache.Get(netip.MustParseAddr("1.2.3.4"))
if err == nil {
t.Fatalf("cache.Get(\"1.2.3.4\") = got nil, want error")
}
}

View file

@ -27,7 +27,6 @@ import (
"github.com/go-kit/log" "github.com/go-kit/log"
"github.com/go-kit/log/level" "github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"golang.adora.codes/ntppool-exporter/cache"
"golang.adora.codes/ntppool-exporter/models" "golang.adora.codes/ntppool-exporter/models"
) )
@ -53,7 +52,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
// Collect implements Prometheus.Collector. // Collect implements Prometheus.Collector.
func (c Collector) Collect(ch chan<- prometheus.Metric) { func (c Collector) Collect(ch chan<- prometheus.Metric) {
logger := log.With(c.logger, "module", "scraper") logger := log.With(c.logger, "scraper")
level.Debug(logger).Log("msg", "Starting scrape") level.Debug(logger).Log("msg", "Starting scrape")
start := time.Now() start := time.Now()
c.collect(ch, logger) c.collect(ch, logger)
@ -62,24 +61,7 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
} }
func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) { func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) {
score, err := cache.GlobalScoreCache.Get(c.target) httpError := false
if err == nil {
level.Debug(logger).Log("msg", "Serving score from cache",
"server", c.target.String(), "score", score.Score)
serverScoreMetric := prometheus.NewDesc("ntppool_server_score",
"Shows the server score currently assigned at ntppool.org",
nil, nil)
m1 := prometheus.MustNewConstMetric(serverScoreMetric, prometheus.GaugeValue, score.Score)
m1 = prometheus.NewMetricWithTimestamp(time.Now(), m1)
ch <- m1
return
}
level.Debug(logger).Log("msg", "Cache miss, querying API",
"server", c.target.String())
var serverApiScore float64 = 0 var serverApiScore float64 = 0
const apiEndpoint = "https://www.ntppool.org/scores/" const apiEndpoint = "https://www.ntppool.org/scores/"
const apiQuery = "/json?limit=1&monitor=24" const apiQuery = "/json?limit=1&monitor=24"
@ -91,20 +73,19 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) {
if err != nil { if err != nil {
level.Error(logger).Log("msg", "Error sending HTTP request", "url", url, "message", err) level.Error(logger).Log("msg", "Error sending HTTP request", "url", url, "message", err)
return httpError = true
} }
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
level.Error(logger).Log("msg", "Error in HTTP response", "error", err) level.Error(logger).Log("msg", "Error in HTTP response", "status", res.Status)
return httpError = true
} }
defer res.Body.Close() defer res.Body.Close()
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
level.Error(logger).Log("msg", "Error reading HTTP response body", "error", err) level.Error(logger).Log("msg", "Error reading HTTP response body", "error", err)
return httpError = true
} }
var response models.ApiResponse var response models.ApiResponse
@ -112,19 +93,17 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) {
err = json.Unmarshal(body, &response) err = json.Unmarshal(body, &response)
if err != nil { if err != nil {
level.Error(logger).Log("msg", "Error unmarshaling JSON body", "error", err) level.Error(logger).Log("msg", "Error unmarshaling JSON body", "error", err)
return httpError = true
} }
serverApiScore = response.History[0].Score if !httpError {
serverApiScore = response.History[0].Score
cache.GlobalScoreCache.Add(serverApiScore, c.target, time.Now()) }
level.Debug(logger).Log("msg", "Added score to cache",
"server", c.target.String(), "score", score.Score)
// TODO: Test or delete // TODO: Test or delete
serverScoreMetric := prometheus.NewDesc("ntppool_server_score", serverScoreMetric := prometheus.NewDesc("ntppool_score",
"Shows the server score currently assigned at ntppool.org", "Shows the server score currently assigned at ntppool.org",
nil, nil) nil, prometheus.Labels{"server": c.target.String()})
// TODO: Test or delete // TODO: Test or delete
//c.metrics.NtpppolServerScore.Add(serverApiScore) //c.metrics.NtpppolServerScore.Add(serverApiScore)

View file

@ -1,8 +0,0 @@
services:
ntppool_exporter:
image: lauralani/ntppool-exporter:latest
restart: always
ports:
- '127.0.0.1:43609:43609'
# Add this to enable debug logging:
# command: --log.level=debug

View file

@ -1,25 +0,0 @@
/*
Copyright 2024 Adora Laura Kalb <adora@lila.network>
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 helpers
import (
"time"
)
func UnixToTime(i int64) time.Time {
return time.Unix(i, 0)
}

17
main.go
View file

@ -20,7 +20,6 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"os" "os"
"time"
"github.com/alecthomas/kingpin/v2" "github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log" "github.com/go-kit/log"
@ -33,13 +32,11 @@ import (
"github.com/prometheus/common/version" "github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web" "github.com/prometheus/exporter-toolkit/web"
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
"golang.adora.codes/ntppool-exporter/cache"
"golang.adora.codes/ntppool-exporter/collector" "golang.adora.codes/ntppool-exporter/collector"
) )
const ( const (
namespace = "ntppool" namespace = "ntppool"
appVersion = "0.2.0"
) )
var ( var (
@ -90,7 +87,6 @@ func handler(w http.ResponseWriter, r *http.Request, logger log.Logger, exporter
} }
func main() { func main() {
version.Version = appVersion
promlogConfig := &promlog.Config{} promlogConfig := &promlog.Config{}
flag.AddFlags(kingpin.CommandLine, promlogConfig) flag.AddFlags(kingpin.CommandLine, promlogConfig)
kingpin.Version(version.Print("ntppool_exporter")) kingpin.Version(version.Print("ntppool_exporter"))
@ -98,9 +94,7 @@ func main() {
kingpin.Parse() kingpin.Parse()
logger := promlog.New(promlogConfig) logger := promlog.New(promlogConfig)
cache.GlobalScoreCache = cache.NewScoreCache() level.Info(logger).Log("msg", "Starting ntppool_exporter", "version", version.Info())
level.Info(logger).Log("msg", "Starting ntppool_exporter", "version", appVersion)
level.Info(logger).Log("build_context", version.BuildContext()) level.Info(logger).Log("build_context", version.BuildContext())
exporterMetrics := collector.Metrics{ exporterMetrics := collector.Metrics{
@ -152,12 +146,7 @@ func main() {
http.Handle("/", landingPage) http.Handle("/", landingPage)
} }
srv := &http.Server{ srv := &http.Server{}
ReadHeaderTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil {
level.Error(logger).Log("msg", "Error starting HTTP server", "err", err) level.Error(logger).Log("msg", "Error starting HTTP server", "err", err)
os.Exit(1) os.Exit(1)

View file

@ -34,8 +34,8 @@ type ApiResponse struct {
} }
type ApiResponseHistory struct { type ApiResponseHistory struct {
TimestampInt int64 `json:"ts"` Timestamp int `json:"ts"`
Step int `json:"step"` Step int `json:"step"`
Score float64 `json:"score"` Score float64 `json:"score"`
MonitorID int `json:"monitor_id"` MonitorID int `json:"monitor_id"`
} }