Skip to content

Commit 319a7fd

Browse files
nabokihmspleshakov
authored andcommitted
Add ability to scrape and listen unix domain sockets
Signed-off-by: m.nabokikh <[email protected]>
1 parent d59869f commit 319a7fd

File tree

3 files changed

+138
-12
lines changed

3 files changed

+138
-12
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ To start the exporter we use the [docker run](https://docs.docker.com/engine/ref
5454
```
5555
where `<nginx-plus>` is the IP address/DNS name, through which NGINX Plus is available.
5656
57+
* To export and scrape NGINX metrics with unix domain sockets, run:
58+
```
59+
$ nginx-prometheus-exporter -nginx.scrape-uri unix:<nginx>:/stub_status -web.listen-address unix:/path/to/socket.sock
60+
```
61+
where `<nginx>` is the path to unix domain socket, through which NGINX stub status is available.
62+
5763
**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.
5864
5965
## Usage
@@ -69,14 +75,14 @@ Usage of ./nginx-prometheus-exporter:
6975
-nginx.retry-interval duration
7076
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)
7177
-nginx.scrape-uri string
72-
A URI for scraping NGINX or NGINX Plus metrics.
78+
A URI or unix domain socket path for scraping NGINX or NGINX Plus metrics.
7379
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")
7480
-nginx.ssl-verify
7581
Perform SSL certificate verification. The default value can be overwritten by SSL_VERIFY environment variable. (default true)
7682
-nginx.timeout duration
7783
A timeout for scraping metrics from NGINX or NGINX Plus. The default value can be overwritten by TIMEOUT environment variable. (default 5s)
7884
-web.listen-address string
79-
An address to listen on for web interface and telemetry. The default value can be overwritten by LISTEN_ADDRESS environment variable. (default ":9113")
85+
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")
8086
-web.telemetry-path string
8187
A path under which to expose metrics. The default value can be overwritten by TELEMETRY_PATH environment variable. (default "/metrics")
8288
```

exporter.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package main
22

33
import (
4+
"context"
45
"crypto/tls"
56
"flag"
67
"fmt"
78
"log"
9+
"net"
810
"net/http"
911
"os"
1012
"os/signal"
1113
"strconv"
14+
"strings"
1215
"syscall"
1316
"time"
1417

@@ -110,6 +113,43 @@ func createClientWithRetries(getClient func() (interface{}, error), retries uint
110113
return nil, err
111114
}
112115

116+
func parseUnixSocketAddress(address string) (string, string, error) {
117+
addressParts := strings.Split(address, ":")
118+
addressPartsLength := len(addressParts)
119+
120+
if addressPartsLength > 3 || addressPartsLength < 1 {
121+
return "", "", fmt.Errorf("address for unix domain socket has wrong format")
122+
}
123+
124+
unixSocketPath := addressParts[1]
125+
requestPath := ""
126+
if addressPartsLength == 3 {
127+
requestPath = addressParts[2]
128+
}
129+
return unixSocketPath, requestPath, nil
130+
}
131+
132+
func getListener(listenAddress string) (net.Listener, error) {
133+
var listener net.Listener
134+
var err error
135+
136+
if strings.HasPrefix(listenAddress, "unix:") {
137+
path, _, pathError := parseUnixSocketAddress(listenAddress)
138+
if pathError != nil {
139+
return listener, fmt.Errorf("parsing unix domain socket listen address %s failed: %v", listenAddress, pathError)
140+
}
141+
listener, err = net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
142+
} else {
143+
listener, err = net.Listen("tcp", listenAddress)
144+
}
145+
146+
if err != nil {
147+
return listener, err
148+
}
149+
log.Printf("Listening on %s", listenAddress)
150+
return listener, nil
151+
}
152+
113153
var (
114154
// Set during go build
115155
version string
@@ -128,7 +168,7 @@ var (
128168
// Command-line flags
129169
listenAddr = flag.String("web.listen-address",
130170
defaultListenAddress,
131-
"An address to listen on for web interface and telemetry. The default value can be overwritten by LISTEN_ADDRESS environment variable.")
171+
"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.")
132172
metricsPath = flag.String("web.telemetry-path",
133173
defaultMetricsPath,
134174
"A path under which to expose metrics. The default value can be overwritten by TELEMETRY_PATH environment variable.")
@@ -137,8 +177,8 @@ var (
137177
"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.")
138178
scrapeURI = flag.String("nginx.scrape-uri",
139179
defaultScrapeURI,
140-
`A URI for scraping NGINX or NGINX Plus metrics.
141-
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.`)
180+
`A URI or unix domain socket path for scraping NGINX or NGINX Plus metrics.
181+
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.`)
142182
sslVerify = flag.Bool("nginx.ssl-verify",
143183
defaultSslVerify,
144184
"Perform SSL certificate verification. The default value can be overwritten by SSL_VERIFY environment variable.")
@@ -177,17 +217,37 @@ func main() {
177217

178218
registry.MustRegister(buildInfoMetric)
179219

220+
transport := &http.Transport{
221+
TLSClientConfig: &tls.Config{InsecureSkipVerify: !*sslVerify},
222+
}
223+
if strings.HasPrefix(*scrapeURI, "unix:") {
224+
socketPath, requestPath, err := parseUnixSocketAddress(*scrapeURI)
225+
if err != nil {
226+
log.Fatalf("Parsing unix domain socket scrape address %s failed: %v", *scrapeURI, err)
227+
}
228+
229+
transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
230+
return net.Dial("unix", socketPath)
231+
}
232+
newScrapeURI := "http://unix" + requestPath
233+
scrapeURI = &newScrapeURI
234+
}
235+
180236
httpClient := &http.Client{
181-
Timeout: timeout.Duration,
182-
Transport: &http.Transport{
183-
TLSClientConfig: &tls.Config{InsecureSkipVerify: !*sslVerify},
184-
},
237+
Timeout: timeout.Duration,
238+
Transport: transport,
185239
}
186240

241+
srv := http.Server{}
242+
187243
signalChan := make(chan os.Signal, 1)
188-
signal.Notify(signalChan, syscall.SIGTERM)
244+
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
189245
go func() {
190-
log.Printf("SIGTERM received: %v. Exiting...", <-signalChan)
246+
log.Printf("Signal received: %v. Exiting...", <-signalChan)
247+
err := srv.Close()
248+
if err != nil {
249+
log.Fatalf("Error occurred while closing the server: %v", err)
250+
}
191251
os.Exit(0)
192252
}()
193253

@@ -221,6 +281,12 @@ func main() {
221281
log.Printf("Error while sending a response for the '/' path: %v", err)
222282
}
223283
})
284+
285+
listener, err := getListener(*listenAddr)
286+
if err != nil {
287+
log.Fatalf("Could not create listener: %v", err)
288+
}
289+
224290
log.Printf("NGINX Prometheus Exporter has successfully started")
225-
log.Fatal(http.ListenAndServe(*listenAddr, nil))
291+
log.Fatal(srv.Serve(listener))
226292
}

