4 files changed
+374
-319
No files matched your search
@@ -64,6 +64,8 @@ Usage of openvpn_exporter:
|
|||||||
Address to listen on for web interface and telemetry. (default ":9176")
|
Address to listen on for web interface and telemetry. (default ":9176")
|
||||||
-web.telemetry-path string
|
-web.telemetry-path string
|
||||||
Path under which to expose metrics. (default "/metrics")
|
Path under which to expose metrics. (default "/metrics")
|
||||||
|
-ignore.individuals bool
|
||||||
|
If ignoring metrics for individuals (default false)
|
||||||
```
|
```
|
||||||
|
|
||||||
E.g:
|
E.g:
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ CLIENT_LIST,redacted2,0.0.0.0:60536,0.0.0.0,2925752,3145665,Thu Mar 16 17:08:57
|
|||||||
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,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,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
|
CLIENT_LIST,redacted5,0.0.0.0:51865,0.0.0.0,277017840,1544465106,Thu Mar 16 17:09:01 2017,1489680541,UNDEF
|
||||||
|
CLIENT_LIST,redacted1,0.0.0.0:19021,0.0.0.0,693438277,228390856,Thu Mar 16 17:09:03 2017,1489680543,UNDEF
|
||||||
HEADER,ROUTING_TABLE,Virtual Address,Common Name,Real Address,Last Ref,Last Ref (time_t)
|
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,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,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,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,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
|
ROUTING_TABLE,0.0.0.0,redacted2,0.0.0.0:60536,Thu Mar 16 17:08:58 2017,1489680538
|
||||||
|
ROUTING_TABLE,0.0.0.0,redacted1,0.0.0.0:19021,Tue Mar 21 10:26:48 2017,1490088408
|
||||||
GLOBAL_STATS,Max bcast/mcast queue length,0
|
GLOBAL_STATS,Max bcast/mcast queue length,0
|
||||||
END
|
END
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
package exporters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenvpnServerHeader struct {
|
||||||
|
LabelColumns []string
|
||||||
|
Metrics []OpenvpnServerHeaderField
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenvpnServerHeaderField struct {
|
||||||
|
Column string
|
||||||
|
Desc *prometheus.Desc
|
||||||
|
ValueType prometheus.ValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenVPNExporter struct {
|
||||||
|
statusPaths []string
|
||||||
|
openvpnUpDesc *prometheus.Desc
|
||||||
|
openvpnStatusUpdateTimeDesc *prometheus.Desc
|
||||||
|
openvpnConnectedClientsDesc *prometheus.Desc
|
||||||
|
openvpnClientDescs map[string]*prometheus.Desc
|
||||||
|
openvpnServerHeaders map[string]OpenvpnServerHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenVPNExporter(statusPaths []string, ignoreIndividuals bool) (*OpenVPNExporter, error) {
|
||||||
|
// 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.
|
||||||
|
openvpnConnectedClientsDesc := prometheus.NewDesc(
|
||||||
|
prometheus.BuildFQName("openvpn", "", "server_connected_clients"),
|
||||||
|
"Number Of Connected Clients",
|
||||||
|
[]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),
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverHeaderClientLabels []string
|
||||||
|
var serverHeaderClientLabelColumns []string
|
||||||
|
var serverHeaderRoutingLabels []string
|
||||||
|
var serverHeaderRoutingLabelColumns []string
|
||||||
|
if ignoreIndividuals {
|
||||||
|
serverHeaderClientLabels = []string{"status_path", "common_name"}
|
||||||
|
serverHeaderClientLabelColumns = []string{"Common Name"}
|
||||||
|
serverHeaderRoutingLabels = []string{"status_path", "common_name"}
|
||||||
|
serverHeaderRoutingLabelColumns = []string{"Common Name"}
|
||||||
|
} else {
|
||||||
|
serverHeaderClientLabels = []string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"}
|
||||||
|
serverHeaderClientLabelColumns = []string{"Common Name", "Connected Since (time_t)", "Real Address", "Virtual Address", "Username"}
|
||||||
|
serverHeaderRoutingLabels = []string{"status_path", "common_name", "real_address", "virtual_address"}
|
||||||
|
serverHeaderRoutingLabelColumns = []string{"Common Name", "Real Address", "Virtual Address"}
|
||||||
|
}
|
||||||
|
|
||||||
|
openvpnServerHeaders := map[string]OpenvpnServerHeader{
|
||||||
|
"CLIENT_LIST": {
|
||||||
|
LabelColumns: serverHeaderClientLabelColumns,
|
||||||
|
Metrics: []OpenvpnServerHeaderField{
|
||||||
|
{
|
||||||
|
Column: "Bytes Received",
|
||||||
|
Desc: prometheus.NewDesc(
|
||||||
|
prometheus.BuildFQName("openvpn", "server", "client_received_bytes_total"),
|
||||||
|
"Amount of data received over a connection on the VPN server, in bytes.",
|
||||||
|
serverHeaderClientLabels, nil),
|
||||||
|
ValueType: prometheus.CounterValue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Column: "Bytes Sent",
|
||||||
|
Desc: prometheus.NewDesc(
|
||||||
|
prometheus.BuildFQName("openvpn", "server", "client_sent_bytes_total"),
|
||||||
|
"Amount of data sent over a connection on the VPN server, in bytes.",
|
||||||
|
serverHeaderClientLabels, nil),
|
||||||
|
ValueType: prometheus.CounterValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ROUTING_TABLE": {
|
||||||
|
LabelColumns: serverHeaderRoutingLabelColumns,
|
||||||
|
Metrics: []OpenvpnServerHeaderField{
|
||||||
|
{
|
||||||
|
Column: "Last Ref (time_t)",
|
||||||
|
Desc: prometheus.NewDesc(
|
||||||
|
prometheus.BuildFQName("openvpn", "server", "route_last_reference_time_seconds"),
|
||||||
|
"Time at which a route was last referenced, in seconds.",
|
||||||
|
serverHeaderRoutingLabels, nil),
|
||||||
|
ValueType: prometheus.GaugeValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OpenVPNExporter{
|
||||||
|
statusPaths: statusPaths,
|
||||||
|
openvpnUpDesc: openvpnUpDesc,
|
||||||
|
openvpnStatusUpdateTimeDesc: openvpnStatusUpdateTimeDesc,
|
||||||
|
openvpnConnectedClientsDesc: openvpnConnectedClientsDesc,
|
||||||
|
openvpnClientDescs: openvpnClientDescs,
|
||||||
|
openvpnServerHeaders: openvpnServerHeaders,
|
||||||
|
}, 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 (e *OpenVPNExporter) 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 e.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 e.collectServerStatusFromReader(statusPath, reader, ch, "\t")
|
||||||
|
} else if bytes.HasPrefix(buf, []byte("OpenVPN STATISTICS")) {
|
||||||
|
// Client statistics.
|
||||||
|
return e.collectClientStatusFromReader(statusPath, reader, ch)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unexpected file contents: %q", buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts OpenVPN server status information into Prometheus metrics.
|
||||||
|
func (e *OpenVPNExporter) collectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric, separator string) error {
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
headersFound := map[string][]string{}
|
||||||
|
// counter of connected client
|
||||||
|
numberConnectedClient := 0
|
||||||
|
|
||||||
|
recordedMetrics := map[OpenvpnServerHeaderField][]string{}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Split(scanner.Text(), separator)
|
||||||
|
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.
|
||||||
|
headersFound[fields[1]] = fields[2:]
|
||||||
|
} else if fields[0] == "TIME" && len(fields) == 3 {
|
||||||
|
// Time at which the statistics were updated.
|
||||||
|
timeStartStats, err := strconv.ParseFloat(fields[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
e.openvpnStatusUpdateTimeDesc,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
timeStartStats,
|
||||||
|
statusPath)
|
||||||
|
} else if fields[0] == "TITLE" && len(fields) == 2 {
|
||||||
|
// OpenVPN version number.
|
||||||
|
} else if header, ok := e.openvpnServerHeaders[fields[0]]; ok {
|
||||||
|
if fields[0] == "CLIENT_LIST" {
|
||||||
|
numberConnectedClient++
|
||||||
|
}
|
||||||
|
// Entry that depends on a preceding HEADERS directive.
|
||||||
|
columnNames, ok := headersFound[fields[0]]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s should be preceded by HEADERS", fields[0])
|
||||||
|
}
|
||||||
|
if len(fields) != len(columnNames)+1 {
|
||||||
|
return fmt.Errorf("HEADER for %s describes a different number of columns", fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store entry values in a map indexed by column name.
|
||||||
|
columnValues := map[string]string{}
|
||||||
|
for _, column := range header.LabelColumns {
|
||||||
|
columnValues[column] = ""
|
||||||
|
}
|
||||||
|
for i, column := range columnNames {
|
||||||
|
columnValues[column] = fields[i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract columns that should act as entry labels.
|
||||||
|
labels := []string{statusPath}
|
||||||
|
for _, column := range header.LabelColumns {
|
||||||
|
labels = append(labels, columnValues[column])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export relevant columns as individual metrics.
|
||||||
|
for _, metric := range header.Metrics {
|
||||||
|
if columnValue, ok := columnValues[metric.Column]; ok {
|
||||||
|
if l, _ := recordedMetrics[metric]; ! subslice(labels, l) {
|
||||||
|
value, err := strconv.ParseFloat(columnValue, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
metric.Desc,
|
||||||
|
metric.ValueType,
|
||||||
|
value,
|
||||||
|
labels...)
|
||||||
|
recordedMetrics[metric] = append(recordedMetrics[metric], labels...)
|
||||||
|
} else {
|
||||||
|
log.Printf("Metric entry with same labels: %s, %s", metric.Column, labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unsupported key: %q", fields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add the number of connected client
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
e.openvpnConnectedClientsDesc,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(numberConnectedClient),
|
||||||
|
statusPath)
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does slice contain string
|
||||||
|
func contains(s []string, e string) bool {
|
||||||
|
for _, a := range s {
|
||||||
|
if a == e {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is a sub-slice of slice
|
||||||
|
func subslice(sub []string, main []string) bool {
|
||||||
|
if len(sub) > len(main) {return false}
|
||||||
|
for _, s := range sub {
|
||||||
|
if ! contains(main, s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts OpenVPN client status information into Prometheus metrics.
|
||||||
|
func (e *OpenVPNExporter) 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.
|
||||||
|
location, _ := time.LoadLocation("Local")
|
||||||
|
timeParser, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", fields[1], location)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
e.openvpnStatusUpdateTimeDesc,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(timeParser.Unix()),
|
||||||
|
statusPath)
|
||||||
|
} else if desc, ok := e.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 (e *OpenVPNExporter) collectStatusFromFile(statusPath string, ch chan<- prometheus.Metric) error {
|
||||||
|
conn, err := os.Open(statusPath)
|
||||||
|
defer conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.collectStatusFromReader(statusPath, conn, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *OpenVPNExporter) Describe(ch chan<- *prometheus.Desc) {
|
||||||
|
ch <- e.openvpnUpDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *OpenVPNExporter) Collect(ch chan<- prometheus.Metric) {
|
||||||
|
for _, statusPath := range e.statusPaths {
|
||||||
|
err := e.collectStatusFromFile(statusPath, ch)
|
||||||
|
if err == nil {
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
e.openvpnUpDesc,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
1.0,
|
||||||
|
statusPath)
|
||||||
|
} else {
|
||||||
|
log.Printf("Failed to scrape showq socket: %s", err)
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
e.openvpnUpDesc,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
0.0,
|
||||||
|
statusPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,334 +14,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"github.com/kumina/openvpn_exporter/exporters"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenvpnServerHeader struct {
|
|
||||||
LabelColumns []string
|
|
||||||
Metrics []OpenvpnServerHeaderField
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenvpnServerHeaderField struct {
|
|
||||||
Column string
|
|
||||||
Desc *prometheus.Desc
|
|
||||||
ValueType prometheus.ValueType
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
openvpnConnectedClientsDesc = prometheus.NewDesc(
|
|
||||||
prometheus.BuildFQName("openvpn", "", "openvpn_server_connected_clients"),
|
|
||||||
"Number Of Connected Clients",
|
|
||||||
[]string{"status_path"}, nil)
|
|
||||||
|
|
||||||
openvpnServerHeaders = map[string]OpenvpnServerHeader{
|
|
||||||
"CLIENT_LIST": {
|
|
||||||
LabelColumns: []string{
|
|
||||||
"Common Name",
|
|
||||||
"Connected Since (time_t)",
|
|
||||||
"Real Address",
|
|
||||||
"Virtual Address",
|
|
||||||
"Username",
|
|
||||||
},
|
|
||||||
Metrics: []OpenvpnServerHeaderField{
|
|
||||||
{
|
|
||||||
Column: "Bytes Received",
|
|
||||||
Desc: prometheus.NewDesc(
|
|
||||||
prometheus.BuildFQName("openvpn", "server", "client_received_bytes_total"),
|
|
||||||
"Amount of data received over a connection on the VPN server, in bytes.",
|
|
||||||
[]string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"}, nil),
|
|
||||||
ValueType: prometheus.CounterValue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Column: "Bytes Sent",
|
|
||||||
Desc: prometheus.NewDesc(
|
|
||||||
prometheus.BuildFQName("openvpn", "server", "client_sent_bytes_total"),
|
|
||||||
"Amount of data sent over a connection on the VPN server, in bytes.",
|
|
||||||
[]string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"}, nil),
|
|
||||||
ValueType: prometheus.CounterValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ROUTING_TABLE": {
|
|
||||||
LabelColumns: []string{
|
|
||||||
"Common Name",
|
|
||||||
"Real Address",
|
|
||||||
"Virtual Address",
|
|
||||||
},
|
|
||||||
Metrics: []OpenvpnServerHeaderField{
|
|
||||||
{
|
|
||||||
Column: "Last Ref (time_t)",
|
|
||||||
Desc: prometheus.NewDesc(
|
|
||||||
prometheus.BuildFQName("openvpn", "server", "route_last_reference_time_seconds"),
|
|
||||||
"Time at which a route was last referenced, in seconds.",
|
|
||||||
[]string{"status_path", "common_name", "real_address", "virtual_address"}, nil),
|
|
||||||
ValueType: prometheus.GaugeValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
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 {
|
|
||||||
// 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.
|
|
||||||
headersFound[fields[1]] = fields[2:]
|
|
||||||
} else if fields[0] == "TIME" && len(fields) == 3 {
|
|
||||||
// Time at which the statistics were updated.
|
|
||||||
timeStartStats, err := strconv.ParseFloat(fields[2], 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ch <- prometheus.MustNewConstMetric(
|
|
||||||
openvpnStatusUpdateTimeDesc,
|
|
||||||
prometheus.GaugeValue,
|
|
||||||
timeStartStats,
|
|
||||||
statusPath)
|
|
||||||
} else if fields[0] == "TITLE" && len(fields) == 2 {
|
|
||||||
// OpenVPN version number.
|
|
||||||
} else if header, ok := openvpnServerHeaders[fields[0]]; ok {
|
|
||||||
if fields[0] == "CLIENT_LIST"{
|
|
||||||
numberConnectedClient ++
|
|
||||||
}
|
|
||||||
// Entry that depends on a preceding HEADERS directive.
|
|
||||||
columnNames, ok := headersFound[fields[0]]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%s should be preceded by HEADERS", fields[0])
|
|
||||||
}
|
|
||||||
if len(fields) != len(columnNames)+1 {
|
|
||||||
return fmt.Errorf("HEADER for %s describes a different number of columns", fields[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store entry values in a map indexed by column name.
|
|
||||||
columnValues := map[string]string{}
|
|
||||||
for _, column := range header.LabelColumns {
|
|
||||||
columnValues[column] = ""
|
|
||||||
}
|
|
||||||
for i, column := range columnNames {
|
|
||||||
columnValues[column] = fields[i+1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract columns that should act as entry labels.
|
|
||||||
labels := []string{statusPath}
|
|
||||||
for _, column := range header.LabelColumns {
|
|
||||||
labels = append(labels, columnValues[column])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export relevant columns as individual metrics.
|
|
||||||
for _, metric := range header.Metrics {
|
|
||||||
if columnValue, ok := columnValues[metric.Column]; ok {
|
|
||||||
value, err := strconv.ParseFloat(columnValue, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ch <- prometheus.MustNewConstMetric(
|
|
||||||
metric.Desc,
|
|
||||||
metric.ValueType,
|
|
||||||
value,
|
|
||||||
labels...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("unsupported key: %q", fields[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// add the number of connected client
|
|
||||||
ch <- prometheus.MustNewConstMetric(
|
|
||||||
openvpnConnectedClientsDesc,
|
|
||||||
prometheus.GaugeValue,
|
|
||||||
float64(numberConnectedClient),
|
|
||||||
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.
|
|
||||||
location, _ := time.LoadLocation("Local")
|
|
||||||
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(timeParser.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)
|
|
||||||
defer conn.Close()
|
|
||||||
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() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
listenAddress = flag.String("web.listen-address", ":9176", "Address to listen on for web interface and telemetry.")
|
listenAddress = flag.String("web.listen-address", ":9176", "Address to listen on for web interface and telemetry.")
|
||||||
metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.")
|
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.")
|
openvpnStatusPaths = flag.String("openvpn.status_paths", "examples/client.status,examples/server2.status,examples/server3.status", "Paths at which OpenVPN places its status files.")
|
||||||
|
ignoreIndividuals = flag.Bool("ignore.individuals", false, "If ignoring metrics for individuals")
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -349,8 +36,9 @@ func main() {
|
|||||||
log.Printf("Listen address: %v\n", *listenAddress)
|
log.Printf("Listen address: %v\n", *listenAddress)
|
||||||
log.Printf("Metrics path: %v\n", *metricsPath)
|
log.Printf("Metrics path: %v\n", *metricsPath)
|
||||||
log.Printf("openvpn.status_path: %v\n", *openvpnStatusPaths)
|
log.Printf("openvpn.status_path: %v\n", *openvpnStatusPaths)
|
||||||
|
log.Printf("Ignore Individuals: %v\n", *ignoreIndividuals)
|
||||||
|
|
||||||
exporter, err := NewOpenVPNExporter(strings.Split(*openvpnStatusPaths, ","))
|
exporter, err := exporters.NewOpenVPNExporter(strings.Split(*openvpnStatusPaths, ","), *ignoreIndividuals)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user