Repository: prometheus/snmp_exporter (go)

Issue:
"make generate" reports msg="NetSNMP reported parse error(s)" errors=2
### Host operating system: output of `uname -a`

```
Darwin MacBook-Pro-6.local 21.6.0 Darwin Kernel Version 21.6.0: Thu Mar  9 20:08:59 PST 2023; root:xnu-8020.240.18.700.8~1/RELEASE_X86_64 x86_64
```

(macOS 12.6.5, Intel)

### snmp_exporter version: output of `snmp_exporter -version`

I installed generator using `go install github.com/prometheus/snmp_exporter/generator@latest` on 2023-04-14

`generate` doesn't have a "version" or "--version" subcommand.

### What device/snmpwalk OID are you using?

N/A - using supplied generator.yml and `make generate` to fetch the MIBs

### If this is a new device, please link to the MIB(s).

N/A

### What did you do that produced an error?

```
cd generator
make generate
```

### What did you expect to see?

Completion without errors

### What did you see instead?

`"NetSNMP reported parse error(s)" errors=2` followed by make terminating.  Full log:

```
$ make generate
go build
go: downloading github.com/prometheus/common v0.40.0
>> Downloading apc-powernet-mib
>> Downloading Cisco AIRESPACE-REF-MIB
>> Downloading Cisco AIRESPACE-WIRELESS-MIB
>> Downloading ARISTA-ENTITY-SENSOR-MIB
>> Downloading ARISTA-SMI-MIB
>> Downloading ARISTA-SW-IP-FORWARDING-MIB
>> Downloading Cisco ENTITY-MIB
>> Downloading Cisco ENTITY-SENSOR-MIB
>> Downloading Cisco ENTITY-STATE-MIB
>> Downloading Cisco ENTITY-STATE-TC-MIB
>> Downloading IANA charset MIB
>> Downloading IANA ifType MIB
>> Downloading IANA printer MIB
>> Downloading Cisco ISDN-MIB
>> Downloading KEEPALIVED-MIB
>> Downloading VRRP-MIB
>> Downloading VRRPv3-MIB
>> Downloading Kemp LM MIBs to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.qcLN0K4H
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.qcLN0K4H
  inflating: mibs/7.2.50.0.18765.RELEASE-ONE4NET-MIB.txt
  inflating: mibs/7.2.50.0.18765.RELEASE-B100-MIB.txt
  inflating: mibs/7.2.50.0.18765.RELEASE-CERTS-MIB.txt
  inflating: mibs/7.2.50.0.18765.RELEASE-IPVS-MIB.txt
# Workaround invalid timestamps.
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.qcLN0K4H
>> Downloading MIKROTIK-MIB
>> Downloading NET-SNMP mibs
>> Downloading paloalto_pano to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.KcLIq6VT
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.KcLIq6VT
 extracting: mibs/PAN-COMMON-MIB.md5
  inflating: mibs/PAN-COMMON-MIB.my
 extracting: mibs/PAN-ENTITY-EXT-MIB.md5
  inflating: mibs/PAN-ENTITY-EXT-MIB.my
 extracting: mibs/PAN-GLOBAL-REG-MIB.md5
  inflating: mibs/PAN-GLOBAL-REG-MIB.my
 extracting: mibs/PAN-GLOBAL-TC-MIB.md5
  inflating: mibs/PAN-GLOBAL-TC-MIB.my
 extracting: mibs/PAN-LC-MIB.md5
  inflating: mibs/PAN-LC-MIB.my
 extracting: mibs/PAN-PRODUCT-MIB.md5
  inflating: mibs/PAN-PRODUCT-MIB.my
 extracting: mibs/PAN-TRAPS.md5
  inflating: mibs/PAN-TRAPS.my
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.KcLIq6VT
>> Downloading PICO-IPSEC-FLOW-MONITOR-MIB.txt
>> Downloading PICO-SMI-ID-MIB.txt
>> Downloading PICO-SMI-MIB.txt
>> Downloading Printer MIB v2
>> Downloading servertech-sentry3-mib
>> Downloading servertech-sentry4-mib
>> Downloading synology to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.nXkpo5ZJ
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.nXkpo5ZJ
  inflating: mibs/SYNOLOGY-UPS-MIB.txt
  inflating: mibs/SYNOLOGY-SHA-MIB.txt
  inflating: mibs/SYNOLOGY-DISK-MIB.txt
  inflating: mibs/SYNOLOGY-GPUINFO-MIB.txt
  inflating: mibs/SYNOLOGY-PORT-MIB.txt
  inflating: mibs/SYNOLOGY-SYSTEM-MIB.txt
  inflating: mibs/SYNOLOGY-FLASHCACHE-MIB.txt
  inflating: mibs/SYNOLOGY-SPACEIO-MIB.txt
  inflating: mibs/SYNOLOGY-NFS-MIB.txt
  inflating: mibs/SYNOLOGY-ISCSILUN-MIB.txt
  inflating: mibs/SYNOLOGY-EBOX-MIB.txt
  inflating: mibs/SYNOLOGY-SMART-MIB.txt
  inflating: mibs/SYNOLOGY-RAID-MIB.txt
  inflating: mibs/SYNOLOGY-SERVICES-MIB.txt
  inflating: mibs/SYNOLOGY-ISCSITarget-MIB.txt
  inflating: mibs/SYNOLOGY-STORAGEIO-MIB.txt
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.nXkpo5ZJ
>> Downloading UBNT-UniFi-MIB
>> Downloading UBNT-AirFiber-MIB
>> Downloading ubnt-airos to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.W9jinGQT
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.W9jinGQT
  inflating: mibs/UBNT-AirMAX-MIB.txt
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.W9jinGQT
>> Downloading WIENER-CRATE-MIB to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.T8iJBYg3
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.T8iJBYg3
  inflating: mibs/WIENER-CRATE-MIB-5704.txt
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.T8iJBYg3
>> Downloading PDU-MIB
>> Downloading Infrapower-MIB.mib to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.vGfZDzx7
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.vGfZDzx7
  inflating: mibs/IPD-03-S-MIB_Q320V1.mib
mibs/IPD-03-S-MIB_Q320V1.mib -> mibs/Infrapower-MIB.mib
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.vGfZDzx7
>> Downloading LIEBERT_GP_PDU.MIB to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.stGpbTRe
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.stGpbTRe
  inflating: mibs/LIEBERT_GP_PDU.MIB
  inflating: mibs/LIEBERT_GP_REG.MIB
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.stGpbTRe
>> Downloading CyberPower.MIB to /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.cgE4TST3
Archive:  /var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.cgE4TST3
  inflating: mibs/CyberPower_MIB_v2.10.MIB
mibs/CyberPower_MIB_v2.10.MIB -> mibs/CyberPower.MIB
/var/folders/f8/nggm8yyx2qvbflgmqg1zq5gh0000gp/T/tmp.cgE4TST3
MIBDIRS='mibs' ./generator --fail-on-parse-errors generate
ts=2023-04-14T13:58:17.158Z caller=net_snmp.go:161 level=info msg="Loading MIBs" from=mibs
ts=2023-04-14T13:58:17.560Z caller=main.go:119 level=warn msg="NetSNMP reported parse error(s)" errors=2
ts=2023-04-14T13:58:17.636Z caller=main.go:51 level=info msg="Generating config for module" module=raritan
ts=2023-04-14T13:58:17.662Z caller=main.go:66 level=info msg="Generated metrics" module=raritan metrics=11
ts=2023-04-14T13:58:17.662Z caller=main.go:51 level=info msg="Generating config for module" module=wiener_mpod
ts=2023-04-14T13:58:17.678Z caller=main.go:66 level=info msg="Generated metrics" module=wiener_mpod metrics=20
modules:
ts=2023-04-14T13:58:17.678Z caller=main.go:51 level=info msg="Generating config for module" module=keepalived
ts=2023-04-14T13:58:17.712Z caller=main.go:66 level=info msg="Generated metrics" module=keepalived metrics=186
ts=2023-04-14T13:58:17.712Z caller=main.go:51 level=info msg="Generating config for module" module=ubiquiti_airmax
ts=2023-04-14T13:58:17.726Z caller=main.go:66 level=info msg="Generated metrics" module=ubiquiti_airmax metrics=122
ts=2023-04-14T13:58:17.726Z caller=main.go:51 level=info msg="Generating config for module" module=nec_ix
ts=2023-04-14T13:58:17.757Z caller=main.go:66 level=info msg="Generated metrics" module=nec_ix metrics=261
ts=2023-04-14T13:58:17.757Z caller=main.go:51 level=info msg="Generating config for module" module=infrapower_pdu
ts=2023-04-14T13:58:17.775Z caller=main.go:66 level=info msg="Generated metrics" module=infrapower_pdu metrics=4
ts=2023-04-14T13:58:17.775Z caller=main.go:51 level=info msg="Generating config for module" module=servertech_sentry4
ts=2023-04-14T13:58:17.795Z caller=main.go:66 level=info msg="Generated metrics" module=servertech_sentry4 metrics=55
ts=2023-04-14T13:58:17.795Z caller=main.go:51 level=info msg="Generating config for module" module=cisco_wlc
ts=2023-04-14T13:58:17.811Z caller=main.go:66 level=info msg="Generated metrics" module=cisco_wlc metrics=62
ts=2023-04-14T13:58:17.811Z caller=main.go:51 level=info msg="Generating config for module" module=apcups
ts=2023-04-14T13:58:17.841Z caller=main.go:66 level=info msg="Generated metrics" module=apcups metrics=332
ts=2023-04-14T13:58:17.841Z caller=main.go:51 level=info msg="Generating config for module" module=servertech_sentry3
ts=2023-04-14T13:58:17.864Z caller=main.go:66 level=info msg="Generated metrics" module=servertech_sentry3 metrics=44
ts=2023-04-14T13:58:17.864Z caller=main.go:51 level=info msg="Generating config for module" module=paloalto_fw
ts=2023-04-14T13:58:17.879Z caller=main.go:66 level=info msg="Generated metrics" module=paloalto_fw metrics=228
ts=2023-04-14T13:58:17.879Z caller=main.go:51 level=info msg="Generating config for module" module=synology
ts=2023-04-14T13:58:17.904Z caller=main.go:66 level=info msg="Generated metrics" module=synology metrics=293
ts=2023-04-14T13:58:17.904Z caller=main.go:51 level=info msg="Generating config for module" module=ubiquiti_airfiber
ts=2023-04-14T13:58:17.924Z caller=main.go:66 level=info msg="Generated metrics" module=ubiquiti_airfiber metrics=193
ts=2023-04-14T13:58:17.924Z caller=main.go:51 level=info msg="Generating config for module" module=ubiquiti_unifi
ts=2023-04-14T13:58:17.955Z caller=main.go:66 level=info msg="Generated metrics" module=ubiquiti_unifi metrics=97
ts=2023-04-14T13:58:17.955Z caller=main.go:51 level=info msg="Generating config for module" module=if_mib
ts=2023-04-14T13:58:17.971Z caller=main.go:66 level=info msg="Generated metrics" module=if_mib metrics=40
ts=2023-04-14T13:58:17.971Z caller=main.go:51 level=info msg="Generating config for module" module=liebert_pdu
ts=2023-04-14T13:58:17.994Z caller=main.go:66 level=info msg="Generated metrics" module=liebert_pdu metrics=117
ts=2023-04-14T13:58:17.994Z caller=main.go:51 level=info msg="Generating config for module" module=kemp_loadmaster
ts=2023-04-14T13:58:18.015Z caller=main.go:66 level=info msg="Generated metrics" module=kemp_loadmaster metrics=141
ts=2023-04-14T13:58:18.015Z caller=main.go:51 level=info msg="Generating config for module" module=ddwrt
ts=2023-04-14T13:58:18.033Z caller=main.go:66 level=info msg="Generated metrics" module=ddwrt metrics=111
ts=2023-04-14T13:58:18.033Z caller=main.go:51 level=info msg="Generating config for module" module=printer_mib
ts=2023-04-14T13:58:18.060Z caller=main.go:66 level=info msg="Generated metrics" module=printer_mib metrics=13
ts=2023-04-14T13:58:18.060Z caller=main.go:51 level=info msg="Generating config for module" module=mikrotik
ts=2023-04-14T13:58:18.098Z caller=main.go:66 level=info msg="Generated metrics" module=mikrotik metrics=443
ts=2023-04-14T13:58:18.098Z caller=main.go:51 level=info msg="Generating config for module" module=cyberpower
ts=2023-04-14T13:58:18.128Z caller=main.go:66 level=info msg="Generated metrics" module=cyberpower metrics=306
ts=2023-04-14T13:58:18.128Z caller=main.go:51 level=info msg="Generating config for module" module=arista_sw
ts=2023-04-14T13:58:18.171Z caller=main.go:66 level=info msg="Generated metrics" module=arista_sw metrics=91
ts=2023-04-14T13:58:18.486Z caller=main.go:91 level=info msg="Config written" file=/Users/brian/git/snmp_exporter/generator/snmp.yml
make: *** [generate] Error 1
$ 
```