exporter_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,57 @@ func TestParsePositiveDuration(t *testing.T) {
121121
})
122122
}
123123
}
124+
125+
func TestParseUnixSocketAddress(t *testing.T) {
126+
tests := []struct {
127+
name string
128+
testInput string
129+
wantSocketPath string
130+
wantRequestPath string
131+
wantErr bool
132+
}{
133+
{
134+
"Normal unix socket address",
135+
"unix:/path/to/socket",
136+
"/path/to/socket",
137+
"",
138+
false,
139+
},
140+
{
141+
"Normal unix socket address with location",
142+
"unix:/path/to/socket:/with/location",
143+
"/path/to/socket",
144+
"/with/location",
145+
false,
146+
},
147+
{
148+
"Unix socket address with trailing ",
149+
"unix:/trailing/path:",
150+
"/trailing/path",
151+
"",
152+
false,
153+
},
154+
{
155+
"Unix socket address with too many colons",
156+
"unix:/too:/many:colons:",
157+
"",
158+
"",
159+
true,
160+
},
161+
}
162+
for _, tt := range tests {
163+
t.Run(tt.name, func(t *testing.T) {
164+
socketPath, requestPath, err := parseUnixSocketAddress(tt.testInput)
165+
if (err != nil) != tt.wantErr {
166+
t.Errorf("parseUnixSocketAddress() error = %v, wantErr %v", err, tt.wantErr)
167+
return
168+
}
169+
if !reflect.DeepEqual(socketPath, tt.wantSocketPath) {
170+
t.Errorf("socket path: parseUnixSocketAddress() = %v, want %v", socketPath, tt.wantSocketPath)
171+
}
172+
if !reflect.DeepEqual(requestPath, tt.wantRequestPath) {
173+
t.Errorf("request path: parseUnixSocketAddress() = %v, want %v", requestPath, tt.wantRequestPath)
174+
}
175+
})
176+
}
177+
}

0 commit comments

Comments
 (0)