mirror of
https://github.com/nginxinc/nginx-prometheus-exporter.git
synced 2025-04-19 23:42:14 +03:00
308 lines
9.7 KiB
Go
308 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
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/promlog"
|
|
"github.com/prometheus/common/promlog/flag"
|
|
"github.com/prometheus/common/version"
|
|
|
|
"github.com/prometheus/exporter-toolkit/web"
|
|
"github.com/prometheus/exporter-toolkit/web/kingpinflag"
|
|
)
|
|
|
|
// positiveDuration is a wrapper of time.Duration to ensure only positive values are accepted
|
|
type positiveDuration struct{ time.Duration }
|
|
|
|
func (pd *positiveDuration) Set(s string) error {
|
|
dur, err := parsePositiveDuration(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pd.Duration = dur.Duration
|
|
return nil
|
|
}
|
|
|
|
func parsePositiveDuration(s string) (positiveDuration, error) {
|
|
dur, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return positiveDuration{}, err
|
|
}
|
|
if dur < 0 {
|
|
return positiveDuration{}, fmt.Errorf("negative duration %v is not valid", dur)
|
|
}
|
|
return positiveDuration{dur}, nil
|
|
}
|
|
|
|
func createPositiveDurationFlag(s kingpin.Settings) (target *time.Duration) {
|
|
target = new(time.Duration)
|
|
s.SetValue(&positiveDuration{Duration: *target})
|
|
return
|
|
}
|
|
|
|
func parseUnixSocketAddress(address string) (string, string, error) {
|
|
addressParts := strings.Split(address, ":")
|
|
addressPartsLength := len(addressParts)
|
|
|
|
if addressPartsLength > 3 || addressPartsLength < 1 {
|
|
return "", "", fmt.Errorf("address for unix domain socket has wrong format")
|
|
}
|
|
|
|
unixSocketPath := addressParts[1]
|
|
requestPath := ""
|
|
if addressPartsLength == 3 {
|
|
requestPath = addressParts[2]
|
|
}
|
|
return unixSocketPath, requestPath, nil
|
|
}
|
|
|
|
var (
|
|
constLabels = map[string]string{}
|
|
|
|
// Command-line flags
|
|
webConfig = kingpinflag.AddFlags(kingpin.CommandLine, ":9113")
|
|
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()
|
|
scrapeURIs = 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. Repeatable for multiple URIs.").Default("http://127.0.0.1:8080/stub_status").Envar("SCRAPE_URI").HintOptions("http://127.0.0.1:8080/stub_status", "http://127.0.0.1:8080/api").Strings()
|
|
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()
|
|
|
|
// Custom command-line flags
|
|
timeout = createPositiveDurationFlag(kingpin.Flag("nginx.timeout", "A timeout for scraping metrics from NGINX or NGINX Plus.").Default("5s").Envar("TIMEOUT").HintOptions("5s", "10s", "30s", "1m", "5m"))
|
|
)
|
|
|
|
const exporterName = "nginx_exporter"
|
|
|
|
func main() {
|
|
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)
|
|
|
|
// convert deprecated flags to new format
|
|
for i, arg := range os.Args {
|
|
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) > 2 {
|
|
newArg := fmt.Sprintf("-%s", arg)
|
|
fmt.Printf("the flag format is deprecated and will be removed in a future release, please use the new format: %s\n", newArg)
|
|
os.Args[i] = newArg
|
|
}
|
|
}
|
|
|
|
promlogConfig := &promlog.Config{}
|
|
|
|
flag.AddFlags(kingpin.CommandLine, promlogConfig)
|
|
kingpin.Version(version.Print(exporterName))
|
|
kingpin.HelpFlag.Short('h')
|
|
|
|
addMissingEnvironmentFlags(kingpin.CommandLine)
|
|
|
|
kingpin.Parse()
|
|
logger := promlog.New(promlogConfig)
|
|
|
|
level.Info(logger).Log("msg", "Starting nginx-prometheus-exporter", "version", version.Info())
|
|
level.Info(logger).Log("msg", "Build context", "build_context", version.BuildContext())
|
|
|
|
prometheus.MustRegister(version.NewCollector(exporterName))
|
|
|
|
if len(*scrapeURIs) == 0 {
|
|
level.Error(logger).Log("msg", "No scrape addresses provided")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// #nosec G402
|
|
sslConfig := &tls.Config{InsecureSkipVerify: !*sslVerify}
|
|
if *sslCaCert != "" {
|
|
caCert, err := os.ReadFile(*sslCaCert)
|
|
if err != nil {
|
|
level.Error(logger).Log("msg", "Loading CA cert failed", "err", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
sslCaCertPool := x509.NewCertPool()
|
|
ok := sslCaCertPool.AppendCertsFromPEM(caCert)
|
|
if !ok {
|
|
level.Error(logger).Log("msg", "Parsing CA cert file failed.")
|
|
os.Exit(1)
|
|
}
|
|
sslConfig.RootCAs = sslCaCertPool
|
|
}
|
|
|
|
if *sslClientCert != "" && *sslClientKey != "" {
|
|
clientCert, err := tls.LoadX509KeyPair(*sslClientCert, *sslClientKey)
|
|
if err != nil {
|
|
level.Error(logger).Log("msg", "Loading client certificate failed", "error", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
sslConfig.Certificates = []tls.Certificate{clientCert}
|
|
}
|
|
|
|
transport := &http.Transport{
|
|
TLSClientConfig: sslConfig,
|
|
}
|
|
|
|
if len(*scrapeURIs) == 1 {
|
|
registerCollector(logger, transport, (*scrapeURIs)[0], constLabels)
|
|
} else {
|
|
for _, addr := range *scrapeURIs {
|
|
// add scrape URI to const labels
|
|
labels := maps.Clone(constLabels)
|
|
labels["addr"] = addr
|
|
|
|
registerCollector(logger, transport, addr, labels)
|
|
}
|
|
}
|
|
|
|
http.Handle(*metricsPath, promhttp.Handler())
|
|
|
|
if *metricsPath != "/" && *metricsPath != "" {
|
|
landingConfig := web.LandingConfig{
|
|
Name: "NGINX Prometheus Exporter",
|
|
Description: "Prometheus Exporter for NGINX and NGINX Plus",
|
|
HeaderColor: "#039900",
|
|
Version: version.Info(),
|
|
Links: []web.LandingLinks{
|
|
{
|
|
Address: *metricsPath,
|
|
Text: "Metrics",
|
|
},
|
|
},
|
|
}
|
|
landingPage, err := web.NewLandingPage(landingConfig)
|
|
if err != nil {
|
|
level.Error(logger).Log("err", err)
|
|
os.Exit(1)
|
|
}
|
|
http.Handle("/", landingPage)
|
|
}
|
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
srv := &http.Server{
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
if err := web.ListenAndServe(srv, webConfig, logger); err != nil {
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
level.Info(logger).Log("msg", "HTTP server closed")
|
|
os.Exit(0)
|
|
}
|
|
level.Error(logger).Log("err", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
level.Info(logger).Log("msg", "Shutting down")
|
|
srvCtx, srvCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer srvCancel()
|
|
_ = srv.Shutdown(srvCtx)
|
|
}
|
|
|
|
func registerCollector(logger log.Logger, transport *http.Transport,
|
|
addr string, labels map[string]string,
|
|
) {
|
|
if strings.HasPrefix(addr, "unix:") {
|
|
socketPath, requestPath, err := parseUnixSocketAddress(addr)
|
|
if err != nil {
|
|
level.Error(logger).Log("msg", "Parsing unix domain socket scrape address failed", "uri", addr, "error", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
return net.Dial("unix", socketPath)
|
|
}
|
|
addr = "http://unix" + requestPath
|
|
}
|
|
|
|
userAgent := fmt.Sprintf("NGINX-Prometheus-Exporter/v%v", version.Version)
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: *timeout,
|
|
Transport: &userAgentRoundTripper{
|
|
agent: userAgent,
|
|
rt: transport,
|
|
},
|
|
}
|
|
|
|
if *nginxPlus {
|
|
plusClient, err := plusclient.NewNginxClient(addr, plusclient.WithHTTPClient(httpClient))
|
|
if err != nil {
|
|
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, nil, nil)
|
|
prometheus.MustRegister(collector.NewNginxPlusCollector(plusClient, "nginxplus", variableLabelNames, labels, logger))
|
|
} else {
|
|
ossClient := client.NewNginxClient(httpClient, addr)
|
|
prometheus.MustRegister(collector.NewNginxCollector(ossClient, "nginx", labels, logger))
|
|
}
|
|
}
|
|
|
|
type userAgentRoundTripper struct {
|
|
agent string
|
|
rt http.RoundTripper
|
|
}
|
|
|
|
func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
req = cloneRequest(req)
|
|
req.Header.Set("User-Agent", rt.agent)
|
|
return rt.rt.RoundTrip(req)
|
|
}
|
|
|
|
func cloneRequest(req *http.Request) *http.Request {
|
|
r := new(http.Request)
|
|
*r = *req // shallow clone
|
|
|
|
// deep copy headers
|
|
r.Header = make(http.Header, len(req.Header))
|
|
for key, values := range req.Header {
|
|
newValues := make([]string, len(values))
|
|
copy(newValues, values)
|
|
r.Header[key] = newValues
|
|
}
|
|
return r
|
|
}
|
|
|
|
// addMissingEnvironmentFlags sets Envar on any flag which has
|
|
// the "web." prefix which doesn't already have an Envar set
|
|
func addMissingEnvironmentFlags(ka *kingpin.Application) {
|
|
for _, f := range ka.Model().FlagGroupModel.Flags {
|
|
if strings.HasPrefix(f.Name, "web.") && f.Envar == "" {
|
|
flag := ka.GetFlag(f.Name)
|
|
if flag != nil {
|
|
flag.Envar(convertFlagToEnvar(strings.TrimPrefix(f.Name, "web.")))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func convertFlagToEnvar(f string) string {
|
|
env := strings.ToUpper(f)
|
|
for _, s := range []string{"-", "."} {
|
|
env = strings.ReplaceAll(env, s, "_")
|
|
}
|
|
return env
|
|
}
|