I note that snmptranslate seems happy when given a simple test:

```
$ MIBDIRS=mibs MIBS=: snmptranslate -On IF-MIB::ifOperStatus
.1.3.6.1.2.1.2.2.1.8
```

It's unclear which MIBs have the parse errors in them. snmp.yml *is* created, but a bit smaller than the official one:

```
$ ls -l snmp.yml ../snmp.yml
-rw-r--r--  1 brian  staff  1501213 28 Mar 07:51 ../snmp.yml
-rw-r--r--  1 brian  staff  1486060 14 Apr 15:21 snmp.yml
```

Diffing them, I see that some entries from CPS-MIB are different, but commenting out CyberPower from generator.yml doesn't make a difference.




Pull request:
Patch MIBs to import DisplayString from SNMPv2-TC instead of RFC1213-MIB
Fixes #867

Patch:
diff --git a/collector/collector.go b/collector/collector.go
index 8343880..1c8366a 100644
--- a/collector/collector.go
+++ b/collector/collector.go
@@ -23,12 +23,12 @@ import (
 	"strings"
 	"time"
 
+	"github.com/alecthomas/kingpin/v2"
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
 	"github.com/gosnmp/gosnmp"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
-	"gopkg.in/alecthomas/kingpin.v2"
 
 	"github.com/prometheus/snmp_exporter/config"
 )
