Commit Diff


commit - /dev/null
commit + 58479bf180e8681f42a6e424c7fcd3ce8f41b640
blob - /dev/null
blob + e115458fc34367612136caf396dcea0b5e8a169a (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,13 @@
+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
@@ -0,0 +1,39 @@
+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
@@ -0,0 +1,40 @@
+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
@@ -0,0 +1,3 @@
+module olowe.co/nlnet
+
+go 1.18
blob - /dev/null
blob + f247c296c0cb3f1b4b8b4675c0f4afcafe060ed9 (mode 644)
--- /dev/null
+++ stats.go
@@ -0,0 +1,59 @@
+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
@@ -0,0 +1,19 @@
+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
@@ -0,0 +1,35 @@
+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