commit 0fe68556c57db5648f64a9159bbdc20d99e3edce Author: adoralaura Date: Thu Jul 11 10:37:53 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed0be89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# vscode Go debugging files +__debug_* +template-exporter +bin/ + +# vscode editor config +.vscode/ + diff --git a/.woodpecker/docker-deploy.yml b/.woodpecker/docker-deploy.yml new file mode 100644 index 0000000..dc5ca9e --- /dev/null +++ b/.woodpecker/docker-deploy.yml @@ -0,0 +1,43 @@ +when: + - event: [pull_request, tag] + +# Change this when using template +variables: + - &DOCKER_USERNAME 'adoralaura' + - &DOCKER_REPOSITORY 'adoralaura/template-exporter' + - &DOCKER_REGISTRY 'https://index.docker.io/v1/' + +# Necessary Secrets: +# DOCKERHUB_TOKEN: https://hub.docker.com/settings/security -> New Access-Token -> Read, Write + +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: *DOCKER_REPOSITORY + registry: *DOCKER_REGISTRY + tag: pr-${CI_COMMIT_PULL_REQUEST} + username: *DOCKER_USERNAME + 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: *DOCKER_REPOSITORY + registry: *DOCKER_REGISTRY + auto_tag: true + username: *DOCKER_USERNAME + password: + from_secret: dockerhub_token + + diff --git a/.woodpecker/golang-test.yml b/.woodpecker/golang-test.yml new file mode 100644 index 0000000..e825a0f --- /dev/null +++ b/.woodpecker/golang-test.yml @@ -0,0 +1,9 @@ +when: + - event: push + +steps: + test: + image: golang:1.22-bullseye + commands: + - go mod download + - go test -v ./... diff --git a/.woodpecker/readme.md b/.woodpecker/readme.md new file mode 100644 index 0000000..e769594 --- /dev/null +++ b/.woodpecker/readme.md @@ -0,0 +1,6 @@ +# Woodpecker CI Directory +Please lint the woodpecker configs in this directory before committing! + +```shell +woodpecker-cli lint .woodpecker/*.yml +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..de92a4f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +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 +- asd + +## [0.2.0] - 1970-01-01 +### ⚠️ Breaking Changes +- asd + +### Changed +- asd + +## [0.1.1] - 1970-01-01 +### Fixed +- asd + +## [0.1.0] - 1970-01-01 + +### Added +- This CHANGELOG file to document all significant changes +- LICENSE +- CONTRIBUTING guidelines +- MAINTAINERS file +- minimal implementation of the exporter + +[unreleased]: https://code.lila.network/adoralaura/template-exporter/compare/0.2.0...HEAD +[0.2.0]: https://code.lila.network/adoralaura/template-exporter/compare/0.1.1...0.2.0 +[0.1.1]: https://code.lila.network/adoralaura/template-exporter/compare/0.1.0...0.1.1 +[0.1.0]: https://code.lila.network/adoralaura/template-exporter/releases/tag/0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fbbc679 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +I use my own [Forgejo Instance](https://code.lila.network) to manage issues and 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=%5Btemplate-exporter%5D). + +# What to contribute + +The best way to help without speaking a lot of Go would be to share your +configuration, alerts, dashboards, and recording rules. If you have something +that works and is not in the repository, please pay it forward and +share what works. + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10272ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.22-bullseye AS dev + +COPY . /var/app +WORKDIR /var/app + + +ENV GO111MODULE="on" \ + CGO_ENABLED=0 \ + GOOS=linux + +EXPOSE 80 +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 template-exporter main.go +RUN chmod +x template-exporter + + +FROM gcr.io/distroless/static-debian12 AS prod + +WORKDIR /app +COPY --from=build /var/app/template-exporter /bin/ + +LABEL maintainer="Adora Laura Kalb " + +EXPOSE 43609 +ENTRYPOINT ["/bin/template-exporter"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..85a37ac --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1 @@ +* Adora Laura Kalb @adoralaura diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e329429 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ + +test: + go test ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a9d029 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Template Exporter + +This repository is a template repository for @adoralaura. It's used to easily implement (small) Prometheus Exporters. + +If you run into issues you may inform @adoralaura, but know that you use this template on your own risk! + +# Usage + +## Installation + +Pre-built docker images can be found at [Docker Hub](https://hub.docker.com/r/lauralani/template-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. + +## Running + +Start `template-exporter` as a daemon or from CLI: + +```sh +./template-exporter +``` + +Visit where `203.0.113.8` is the IP +of the NTP server you want to get the score from. + +## Prometheus Configuration + +The URL parameter `target` can be controlled through relabelling. + +Example config: +```YAML +scrape_configs: + - job_name: 'template' + static_configs: + - targets: + - 203.0.113.8 # change this + - 2003:db8::1 # change this + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: 127.0.0.1:43609 # The template exporter's real hostname:port. +``` + +Similarly to [blackbox_exporter](https://github.com/prometheus/blackbox_exporter), +`template-exporter` is meant to run on a few central machines and can be thought of +like a "Prometheus proxy". + +### TLS and basic authentication + +The Template Exporter does **not** support TLS and basic authentication yet. +This may be added in a future release. + +# Once you have it running + +It can be opaque to get started with all this. To make it +easier for others, please consider contributing back your configurations to +us. + +# Contributing +If you have any ideas, enhancements, bug reports or general questions, please don't hesitate to [create a new issue](https://gitlab.com/adoralaura/template-exporter/-/issues/new). + +If you want to contribute to this project, please read the [Contribution Guidelines](CONTRIBUTING.md) + +# License +This project is available under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). See the LICENSE file for more info. diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..dfd64d8 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,93 @@ +/* +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 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() +} diff --git a/cache/cache_test.go b/cache/cache_test.go new file mode 100644 index 0000000..e5f1e96 --- /dev/null +++ b/cache/cache_test.go @@ -0,0 +1,49 @@ +/* +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 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") + } +} diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 0000000..893b35d --- /dev/null +++ b/collector/collector.go @@ -0,0 +1,138 @@ +/* +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/template-exporter/cache" + "golang.adora.codes/template-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, "module", "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) { + 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("template_server_score", + "Shows the server score currently assigned at template.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 + const apiEndpoint = "https://www.template.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) + return + } + res, err := client.Do(req) + if err != nil { + level.Error(logger).Log("msg", "Error in HTTP response", "error", err) + return + + } + 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 + } + + var response models.ApiResponse + + err = json.Unmarshal(body, &response) + if err != nil { + level.Error(logger).Log("msg", "Error unmarshaling JSON body", "error", err) + return + } + + 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("template_server_score", + "Shows the server score currently assigned at template.org", + nil, nil) + + // 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0523629 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + template_exporter: + image: lauralani/template-exporter:latest + restart: always + ports: + - '127.0.0.1:43609:43609' + # Add this to enable debug logging: + # command: --log.level=debug diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c722330 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module golang.adora.codes/template-exporter + +go 1.22.2 + +require ( + github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/go-kit/log v0.2.1 + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/common v0.55.0 + github.com/prometheus/exporter-toolkit v0.11.0 +) + +require ( + github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8a3b6fa --- /dev/null +++ b/go.sum @@ -0,0 +1,77 @@ +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-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg= +github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers/helpers.go b/helpers/helpers.go new file mode 100644 index 0000000..98ccb04 --- /dev/null +++ b/helpers/helpers.go @@ -0,0 +1,25 @@ +/* +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 helpers + +import ( + "time" +) + +func UnixToTime(i int64) time.Time { + return time.Unix(i, 0) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4307caf --- /dev/null +++ b/main.go @@ -0,0 +1,165 @@ +/* +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" + "time" + + "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/template-exporter/cache" + "golang.adora.codes/template-exporter/collector" +) + +const ( + namespace = "template" + appVersion = "0.2.0" +) + +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. + templateRequestErrors = 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) + templateRequestErrors.Inc() + return + } + + targetIP, ipError := netip.ParseAddr(target) + if ipError != nil { + http.Error(w, "'target' parameter must be a valid IPv4/IPv6 address", http.StatusBadRequest) + templateRequestErrors.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() { + version.Version = appVersion + promlogConfig := &promlog.Config{} + flag.AddFlags(kingpin.CommandLine, promlogConfig) + kingpin.Version(version.Print("template_exporter")) + kingpin.HelpFlag.Short('h') + kingpin.Parse() + logger := promlog.New(promlogConfig) + + cache.GlobalScoreCache = cache.NewScoreCache() + + level.Info(logger).Log("msg", "Starting template_exporter", "version", appVersion) + level.Info(logger).Log("build_context", version.BuildContext()) + + exporterMetrics := collector.Metrics{ + NtpppolServerScore: promauto.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "server_score", + Help: "Current template 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: "template 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{ + 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 { + 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..014ee77 --- /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 { + TimestampInt int64 `json:"ts"` + Step int `json:"step"` + Score float64 `json:"score"` + MonitorID int `json:"monitor_id"` +}