Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ executors:
# should also be updated.
golang:
docker:
- image: cimg/go:1.24
- image: cimg/go:1.25
parameters:
working_dir:
type: string
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ jobs:
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Install snmp_exporter/generator dependencies
run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
if: github.repository == 'prometheus/snmp_exporter'
- name: Get golangci-lint version
id: golangci-lint-version
run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT
- name: Lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with:
args: --verbose
version: v2.2.1
version: ${{ steps.golangci-lint-version.outputs.version }}
23 changes: 23 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
version: "2"
linters:
enable:
- errorlint
- gocritic
- misspell
- revive
- sloglint
Expand All @@ -10,6 +12,11 @@ linters:
errcheck:
exclude-functions:
- (net/http.ResponseWriter).Write
gocritic:
enable-all: true
disabled-checks:
- hugeParam
- unnamedResult
revive:
rules:
- name: unused-parameter
Expand All @@ -26,3 +33,19 @@ linters:
- errcheck
- govet
path: _test.go
formatters:
enable:
- gci
- gofumpt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/prometheus/snmp_exporter)
gofumpt:
extra-rules: true
goimports:
local-prefixes:
- github.com/prometheus/snmp_exporter
2 changes: 1 addition & 1 deletion .promu.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
go:
# Whenever the Go version is updated here,
# .circle/config.yml should also be updated.
version: 1.24
version: 1.25
repository:
path: github.com/prometheus/snmp_exporter
build:
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ ARG ARCH="amd64"
ARG OS="linux"
COPY .build/${OS}-${ARCH}/snmp_exporter /bin/snmp_exporter
COPY snmp.yml /etc/snmp_exporter/snmp.yml
COPY LICENSE /LICENSE
COPY NOTICE /NOTICE


EXPOSE 9116
ENTRYPOINT [ "/bin/snmp_exporter" ]
Expand Down
6 changes: 5 additions & 1 deletion Makefile.common
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v2.2.1
GOLANGCI_LINT_VERSION ?= v2.4.0
GOLANGCI_FMT_OPTS ?=
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
Expand Down Expand Up @@ -266,6 +266,10 @@ $(GOLANGCI_LINT):
| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)
endif

.PHONY: common-print-golangci-lint-version
common-print-golangci-lint-version:
@echo $(GOLANGCI_LINT_VERSION)

.PHONY: precheck
precheck::

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ It is possible to supply an optional `snmp_context` parameter in the URL, like t
<http://localhost:9116/snmp?auth=my_secure_v3&module=ddwrt&target=192.0.0.8&snmp_context=vrf-mgmt>
The `snmp_context` parameter in the URL would override the `context_name` parameter in the `snmp.yml` file.

It is also possible when using SNMPv3 to supply an optional `snmp_engineid` parameter in the URL, like this:
<http://localhost:9116/snmp?auth=my_secure_v3&module=ddwrt&target=192.0.0.8&snmp_engineid=800004f7059c7a0307400529>


