1
0
mirror of https://github.com/nginxinc/nginx-prometheus-exporter.git synced 2025-08-09 16:02:43 +03:00

Replace flag with kingpin and use promlog (#420)

This commit is contained in:
Luca Comellini
2023-06-09 16:03:27 -07:00
committed by GitHub
parent 37472575b7
commit 965d9e423d
10 changed files with 205 additions and 379 deletions

View File

@@ -4,27 +4,16 @@ on:
push:
branches:
- main
paths-ignore:
- '**.md'
- 'LICENSE'
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- "v[0-9]+.[0-9]+.[0-9]+"
pull_request:
branches:
- main
paths-ignore:
- '**.md'
- 'LICENSE'
types:
- opened
- reopened
- synchronize
env:
DOCKER_PLATFORMS: "linux/arm,linux/arm64,linux/amd64,linux/ppc64le,linux/s390x,linux/mips64le,linux/386"
jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-22.04
@@ -65,7 +54,6 @@ jobs:
run: |
echo "go_path=$(go env GOPATH)" >> $GITHUB_OUTPUT
- name: Setup QEMU
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0
with:
platforms: arm,arm64,ppc64le,s390x,mips64le,386
@@ -142,18 +130,22 @@ jobs:
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0
with:
version: latest
args: ${{ !startsWith(github.ref, 'refs/tags/') && 'build --snapshot' || 'release' }} ${{ github.event_name == 'pull_request' && '--single-target' || '' }} --rm-dist
args: ${{ !startsWith(github.ref, 'refs/tags/') && 'build --snapshot' || 'release' }} ${{ github.event_name == 'pull_request' && '--single-target' || '' }} --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOPATH: ${{ steps.go.outputs.go_path }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.NGINX_PAT }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_COMMUNITY }}
- name: Print NGINX Prometheus Exporter info
run: ./dist/nginx-prometheus-exporter_linux_amd64_v1/nginx-prometheus-exporter --version
continue-on-error: true
- name: Build and Push Docker Image
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 # v4.0.0
with:
file: build/Dockerfile
context: '.'
context: "."
target: goreleaser
platforms: ${{ github.event_name != 'pull_request' && env.DOCKER_PLATFORMS || '' }}
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -28,6 +28,9 @@ linters-settings:
- name: unused-parameter
- name: var-declaration
- name: var-naming
errcheck:
exclude-functions:
- (github.com/go-kit/log.Logger).Log
linters:
enable:

View File

@@ -1,8 +1,3 @@
before:
hooks:
- go mod tidy
- go mod verify
builds:
- env:
- CGO_ENABLED=0
@@ -33,26 +28,27 @@ builds:
- all=-trimpath={{.Env.GOPATH}}
asmflags:
- all=-trimpath={{.Env.GOPATH}}
sboms:
- artifacts: archive
archives:
- format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'sha256sums.txt'
ldflags:
- '-s -w -X github.com/prometheus/common/version.Version={{.Version}} -X github.com/prometheus/common/version.BuildDate={{.Date}} -X github.com/prometheus/common/version.Branch={{.Branch}} -X github.com/prometheus/common/version.BuildUser=goreleaser'
changelog:
skip: true
archives:
- format_overrides:
- goos: windows
format: zip
sboms:
- artifacts: archive
documents:
- "${artifact}.spdx.json"
brews:
- tap:
owner: nginxinc
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
token: '{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}'
folder: Formula
homepage: https://www.nginx.com/
description: NGINX Prometheus Exporter for NGINX and NGINX Plus
@@ -71,3 +67,6 @@ announce:
milestones:
- close: true
snapshot:
name_template: 'edge'

View File

@@ -4,12 +4,12 @@ PREFIX = nginx/nginx-prometheus-exporter
.PHONY: nginx-prometheus-exporter
nginx-prometheus-exporter:
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(VERSION)" -o nginx-prometheus-exporter
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X github.com/prometheus/common/version.Version=$(VERSION)" -o nginx-prometheus-exporter
.PHONY: build-goreleaser
build-goreleaser: ## Build all binaries using GoReleaser
@goreleaser -v || (code=$$?; printf "\033[0;31mError\033[0m: there was a problem with GoReleaser. Follow the docs to install it https://goreleaser.com/install\n"; exit $$code)
GOPATH=$(shell go env GOPATH) goreleaser build --rm-dist --snapshot
GOPATH=$(shell go env GOPATH) goreleaser build --clean --snapshot
.PHONY: lint
lint:

View File

