Skip to content

Commit

Permalink
Use web configuration from exporter-toolkit
Browse files Browse the repository at this point in the history
Replaces manual implementation of TLS and adds Basic Auth.
  • Loading branch information
lucacome committed Jul 20, 2023
1 parent 6395eb7 commit b7531cb
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 82 deletions.
24 changes: 24 additions & 0 deletions examples/basic_auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# NGINX Prometheus Exporter with Web Configuration for Basic Authentication

This example shows how to run NGINX Prometheus Exporter with web configuration. In this folder you will find an example configuration `web-config.yml` that enables basic authentication. It is configured to have a single user `alice` with password `password`.

The full documentation for the web configuration can be found [here](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md).

## Prerequisites

* NGINX Prometheus Exporter binary. See the [main README](../../README.md) for installation instructions.
* NGINX or NGINX Plus running on the same machine.

## Running NGINX Prometheus Exporter with Web Configuration in Basic Authentication mode

You can run NGINX Prometheus Exporter with web configuration in Basic Authentication mode using the following command:

```console
nginx-prometheus-exporter --web.config.file=web-config.yml --nginx.scrape-uri="http://127.0.0.1:8080/stub_status"
```

Depending on your environment, you may need to specify the full path to the binary or change the path to the web configuration file.

## Verification

Run `curl -u alice:password http://localhost:9113/metrics` to see the metrics exposed by the exporter. Without the `-u` flag, the request will fail with `401 Unauthorized`.
2 changes: 2 additions & 0 deletions examples/basic_auth/web-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
basic_auth_users:
alice: $2y$10$6xfhlaIhUDCUl60zPxkqLudN3QjL3Lfjg5gPAWiqElTLErpxAxJbC
30 changes: 30 additions & 0 deletions examples/systemd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# NGINX Prometheus Exporter with systemd-activated socket

This example shows how to run NGINX Prometheus Exporter with systemd-activated socket.

## Prerequisites