## Multi-Module Handling
The multi-module functionality allows you to specify multiple modules, enabling the retrieval of information from several modules in a single scrape.
The concurrency can be specified using the snmp-exporter option `--snmp.module-concurrency` (the default is 1).
Expand Down
118 changes: 62 additions & 56 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package collector
import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"log/slog"
"net"
Expand Down Expand Up @@ -62,7 +63,7 @@ var combinedTypeMapping = map[string]map[int]string{

func oidToList(oid string) []int {
result := []int{}
for _, x := range strings.Split(oid, ".") {
for x := range strings.SplitSeq(oid, ".") {
o, _ := strconv.Atoi(x)
result = append(result, o)
}
Expand Down Expand Up @@ -117,10 +118,7 @@ func ScrapeTarget(snmp scraper.SNMPScraper, target string, auth *config.Auth, mo
maxOids = 1
}
for len(getOids) > 0 {
oids := len(getOids)
if oids > maxOids {
oids = maxOids
}
oids := min(len(getOids), maxOids)

packet, err := snmp.Get(getOids[:oids])
if err != nil {
Expand Down Expand Up @@ -291,30 +289,32 @@ func NewNamedModule(name string, module *config.Module) *NamedModule {
}

type Collector struct {
ctx context.Context
target string
auth *config.Auth
authName string
modules []*NamedModule
logger *slog.Logger
metrics Metrics
concurrency int
snmpContext string
debugSNMP bool
}

func New(ctx context.Context, target, authName, snmpContext string, auth *config.Auth, modules []*NamedModule, logger *slog.Logger, metrics Metrics, conc int, debugSNMP bool) *Collector {
ctx context.Context
target string
auth *config.Auth
authName string
modules []*NamedModule
logger *slog.Logger
metrics Metrics
concurrency int
snmpContext string
snmpEngineID string
debugSNMP bool
}

func New(ctx context.Context, target, authName, snmpContext, snmpEngineID string, auth *config.Auth, modules []*NamedModule, logger *slog.Logger, metrics Metrics, conc int, debugSNMP bool) *Collector {
return &Collector{
ctx: ctx,
target: target,
authName: authName,
auth: auth,
modules: modules,
snmpContext: snmpContext,
logger: logger.With("source_address", *srcAddress),
metrics: metrics,
concurrency: conc,
debugSNMP: debugSNMP,
ctx: ctx,
target: target,
authName: authName,
auth: auth,
modules: modules,
snmpContext: snmpContext,
snmpEngineID: snmpEngineID,
logger: logger.With("source_address", *srcAddress),
metrics: metrics,
concurrency: conc,
debugSNMP: debugSNMP,
}
}

Expand Down Expand Up @@ -352,7 +352,7 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger *slog.Logger, cli
g.MaxRepetitions = module.WalkParams.MaxRepetitions
g.UseUnconnectedUDPSocket = module.WalkParams.UseUnconnectedUDPSocket
if module.WalkParams.AllowNonIncreasingOIDs {
g.AppOpts = map[string]interface{}{
g.AppOpts = map[string]any{
"c": true,
}
}
Expand Down Expand Up @@ -420,10 +420,7 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger *slog.Logger, cli
// Collect implements Prometheus.Collector.
func (c Collector) Collect(ch chan<- prometheus.Metric) {
wg := sync.WaitGroup{}
workerCount := c.concurrency
if workerCount < 1 {
workerCount = 1
}
workerCount := max(c.concurrency, 1)
ctx, cancel := context.WithCancel(c.ctx)
defer cancel()
workerChan := make(chan *NamedModule)
Expand All @@ -447,6 +444,15 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
break
}
}
// Set EngineID option if one is configured and we're using SNMPv3
if c.snmpEngineID != "" && c.auth.Version == 3 {
// Convert the SNMP Engine ID to a byte string
sEID, _ := hex.DecodeString(c.snmpEngineID)
// Set the options.
client.SetOptions(func(g *gosnmp.GoSNMP) {
g.ContextEngineID = string(sEID)
})
}
// Set the options.
client.SetOptions(func(g *gosnmp.GoSNMP) {
g.Context = ctx
Expand Down Expand Up @@ -531,14 +537,14 @@ func parseDateAndTime(pdu *gosnmp.SnmpPDU) (float64, error) {
locString := fmt.Sprintf("%s%02d%02d", string(v[8]), v[9], v[10])
loc, err := time.Parse("-0700", locString)
if err != nil {
return 0, fmt.Errorf("error parsing location string: %q, error: %s", locString, err)
return 0, fmt.Errorf("error parsing location string: %q, error: %w", locString, err)
}
tz = loc.Location()
default:
return 0, fmt.Errorf("invalid DateAndTime length %v", pduLength)
}
if err != nil {
return 0, fmt.Errorf("unable to parse DateAndTime %q, error: %s", v, err)
return 0, fmt.Errorf("unable to parse DateAndTime %q, error: %w", v, err)
}
// Build the date from the various fields and time zone.
t := time.Date(
Expand All @@ -557,13 +563,13 @@ func parseDateAndTimeWithPattern(metric *config.Metric, pdu *gosnmp.SnmpPDU, met
pduValue := pduValueAsString(pdu, "DisplayString", metrics)
t, err := timefmt.Parse(pduValue, metric.DateTimePattern)
if err != nil {
return 0, fmt.Errorf("error parsing date and time %q", err)
return 0, fmt.Errorf("error parsing date and time %w", err)
}
return float64(t.Unix()), nil
}

func parseNtpTimestamp(pdu *gosnmp.SnmpPDU) (float64, error) {
var data = pdu.Value.([]byte)
data := pdu.Value.([]byte)

// Prometheus uses the Unix time epoch (seconds since 1970).
// NTP seconds are counted since 1900 and must be corrected
Expand Down Expand Up @@ -668,7 +674,7 @@ func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, o
t, value, labelvalues...)
if err != nil {
sample = prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error calling NewConstMetric", nil, nil),
fmt.Errorf("error for metric %s with labels %v from indexOids %v: %v", metric.Name, labelvalues, indexOids, err))
fmt.Errorf("error for metric %s with labels %v from indexOids %v: %w", metric.Name, labelvalues, indexOids, err))
}

return []prometheus.Metric{sample}
Expand All @@ -693,7 +699,7 @@ func applyRegexExtracts(metric *config.Metric, pduValue string, labelnames, labe
prometheus.GaugeValue, v, labelvalues...)
if err != nil {
newMetric = prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error calling NewConstMetric for regex_extract", nil, nil),
fmt.Errorf("error for metric %s with labels %v: %v", metric.Name+name, labelvalues, err))
fmt.Errorf("error for metric %s with labels %v: %w", metric.Name+name, labelvalues, err))
}
results = append(results, newMetric)
break
Expand All @@ -715,7 +721,7 @@ func enumAsInfo(metric *config.Metric, value int, labelnames, labelvalues []stri
prometheus.GaugeValue, 1.0, labelvalues...)
if err != nil {
newMetric = prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error calling NewConstMetric for EnumAsInfo", nil, nil),
fmt.Errorf("error for metric %s with labels %v: %v", metric.Name, labelvalues, err))
fmt.Errorf("error for metric %s with labels %v: %w", metric.Name, labelvalues, err))
}
return []prometheus.Metric{newMetric}
}
Expand All @@ -733,7 +739,7 @@ func enumAsStateSet(metric *config.Metric, value int, labelnames, labelvalues []
prometheus.GaugeValue, 1.0, append(labelvalues, state)...)
if err != nil {
newMetric = prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error calling NewConstMetric for EnumAsStateSet", nil, nil),
fmt.Errorf("error for metric %s with labels %v: %v", metric.Name, labelvalues, err))
fmt.Errorf("error for metric %s with labels %v: %w", metric.Name, labelvalues, err))
}
results = append(results, newMetric)

