From 01e4eb4e67d5c830df0abfed5f439dce981dd962 Mon Sep 17 00:00:00 2001
From: William Rouesnel <William.Rouesnel@netregistry.com.au>
Date: Wed, 9 Dec 2015 16:41:28 +1100
Subject: [PATCH] Add support for collecting runtime variables.

---
 postgres_exporter.go | 111 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 107 insertions(+), 4 deletions(-)

diff --git a/postgres_exporter.go b/postgres_exporter.go
index fd0df2d2..78b98c91 100644
--- a/postgres_exporter.go
+++ b/postgres_exporter.go
@@ -16,6 +16,7 @@ import (
 	_ "github.com/lib/pq"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/log"
+	"strconv"
 )
 
 var (
@@ -56,9 +57,11 @@ const (
 	COUNTER ColumnUsage = iota	// Use this column as a counter
 	GAUGE ColumnUsage = iota	// Use this column as a gauge
 	MAPPEDMETRIC ColumnUsage = iota	// Use this column with the supplied mapping of text values
+	DURATION ColumnUsage = iota	// This column should be interpreted as a text duration (and converted to milliseconds)
 )
 
-
+// Which metric mapping should be acquired using "SHOW" queries
+const SHOW_METRIC = "pg_runtime_variables"
 
 // User-friendly representation of a prometheus descriptor map
 type ColumnMapping struct {
@@ -79,10 +82,27 @@ type MetricMap struct {
 	discard bool				// Should metric be discarded during mapping?
 	vtype prometheus.ValueType	// Prometheus valuetype
 	desc  *prometheus.Desc		// Prometheus descriptor
-	mapping map[string]float64 // If not nil, maps text values to float64s
+	conversion func(interface{}) (float64, bool)	// Conversion function to turn PG result into float64
 }
 
 // Metric descriptors for dynamically created metrics.
+var variableMaps = map[string]map[string]ColumnMapping{
+	"pg_runtime_variable" : map[string]ColumnMapping {
+		"max_connections" : {GAUGE, "Sets the maximum number of concurrent connections." , nil},
+		"max_files_per_process" : {GAUGE, "Sets the maximum number of simultaneously open files for each server process.", nil },
+		"max_function_args" : {GAUGE, "Shows the maximum number of function arguments.", nil },
+		"max_identifier_length" : {GAUGE, "Shows the maximum identifier length.", nil },
+		"max_index_keys" : {GAUGE, "Shows the maximum number of index keys.", nil },
+		"max_locks_per_transaction" : {GAUGE, "Sets the maximum number of locks per transaction.", nil },
+		"max_pred_locks_per_transaction" : {GAUGE, "Sets the maximum number of predicate locks per transaction.", nil },
+		"max_prepared_transactions" : {GAUGE, "Sets the maximum number of simultaneously prepared transactions.", nil },
+		//"max_stack_depth" : { GAUGE, "Sets the maximum number of concurrent connections.", nil }, // No dehumanize support yet
+		"max_standby_archive_delay" : {DURATION, "Sets the maximum delay before canceling queries when a hot standby server is processing archived WAL data.", nil },
+		"max_standby_streaming_delay" : {DURATION, "Sets the maximum delay before canceling queries when a hot standby server is processing streamed WAL data.", nil },
+		"max_wal_senders" : {GAUGE, "Sets the maximum number of simultaneously running WAL sender processes.", nil },
+	},
+}
+
 var metricMaps = map[string]map[string]ColumnMapping {
 	"pg_stat_bgwriter" : map[string]ColumnMapping {
 		"checkpoints_timed" : { COUNTER, "Number of scheduled checkpoints that have been performed", nil }, 
@@ -149,22 +169,66 @@ func makeDescMap(metricMaps map[string]map[string]ColumnMapping) map[string]Metr
 				case DISCARD, LABEL:
 					thisMap[columnName] = MetricMap{
 						discard : true,
+						conversion: func(in interface{}) (float64, bool) {
+							return math.NaN(), true
+						},
 					}
 				case COUNTER:
 					thisMap[columnName] = MetricMap{
 						vtype : prometheus.CounterValue,
 						desc : prometheus.NewDesc(fmt.Sprintf("%s_%s", namespace, columnName), columnMapping.description, constLabels, nil),
+						conversion: func(in interface{}) (float64, bool) {
+							return dbToFloat64(in)
+						},
 					}
 				case GAUGE:
 					thisMap[columnName] = MetricMap{
 						vtype : prometheus.GaugeValue,
 						desc : prometheus.NewDesc(fmt.Sprintf("%s_%s", namespace, columnName), columnMapping.description, constLabels, nil),
+						conversion: func(in interface{}) (float64, bool) {
+							return dbToFloat64(in)
+						},
 					}
 				case MAPPEDMETRIC:
 					thisMap[columnName] = MetricMap{
 						vtype : prometheus.GaugeValue,
 						desc : prometheus.NewDesc(fmt.Sprintf("%s_%s", namespace, columnName), columnMapping.description, constLabels, nil),
-						mapping: columnMapping.mapping,
+						conversion: func(in interface{}) (float64, bool) {
+							text, ok := in.(string)
+							if !ok {
+								return math.NaN(), false
+							}
+
+							val, ok := columnMapping.mapping[text]
+							if !ok {
+								return math.NaN(), false
+							}
+							return val, true
+						},
+					}
+				case DURATION:
+					thisMap[columnName] = MetricMap{
+						vtype : prometheus.GaugeValue,
+						desc : prometheus.NewDesc(fmt.Sprintf("%s_%s_milliseconds", namespace, columnName), columnMapping.description, constLabels, nil),
+						conversion: func(in interface{}) (float64, bool) {
+							var durationString string
+							switch t := in.(type) {
+							case []byte:
+								durationString = string(t)
+							case string:
+								durationString = t
+							default:
+								log.Errorln("DURATION conversion metric was not a string")
+								return math.NaN(), false
+							}
+
+							d, err := time.ParseDuration(durationString)
+							if err != nil {
+								log.Errorln("Failed converting result to metric:", columnName, in, err)
+								return math.NaN(), false
+							}
+							return float64(d / time.Millisecond), true
+						},
 					}
 			}
 		}
@@ -185,6 +249,14 @@ func dbToFloat64(t interface{}) (float64, bool) {
 		return v, true
 	case time.Time:
 		return float64(v.Unix()), true
+	case []byte:
+		// Try and convert to string and then parse to a float64
+		strV := string(v)
+		result, err := strconv.ParseFloat(strV, 64)
+		if err != nil {
+			return math.NaN(), false
+		}
+		return result, true
 	case nil:
 		return math.NaN(), true
 	default:
@@ -213,11 +285,12 @@ func dbToString(t interface{}) (string, bool) {
 	}
 }
 
-// Exporter collects MySQL metrics. It implements prometheus.Collector.
+// Exporter collects Postgres metrics. It implements prometheus.Collector.
 type Exporter struct {
 	dsn             string
 	duration, error prometheus.Gauge
 	totalScrapes    prometheus.Counter
+	variableMap		map[string]MetricMapNamespace
 	metricMap		map[string]MetricMapNamespace
 }
 
@@ -243,6 +316,7 @@ func NewExporter(dsn string) *Exporter {
 			Name:      "last_scrape_error",
 			Help:      "Whether the last scrape of metrics from PostgreSQL resulted in an error (1 for error, 0 for success).",
 		}),
+		variableMap : makeDescMap(variableMaps),
 		metricMap : makeDescMap(metricMaps),
 	}
 }
@@ -307,6 +381,35 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
 	}
 	defer db.Close()
 
+	log.Debugln("Querying SHOW variables")
+	for _, mapping := range e.variableMap {
+		for columnName, columnMapping := range mapping.columnMappings {
+			// Check for a discard request on this value
+			if columnMapping.discard {
+				continue
+			}
+
+			// Use SHOW to get the value
+			row := db.QueryRow(fmt.Sprintf("SHOW %s;", columnName))
+
+			var val interface{};
+			err := row.Scan(&val)
+			if err != nil {
+				log.Errorln("Error scanning runtime variable:", columnName, err)
+				continue
+			}
+
+			fval, ok := columnMapping.conversion(val)
+			if ! ok {
+				e.error.Set(1)
+				log.Errorln("Unexpected error parsing column: ", namespace, columnName, val)
+				continue
+			}
+
+			ch <- prometheus.MustNewConstMetric(columnMapping.desc, columnMapping.vtype, fval)
+		}
+	}
+
 	for namespace, mapping := range e.metricMap {
 		log.Debugln("Querying namespace: ", namespace)
 		func () {	// Don't fail on a bad scrape of one metric