diff --git a/generator/Makefile b/generator/Makefile
index 579610d..c5b5715 100644
--- a/generator/Makefile
+++ b/generator/Makefile
@@ -129,6 +129,9 @@ mibs: \
 $(MIBDIR)/apc-powernet-mib:
 	@echo ">> Downloading apc-powernet-mib"
 	@curl $(CURL_OPTS) -o $(MIBDIR)/apc-powernet-mib "$(APC_URL)"
+	# Workaround to make DisplayString available (#867)
+	@sed -i.bak -E 's/(DisplayString[[:space:]]*FROM )RFC1213-MIB/\1SNMPv2-TC/' $(MIBDIR)/apc-powernet-mib
+	@rm $(MIBDIR)/apc-powernet-mib.bak
 
 $(MIBDIR)/ARISTA-ENTITY-SENSOR-MIB:
 	@echo ">> Downloading ARISTA-ENTITY-SENSOR-MIB"
@@ -313,4 +316,7 @@ $(MIBDIR)/CyberPower.MIB:
 	@curl $(CURL_OPTS) -o $(TMP) $(CYBERPOWER_URL)
 	@unzip -j -d $(MIBDIR) $(TMP) CyberPower_MIB_v$(CYBERPOWER_VERSION).MIB
 	@mv -v $(MIBDIR)/CyberPower_MIB_v$(CYBERPOWER_VERSION).MIB $@
