From 8221d6daf9bdba1f916b84f1d7d13aa114ea2f42 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 22 Nov 2017 09:09:07 +0100 Subject: [PATCH] add openvpn_connected_clients + dockerfile + minor go review --- Dockerfile | 42 +++++++++++++++++++++++++++++++++++++ README.md | 51 +++++++++++++++++++++++++++++++++++++++++++-- build_static.sh | 17 --------------- openvpn_exporter.go | 46 ++++++++++++++++++++++++++++------------ 4 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 Dockerfile delete mode 100755 build_static.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b710f14 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# BUILD: +# docker build --force-rm=true -t openvpn_exporter . + +# RUN: +# docker run -it -p 9176:9176 -v /path/to/openvpn_server.status:/etc/openvpn_exporter/server.status openvpn_exporter + +FROM golang as builder + +RUN mkdir /app +RUN mkdir /go/src/app +ADD . /go/src/app +WORKDIR /go/src/app + +# Go dep +RUN go get -d ./... + +# Build a standalone binary +RUN set -ex && \ + CGO_ENABLED=0 go build \ + -tags netgo \ + -o /app/openvpn_exporter \ + -v -a \ + -ldflags '-extldflags "-static"' && \ + ls + +# Create the second stage with a basic image. +# this will drop any previous +# stages (defined as `FROM as `) +# allowing us to start with a fat build image and end up with +# a very small runtime image. + +FROM busybox + +# add compiled binary +COPY --from=builder /app/openvpn_exporter /openvpn_exporter + +# add a default file to be processed +ADD examples/server2.status /etc/openvpn_exporter/server.status + +# run +EXPOSE 9176 +CMD ["/openvpn_exporter", "-openvpn.status_paths", "/etc/openvpn_exporter/server.status"] diff --git a/README.md b/README.md index c306d4c..fbda037 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ exported over TCP port 9176. Please refer to this utility's `main()` function for a full list of supported command line flags. -## Client statistics +## Exposed metrics example + +### Client statistics For clients status files, the exporter generates metrics that may look like this: @@ -37,7 +39,7 @@ openvpn_status_update_time_seconds{status_path="..."} 1.490092749e+09 openvpn_up{status_path="..."} 1 ``` -## Server statistics +### Server statistics For server status files (both version 2 and 3), the exporter generates metrics that may look like this: @@ -48,4 +50,49 @@ openvpn_server_client_sent_bytes_total{common_name="...",connection_time="...",r openvpn_server_route_last_reference_time_seconds{common_name="...",real_address="...",status_path="...",virtual_address="..."} 1.493018841e+09 openvpn_status_update_time_seconds{status_path="..."} 1.490089154e+09 openvpn_up{status_path="..."} 1 +openvpn_server_connected_clients 1 +``` + +## Usage + +Usage of openvpn_exporter: +``` + -openvpn.status_paths string + Paths at which OpenVPN places its status files. (default "examples/client.status,examples/server2.status,examples/server3.status") + -web.listen-address string + Address to listen on for web interface and telemetry. (default ":9176") + -web.telemetry-path string + Path under which to expose metrics. (default "/metrics") +``` + +E.g: +``` +openvpn_exporter -openvpn.status_paths /etc/openvpn/openvpn-status.log +``` + +## Docker + +Build the image: +``` +docker build --force-rm=true -t openvpn_exporter . +``` + +The final image is around 8MB. A temporary image has been downloaded(Golang) to make the final one. Once built, this temporary image become orphan, you can delete it: +``` +docker rmi -f $(docker images | grep "" | awk "{print \$3}") +``` + +To use with docker you must mount your status file to `/etc/openvpn_exporter/server.status`. +``` +docker run -it -p 9176:9176 -v /path/to/openvpn_server.status:/etc/openvpn_exporter/server.status openvpn_exporter +``` + +Metrics should be available on your host IP: http://:9176/metrics. E.g: http://10.39.9.94:9176/metrics + + +## Get a standalone executable binary + +Use the docker image to copy the built binary into a mounted volume +```bash +docker run -it -v /local/empty/folder:/volume openvpn_exporter cp openvpn_exporter /volume ``` diff --git a/build_static.sh b/build_static.sh deleted file mode 100755 index c5f4232..0000000 --- a/build_static.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -docker run -i -v `pwd`:/openvpn_exporter alpine:edge /bin/sh << 'EOF' -set -ex - -# Install prerequisites for the build process. -apk update -apk add ca-certificates git go libc-dev -update-ca-certificates - -# Build the openvpn_exporter. -cd /openvpn_exporter -export GOPATH=/gopath -go get -d ./... -go build --ldflags '-extldflags "-static"' -strip openvpn_exporter -EOF diff --git a/openvpn_exporter.go b/openvpn_exporter.go index 8babecb..49449d9 100644 --- a/openvpn_exporter.go +++ b/openvpn_exporter.go @@ -27,6 +27,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) type OpenvpnServerHeader struct { @@ -52,8 +53,12 @@ var ( []string{"status_path"}, nil) // Metrics specific to OpenVPN servers. + openvpnConnectedClientsDesc = prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "", "openvpn_server_connected_clients"), + "Number Of Connected Clients", nil, nil) + openvpnServerHeaders = map[string]OpenvpnServerHeader{ - "CLIENT_LIST": OpenvpnServerHeader{ + "CLIENT_LIST": { LabelColumns: []string{ "Common Name", "Connected Since (time_t)", @@ -62,7 +67,7 @@ var ( "Username", }, Metrics: []OpenvpnServerHeaderField{ - OpenvpnServerHeaderField{ + { Column: "Bytes Received", Desc: prometheus.NewDesc( prometheus.BuildFQName("openvpn", "server", "client_received_bytes_total"), @@ -70,7 +75,7 @@ var ( []string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"}, nil), ValueType: prometheus.CounterValue, }, - OpenvpnServerHeaderField{ + { Column: "Bytes Sent", Desc: prometheus.NewDesc( prometheus.BuildFQName("openvpn", "server", "client_sent_bytes_total"), @@ -80,14 +85,14 @@ var ( }, }, }, - "ROUTING_TABLE": OpenvpnServerHeader{ + "ROUTING_TABLE": { LabelColumns: []string{ "Common Name", "Real Address", "Virtual Address", }, Metrics: []OpenvpnServerHeaderField{ - OpenvpnServerHeaderField{ + { Column: "Last Ref (time_t)", Desc: prometheus.NewDesc( prometheus.BuildFQName("openvpn", "server", "route_last_reference_time_seconds"), @@ -159,7 +164,7 @@ func CollectStatusFromReader(statusPath string, file io.Reader, ch chan<- promet // Client statistics. return CollectClientStatusFromReader(statusPath, reader, ch) } else { - return fmt.Errorf("Unexpected file contents: %q", buf) + return fmt.Errorf("unexpected file contents: %q", buf) } } @@ -168,6 +173,9 @@ func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) headersFound := map[string][]string{} + // counter of connected client + numberConnectedClient := 0 + for scanner.Scan() { fields := strings.Split(scanner.Text(), separator) if fields[0] == "END" && len(fields) == 1 { @@ -179,17 +187,19 @@ func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- headersFound[fields[1]] = fields[2:] } else if fields[0] == "TIME" && len(fields) == 3 { // Time at which the statistics were updated. - time, err := strconv.ParseFloat(fields[2], 64) + timeStartStats, err := strconv.ParseFloat(fields[2], 64) if err != nil { return err } ch <- prometheus.MustNewConstMetric( openvpnStatusUpdateTimeDesc, prometheus.GaugeValue, - time, + timeStartStats, statusPath) } else if fields[0] == "TITLE" && len(fields) == 2 { // OpenVPN version number. + } else if fields[0] == "CLIENT_LIST"{ + numberConnectedClient ++ } else if header, ok := openvpnServerHeaders[fields[0]]; ok { // Entry that depends on a preceding HEADERS directive. columnNames, ok := headersFound[fields[0]] @@ -230,9 +240,14 @@ func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- } } } else { - return fmt.Errorf("Unsupported key: %q", fields[0]) + return fmt.Errorf("unsupported key: %q", fields[0]) } } + // add the number of connected client + ch <- prometheus.MustNewConstMetric( + openvpnConnectedClientsDesc, + prometheus.GaugeValue, + float64(numberConnectedClient)) return scanner.Err() } @@ -249,14 +264,14 @@ func CollectClientStatusFromReader(statusPath string, file io.Reader, ch chan<- } else if fields[0] == "Updated" && len(fields) == 2 { // Time at which the statistics were updated. location, _ := time.LoadLocation("Local") - time, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", fields[1], location) + timeParser, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", fields[1], location) if err != nil { return err } ch <- prometheus.MustNewConstMetric( openvpnStatusUpdateTimeDesc, prometheus.GaugeValue, - float64(time.Unix()), + float64(timeParser.Unix()), statusPath) } else if desc, ok := openvpnClientDescs[fields[0]]; ok && len(fields) == 2 { // Traffic counters. @@ -270,7 +285,7 @@ func CollectClientStatusFromReader(statusPath string, file io.Reader, ch chan<- value, statusPath) } else { - return fmt.Errorf("Unsupported key: %q", fields[0]) + return fmt.Errorf("unsupported key: %q", fields[0]) } } return scanner.Err() @@ -327,13 +342,18 @@ func main() { ) flag.Parse() + log.Printf("Starting OpenVPN Exporter\n") + log.Printf("Listen address: %v\n", *listenAddress) + log.Printf("Metrics path: %v\n", *metricsPath) + log.Printf("openvpn.status_path: %v\n", *openvpnStatusPaths) + exporter, err := NewOpenVPNExporter(strings.Split(*openvpnStatusPaths, ",")) if err != nil { panic(err) } prometheus.MustRegister(exporter) - http.Handle(*metricsPath, prometheus.Handler()) + http.Handle(*metricsPath, promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`