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
__debug_*
ntppool-exporter
bin/
# vscode editor config
.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).
## [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
### Added
- This CHANGELOG file to document all significant changes
- LICENSE
- CONTRIBUTING guidelines
- MAINTAINERS file
- minimal implementation of the exporter
[unreleased]: https://gitlab.com/adoralaura/ntppool-exporter/compare/0.2.0...HEAD
[0.2.0]: https://gitlab.com/adoralaura/ntppool-exporter/compare/0.1.1...0.2.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
[unreleased]: https://gitlab.com/adoralaura/ntppool-exporter/compare/v0.1.0...HEAD
[0.1.0]: https://gitlab.com/adoralaura/ntppool-exporter/releases/tag/v0.1.0

View file

@ -1,12 +1,12 @@
# 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,
addressing (with `@...`) the maintainer of this repository (see
[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

View file

@ -1,9 +1,8 @@
FROM golang:1.22-bullseye AS dev
FROM golang:1.22-alpine AS dev
COPY . /var/app
WORKDIR /var/app
ENV GO111MODULE="on" \
CGO_ENABLED=0 \
GOOS=linux
@ -14,13 +13,12 @@ ENTRYPOINT ["sh"]
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 build -o ntppool-exporter main.go
RUN go build -o ntppool-exporter main.go$
RUN chmod +x ntppool-exporter
FROM gcr.io/distroless/static-debian12 AS prod
FROM scratch AS prod
WORKDIR /app
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.
# Usage
## 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 built by cloning this repository and run `go build main.go` with [Golang](https://go.dev/) installed.
Binaries can be downloaded soon from the [GitLab releases page](https://gitlab.com/adoralaura/ntppool-exporter/-/releases) and need no
special installation
## 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/level"
"github.com/prometheus/client_golang/prometheus"
"golang.adora.codes/ntppool-exporter/cache"
"golang.adora.codes/ntppool-exporter/models"
)
@ -53,7 +52,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
// Collect implements Prometheus.Collector.
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")
start := time.Now()
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) {
score, err := cache.GlobalScoreCache.Get(c.target)
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())
httpError := false
var serverApiScore float64 = 0
const apiEndpoint = "https://www.ntppool.org/scores/"
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 {
level.Error(logger).Log("msg", "Error sending HTTP request", "url", url, "message", err)
return
httpError = true
}
res, err := client.Do(req)
if err != nil {
level.Error(logger).Log("msg", "Error in HTTP response", "error", err)
return
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)
return
httpError = true
}
var response models.ApiResponse
@ -112,19 +93,17 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) {
err = json.Unmarshal(body, &response)
if err != nil {
level.Error(logger).Log("msg", "Error unmarshaling JSON body", "error", err)
return
httpError = true
}
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
serverScoreMetric := prometheus.NewDesc("ntppool_server_score",
serverScoreMetric := prometheus.NewDesc("ntppool_score",
"Shows the server score currently assigned at ntppool.org",
nil, nil)
nil, prometheus.Labels{"server": c.target.String()})
// TODO: Test or delete
//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)
}

15
main.go
View file

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

View file

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