Expand All @@ -745,14 +751,14 @@ func enumAsStateSet(metric *config.Metric, value int, labelnames, labelvalues []
prometheus.GaugeValue, 0.0, append(labelvalues, v)...)
if err != nil {
newMetric = prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error calling NewConstMetric for EnumAsStateSet", nil, nil),
fmt.Errorf("error for metric %s with labels %v: %v", metric.Name, labelvalues, err))
fmt.Errorf("error for metric %s with labels %v: %w", metric.Name, labelvalues, err))
}
results = append(results, newMetric)
}
return results
}

func bits(metric *config.Metric, value interface{}, labelnames, labelvalues []string) []prometheus.Metric {
func bits(metric *config.Metric, value any, labelnames, labelvalues []string) []prometheus.Metric {
bytes, ok := value.([]byte)
if !ok {
return []prometheus.Metric{prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "BITS type was not a BISTRING on the wire.", nil, nil),
Expand All @@ -773,7 +779,7 @@ func bits(metric *config.Metric, value interface{}, labelnames, labelvalues []st
prometheus.GaugeValue, bit, append(labelvalues, v)...)
if err != nil {
newMetric = prometheus.NewInvalidMetric(prometheus.NewDesc("snmp_error", "Error calling NewConstMetric for Bits", nil, nil),
fmt.Errorf("error for metric %s with labels %v: %v", metric.Name, labelvalues, err))
fmt.Errorf("error for metric %s with labels %v: %w", metric.Name, labelvalues, err))
}
results = append(results, newMetric)
}
Expand All @@ -797,36 +803,36 @@ func splitOid(oid []int, count int) ([]int, []int) {

// This mirrors decodeValue in gosnmp's helper.go.
func pduValueAsString(pdu *gosnmp.SnmpPDU, typ string, metrics Metrics) string {
switch pdu.Value.(type) {
switch v := pdu.Value.(type) {
case int:
return strconv.Itoa(pdu.Value.(int))
return strconv.Itoa(v)
case uint:
return strconv.FormatUint(uint64(pdu.Value.(uint)), 10)
return strconv.FormatUint(uint64(v), 10)
case uint64:
return strconv.FormatUint(pdu.Value.(uint64), 10)
return strconv.FormatUint(v, 10)
case float32:
return strconv.FormatFloat(float64(pdu.Value.(float32)), 'f', -1, 32)
return strconv.FormatFloat(float64(v), 'f', -1, 32)
case float64:
return strconv.FormatFloat(pdu.Value.(float64), 'f', -1, 64)
return strconv.FormatFloat(v, 'f', -1, 64)
case string:
if pdu.Type == gosnmp.ObjectIdentifier {
// Trim leading period.
return pdu.Value.(string)[1:]
return v[1:]
}
// DisplayString.
return strings.ToValidUTF8(pdu.Value.(string), "�")
return strings.ToValidUTF8(v, "�")
case []byte:
if typ == "" || typ == "Bits" {
typ = "OctetString"
}
// Reuse the OID index parsing code.
parts := make([]int, len(pdu.Value.([]byte)))
for i, o := range pdu.Value.([]byte) {
parts := make([]int, len(v))
for i, o := range v {
parts[i] = int(o)
}
if typ == "OctetString" || typ == "DisplayString" {
// Prepend the length, as it is explicit in an index.
parts = append([]int{len(pdu.Value.([]byte))}, parts...)
parts = append([]int{len(v)}, parts...)
}
str, _, _ := indexOidsAsString(parts, typ, 0, false, nil)
return strings.ToValidUTF8(str, "�")
Expand Down Expand Up @@ -924,7 +930,7 @@ func indexOidsAsString(indexOids []int, typ string, fixedSize int, implied bool,
return strings.Join(parts, "."), subOid, indexOids
case "InetAddressIPv6":
subOid, indexOids := splitOid(indexOids, 16)
parts := make([]interface{}, 16)
parts := make([]any, 16)
for i, o := range subOid {
parts[i] = o
}
Expand Down
Loading