* Linux machine with [systemd](https://www.freedesktop.org/wiki/Software/systemd/).
* NGINX Prometheus Exporter binary in `/usr/local/bin/nginx-prometheus-exporter` or a location of your choice. See the [main README](../../README.md) for installation instructions.
* NGINX or NGINX Plus running on the same machine.

## Customization

Modify `nginx_exporter.service` and `nginx_exporter.socket` to match your environment.

The default configuration assumes that NGINX Prometheus Exporter binary is located in `/usr/local/bin/nginx-prometheus-exporter`.

The `ExecStart` directive has the flag `--web.systemd-socket` which tells the exporter to listen on the socket specified in the `nginx_exporter.socket` file.

The `ListenStream` directive in `nginx_exporter.socket` specifies the socket to listen on. The default configuration uses `9113` port, but the address can be written in various formats, for example `/run/nginx_exporter.sock`. To see the full list of supported formats, run `man systemd.socket`.

## Installation

1. Copy `nginx_exporter.service` and `nginx_exporter.socket` to `/etc/systemd/system/`
2. Run `systemctl daemon-reload`
3. Run `systemctl start nginx_exporter`
4. Run `systemctl status nginx_exporter` to check the status of the service

## Verification

1. Run `curl http://localhost:9113/metrics` to see the metrics exposed by the exporter.
10 changes: 10 additions & 0 deletions examples/systemd/nginx_exporter.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=NGINX Prometheus Exporter
Requires=nginx_exporter.socket

[Service]
User=nginx_exporter
ExecStart=/usr/local/bin/nginx-prometheus-exporter --nginx.scrape-uri="http://127.0.0.1:8080/stub_status" --web.systemd-socket

[Install]
WantedBy=multi-user.target
8 changes: 8 additions & 0 deletions examples/systemd/nginx_exporter.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=NGINX Prometheus Exporter

[Socket]
ListenStream=9113

[Install]
WantedBy=sockets.target
32 changes: 32 additions & 0 deletions examples/tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# NGINX Prometheus Exporter with Web Configuration for TLS

This example shows how to run NGINX Prometheus Exporter with web configuration. In this folder you will find an example configuration `web-config.yml` that enables TLS and specifies the path to the TLS certificate and key files. Additionally, there are two example TLS files `server.crt` and `server.key` that are used in the configuration.

The full documentation for the web configuration can be found [here](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md).

## Prerequisites

* NGINX Prometheus Exporter binary. See the [main README](../../README.md) for installation instructions.
* NGINX or NGINX Plus running on the same machine.

## Running NGINX Prometheus Exporter with Web Configuration in TLS mode

You can run NGINX Prometheus Exporter with web configuration in TLS mode using the following command:

```console
nginx-prometheus-exporter --web.config.file=web-config.yml --nginx.scrape-uri="http://127.0.0.1:8080/stub_status"
```

you should see an output similar to this:

```console
...
ts=2023-07-20T02:00:26.932Z caller=tls_config.go:274 level=info msg="Listening on" address=[::]:9113
ts=2023-07-20T02:00:26.936Z caller=tls_config.go:310 level=info msg="TLS is enabled." http2=true address=[::]:9113
```

Depending on your environment, you may need to specify the full path to the binary or change the path to the web configuration file.

## Verification

Run `curl -k https://localhost:9113/metrics` to see the metrics exposed by the exporter. The `-k` flag is needed because the certificate is self-signed.
3 changes: 3 additions & 0 deletions examples/tls/web-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tls_server_config:
cert_file: server.crt
key_file: server.key
129 changes: 47 additions & 82 deletions exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os"
"os/signal"
"strings"
"syscall"
"time"

plusclient "github.com/nginxinc/nginx-plus-go-client/client"
Expand All @@ -25,6 +24,9 @@ import (
"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
Expand Down Expand Up @@ -90,42 +92,19 @@ func parseUnixSocketAddress(address string) (string, string, error) {
return unixSocketPath, requestPath, nil
}

func getListener(listenAddress string) (net.Listener, error) {
var listener net.Listener
var err error

if strings.HasPrefix(listenAddress, "unix:") {
path, _, pathError := parseUnixSocketAddress(listenAddress)
if pathError != nil {
return listener, fmt.Errorf("parsing unix domain socket listen address %s failed: %w", listenAddress, pathError)
}
listener, err = net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
} else {
listener, err = net.Listen("tcp", listenAddress)
}

if err != nil {
return listener, err
}
return listener, nil
}

var (
constLabels = map[string]string{}

// Command-line flags
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()
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()
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(kingpin.Flag("nginx.timeout", "A timeout for scraping metrics from NGINX or NGINX Plus.").Default("5s").Envar("TIMEOUT"))
Expand Down Expand Up @@ -203,22 +182,6 @@ func main() {
Transport: userAgentRT,
}

srv := http.Server{
ReadHeaderTimeout: 5 * time.Second,
}

signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
go func() {
level.Info(logger).Log("msg", "Signal received, exiting...", "signal", <-signalChan)
err := srv.Close()
if err != nil {
level.Error(logger).Log("msg", "Error occurred while closing the server", "error", err.Error())
os.Exit(1)
}
os.Exit(0)
}()

if *nginxPlus {
plusClient, err := createClientWithRetries(func() (interface{}, error) {
return plusclient.NewNginxClient(httpClient, *scrapeURI)
Expand All @@ -242,48 +205,50 @@ func main() {

http.Handle(*metricsPath, promhttp.Handler())

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, `<!DOCTYPE html>
<title>NGINX Exporter</title>
<h1>NGINX Exporter</h1>
<p><a href=%q>Metrics</a></p>`,
*metricsPath)
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("msg", "Error while sending a response for the '/' path", "error", err.Error())
level.Error(logger).Log("err", err)
os.Exit(1)
}
})
http.Handle("/", landingPage)
}

listener, err := getListener(*listenAddr)
if err != nil {
level.Error(logger).Log("msg", "Could not create listener", "error", err.Error())
os.Exit(1)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()

srv := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
}
level.Info(logger).Log("msg", "Listening on address", "address", *listenAddr)

if *securedMetrics {
_, err = os.Stat(*sslServerCert)
if err != nil {
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 {
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())
go func() {
if err := web.ListenAndServe(srv, webConfig, logger); err != nil {
if err == http.ErrServerClosed {
level.Info(logger).Log("msg", "HTTP server closed")
os.Exit(0)
}
level.Error(logger).Log("err", err)
os.Exit(1)
}
}
}()

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)
}
<-ctx.Done()
level.Info(logger).Log("msg", "Shutting down")
srvCtx, srvCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer srvCancel()
_ = srv.Shutdown(srvCtx)
}

type userAgentRoundTripper struct {
Expand Down
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@ require (
github.com/nginxinc/nginx-plus-go-client v0.10.0
github.com/prometheus/client_golang v1.16.0
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.10.1 // 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.2.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
)
Loading

0 comments on commit b7531cb

Please sign in to comment.