@@ -1,9 +1,10 @@
package collector
import (
"log"
"sync"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/nginxinc/nginx-prometheus-exporter/client"
"github.com/prometheus/client_golang/prometheus"
)
@@ -14,12 +15,14 @@ type NginxCollector struct {
metrics map[string]*prometheus.Desc
upMetric prometheus.Gauge
mutex sync.Mutex
logger log.Logger
}
// NewNginxCollector creates an NginxCollector.
func NewNginxCollector(nginxClient *client.NginxClient, namespace string, constLabels map[string]string) *NginxCollector {
func NewNginxCollector(nginxClient *client.NginxClient, namespace string, constLabels map[string]string, logger log.Logger) *NginxCollector {
return &NginxCollector{
nginxClient: nginxClient,
logger: logger,
metrics: map[string]*prometheus.Desc{
"connections_active": newGlobalMetric(namespace, "connections_active", "Active client connections", constLabels),
"connections_accepted": newGlobalMetric(namespace, "connections_accepted", "Accepted client connections", constLabels),
@@ -52,7 +55,7 @@ func (c *NginxCollector) Collect(ch chan<- prometheus.Metric) {
if err != nil {
c.upMetric.Set(nginxDown)
ch <- c.upMetric
log.Printf("Error getting stats: %v", err)
level.Error(c.logger).Log("msg", "Error getting stats", "error", err.Error())
return
}

View File

@@ -2,9 +2,10 @@ package collector
import (
"fmt"
"log"
"sync"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
plusclient "github.com/nginxinc/nginx-plus-go-client/client"
"github.com/prometheus/client_golang/prometheus"
)
@@ -51,6 +52,7 @@ type NginxPlusCollector struct {
upstreamServerPeerLabels map[string][]string
streamUpstreamServerPeerLabels map[string][]string
variableLabelsMutex sync.RWMutex
logger log.Logger
}
// UpdateUpstreamServerPeerLabels updates the Upstream Server Peer Labels
@@ -222,7 +224,7 @@ func NewVariableLabelNames(upstreamServerVariableLabelNames []string, serverZone
}
// NewNginxPlusCollector creates an NginxPlusCollector.
func NewNginxPlusCollector(nginxClient *plusclient.NginxClient, namespace string, variableLabelNames VariableLabelNames, constLabels map[string]string) *NginxPlusCollector {
func NewNginxPlusCollector(nginxClient *plusclient.NginxClient, namespace string, variableLabelNames VariableLabelNames, constLabels map[string]string, logger log.Logger) *NginxPlusCollector {
upstreamServerVariableLabelNames := append(variableLabelNames.UpstreamServerVariableLabelNames, variableLabelNames.UpstreamServerPeerVariableLabelNames...)
streamUpstreamServerVariableLabelNames := append(variableLabelNames.StreamUpstreamServerVariableLabelNames, variableLabelNames.StreamUpstreamServerPeerVariableLabelNames...)
return &NginxPlusCollector{
@@ -234,6 +236,7 @@ func NewNginxPlusCollector(nginxClient *plusclient.NginxClient, namespace string
streamUpstreamServerPeerLabels: make(map[string][]string),
streamUpstreamServerLabels: make(map[string][]string),
nginxClient: nginxClient,
logger: logger,
totalMetrics: map[string]*prometheus.Desc{
"connections_accepted": newGlobalMetric(namespace, "connections_accepted", "Accepted client connections", constLabels),
"connections_dropped": newGlobalMetric(namespace, "connections_dropped", "Dropped client connections", constLabels),
@@ -551,7 +554,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
if err != nil {
c.upMetric.Set(nginxDown)
ch <- c.upMetric
log.Printf("Error getting stats: %v", err)
level.Warn(c.logger).Log("msg", "Error getting stats", "error", err.Error())
return
}
@@ -582,8 +585,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
varLabelValues := c.getServerZoneLabelValues(name)
if c.variableLabelNames.ServerZoneVariableLabelNames != nil && len(varLabelValues) != len(c.variableLabelNames.ServerZoneVariableLabelNames) {
log.Printf("wrong number of labels for http zone %v. For labels %v, got values: %v. Empty labels will be used instead",
name, c.variableLabelNames.ServerZoneVariableLabelNames, varLabelValues)
level.Warn(c.logger).Log("msg", "wrong number of labels for http zone, empty labels will be used instead", "zone", name, "expected", len(c.variableLabelNames.ServerZoneVariableLabelNames), "got", len(varLabelValues))
for range c.variableLabelNames.ServerZoneVariableLabelNames {
labelValues = append(labelValues, "")
}
@@ -704,8 +706,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
varLabelValues := c.getStreamServerZoneLabelValues(name)
if c.variableLabelNames.StreamServerZoneVariableLabelNames != nil && len(varLabelValues) != len(c.variableLabelNames.StreamServerZoneVariableLabelNames) {
log.Printf("wrong number of labels for stream server zone %v. For labels %v, got values: %v. Empty labels will be used instead",
name, c.variableLabelNames.StreamServerZoneVariableLabelNames, varLabelValues)
level.Warn(c.logger).Log("msg", "wrong number of labels for stream server zone, empty labels will be used instead", "zone", name, "expected", len(c.variableLabelNames.StreamServerZoneVariableLabelNames), "got", len(varLabelValues))
for range c.variableLabelNames.StreamServerZoneVariableLabelNames {
labelValues = append(labelValues, "")
}
@@ -742,8 +743,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
varLabelValues := c.getUpstreamServerLabelValues(name)
if c.variableLabelNames.UpstreamServerVariableLabelNames != nil && len(varLabelValues) != len(c.variableLabelNames.UpstreamServerVariableLabelNames) {
log.Printf("wrong number of labels for upstream %v. For labels %v, got values: %v. Empty labels will be used instead",
name, c.variableLabelNames.UpstreamServerVariableLabelNames, varLabelValues)
level.Warn(c.logger).Log("msg", "wrong number of labels for upstream, empty labels will be used instead", "upstream", name, "expected", len(c.variableLabelNames.UpstreamServerVariableLabelNames), "got", len(varLabelValues))
for range c.variableLabelNames.UpstreamServerVariableLabelNames {
labelValues = append(labelValues, "")
}
@@ -754,8 +754,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
upstreamServer := fmt.Sprintf("%v/%v", name, peer.Server)
varPeerLabelValues := c.getUpstreamServerPeerLabelValues(upstreamServer)
if c.variableLabelNames.UpstreamServerPeerVariableLabelNames != nil && len(varPeerLabelValues) != len(c.variableLabelNames.UpstreamServerPeerVariableLabelNames) {
log.Printf("wrong number of labels for upstream peer %v. For labels %v, got values: %v. Empty labels will be used instead",
peer.Server, c.variableLabelNames.UpstreamServerPeerVariableLabelNames, varPeerLabelValues)
level.Warn(c.logger).Log("msg", "wrong number of labels for upstream peer, empty labels will be used instead", "upstream", name, "peer", peer.Server, "expected", len(c.variableLabelNames.UpstreamServerPeerVariableLabelNames), "got", len(varPeerLabelValues))
for range c.variableLabelNames.UpstreamServerPeerVariableLabelNames {
labelValues = append(labelValues, "")
}
@@ -901,8 +900,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
varLabelValues := c.getStreamUpstreamServerLabelValues(name)
if c.variableLabelNames.StreamUpstreamServerVariableLabelNames != nil && len(varLabelValues) != len(c.variableLabelNames.StreamUpstreamServerVariableLabelNames) {
log.Printf("wrong number of labels for stream server %v. For labels %v, got values: %v. Empty labels will be used instead",
name, c.variableLabelNames.StreamUpstreamServerVariableLabelNames, varLabelValues)
level.Warn(c.logger).Log("msg", "wrong number of labels for stream server, empty labels will be used instead", "server", name, "labels", c.variableLabelNames.StreamUpstreamServerVariableLabelNames, "values", varLabelValues)
for range c.variableLabelNames.StreamUpstreamServerVariableLabelNames {
labelValues = append(labelValues, "")
}
@@ -913,8 +911,7 @@ func (c *NginxPlusCollector) Collect(ch chan<- prometheus.Metric) {
upstreamServer := fmt.Sprintf("%v/%v", name, peer.Server)
varPeerLabelValues := c.getStreamUpstreamServerPeerLabelValues(upstreamServer)
if c.variableLabelNames.StreamUpstreamServerPeerVariableLabelNames != nil && len(varPeerLabelValues) != len(c.variableLabelNames.StreamUpstreamServerPeerVariableLabelNames) {
log.Printf("wrong number of labels for stream upstream peer %v. For labels %v, got values: %v. Empty labels will be used instead",
peer.Server, c.variableLabelNames.StreamUpstreamServerPeerVariableLabelNames, varPeerLabelValues)
level.Warn(c.logger).Log("msg", "wrong number of labels for stream upstream peer, empty labels will be used instead", "server", upstreamServer, "labels", c.variableLabelNames.StreamUpstreamServerPeerVariableLabelNames, "values", varPeerLabelValues)
for range c.variableLabelNames.StreamUpstreamServerPeerVariableLabelNames {
labelValues = append(labelValues, "")
}

View File

@@ -4,16 +4,11 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"runtime/debug"
"strconv"
"strings"
"syscall"
"time"
@@ -21,69 +16,17 @@ import (
plusclient "github.com/nginxinc/nginx-plus-go-client/client"
"github.com/nginxinc/nginx-prometheus-exporter/client"
"github.com/nginxinc/nginx-prometheus-exporter/collector"
"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
)
func getEnv(key, defaultValue string) string {
value, ok := os.LookupEnv(key)
if !ok {
return defaultValue
}
return value
}
func getEnvUint(key string, defaultValue uint) uint {
value, ok := os.LookupEnv(key)
if !ok {
return defaultValue
}
i, err := strconv.ParseUint(value, 10, 64)
if err != nil {
log.Fatalf("Environment variable value for %s must be an uint: %v", key, err)
}
return uint(i)
}
func getEnvBool(key string, defaultValue bool) bool {
value, ok := os.LookupEnv(key)
if !ok {
return defaultValue
}
b, err := strconv.ParseBool(value)
if err != nil {
log.Fatalf("Environment variable value for %s must be a boolean: %v", key, err)
}
return b
}
func getEnvPositiveDuration(key string, defaultValue time.Duration) positiveDuration {
value, ok := os.LookupEnv(key)
if !ok {
return positiveDuration{defaultValue}
}
posDur, err := parsePositiveDuration(value)
if err != nil {
log.Fatalf("Environment variable value for %s must be a positive duration: %v", key, err)
}
return posDur
}
func getEnvConstLabels(key string, defaultValue map[string]string) constLabel {
value, ok := os.LookupEnv(key)
if !ok {
return constLabel{defaultValue}
}
cLabel, err := parseConstLabels(value)
if err != nil {
log.Fatalf("Environment variable value for %s must be a const label or a list of const labels: %v", key, err)
}
return cLabel
}
// positiveDuration is a wrapper of time.Duration to ensure only positive values are accepted
type positiveDuration struct{ time.Duration }
@@ -108,62 +51,13 @@ func parsePositiveDuration(s string) (positiveDuration, error) {
return positiveDuration{dur}, nil
}
func createPositiveDurationFlag(name string, value positiveDuration, usage string) *positiveDuration {
flag.Var(&value, name, usage)
return &value
func createPositiveDurationFlag(s kingpin.Settings) (target *time.Duration) {
target = new(time.Duration)
s.SetValue(&positiveDuration{Duration: *target})
return
}
type constLabel struct{ labels map[string]string }
func (cl *constLabel) Set(s string) error {
labelList, err := parseConstLabels(s)
if err != nil {
return err
}
cl.labels = labelList.labels
return nil
}
func (cl *constLabel) String() string {
return fmt.Sprint(cl.labels)
}
func parseConstLabels(labels string) (constLabel, error) {
if labels == "" {
return constLabel{}, nil
}
constLabels := make(map[string]string)
labelList := strings.Split(labels, ",")
for _, l := range labelList {
dat := strings.Split(l, "=")
if len(dat) != 2 {
return constLabel{}, fmt.Errorf("const label %s has wrong format. Example valid input 'labelName=labelValue'", l)
}
labelName := model.LabelName(dat[0])
if !labelName.IsValid() {
return constLabel{}, fmt.Errorf("const label %s has wrong format. %s contains invalid characters", l, labelName)
}
labelValue := model.LabelValue(dat[1])
if !labelValue.IsValid() {
return constLabel{}, fmt.Errorf("const label %s has wrong format. %s contains invalid characters", l, labelValue)
}
constLabels[dat[0]] = dat[1]
}
return constLabel{labels: constLabels}, nil
}
func createConstLabelsFlag(name string, value constLabel, usage string) *constLabel {
flag.Var(&value, name, usage)
return &value
}
func createClientWithRetries(getClient func() (interface{}, error), retries uint, retryInterval time.Duration) (interface{}, error) {
func createClientWithRetries(getClient func() (interface{}, error), retries uint, retryInterval time.Duration, logger log.Logger) (interface{}, error) {
var err error
var nginxClient interface{}
@@ -173,7 +67,7 @@ func createClientWithRetries(getClient func() (interface{}, error), retries uint
return nginxClient, nil
}
if i < int(retries) {
log.Printf("Could not create Nginx Client. Retrying in %v...", retryInterval)
level.Error(logger).Log("msg", fmt.Sprintf("Could not create Nginx Client. Retrying in %v...", retryInterval))
time.Sleep(retryInterval)
}
}
@@ -213,135 +107,61 @@ func getListener(listenAddress string) (net.Listener, error) {
if err != nil {
return listener, err
}
log.Printf("Listening on %s", listenAddress)
return listener, nil
}
var (
// Set during go build
version string
// Defaults values
defaultListenAddress = getEnv("LISTEN_ADDRESS", ":9113")
defaultSecuredMetrics = getEnvBool("SECURED_METRICS", false)
defaultSslServerCert = getEnv("SSL_SERVER_CERT", "")
defaultSslServerKey = getEnv("SSL_SERVER_KEY", "")
defaultMetricsPath = getEnv("TELEMETRY_PATH", "/metrics")
defaultNginxPlus = getEnvBool("NGINX_PLUS", false)
defaultScrapeURI = getEnv("SCRAPE_URI", "http://127.0.0.1:8080/stub_status")
defaultSslVerify = getEnvBool("SSL_VERIFY", true)
defaultSslCaCert = getEnv("SSL_CA_CERT", "")
defaultSslClientCert = getEnv("SSL_CLIENT_CERT", "")
defaultSslClientKey = getEnv("SSL_CLIENT_KEY", "")
defaultTimeout = getEnvPositiveDuration("TIMEOUT", time.Second*5)
defaultNginxRetries = getEnvUint("NGINX_RETRIES", 0)
defaultNginxRetryInterval = getEnvPositiveDuration("NGINX_RETRY_INTERVAL", time.Second*5)
defaultConstLabels = getEnvConstLabels("CONST_LABELS", map[string]string{})
constLabels = map[string]string{}
// Command-line flags
listenAddr = flag.String("web.listen-address",
defaultListenAddress,
"An address or unix domain socket path to listen on for web interface and telemetry. The default value can be overwritten by LISTEN_ADDRESS environment variable.")
securedMetrics = flag.Bool("web.secured-metrics",
defaultSecuredMetrics,
"Expose metrics using https. The default value can be overwritten by SECURED_METRICS variable.")
sslServerCert = flag.String("web.ssl-server-cert",
defaultSslServerCert,
"Path to the PEM encoded certificate for the nginx-exporter metrics server(when web.secured-metrics=true). The default value can be overwritten by SSL_SERVER_CERT variable.")
sslServerKey = flag.String("web.ssl-server-key",
defaultSslServerKey,
"Path to the PEM encoded key for the nginx-exporter metrics server (when web.secured-metrics=true). The default value can be overwritten by SSL_SERVER_KEY variable.")
metricsPath = flag.String("web.telemetry-path",
defaultMetricsPath,
"A path under which to expose metrics. The default value can be overwritten by TELEMETRY_PATH environment variable.")
nginxPlus = flag.Bool("nginx.plus",
defaultNginxPlus,
"Start the exporter for NGINX Plus. By default, the exporter is started for NGINX. The default value can be overwritten by NGINX_PLUS environment variable.")
scrapeURI = flag.String("nginx.scrape-uri",
defaultScrapeURI,
`A URI or unix domain socket path for scraping NGINX or NGINX Plus metrics.
For NGINX, the stub_status page must be available through the URI. For NGINX Plus -- the API. The default value can be overwritten by SCRAPE_URI environment variable.`)
sslVerify = flag.Bool("nginx.ssl-verify",
defaultSslVerify,
"Perform SSL certificate verification. The default value can be overwritten by SSL_VERIFY environment variable.")
sslCaCert = flag.String("nginx.ssl-ca-cert",
defaultSslCaCert,
"Path to the PEM encoded CA certificate file used to validate the servers SSL certificate. The default value can be overwritten by SSL_CA_CERT environment variable.")
sslClientCert = flag.String("nginx.ssl-client-cert",
defaultSslClientCert,
"Path to the PEM encoded client certificate file to use when connecting to the server. The default value can be overwritten by SSL_CLIENT_CERT environment variable.")
sslClientKey = flag.String("nginx.ssl-client-key",
defaultSslClientKey,
"Path to the PEM encoded client certificate key file to use when connecting to the server. The default value can be overwritten by SSL_CLIENT_KEY environment variable.")
nginxRetries = flag.Uint("nginx.retries",
defaultNginxRetries,
"A number of retries the exporter will make on start to connect to the NGINX stub_status page/NGINX Plus API before exiting with an error. The default value can be overwritten by NGINX_RETRIES environment variable.")
displayVersion = flag.Bool("version",
false,
"Display the NGINX exporter version.")
listenAddr = kingpin.Flag("web.listen-address", "An address or unix domain socket path to listen on for web interface and telemetry.").Default(":9113").Envar("LISTEN_ADDRESS").String()
securedMetrics = kingpin.Flag("web.secured-metrics", "Expose metrics using https.").Default("false").Envar("SECURED_METRICS").Bool()
sslServerCert = kingpin.Flag("web.ssl-server-cert", "Path to the PEM encoded certificate for the nginx-exporter metrics server(when web.secured-metrics=true).").Default("").Envar("SSL_SERVER_CERT").String()
sslServerKey = kingpin.Flag("web.ssl-server-key", "Path to the PEM encoded key for the nginx-exporter metrics server(when web.secured-metrics=true).").Default("").Envar("SSL_SERVER_KEY").String()
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").Envar("TELEMETRY_PATH").String()
nginxPlus = kingpin.Flag("nginx.plus", "Start the exporter for NGINX Plus. By default, the exporter is started for NGINX.").Default("false").Envar("NGINX_PLUS").Bool()
scrapeURI = kingpin.Flag("nginx.scrape-uri", "A URI or unix domain socket path for scraping NGINX or NGINX Plus metrics. For NGINX, the stub_status page must be available through the URI. For NGINX Plus -- the API.").Default("http://127.0.0.1:8080/stub_status").String()
sslVerify = kingpin.Flag("nginx.ssl-verify", "Perform SSL certificate verification.").Default("false").Envar("SSL_VERIFY").Bool()
sslCaCert = kingpin.Flag("nginx.ssl-ca-cert", "Path to the PEM encoded CA certificate file used to validate the servers SSL certificate.").Default("").Envar("SSL_CA_CERT").String()
sslClientCert = kingpin.Flag("nginx.ssl-client-cert", "Path to the PEM encoded client certificate file to use when connecting to the server.").Default("").Envar("SSL_CLIENT_CERT").String()
sslClientKey = kingpin.Flag("nginx.ssl-client-key", "Path to the PEM encoded client certificate key file to use when connecting to the server.").Default("").Envar("SSL_CLIENT_KEY").String()
nginxRetries = kingpin.Flag("nginx.retries", "A number of retries the exporter will make on start to connect to the NGINX stub_status page/NGINX Plus API before exiting with an error.").Default("0").Envar("NGINX_RETRIES").Uint()
// Custom command-line flags
timeout = createPositiveDurationFlag("nginx.timeout",
defaultTimeout,
"A timeout for scraping metrics from NGINX or NGINX Plus. The default value can be overwritten by TIMEOUT environment variable.")
nginxRetryInterval = createPositiveDurationFlag("nginx.retry-interval",
defaultNginxRetryInterval,
"An interval between retries to connect to the NGINX stub_status page/NGINX Plus API on start. The default value can be overwritten by NGINX_RETRY_INTERVAL environment variable.")
constLabels = createConstLabelsFlag("prometheus.const-labels",
defaultConstLabels,
"A comma separated list of constant labels that will be used in every metric. Format is label1=value1,label2=value2... The default value can be overwritten by CONST_LABELS environment variable.")
timeout = createPositiveDurationFlag(kingpin.Flag("nginx.timeout", "A timeout for scraping metrics from NGINX or NGINX Plus.").Default("5s").Envar("TIMEOUT"))
nginxRetryInterval = createPositiveDurationFlag(kingpin.Flag("nginx.retry-interval", "An interval between retries to connect to the NGINX stub_status page/NGINX Plus API on start.").Default("5s").Envar("NGINX_RETRY_INTERVAL"))
)
const exporterName = "nginx_exporter"
func main() {
flag.Parse()
kingpin.Flag("prometheus.const-label", "Label that will be used in every metric. Format is label=value. It can be repeated multiple times.").Envar("CONST_LABELS").StringMapVar(&constLabels)
commitHash, commitTime, dirtyBuild := getBuildInfo()
arch := fmt.Sprintf("%v/%v", runtime.GOOS, runtime.GOARCH)
promlogConfig := &promlog.Config{}
flag.AddFlags(kingpin.CommandLine, promlogConfig)
kingpin.Version(version.Print(exporterName))
kingpin.HelpFlag.Short('h')
kingpin.Parse()
logger := promlog.New(promlogConfig)
fmt.Printf("NGINX Prometheus Exporter version=%v commit=%v date=%v, dirty=%v, arch=%v, go=%v\n", version, commitHash, commitTime, dirtyBuild, arch, runtime.Version())
level.Info(logger).Log("msg", "Starting nginx-prometheus-exporter", "version", version.Info())
level.Info(logger).Log("msg", "Build context", "build_context", version.BuildContext())
if *displayVersion {
os.Exit(0)
}
log.Printf("Starting...")
registry := prometheus.NewRegistry()
buildInfoMetric := prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "nginxexporter_build_info",
Help: "Exporter build information",
ConstLabels: collector.MergeLabels(
constLabels.labels,
prometheus.Labels{
"version": version,
"commit": commitHash,
"date": commitTime,
"dirty": strconv.FormatBool(dirtyBuild),
"arch": arch,
"go": runtime.Version(),
},
),
},
)
buildInfoMetric.Set(1)
registry.MustRegister(buildInfoMetric)
prometheus.MustRegister(version.NewCollector(exporterName))
// #nosec G402
sslConfig := &tls.Config{InsecureSkipVerify: !*sslVerify}
if *sslCaCert != "" {
caCert, err := os.ReadFile(*sslCaCert)
if err != nil {
log.Fatalf("Loading CA cert failed: %v", err)
level.Error(logger).Log("msg", "Loading CA cert failed", "err", err.Error())
os.Exit(1)
}
sslCaCertPool := x509.NewCertPool()
ok := sslCaCertPool.AppendCertsFromPEM(caCert)
if !ok {
log.Fatal("Parsing CA cert file failed.")
level.Error(logger).Log("msg", "Parsing CA cert file failed.")
os.Exit(1)
}
sslConfig.RootCAs = sslCaCertPool
}
@@ -349,7 +169,8 @@ func main() {
if *sslClientCert != "" && *sslClientKey != "" {
clientCert, err := tls.LoadX509KeyPair(*sslClientCert, *sslClientKey)
if err != nil {
log.Fatalf("Loading client certificate failed: %v", err)
level.Error(logger).Log("msg", "Loading client certificate failed", "error", err.Error())
os.Exit(1)
}
sslConfig.Certificates = []tls.Certificate{clientCert}
}
@@ -360,7 +181,8 @@ func main() {
if strings.HasPrefix(*scrapeURI, "unix:") {
socketPath, requestPath, err := parseUnixSocketAddress(*scrapeURI)
if err != nil {
log.Fatalf("Parsing unix domain socket scrape address %s failed: %v", *scrapeURI, err)
level.Error(logger).Log("msg", "Parsing unix domain socket scrape address failed", "uri", *scrapeURI, "error", err.Error())
os.Exit(1)
}
transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
@@ -370,14 +192,14 @@ func main() {
scrapeURI = &newScrapeURI
}
userAgent := fmt.Sprintf("NGINX-Prometheus-Exporter/v%v", version)
userAgent := fmt.Sprintf("NGINX-Prometheus-Exporter/v%v", version.Version)
userAgentRT := &userAgentRoundTripper{
agent: userAgent,
rt: transport,
}
httpClient := &http.Client{
Timeout: timeout.Duration,
Timeout: *timeout,
Transport: userAgentRT,
}
@@ -388,10 +210,11 @@ func main() {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
go func() {
log.Printf("Signal received: %v. Exiting...", <-signalChan)
level.Info(logger).Log("msg", "Signal received, exiting...", "signal", <-signalChan)
err := srv.Close()
if err != nil {
log.Fatalf("Error occurred while closing the server: %v", err)
level.Error(logger).Log("msg", "Error occurred while closing the server", "error", err.Error())
os.Exit(1)
}
os.Exit(0)
}()
@@ -399,22 +222,25 @@ func main() {
if *nginxPlus {
plusClient, err := createClientWithRetries(func() (interface{}, error) {
return plusclient.NewNginxClient(httpClient, *scrapeURI)
}, *nginxRetries, nginxRetryInterval.Duration)
}, *nginxRetries, *nginxRetryInterval, logger)
if err != nil {
log.Fatalf("Could not create Nginx Plus Client: %v", err)
level.Error(logger).Log("msg", "Could not create Nginx Plus Client", "error", err.Error())
os.Exit(1)
}
variableLabelNames := collector.NewVariableLabelNames(nil, nil, nil, nil, nil, nil)
registry.MustRegister(collector.NewNginxPlusCollector(plusClient.(*plusclient.NginxClient), "nginxplus", variableLabelNames, constLabels.labels))
prometheus.MustRegister(collector.NewNginxPlusCollector(plusClient.(*plusclient.NginxClient), "nginxplus", variableLabelNames, constLabels, logger))
} else {
ossClient, err := createClientWithRetries(func() (interface{}, error) {
return client.NewNginxClient(httpClient, *scrapeURI)
}, *nginxRetries, nginxRetryInterval.Duration)
}, *nginxRetries, *nginxRetryInterval, logger)
if err != nil {
log.Fatalf("Could not create Nginx Client: %v", err)
level.Error(logger).Log("msg", "Could not create Nginx Client", "error", err.Error())
os.Exit(1)
}
registry.MustRegister(collector.NewNginxCollector(ossClient.(*client.NginxClient), "nginx", constLabels.labels))
prometheus.MustRegister(collector.NewNginxCollector(ossClient.(*client.NginxClient), "nginx", constLabels, logger))
}
http.Handle(*metricsPath, promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
http.Handle(*metricsPath, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, `<!DOCTYPE html>
@@ -423,30 +249,41 @@ func main() {
<p><a href=%q>Metrics</a></p>`,
*metricsPath)
if err != nil {
log.Printf("Error while sending a response for the '/' path: %v", err)
level.Error(logger).Log("msg", "Error while sending a response for the '/' path", "error", err.Error())
os.Exit(1)
}
})
listener, err := getListener(*listenAddr)
if err != nil {
log.Fatalf("Could not create listener: %v", err)
level.Error(logger).Log("msg", "Could not create listener", "error", err.Error())
os.Exit(1)
}
level.Info(logger).Log("msg", "Listening on address", "address", *listenAddr)
if *securedMetrics {
_, err = os.Stat(*sslServerCert)
if err != nil {
log.Fatalf("Cert file is not set, not readable or non-existent. Make sure you set -web.ssl-server-cert when starting your exporter with -web.secured-metrics=true: %v", err)
level.Error(logger).Log("msg", "Cert file is not set, not readable or non-existent. Make sure you set -web.ssl-server-cert when starting your exporter with -web.secured-metrics=true", "error", err.Error())
os.Exit(1)
}
_, err = os.Stat(*sslServerKey)
if err != nil {
log.Fatalf("Key file is not set, not readable or non-existent. Make sure you set -web.ssl-server-key when starting your exporter with -web.secured-metrics=true: %v", err)
level.Error(logger).Log("msg", "Key file is not set, not readable or non-existent. Make sure you set -web.ssl-server-key when starting your exporter with -web.secured-metrics=true", "error", err.Error())
os.Exit(1)
}
level.Info(logger).Log("msg", "NGINX Prometheus Exporter has successfully started using https")
if err := srv.ServeTLS(listener, *sslServerCert, *sslServerKey); err != nil {
level.Error(logger).Log("msg", "Error while serving", "error", err.Error())
os.Exit(1)
}
log.Printf("NGINX Prometheus Exporter has successfully started using https")
log.Fatal(srv.ServeTLS(listener, *sslServerCert, *sslServerKey))
}
log.Printf("NGINX Prometheus Exporter has successfully started")
log.Fatal(srv.Serve(listener))
level.Info(logger).Log("msg", "NGINX Prometheus Exporter has successfully started")
if err := srv.Serve(listener); err != nil {
level.Error(logger).Log("msg", "Error while serving", "error", err.Error())
os.Exit(1)
}
}
type userAgentRoundTripper struct {
@@ -473,23 +310,3 @@ func cloneRequest(req *http.Request) *http.Request {
}
return r
}
func getBuildInfo() (string, string, bool) {
var commitHash, commitTime string
var dirtyBuild bool
info, ok := debug.ReadBuildInfo()
if !ok {
return "", "", false
}
for _, kv := range info.Settings {
switch kv.Key {
case "vcs.revision":
commitHash = kv.Value
case "vcs.time":
commitTime = kv.Value
case "vcs.modified":
dirtyBuild = kv.Value == "true"
}
}
return commitHash, commitTime, dirtyBuild
}

View File

@@ -5,6 +5,8 @@ import (
"reflect"
"testing"
"time"
"github.com/go-kit/log"
)
func TestCreateClientWithRetries(t *testing.T) {
@@ -65,7 +67,7 @@ func TestCreateClientWithRetries(t *testing.T) {
return tt.args.client, tt.args.err
}
got, err := createClientWithRetries(getClient, tt.args.retries, tt.args.retryInterval)
got, err := createClientWithRetries(getClient, tt.args.retries, tt.args.retryInterval, log.NewNopLogger())
actualRetries := invocations - 1
@@ -181,63 +183,3 @@ func TestParseUnixSocketAddress(t *testing.T) {
})
}
}
func TestParseConstLabels(t *testing.T) {
t.Parallel()
tests := []struct {
name string
labels string
want constLabel
wantErr bool
}{
{
name: "Const labels with no labels",
labels: "",
want: constLabel{},
wantErr: false,
},
{
name: "Const labels with one label with valid format",
labels: "label=valid",
want: constLabel{labels: map[string]string{"label": "valid"}},
wantErr: false,
},
{
name: "Const labels with one label with invalid format",
labels: "label: invalid",
want: constLabel{},
wantErr: true,
},
{
name: "Const labels with invalid format for multiple labels",
labels: "label=valid,,label2=wrongformat",
want: constLabel{},
wantErr: true,
},
{
name: "Const labels with multiple labels, one label with invalid format",
labels: "label=valid,label2:wrongformat",
want: constLabel{},
wantErr: true,
},
{
name: "Const labels with label name containing invalid char",
labels: "l bel=invalid",
want: constLabel{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseConstLabels(tt.labels)
if (err != nil) != tt.wantErr {
t.Errorf("parseConstLabels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseConstLabels() = %v, want %v", got, tt.want)
}
})
}
}

18
go.mod
View File

@@ -3,18 +3,36 @@ module github.com/nginxinc/nginx-prometheus-exporter
go 1.19
require (
github.com/alecthomas/kingpin/v2 v2.3.2
github.com/go-kit/log v0.2.1
github.com/nginxinc/nginx-plus-go-client v0.10.0
github.com/prometheus/client_golang v1.15.1
github.com/prometheus/common v0.44.0
github.com/prometheus/exporter-toolkit v0.10.0
)
require (
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.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.3 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/procfs v0.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.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

55
go.sum
View File

@@ -1,31 +1,86 @@
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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
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/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/nginxinc/nginx-plus-go-client v0.10.0 h1:3zsMMkPvRDo8D7ZSprXtbAEW/SDmezZWzxdyS+6oAlc=
github.com/nginxinc/nginx-plus-go-client v0.10.0/go.mod h1:0v3RsQCvRn/IyrMtW+DK6CNkz+PxEsXDJPjQ3yUMBF0=
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.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.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.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=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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/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=