commit - /dev/null
commit + 58479bf180e8681f42a6e424c7fcd3ce8f41b640
blob - /dev/null
blob + e115458fc34367612136caf396dcea0b5e8a169a (mode 644)
--- /dev/null
+++ LICENSE
+Copyright (c) 2022 Oliver Lowe <o@olowe.co>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + c1bcbcf12b182b539cab145b196d66da704486ce (mode 644)
--- /dev/null
+++ cmd/nsd_exporter/nsd_exporter.go
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+
+ "olowe.co/nlnet"
+)
+
+const metricUp = "unbound_up 1"
+const metricDown = "unbound_up 0"
+
+func printMetrics(w io.Writer, stats map[string]float64) {
+ for name, value := range stats {
+ name = strings.ReplaceAll(name, ".", "_")
+ if strings.HasSuffix(name, "avg") || strings.HasSuffix(name, "median") {
+ fmt.Fprintf(w, "# TYPE %s gauge\n", name)
+ }
+ fmt.Fprintf(w, "unbound_%s %f\n", name, value)
+ }
+}
+
+func handleMetrics(w http.ResponseWriter, req *http.Request) {
+ stats, err := nlnet.ReadUnboundStats()
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ printMetrics(w, stats)
+}
+
+func main() {
+ http.HandleFunc("/metrics", handleMetrics)
+ log.Fatal(http.ListenAndServe(":9948", nil))
+}
blob - /dev/null
blob + d525d9f33c1693c0fa969552d519f98b047acb1e (mode 644)
--- /dev/null
+++ cmd/unbound_exporter/unbound_exporter.go
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+
+ "olowe.co/nlnet"
+)
+
+const metricUp = "unbound_up 1"
+const metricDown = "unbound_up 0"
+
+func printMetrics(w io.Writer, stats map[string]float64) {
+ for name, value := range stats {
+ name = strings.ReplaceAll(name, ".", "_")
+ if strings.HasSuffix(name, "avg") || strings.HasSuffix(name, "median") {
+ fmt.Fprintf(w, "# TYPE %s gauge\n", name)
+ }
+ fmt.Fprintf(w, "unbound_%s %f\n", name, value)
+ }
+}
+
+func handleMetrics(w http.ResponseWriter, req *http.Request) {
+ log.Println("ok here we go...")
+ stats, err := nlnet.ReadUnboundStats()
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ printMetrics(w, stats)
+}
+
+func main() {
+ http.HandleFunc("/metrics", handleMetrics)
+ log.Fatal(http.ListenAndServe(":9948", nil))
+}
blob - /dev/null
blob + 2248e8586594d0eb627e434795a9ad781f114898 (mode 644)
--- /dev/null
+++ go.mod
+module olowe.co/nlnet
+
+go 1.18
blob - /dev/null
blob + f247c296c0cb3f1b4b8b4675c0f4afcafe060ed9 (mode 644)
--- /dev/null
+++ stats.go
+package nlnet
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "os/exec"
+ "strconv"
+ "strings"
+)
+
+type stat struct {
+ name string
+ value float64
+}
+
+func parseStats(r io.Reader) (map[string]float64, error) {
+ stats := make(map[string]float64)
+ sc := bufio.NewScanner(r)
+ for sc.Scan() {
+ stat, err := parseStat(sc.Text())
+ if err != nil {
+ return stats, fmt.Errorf("parse stat: %v", err)
+ }
+ stats[stat.name] = stat.value
+ }
+ return stats, sc.Err()
+}
+
+func parseStat(line string) (stat, error) {
+ k, v, found := strings.Cut(line, "=")
+ if !found {
+ return stat{}, fmt.Errorf("%q not found", "=")
+ }
+ f, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return stat{}, fmt.Errorf("parse %s value: %v", k, err)
+ }
+ return stat{name: k, value: f}, nil
+}
+
+func execForStats(path string) (map[string]float64, error) {
+ cmd := exec.Command(path, "stats_noreset")
+ buf := &bytes.Buffer{}
+ cmd.Stdout = buf
+ if err := cmd.Run(); err != nil {
+ return nil, err
+ }
+ return parseStats(buf)
+}
+
+func ReadUnboundStats() (map[string]float64, error) {
+ return execForStats("unbound-control")
+}
+
+func ReadNSDStats() (map[string]float64, error) {
+ return execForStats("nsd-control")
+}
blob - /dev/null
blob + d787101794c36711c91e67efd8347c0e4524db4a (mode 644)
--- /dev/null
+++ stats_test.go
+package nlnet
+
+import (
+ "os"
+ "testing"
+)
+
+func TestParseStats(t *testing.T) {
+ f, err := os.Open("unbound.stats")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ m, err := parseStats(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(m)
+}
\ No newline at end of file
blob - /dev/null
blob + 649c682e899059aa55dca89ae208612f01a7d5e7 (mode 644)
--- /dev/null
+++ unbound.stats
+thread0.num.queries=286342
+thread0.num.queries_ip_ratelimited=0
+thread0.num.cachehits=105482
+thread0.num.cachemiss=180860
+thread0.num.prefetch=0
+thread0.num.expired=0
+thread0.num.recursivereplies=180860
+thread0.requestlist.avg=4.13004
+thread0.requestlist.max=287
+thread0.requestlist.overwritten=0
+thread0.requestlist.exceeded=0
+thread0.requestlist.current.all=0
+thread0.requestlist.current.user=0
+thread0.recursion.time.avg=0.170479
+thread0.recursion.time.median=0.0269844
+thread0.tcpusage=0
+total.num.queries=286342
+total.num.queries_ip_ratelimited=0
+total.num.cachehits=105482
+total.num.cachemiss=180860
+total.num.prefetch=0
+total.num.expired=0
+total.num.recursivereplies=180860
+total.requestlist.avg=4.13004
+total.requestlist.max=287
+total.requestlist.overwritten=0
+total.requestlist.exceeded=0
+total.requestlist.current.all=0
+total.requestlist.current.user=0
+total.recursion.time.avg=0.170479
+total.recursion.time.median=0.0269844
+total.tcpusage=0
+time.now=1671083975.298578
+time.up=1107885.990204
+time.elapsed=1107885.990204