From fe73b8c8bca07e2201974708c20bc3ae5132ecfc Mon Sep 17 00:00:00 2001 From: Ed Schouten Date: Tue, 21 Mar 2017 13:27:55 +0100 Subject: [PATCH] Check in an initial version of the OpenVPN metrics exporter. --- examples/client.status | 12 ++ examples/server2.status | 16 +++ examples/server3.status | 16 +++ openvpn_exporter.go | 274 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 examples/client.status create mode 100644 examples/server2.status create mode 100644 examples/server3.status create mode 100644 openvpn_exporter.go diff --git a/examples/client.status b/examples/client.status new file mode 100644 index 0000000..fb50a79 --- /dev/null +++ b/examples/client.status @@ -0,0 +1,12 @@ +OpenVPN STATISTICS +Updated,Tue Mar 21 10:39:09 2017 +TUN/TAP read bytes,153789941 +TUN/TAP write bytes,308764078 +TCP/UDP read bytes,292806201 +TCP/UDP write bytes,197558969 +Auth read bytes,308854782 +pre-compress bytes,45388190 +post-compress bytes,45446864 +pre-decompress bytes,162596168 +post-decompress bytes,216965355 +END diff --git a/examples/server2.status b/examples/server2.status new file mode 100644 index 0000000..d5499ed --- /dev/null +++ b/examples/server2.status @@ -0,0 +1,16 @@ +TITLE,OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Dec 2 2014 +TIME,Tue Mar 21 10:39:14 2017,1490089154 +HEADER,CLIENT_LIST,Common Name,Real Address,Virtual Address,Bytes Received,Bytes Sent,Connected Since,Connected Since (time_t),Username +CLIENT_LIST,redacted1,0.0.0.0:19021,0.0.0.0,693438277,228390856,Thu Mar 16 17:09:03 2017,1489680543,UNDEF +CLIENT_LIST,redacted2,0.0.0.0:60536,0.0.0.0,2925752,3145665,Thu Mar 16 17:08:57 2017,1489680537,UNDEF +CLIENT_LIST,redacted3,0.0.0.0:28331,0.0.0.0,57316467,611736741,Thu Mar 16 17:08:57 2017,1489680537,UNDEF +CLIENT_LIST,redacted4,0.0.0.0:52335,0.0.0.0,24289622392,70914674697,Fri Mar 17 11:16:29 2017,1489745789,UNDEF +CLIENT_LIST,redacted5,0.0.0.0:51865,0.0.0.0,277017840,1544465106,Thu Mar 16 17:09:01 2017,1489680541,UNDEF +HEADER,ROUTING_TABLE,Virtual Address,Common Name,Real Address,Last Ref,Last Ref (time_t) +ROUTING_TABLE,0.0.0.0,redacted1,0.0.0.0:19021,Tue Mar 21 10:26:48 2017,1490088408 +ROUTING_TABLE,0.0.0.0,redacted5,0.0.0.0:51865,Tue Mar 21 10:38:26 2017,1490089106 +ROUTING_TABLE,0.0.0.0,redacted3,0.0.0.0:28331,Tue Mar 21 10:39:06 2017,1490089146 +ROUTING_TABLE,0.0.0.0,redacted4,0.0.0.0:52335,Tue Mar 21 10:39:13 2017,1490089153 +ROUTING_TABLE,0.0.0.0,redacted2,0.0.0.0:60536,Thu Mar 16 17:08:58 2017,1489680538 +GLOBAL_STATS,Max bcast/mcast queue length,0 +END diff --git a/examples/server3.status b/examples/server3.status new file mode 100644 index 0000000..12e664e --- /dev/null +++ b/examples/server3.status @@ -0,0 +1,16 @@ +TITLE OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Dec 2 2014 +TIME Tue Mar 21 10:39:14 2017 1490089154 +HEADER CLIENT_LIST Common Name Real Address Virtual Address Bytes Received Bytes Sent Connected Since Connected Since (time_t) Username +CLIENT_LIST redacted1 0.0.0.0:19021 0.0.0.0 693438277 228390856 Thu Mar 16 17:09:03 2017 1489680543 UNDEF +CLIENT_LIST redacted2 0.0.0.0:60536 0.0.0.0 2925752 3145665 Thu Mar 16 17:08:57 2017 1489680537 UNDEF +CLIENT_LIST redacted3 0.0.0.0:28331 0.0.0.0 57316467 611736741 Thu Mar 16 17:08:57 2017 1489680537 UNDEF +CLIENT_LIST redacted4 0.0.0.0:52335 0.0.0.0 24289622392 70914674697 Fri Mar 17 11:16:29 2017 1489745789 UNDEF +CLIENT_LIST redacted5 0.0.0.0:51865 0.0.0.0 277017840 1544465106 Thu Mar 16 17:09:01 2017 1489680541 UNDEF +HEADER ROUTING_TABLE Virtual Address Common Name Real Address Last Ref Last Ref (time_t) +ROUTING_TABLE 0.0.0.0 redacted1 0.0.0.0:19021 Tue Mar 21 10:26:48 2017 1490088408 +ROUTING_TABLE 0.0.0.0 redacted5 0.0.0.0:51865 Tue Mar 21 10:38:26 2017 1490089106 +ROUTING_TABLE 0.0.0.0 redacted3 0.0.0.0:28331 Tue Mar 21 10:39:06 2017 1490089146 +ROUTING_TABLE 0.0.0.0 redacted4 0.0.0.0:52335 Tue Mar 21 10:39:13 2017 1490089153 +ROUTING_TABLE 0.0.0.0 redacted2 0.0.0.0:60536 Thu Mar 16 17:08:58 2017 1489680538 +GLOBAL_STATS Max bcast/mcast queue length 0 +END diff --git a/openvpn_exporter.go b/openvpn_exporter.go new file mode 100644 index 0000000..ba86856 --- /dev/null +++ b/openvpn_exporter.go @@ -0,0 +1,274 @@ +// Copyright 2017 Kumina, https://kumina.nl/ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + // Metrics exported both for client and server statistics. + openvpnUpDesc = prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "", "up"), + "Whether scraping OpenVPN's metrics was successful.", + []string{"status_path"}, nil) + openvpnStatusUpdateTimeDesc = prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "", "status_update_time_seconds"), + "UNIX timestamp at which the OpenVPN statistics were updated.", + []string{"status_path"}, nil) + + // Metrics specific to OpenVPN servers. + openvpnServerClientsDesc = prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "server", "clients"), + "Number of clients connected to the OpenVPN server.", + []string{"status_path"}, nil) + openvpnServerRoutesDesc = prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "server", "routes"), + "Number of routes propagated by the OpenVPN server.", + []string{"status_path"}, nil) + + // Metrics specific to OpenVPN clients. + openvpnClientDescs = map[string]*prometheus.Desc{ + "TUN/TAP read bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tun_tap_read_bytes_total"), + "Total amount of TUN/TAP traffic read, in bytes.", + []string{"status_path"}, nil), + "TUN/TAP write bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tun_tap_write_bytes_total"), + "Total amount of TUN/TAP traffic written, in bytes.", + []string{"status_path"}, nil), + "TCP/UDP read bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tcp_udp_read_bytes_total"), + "Total amount of TCP/UDP traffic read, in bytes.", + []string{"status_path"}, nil), + "TCP/UDP write bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tcp_udp_write_bytes_total"), + "Total amount of TCP/UDP traffic written, in bytes.", + []string{"status_path"}, nil), + "Auth read bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "auth_read_bytes_total"), + "Total amount of authentication traffic read, in bytes.", + []string{"status_path"}, nil), + "pre-compress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "pre_compress_bytes_total"), + "Total amount of data before compression, in bytes.", + []string{"status_path"}, nil), + "post-compress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "post_compress_bytes_total"), + "Total amount of data after compression, in bytes.", + []string{"status_path"}, nil), + "pre-decompress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "pre_decompress_bytes_total"), + "Total amount of data before decompression, in bytes.", + []string{"status_path"}, nil), + "post-decompress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "post_decompress_bytes_total"), + "Total amount of data after decompression, in bytes.", + []string{"status_path"}, nil), + } +) + +// Converts OpenVPN status information into Prometheus metrics. This +// function automatically detects whether the file contains server or +// client metrics. For server metrics, it also distinguishes between the +// version 2 and 3 file formats. +func CollectStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric) error { + reader := bufio.NewReader(file) + buf, _ := reader.Peek(18) + if bytes.HasPrefix(buf, []byte("TITLE,")) { + // Server statistics, using format version 2. + return CollectServerStatusFromReader(statusPath, reader, ch, ",") + } else if bytes.HasPrefix(buf, []byte("TITLE\t")) { + // Server statistics, using format version 3. The only + // difference compared to version 2 is that it uses tabs + // instead of spaces. + return CollectServerStatusFromReader(statusPath, reader, ch, "\t") + } else if bytes.HasPrefix(buf, []byte("OpenVPN STATISTICS")) { + // Client statistics. + return CollectClientStatusFromReader(statusPath, reader, ch) + } else { + return fmt.Errorf("Unexpected file contents: %q", buf) + } +} + +// Converts OpenVPN server status information into Prometheus metrics. +func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric, separator string) error { + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + clients := 0 + routes := 0 + for scanner.Scan() { + fields := strings.Split(scanner.Text(), separator) + if fields[0] == "CLIENT_LIST" { + // Per-client stats. + clients++ + } else if fields[0] == "END" && len(fields) == 1 { + // Stats footer. + } else if fields[0] == "GLOBAL_STATS" { + // Global server statistics. + } else if fields[0] == "HEADER" && len(fields) > 2 { + // Column names for CLIENT_LIST and ROUTING_TABLE. + } else if fields[0] == "ROUTING_TABLE" { + // Per-route stats. + routes++ + } else if fields[0] == "TIME" && len(fields) == 3 { + // Time at which the statistics were updated. + time, err := strconv.ParseFloat(fields[2], 64) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + openvpnStatusUpdateTimeDesc, + prometheus.GaugeValue, + time, + statusPath) + } else if fields[0] == "TITLE" && len(fields) == 2 { + // OpenVPN version number. + } else { + return fmt.Errorf("Unsupported key: %q", fields[0]) + } + } + ch <- prometheus.MustNewConstMetric( + openvpnServerClientsDesc, + prometheus.GaugeValue, + float64(clients), + statusPath) + ch <- prometheus.MustNewConstMetric( + openvpnServerRoutesDesc, + prometheus.GaugeValue, + float64(routes), + statusPath) + return scanner.Err() +} + +// Converts OpenVPN client status information into Prometheus metrics. +func CollectClientStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric) error { + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + fields := strings.Split(scanner.Text(), ",") + if fields[0] == "END" && len(fields) == 1 { + // Stats footer. + } else if fields[0] == "OpenVPN STATISTICS" && len(fields) == 1 { + // Stats header. + } else if fields[0] == "Updated" && len(fields) == 2 { + // Time at which the statistics were updated. + time, err := time.Parse("Mon Jan 2 15:04:05 2006", fields[1]) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + openvpnStatusUpdateTimeDesc, + prometheus.GaugeValue, + float64(time.Unix()), + statusPath) + } else if desc, ok := openvpnClientDescs[fields[0]]; ok && len(fields) == 2 { + // Traffic counters. + value, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + desc, + prometheus.CounterValue, + value, + statusPath) + } else { + return fmt.Errorf("Unsupported key: %q", fields[0]) + } + } + return scanner.Err() +} + +func CollectStatusFromFile(statusPath string, ch chan<- prometheus.Metric) error { + conn, err := os.Open(statusPath) + if err != nil { + return err + } + return CollectStatusFromReader(statusPath, conn, ch) +} + +type OpenVPNExporter struct { + statusPaths []string +} + +func NewOpenVPNExporter(statusPaths []string) (*OpenVPNExporter, error) { + return &OpenVPNExporter{ + statusPaths: statusPaths, + }, nil +} + +func (e *OpenVPNExporter) Describe(ch chan<- *prometheus.Desc) { + ch <- openvpnUpDesc +} + +func (e *OpenVPNExporter) Collect(ch chan<- prometheus.Metric) { + for _, statusPath := range e.statusPaths { + err := CollectStatusFromFile(statusPath, ch) + if err == nil { + ch <- prometheus.MustNewConstMetric( + openvpnUpDesc, + prometheus.GaugeValue, + 1.0, + statusPath) + } else { + log.Printf("Failed to scrape showq socket: %s", err) + ch <- prometheus.MustNewConstMetric( + openvpnUpDesc, + prometheus.GaugeValue, + 0.0, + statusPath) + } + } +} + +func main() { + var ( + listenAddress = flag.String("web.listen-address", ":9154", "Address to listen on for web interface and telemetry.") + metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") + openvpnStatusPaths = flag.String("openvpn.status_paths", "examples/client.status,examples/server2.status,examples/server3.status", "Paths at which OpenVPN places its status files.") + ) + flag.Parse() + + exporter, err := NewOpenVPNExporter(strings.Split(*openvpnStatusPaths, ",")) + if err != nil { + panic(err) + } + prometheus.MustRegister(exporter) + + http.Handle(*metricsPath, prometheus.Handler()) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + + OpenVPN Exporter + +

OpenVPN Exporter

+

Metrics

+ + `)) + }) + log.Fatal(http.ListenAndServe(*listenAddress, nil)) +}