diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 249d4ec..ea60d6d 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -13,9 +13,10 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version-file: go.mod + cache: false - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - uses: docker/login-action@v1 diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index f6b6fa8..280696e 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -13,9 +13,10 @@ jobs: with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version-file: go.mod + cache: false - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5ff87da..d993944 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -7,13 +7,14 @@ jobs: test: strategy: matrix: - go-version: [1.19.x, 1.20.x] + go-version: [1.20.x] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} + cache: false - run: go test ./... release_test: @@ -21,9 +22,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version-file: go.mod + cache: false - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - run: docker buildx ls diff --git a/Makefile b/Makefile index cfda49b..9576c67 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,15 @@ -GO_VERSION:=1.16 - .PHONY: build build: - goreleaser build --snapshot --single-target --rm-dist + goreleaser build --snapshot --single-target --clean + +.PHONY: run-dev +run-dev: + go run dnsbl_exporter.go --log.debug .PHONY: snapshot snapshot: - goreleaser build --snapshot --rm-dist + goreleaser build --snapshot --clean .PHONY: test test: - docker run \ - -it \ - --rm \ - -v $(CURDIR):/src/github.com/Luzilla/dnsbl_exporter \ - -w /src/github.com/Luzilla/dnsbl_exporter \ - golang:$(GO_VERSION) \ - sh -c "go mod download && go test ./..." + act "pull_request" -j test diff --git a/app/app.go b/app/app.go index 5a6c3b3..a7606a7 100644 --- a/app/app.go +++ b/app/app.go @@ -1,132 +1,173 @@ package app import ( + "errors" + "fmt" + "io" "net/http" "os" + "strings" - "github.com/Luzilla/dnsbl_exporter/collector" "github.com/Luzilla/dnsbl_exporter/config" + "github.com/Luzilla/dnsbl_exporter/internal/index" + "github.com/Luzilla/dnsbl_exporter/internal/metrics" + "github.com/Luzilla/dnsbl_exporter/internal/prober" + "github.com/Luzilla/dnsbl_exporter/internal/setup" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/urfave/cli" - - log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "golang.org/x/exp/slog" ) type DNSBLApp struct { App *cli.App } +var ( + appName, appVersion, appPath string + resolver string +) + // NewApp ... func NewApp(name string, version string) DNSBLApp { - - cli.VersionFlag = cli.BoolFlag{ - Name: "version, V", - Usage: "Print the version information.", - } - - app := cli.NewApp() - app.Name = name - app.Version = version - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "config.dns-resolver", - Value: "127.0.0.1:53", - Usage: "IP address[:port] of the resolver to use.", - EnvVar: "DNSBL_EXP_RESOLVER", + appName = name + appVersion = version + + a := cli.NewApp() + a.Name = appName + a.Version = appVersion + a.Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "config.dns-resolver", + Value: "127.0.0.1:53", + Usage: "IP address[:port] of the resolver to use.", + EnvVars: []string{"DNSBL_EXP_RESOLVER"}, + Destination: &resolver, }, - cli.StringFlag{ - Name: "config.rbls", - Value: "./rbls.ini", - Usage: "Configuration file which contains RBLs", - EnvVar: "DNSBL_EXP_RBLS", + &cli.StringFlag{ + Name: "config.rbls", + Value: "./rbls.ini", + Usage: "Configuration file which contains RBLs", + EnvVars: []string{"DNSBL_EXP_RBLS"}, }, - cli.StringFlag{ - Name: "config.targets", - Value: "./targets.ini", - Usage: "Configuration file which contains the targets to check.", - EnvVar: "DNSBL_EXP_TARGETS", + &cli.StringFlag{ + Name: "config.targets", + Value: "./targets.ini", + Usage: "Configuration file which contains the targets to check.", + EnvVars: []string{"DNSBL_EXP_TARGETS"}, }, - cli.StringFlag{ - Name: "web.listen-address", - Value: ":9211", - Usage: "Address to listen on for web interface and telemetry.", - EnvVar: "DNSBL_EXP_LISTEN", + &cli.StringFlag{ + Name: "web.listen-address", + Value: ":9211", + Usage: "Address to listen on for web interface and telemetry.", + EnvVars: []string{"DNSBL_EXP_LISTEN"}, }, - cli.StringFlag{ - Name: "web.telemetry-path", - Value: "/metrics", - Usage: "Path under which to expose metrics.", + &cli.StringFlag{ + Name: "web.telemetry-path", + Value: "/metrics", + Usage: "Path under which to expose metrics.", + Destination: &appPath, + Action: func(cCtx *cli.Context, v string) error { + if !strings.HasPrefix(v, "/") { + return cli.Exit("Missing / to prefix the path: --web.telemetry-path", 2) + } + return nil + }, }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "web.include-exporter-metrics", Usage: "Include metrics about the exporter itself (promhttp_*, process_*, go_*).", + Value: false, }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "log.debug", Usage: "Enable more output in the logs, otherwise INFO.", + Value: false, }, - cli.StringFlag{ + &cli.StringFlag{ Name: "log.output", Value: "stdout", Usage: "Destination of our logs: stdout, stderr", + Action: func(cCtx *cli.Context, v string) error { + if v != "stdout" && v != "stderr" { + return cli.Exit("We currently support only stdout and stderr: --log.output", 2) + } + return nil + }, }, } return DNSBLApp{ - App: app, + App: a, } } -func (app *DNSBLApp) Bootstrap() { - app.App.Action = func(ctx *cli.Context) error { +func (a *DNSBLApp) Bootstrap() { + a.App.Action = func(ctx *cli.Context) error { // setup logging + fmt.Println("VERSION: " + appVersion) + handler := &slog.HandlerOptions{} + var writer io.Writer + + if ctx.Bool("log.debug") { + handler.Level = slog.LevelDebug + } + switch ctx.String("log.output") { case "stdout": - log.SetOutput(os.Stdout) + writer = os.Stdout case "stderr": - log.SetOutput(os.Stderr) - default: - cli.ShowAppHelp(ctx) - return cli.NewExitError("We currently support only stdout and stderr: --log.output", 2) + writer = os.Stderr } - if ctx.Bool("log.debug") { - log.SetLevel(log.DebugLevel) + + log := slog.New(handler.NewTextHandler(writer)) + + c := config.Config{ + Logger: log.With("area", "config"), } - cfgRbls, err := config.LoadFile(ctx.String("config.rbls"), "rbl") + cfgRbls, err := c.LoadFile(ctx.String("config.rbls")) if err != nil { return err } - cfgTargets, err := config.LoadFile(ctx.String("config.targets"), "targets") + err = c.ValidateConfig(cfgRbls, "rbl") + if err != nil { + return fmt.Errorf("unable to load the rbls from the config: %w", err) + } + + cfgTargets, err := c.LoadFile(ctx.String("config.targets")) if err != nil { return err } - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(` - ` + app.App.Name + ` - -

` + app.App.Name + ` @ ` + app.App.Version + `

-

Metrics

-

Code on Github

- - `)) - }) + err = c.ValidateConfig(cfgTargets, "targets") + if err != nil { + if !errors.Is(err, config.ErrNoServerEntries) && !errors.Is(err, config.ErrNoSuchSection) { + return err + } + log.Info("starting exporter without targets — check the /prober endpoint or correct the .ini file") + } + + iHandler := index.IndexHandler{ + Name: appName, + Version: appVersion, + Path: appPath, + } + + http.HandleFunc("/", iHandler.Handler) - rbls := config.GetRbls(cfgRbls) - targets := config.GetTargets(cfgTargets) + rbls := c.GetRbls(cfgRbls) + targets := c.GetTargets(cfgTargets) - registry := createRegistry() + registry := setup.CreateRegistry() - collector := createCollector(rbls, targets, ctx.String("config.dns-resolver")) - registry.MustRegister(collector) + rblCollector := setup.CreateCollector(rbls, targets, resolver, log.With("area", "metrics")) + registry.MustRegister(rblCollector) - registryExporter := createRegistry() + registryExporter := setup.CreateRegistry() if ctx.Bool("web.include-exporter-metrics") { - log.Infoln("Exposing exporter metrics") + log.Info("Exposing exporter metrics") registryExporter.MustRegister( prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}), @@ -134,20 +175,21 @@ func (app *DNSBLApp) Bootstrap() { ) } - handler := promhttp.HandlerFor( - prometheus.Gatherers{ - registry, - registryExporter, - }, - promhttp.HandlerOpts{ - ErrorHandling: promhttp.ContinueOnError, - Registry: registry, - }, - ) + mHandler := metrics.MetricsHandler{ + Registry: registry, + RegistryExporter: registryExporter, + } - http.Handle(ctx.String("web.telemetry-path"), handler) + http.Handle(ctx.String("web.telemetry-path"), mHandler.Handler()) + + pHandler := prober.ProberHandler{ + Resolver: resolver, + Rbls: rbls, + Logger: log.With("area", "prober"), + } + http.Handle("/prober", pHandler) - log.Infoln("Starting on: ", ctx.String("web.listen-address")) + log.Info("Starting on: " + ctx.String("web.listen-address")) err = http.ListenAndServe(ctx.String("web.listen-address"), nil) if err != nil { return err @@ -157,14 +199,6 @@ func (app *DNSBLApp) Bootstrap() { } } -func (app *DNSBLApp) Run(args []string) error { - return app.App.Run(args) -} - -func createCollector(rbls []string, targets []string, resolver string) *collector.RblCollector { - return collector.NewRblCollector(rbls, targets, resolver) -} - -func createRegistry() *prometheus.Registry { - return prometheus.NewRegistry() +func (a *DNSBLApp) Run(args []string) error { + return a.App.Run(args) } diff --git a/collector/collector.go b/collector/collector.go index 5195dd9..815d1c0 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -2,12 +2,13 @@ package collector import ( "sync" + "time" "github.com/Luzilla/dnsbl_exporter/pkg/dns" "github.com/Luzilla/dnsbl_exporter/pkg/rbl" x "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" ) const namespace = "luzilla" @@ -19,45 +20,61 @@ type RblCollector struct { blacklistedMetric *prometheus.Desc errorsMetrics *prometheus.Desc listedMetric *prometheus.Desc + targetsMetric *prometheus.Desc + durationMetric *prometheus.Desc rbls []string resolver string targets []string + logger *slog.Logger } -func buildFQName(metric string) string { +func BuildFQName(metric string) string { return prometheus.BuildFQName(namespace, subsystem, metric) } // NewRblCollector ... creates the collector -func NewRblCollector(rbls []string, targets []string, resolver string) *RblCollector { +func NewRblCollector(rbls []string, targets []string, resolver string, logger *slog.Logger) *RblCollector { return &RblCollector{ configuredMetric: prometheus.NewDesc( - buildFQName("used"), + BuildFQName("used"), "The number of RBLs to check IPs against (configured via rbls.ini)", nil, nil, ), blacklistedMetric: prometheus.NewDesc( - buildFQName("ips_blacklisted"), + BuildFQName("ips_blacklisted"), "Blacklisted IPs", []string{"rbl", "ip", "hostname"}, nil, ), errorsMetrics: prometheus.NewDesc( - buildFQName("errors"), + BuildFQName("errors"), "The number of errors which occurred testing the RBLs", []string{"rbl"}, nil, ), listedMetric: prometheus.NewDesc( - buildFQName("listed"), + BuildFQName("listed"), "The number of listings in RBLs (this is bad)", []string{"rbl"}, nil, ), + targetsMetric: prometheus.NewDesc( + BuildFQName("targets"), + "The number of targets that are being probed (configured via targets.ini or ?target=)", + nil, + nil, + ), + durationMetric: prometheus.NewDesc( + BuildFQName("duration"), + "The scrape's duration (in seconds)", + nil, + nil, + ), rbls: rbls, resolver: resolver, targets: targets, + logger: logger, } } @@ -78,23 +95,27 @@ func (c *RblCollector) Collect(ch chan<- prometheus.Metric) { float64(len(c.rbls)), ) + ch <- prometheus.MustNewConstMetric( + c.targetsMetric, + prometheus.GaugeValue, + float64(len(hosts)), + ) + + start := time.Now() + // this should be a map of blacklist and a counter (for listings) var listed sync.Map // iterate over hosts -> resolve to ip, check for _, host := range hosts { + logger := c.logger.With("target", host) - log.Debugln("Checking ...", host) + logger.Debug("Starting check") - r := rbl.New(dns.New(new(x.Client), c.resolver)) + r := rbl.New(dns.New(new(x.Client), c.resolver, logger), logger) r.Update(host, c.rbls) for _, result := range r.Results { - // this is an "error" from the RBL - if result.Error { - log.Errorln(result.Text) - } - metricValue := 0 val, _ := listed.LoadOrStore(result.Rbl, 0) @@ -103,9 +124,13 @@ func (c *RblCollector) Collect(ch chan<- prometheus.Metric) { listed.Store(result.Rbl, val.(int)+1) } + logger.Debug(result.Rbl+" listed?", slog.Int("v", metricValue)) + labelValues := []string{result.Rbl, result.Address, host} + // this is an "error" from the RBL if result.Error { + logger.Error(result.Text) ch <- prometheus.MustNewConstMetric( c.errorsMetrics, prometheus.GaugeValue, @@ -123,6 +148,8 @@ func (c *RblCollector) Collect(ch chan<- prometheus.Metric) { } } + c.logger.Debug("building listed metric") + for _, rbl := range c.rbls { val, _ := listed.LoadOrStore(rbl, 0) ch <- prometheus.MustNewConstMetric( @@ -133,4 +160,12 @@ func (c *RblCollector) Collect(ch chan<- prometheus.Metric) { ) } + c.logger.Debug("finished") + + ch <- prometheus.MustNewConstMetric( + c.durationMetric, + prometheus.GaugeValue, + time.Since(start).Seconds(), + ) + } diff --git a/config/config.go b/config/config.go index dd95f5e..fee1992 100644 --- a/config/config.go +++ b/config/config.go @@ -3,19 +3,26 @@ package config import ( "errors" - log "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" "gopkg.in/ini.v1" ) +var ErrNoSuchSection = errors.New("section does not exist") +var ErrNoServerEntries = errors.New("please add a few server= entries to your .ini") + +type Config struct { + Logger *slog.Logger +} + // ValidateConfig validate the supplied configuration, e.g. check if we have "server=" -func ValidateConfig(cfg *ini.File, section string) error { +func (c *Config) ValidateConfig(cfg *ini.File, section string) error { configSection, err := cfg.GetSection(section) if err != nil { - return errors.New("Section does not exists") + return ErrNoSuchSection } if !configSection.HasKey("server") { - return errors.New("Please add a few server= entries to your rbls.ini") + return ErrNoServerEntries } return nil @@ -31,28 +38,23 @@ func loadConfig(path string) (*ini.File, error) { } // GetRbls returns all rbls from the config -func GetRbls(cfg *ini.File) []string { +func (c *Config) GetRbls(cfg *ini.File) []string { return cfg.Section("rbl").Key("server").ValueWithShadows() } // GetTargets returns all targets from the config -func GetTargets(cfg *ini.File) []string { +func (c *Config) GetTargets(cfg *ini.File) []string { return cfg.Section("targets").Key("server").ValueWithShadows() } // LoadFile ... -func LoadFile(path string, section string) (*ini.File, error) { - log.Debugln("Loading configuration...", path) +func (c *Config) LoadFile(path string) (*ini.File, error) { + c.Logger.Debug("Loading configuration file: " + path) cfg, err := loadConfig(path) if err != nil { return nil, err } - err = ValidateConfig(cfg, section) - if err != nil { - return nil, err - } - return cfg, err } diff --git a/config/config_test.go b/config/config_test.go index 2bbdb09..f48bc7d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,53 +1,64 @@ -package config +package config_test import ( - "errors" + "os" "testing" + + "github.com/Luzilla/dnsbl_exporter/config" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slog" ) func TestLoadConfig(t *testing.T) { t.Parallel() tests := []struct { - File string - Key string - Error error + file string + key string + success bool }{ { - File: "./../targets.ini", - Key: "targets", - Error: nil, - }, - { - File: "./../rbls.ini", - Key: "rbl", - Error: nil, + file: "./../targets.ini", + key: "targets", + success: true, }, { - File: "./does-not-exists.ini", - Key: "foo", - Error: errors.New("Section does not exists"), + file: "./../rbls.ini", + key: "rbl", + success: true, }, { - File: "./../targets.ini", - Key: "blah", - Error: errors.New("Section does not exists"), + file: "./does-not-exists.ini", + key: "foo", + success: false, }, } for _, tt := range tests { - tt := tt - t.Run(tt.File, func(t *testing.T) { - _, err := LoadFile(tt.File, tt.Key) - if tt.Error == nil { - if err != nil { - t.Errorf("Could not load '%s' with key '%s': %s", tt.File, tt.Key, err) - } + tc := tt + t.Run(tc.file, func(t *testing.T) { + c := &config.Config{ + Logger: slog.New(slog.NewTextHandler(os.Stderr)), + } + _, err := c.LoadFile(tc.file) + if tc.success { + assert.NoError(t, err, "tc: "+tc.file) } else { - if err == nil { - t.Errorf("Expected error for: '%s', but got none.", tt.File) - } + assert.Error(t, err, "tc: "+tc.file) } }) } } + +func TestValidateConfig(t *testing.T) { + c := &config.Config{ + Logger: slog.New(slog.NewTextHandler(os.Stderr)), + } + + cfg, err := c.LoadFile("./../targets.ini") + assert.NoError(t, err) + + // ensure we return an error when the config section does not exist + err = c.ValidateConfig(cfg, "blah") + assert.Error(t, err) +} diff --git a/dnsbl_exporter.go b/dnsbl_exporter.go index af51dc1..35917dd 100644 --- a/dnsbl_exporter.go +++ b/dnsbl_exporter.go @@ -1,17 +1,15 @@ package main import ( + "fmt" "os" "github.com/Luzilla/dnsbl_exporter/app" - - log "github.com/sirupsen/logrus" ) // The following are customized during build var exporterName string = "dnsbl-exporter" var exporterVersion string -var exporterRev string func main() { dnsbl := app.NewApp(exporterName, exporterVersion) @@ -19,7 +17,8 @@ func main() { err := dnsbl.Run(os.Args) if err != nil { - log.Fatal(err) + fmt.Println("error: " + err.Error()) + os.Exit(1) } } diff --git a/go.mod b/go.mod index 4651699..627f6d0 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module github.com/Luzilla/dnsbl_exporter -go 1.19 +go 1.20 require github.com/prometheus/client_golang v1.15.1 require ( github.com/Luzilla/godnsbl v1.0.0 github.com/miekg/dns v1.1.54 - github.com/sirupsen/logrus v1.9.0 - github.com/urfave/cli v1.22.13 + github.com/stretchr/testify v1.8.2 + github.com/urfave/cli/v2 v2.25.1 + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 gopkg.in/ini.v1 v1.67.0 ) @@ -16,15 +17,21 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/kr/text v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/tools v0.3.0 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 34d8a94..a8be19d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Luzilla/godnsbl v1.0.0 h1:enXCzPjVI/u/bvfqcHDjnU9cSaK7u7UTE2UP7M5/h+Y= github.com/Luzilla/godnsbl v1.0.0/go.mod h1:IMX8t+FUxO3OSwt55zgpYtetfYD07Sfg+Z9YAeFHHfs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -7,6 +6,7 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -17,6 +17,9 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI= @@ -31,27 +34,29 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/urfave/cli v1.22.13 h1:wsLILXG8qCJNse/qAgLNf23737Cx05GflHg/PJGe1Ok= -github.com/urfave/cli v1.22.13/go.mod h1:VufqObjsMTF2BBwKawpx9R8eAneNEWhoO0yx8Vd+FkE= +github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= +github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= @@ -62,9 +67,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -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/internal/index/index.go b/internal/index/index.go new file mode 100644 index 0000000..4923be2 --- /dev/null +++ b/internal/index/index.go @@ -0,0 +1,23 @@ +package index + +import ( + "net/http" +) + +type IndexHandler struct { + Name string + Version string + Path string +} + +func (i IndexHandler) Handler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + ` + i.Name + ` + +

` + i.Name + ` @ ` + i.Version + `

+

Metrics

+

Prober (multi-target export pattern)

+

Code on Github

+ + `)) +} diff --git a/internal/index/index_test.go b/internal/index/index_test.go new file mode 100644 index 0000000..cfc294b --- /dev/null +++ b/internal/index/index_test.go @@ -0,0 +1,31 @@ +package index_test + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Luzilla/dnsbl_exporter/internal/index" + "github.com/stretchr/testify/assert" +) + +func TestIndexHandler(t *testing.T) { + iHandler := index.IndexHandler{ + Name: "test", + Version: "1.2.3", + Path: "/something", + } + + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + iHandler.Handler(w, req) + res := w.Result() + defer res.Body.Close() + data, err := io.ReadAll(res.Body) + assert.NoError(t, err) + + assert.Contains(t, string(data), "test") + assert.Contains(t, string(data), "1.2.3") + assert.Contains(t, string(data), "/something") +} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..6f7cc11 --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,26 @@ +package metrics + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type MetricsHandler struct { + Registry *prometheus.Registry + RegistryExporter *prometheus.Registry +} + +func (m MetricsHandler) Handler() http.Handler { + return promhttp.HandlerFor( + prometheus.Gatherers{ + m.Registry, + m.RegistryExporter, + }, + promhttp.HandlerOpts{ + ErrorHandling: promhttp.ContinueOnError, + Registry: m.Registry, + }, + ) +} diff --git a/internal/prober/prober.go b/internal/prober/prober.go new file mode 100644 index 0000000..86a7964 --- /dev/null +++ b/internal/prober/prober.go @@ -0,0 +1,35 @@ +package prober + +import ( + "net/http" + + "github.com/Luzilla/dnsbl_exporter/internal/setup" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/exp/slog" +) + +type ProberHandler struct { + Resolver string + Rbls []string + Logger *slog.Logger +} + +func (p ProberHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !r.URL.Query().Has("target") { + p.Logger.Error("missing ?target parameter") + http.Error(w, "missing ?target parameter", http.StatusBadRequest) + return + } + + targets := make([]string, 0, 1) + targets = append(targets, r.URL.Query().Get("target")) + + registry := setup.CreateRegistry() + collector := setup.CreateCollector(p.Rbls, targets, p.Resolver, p.Logger) + registry.MustRegister(collector) + + h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{ + ErrorHandling: promhttp.ContinueOnError, + }) + h.ServeHTTP(w, r) +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go new file mode 100644 index 0000000..ad02bbb --- /dev/null +++ b/internal/setup/setup.go @@ -0,0 +1,15 @@ +package setup + +import ( + "github.com/Luzilla/dnsbl_exporter/collector" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/exp/slog" +) + +func CreateCollector(rbls []string, targets []string, resolver string, logger *slog.Logger) *collector.RblCollector { + return collector.NewRblCollector(rbls, targets, resolver, logger) +} + +func CreateRegistry() *prometheus.Registry { + return prometheus.NewRegistry() +} diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index dd4d9d3..bb3a5b0 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -4,18 +4,20 @@ import ( "net" x "github.com/miekg/dns" - log "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" ) type DNSUtil struct { client *x.Client resolver string + logger *slog.Logger } -func New(client *x.Client, resolver string) *DNSUtil { +func New(client *x.Client, resolver string, logger *slog.Logger) *DNSUtil { return &DNSUtil{ client: client, resolver: resolver, + logger: logger, } } @@ -31,7 +33,7 @@ func (d *DNSUtil) GetARecords(target string) ([]string, error) { if err == nil && len(result.Answer) > 0 { for _, ans := range result.Answer { if t, ok := ans.(*x.A); ok { - log.Debugf("We have an A-Record %s for %s", t.A.String(), target) + d.logger.Debug("We have an A-Record", slog.String("target", target), slog.String("v", t.A.String())) list = append(list, t.A.String()) } } @@ -67,7 +69,7 @@ func (d *DNSUtil) makeQuery(msg *x.Msg) (*x.Msg, error) { } result, rt, err := d.client.Exchange(msg, net.JoinHostPort(host, port)) - log.Debugln("Roundtrip", rt) // fixme -> histogram + d.logger.Debug("Roundtrip", slog.Float64("v", rt.Seconds())) // fixme -> histogram return result, err } diff --git a/pkg/rbl/rbl.go b/pkg/rbl/rbl.go index f44c34c..e59ca13 100644 --- a/pkg/rbl/rbl.go +++ b/pkg/rbl/rbl.go @@ -7,7 +7,7 @@ import ( "github.com/Luzilla/dnsbl_exporter/pkg/dns" "github.com/Luzilla/godnsbl" - log "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" ) // Rblresult extends godnsbl and adds RBL name @@ -25,13 +25,15 @@ type Rblresult struct { type Rbl struct { Results []Rblresult util *dns.DNSUtil + logger *slog.Logger } // NewRbl ... factory -func New(util *dns.DNSUtil) Rbl { +func New(util *dns.DNSUtil, logger *slog.Logger) Rbl { var results []Rblresult rbl := Rbl{ + logger: logger, util: util, Results: results, } @@ -49,7 +51,7 @@ func (rbl *Rbl) Update(ip string, rbls []string) { go func(source string, ip string) { defer wg.Done() - log.Debugf("Working blacklist %s (ip: %s)", source, ip) + rbl.logger.Debug("Next up", slog.String("rbl", source), slog.String("ip", ip)) results := rbl.lookup(source, ip) if len(results) == 0 { @@ -66,7 +68,7 @@ func (rbl *Rbl) Update(ip string, rbls []string) { func (rbl *Rbl) query(ip string, blacklist string, result *Rblresult) { result.Listed = false - log.Debugf("Trying to query blacklist '%s' for %s", blacklist, ip) + rbl.logger.Debug("About to query RBL", slog.String("rbl", blacklist), slog.String("ip", ip)) lookup := fmt.Sprintf("%s.%s", ip, blacklist) @@ -94,13 +96,13 @@ func (rbl *Rbl) lookup(rblList string, targetHost string) []Rblresult { if addr == nil { ipsA, err := rbl.util.GetARecords(targetHost) if err != nil { - log.Errorln(err) + rbl.logger.Error(err.Error()) return rbl.Results } ips = ipsA } else { - log.Infoln("We had an ip", addr.String()) + rbl.logger.Info("We had an ip", slog.String("ip", addr.String())) ips = append(ips, addr.String()) } @@ -113,7 +115,7 @@ func (rbl *Rbl) lookup(rblList string, targetHost string) []Rblresult { // attempt to "validate" the IP ValidIPAddress := net.ParseIP(ip) if ValidIPAddress == nil { - log.Errorf("Unable to parse IP: %s", ip) + rbl.logger.Error("Unable to parse IP", slog.String("ip", ip)) continue } diff --git a/pkg/rbl/rbl_test.go b/pkg/rbl/rbl_test.go index ad48f29..918b2d3 100644 --- a/pkg/rbl/rbl_test.go +++ b/pkg/rbl/rbl_test.go @@ -7,16 +7,15 @@ import ( "github.com/Luzilla/dnsbl_exporter/pkg/dns" "github.com/Luzilla/dnsbl_exporter/pkg/rbl" x "github.com/miekg/dns" - log "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" ) func TestUpdate(t *testing.T) { - log.SetLevel(log.DebugLevel) - log.SetOutput(os.Stdout) + logger := slog.New(slog.NewTextHandler(os.Stderr)) - d := dns.New(new(x.Client), "0.0.0.0:53") + d := dns.New(new(x.Client), "0.0.0.0:53", logger) - r := rbl.New(d) + r := rbl.New(d, logger) r.Update("this.is.not.an.ip", []string{"cbl.abuseat.org"}) if len(r.Results) > 0 {