Skip to content

Commit

Permalink
Add ability to scrape and listen unix domain sockets
Browse files Browse the repository at this point in the history
Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com>
  • Loading branch information
nabokihms authored and pleshakov committed Nov 12, 2019
1 parent d59869f commit 319a7fd
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 12 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ To start the exporter we use the [docker run](https://docs.docker.com/engine/ref
```
where `<nginx-plus>` is the IP address/DNS name, through which NGINX Plus is available.

* To export and scrape NGINX metrics with unix domain sockets, run:
```
$ nginx-prometheus-exporter -nginx.scrape-uri unix:<nginx>:/stub_status -web.listen-address unix:/path/to/socket.sock
```
where `<nginx>` is the path to unix domain socket, through which NGINX stub status is available.

**Note**. The `nginx-prometheus-exporter` is not a daemon. To run the exporter as a system service (daemon), configure the init system of your Linux server (such as systemd or Upstart) accordingly. Alternatively, you can run the exporter in a Docker container.

## Usage
Expand All @@ -69,14 +75,14 @@ Usage of ./nginx-prometheus-exporter:
-nginx.retry-interval duration
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. (default 5s)
-nginx.scrape-uri string
A URI for scraping NGINX or NGINX Plus metrics.
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. (default "http://127.0.0.1:8080/stub_status")
-nginx.ssl-verify
Perform SSL certificate verification. The default value can be overwritten by SSL_VERIFY environment variable. (default true)
-nginx.timeout duration
A timeout for scraping metrics from NGINX or NGINX Plus. The default value can be overwritten by TIMEOUT environment variable. (default 5s)
-web.listen-address string
An address to listen on for web interface and telemetry. The default value can be overwritten by LISTEN_ADDRESS environment variable. (default ":9113")
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. (default ":9113")
-web.telemetry-path string
A path under which to expose metrics. The default value can be overwritten by TELEMETRY_PATH environment variable. (default "/metrics")
```
Expand Down
86 changes: 76 additions & 10 deletions exporter.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package main

import (
"context"
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -110,6 +113,43 @@ func createClientWithRetries(getClient func() (interface{}, error), retries uint
return nil, err
}

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
}

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: %v", 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
}
log.Printf("Listening on %s", listenAddress)
return listener, nil
}

var (
// Set during go build
version string
Expand All @@ -128,7 +168,7 @@ var (
// Command-line flags
listenAddr = flag.String("web.listen-address",
defaultListenAddress,
"An address to listen on for web interface and telemetry. The default value can be overwritten by LISTEN_ADDRESS environment variable.")
"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.")
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.")
Expand All @@ -137,8 +177,8 @@ var (
"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 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.`)
`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.")
Expand Down Expand Up @@ -177,17 +217,37 @@ func main() {

registry.MustRegister(buildInfoMetric)

transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: !*sslVerify},
}
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)
}

transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
}
newScrapeURI := "http://unix" + requestPath
scrapeURI = &newScrapeURI
}

httpClient := &http.Client{
Timeout: timeout.Duration,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: !*sslVerify},
},
Timeout: timeout.Duration,
Transport: transport,
}

srv := http.Server{}

signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
go func() {
log.Printf("SIGTERM received: %v. Exiting...", <-signalChan)
log.Printf("Signal received: %v. Exiting...", <-signalChan)
err := srv.Close()
if err != nil {
log.Fatalf("Error occurred while closing the server: %v", err)
}
os.Exit(0)
}()

Expand Down Expand Up @@ -221,6 +281,12 @@ func main() {
log.Printf("Error while sending a response for the '/' path: %v", err)
}
})

listener, err := getListener(*listenAddr)
if err != nil {
log.Fatalf("Could not create listener: %v", err)
}

log.Printf("NGINX Prometheus Exporter has successfully started")
log.Fatal(http.ListenAndServe(*listenAddr, nil))
log.Fatal(srv.Serve(listener))
}
54 changes: 54 additions & 0 deletions exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,57 @@ func TestParsePositiveDuration(t *testing.T) {
})
}
}

func TestParseUnixSocketAddress(t *testing.T) {
tests := []struct {
name string
testInput string
wantSocketPath string
wantRequestPath string
wantErr bool
}{
{
"Normal unix socket address",
"unix:/path/to/socket",
"/path/to/socket",
"",
false,
},
{
"Normal unix socket address with location",
"unix:/path/to/socket:/with/location",
"/path/to/socket",
"/with/location",
false,
},
{
"Unix socket address with trailing ",
"unix:/trailing/path:",
"/trailing/path",
"",
false,
},
{
"Unix socket address with too many colons",
"unix:/too:/many:colons:",
"",
"",
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
socketPath, requestPath, err := parseUnixSocketAddress(tt.testInput)
if (err != nil) != tt.wantErr {
t.Errorf("parseUnixSocketAddress() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(socketPath, tt.wantSocketPath) {
t.Errorf("socket path: parseUnixSocketAddress() = %v, want %v", socketPath, tt.wantSocketPath)
}
if !reflect.DeepEqual(requestPath, tt.wantRequestPath) {
t.Errorf("request path: parseUnixSocketAddress() = %v, want %v", requestPath, tt.wantRequestPath)
}
})
}
}

0 comments on commit 319a7fd

Please sign in to comment.