+	# Workaround to make DisplayString available (#867)
+	@sed -i.bak -E 's/(DisplayString[[:space:]]*FROM )RFC1213-MIB/\1SNMPv2-TC/' $@
+	@rm $@.bak
 	@rm -v $(TMP)
diff --git a/generator/main.go b/generator/main.go
index 113dde0..cf187ae 100644
--- a/generator/main.go
+++ b/generator/main.go
@@ -19,11 +19,11 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/alecthomas/kingpin/v2"
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
 	"github.com/prometheus/common/promlog"
 	"github.com/prometheus/common/promlog/flag"
-	"gopkg.in/alecthomas/kingpin.v2"
 	"gopkg.in/yaml.v2"
 
 	"github.com/prometheus/snmp_exporter/config"
diff --git a/go.mod b/go.mod
index 92f99bd..bab3c20 100644
--- a/go.mod
+++ b/go.mod
@@ -3,39 +3,36 @@ module github.com/prometheus/snmp_exporter
 go 1.19
 
 require (
+	github.com/alecthomas/kingpin/v2 v2.3.2
 	github.com/go-kit/log v0.2.1
 	github.com/gosnmp/gosnmp v1.35.0
 	github.com/prometheus/client_golang v1.14.0
 	github.com/prometheus/client_model v0.3.0
-	github.com/prometheus/common v0.40.0
-	github.com/prometheus/exporter-toolkit v0.8.2
-	gopkg.in/alecthomas/kingpin.v2 v2.2.6
+	github.com/prometheus/common v0.42.0
+	github.com/prometheus/exporter-toolkit v0.10.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
 require (
-	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
-	github.com/coreos/go-systemd/v22 v22.4.0 // indirect
+	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
-	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/kr/pretty v0.3.1 // indirect
+	github.com/kr/text v0.2.0 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
-	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
-	github.com/prometheus/procfs v0.8.0 // indirect
-	golang.org/x/crypto v0.6.0 // indirect
-	golang.org/x/net v0.6.0 // indirect
-	golang.org/x/oauth2 v0.5.0 // indirect
+	github.com/prometheus/procfs v0.9.0 // indirect
+	github.com/rogpeppe/go-internal v1.9.0 // indirect
+	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
+	golang.org/x/crypto v0.8.0 // indirect
+	golang.org/x/net v0.9.0 // indirect
+	golang.org/x/oauth2 v0.6.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index b8cdd97..93347f0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,16 @@
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
+github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
 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.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
-github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/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=
 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.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
@@ -24,64 +23,55 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/gosnmp/gosnmp v1.35.0 h1:EuWWNPxTCdAUx2/NbQcSa3WdNxjzpy4Phv57b4MWpJM=
 github.com/gosnmp/gosnmp v1.35.0/go.mod h1:2AvKZ3n9aEl5TJEo/fFmf/FGO4Nj4cVeEc5yuk88CYc=
 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 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/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 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.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
 github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
 github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
 github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
-github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q=
-github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE=
-github.com/prometheus/exporter-toolkit v0.8.2 h1:sbJAfBXQFkG6sUkbwBun8MNdzW9+wd5YfPYofbmj0YM=
-github.com/prometheus/exporter-toolkit v0.8.2/go.mod h1:00shzmJL7KxcsabLWcONwpyNEuWhREOnFqZW7vadFS0=
-github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
-github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/exporter-toolkit v0.10.0 h1:yOAzZTi4M22ZzVxD+fhy1URTuNRj/36uQJJ5S8IPza8=
+github.com/prometheus/exporter-toolkit v0.10.0/go.mod h1:+sVFzuvV5JDyw+Ih6p3zFxZNVnKQa3x5qPmDSiPu4ZY=
+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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
-golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
@@ -90,12 +80,9 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index c249ffd..44d80db 100644
--- a/main.go
+++ b/main.go
@@ -23,6 +23,7 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/alecthomas/kingpin/v2"
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
 	"github.com/prometheus/client_golang/prometheus"
@@ -33,7 +34,6 @@ import (
 	"github.com/prometheus/common/version"
 	"github.com/prometheus/exporter-toolkit/web"
 	webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
-	"gopkg.in/alecthomas/kingpin.v2"
 	yaml "gopkg.in/yaml.v2"
 
 	"github.com/prometheus/snmp_exporter/collector"
@@ -41,8 +41,12 @@ import (
 )
 
 var (
-	configFile   = kingpin.Flag("config.file", "Path to configuration file.").Default("snmp.yml").String()
-	dryRun       = kingpin.Flag("dry-run", "Only verify configuration is valid and exit.").Default("false").Bool()
+	configFile  = kingpin.Flag("config.file", "Path to configuration file.").Default("snmp.yml").String()
+	dryRun      = kingpin.Flag("dry-run", "Only verify configuration is valid and exit.").Default("false").Bool()
+	metricsPath = kingpin.Flag(
+		"web.telemetry-path",
+		"Path under which to expose metrics.",
+	).Default("/metrics").String()
 	toolkitFlags = webflag.AddFlags(kingpin.CommandLine, ":9116")
 
 	// Metrics about the SNMP exporter itself.
@@ -65,6 +69,11 @@ var (
 	reloadCh chan chan error
 )
 
+const (
+	proberPath = "/snmp"
+	configPath = "/config"
+)
+
 func handler(w http.ResponseWriter, r *http.Request, logger log.Logger) {
 	query := r.URL.Query()
 
@@ -195,41 +204,55 @@ func main() {
 
 	http.Handle("/metrics", promhttp.Handler()) // Normal metrics endpoint for SNMP exporter itself.
 	// Endpoint to do SNMP scrapes.
-	http.HandleFunc("/snmp", func(w http.ResponseWriter, r *http.Request) {
+	http.HandleFunc(proberPath, func(w http.ResponseWriter, r *http.Request) {
 		handler(w, r, logger)
 	})
 	http.HandleFunc("/-/reload", updateConfiguration) // Endpoint to reload configuration.
 
-	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		w.Write([]byte(`<html>
-            <head>
-            <title>SNMP Exporter</title>
-            <style>
-            label{
-            display:inline-block;
-            width:75px;
-            }
-            form label {
-            margin: 10px;
-            }
-            form input {
-            margin: 10px;
-            }
-            </style>
-            </head>
-            <body>
-            <h1>SNMP Exporter</h1>
-            <form action="/snmp">
-            <label>Target:</label> <input type="text" name="target" placeholder="X.X.X.X" value="1.2.3.4"><br>
-            <label>Module:</label> <input type="text" name="module" placeholder="module" value="if_mib"><br>
-            <input type="submit" value="Submit">
-            </form>
-						<p><a href="/config">Config</a></p>
-            </body>
-            </html>`))
-	})
+	if *metricsPath != "/" && *metricsPath != "" {
+		landingConfig := web.LandingConfig{
+			Name:        "SNMP Exporter",
+			Description: "Prometheus Exporter for SNMP targets",
+			Version:     version.Info(),
+			Form: web.LandingForm{
+				Action: proberPath,
+				Inputs: []web.LandingFormInput{
+					{
+						Label:       "Target",
+						Type:        "text",
+						Name:        "target",
+						Placeholder: "X.X.X.X/[::X]",
+						Value:       "::1",
+					},
+					{
+						Label:       "Module",
+						Type:        "text",
+						Name:        "module",
+						Placeholder: "module",
+						Value:       "if_mib",
+					},
+				},
+			},
+			Links: []web.LandingLinks{
+				{
+					Address: configPath,
+					Text:    "Config",
+				},
+				{
+					Address: *metricsPath,
+					Text:    "Metrics",
+				},
+			},
+		}
+		landingPage, err := web.NewLandingPage(landingConfig)
+		if err != nil {
+			level.Error(logger).Log("err", err)
+			os.Exit(1)
+		}
+		http.Handle("/", landingPage)
+	}
 
-	http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
+	http.HandleFunc(configPath, func(w http.ResponseWriter, r *http.Request) {
 		sc.RLock()
 		c, err := yaml.Marshal(sc.C)
 		sc.RUnlock()


Test patch:
diff --git a/collector/collector_test.go b/collector/collector_test.go
index c4010cf..a55bc7b 100644
--- a/collector/collector_test.go
+++ b/collector/collector_test.go
@@ -19,10 +19,10 @@ import (
 	"regexp"
 	"testing"
 
+	kingpin "github.com/alecthomas/kingpin/v2"
 	"github.com/go-kit/log"
 	"github.com/gosnmp/gosnmp"
 	"github.com/prometheus/client_model/go"
-	kingpin "gopkg.in/alecthomas/kingpin.v2"
 
 	"github.com/prometheus/snmp_exporter/config"
 )