package query import ( "github.com/prometheus/client_golang/prometheus" "database/sql" "fmt" "errors" "github.com/prometheus/common/log" "github.com/wrouesnel/postgres_exporter/exporter/metricmaps" "github.com/wrouesnel/postgres_exporter/exporter/dbconv" ) // Query within a namespace mapping and emit metrics. Returns fatal errors if // the scrape fails, and a slice of errors if they were non-fatal. func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace string, mapping metricmaps.MetricMapNamespace, queryOverrides metricmaps.NamespaceOverrideQueryMapping) ([]error, error) { // Check for a query override for this namespace query, found := queryOverrides[namespace] // Was this query disabled (i.e. nothing sensible can be queried on cu // version of PostgreSQL? if query == "" && found { // Return success (no pertinent data) return []error{}, nil } // Don't fail on a bad scrape of one metric var rows *sql.Rows var err error if !found { // I've no idea how to avoid this properly at the moment, but this is // an admin tool so you're not injecting SQL right? rows, err = db.Query(fmt.Sprintf("SELECT * FROM %s;", namespace)) // nolint: gas, safesql } else { rows, err = db.Query(query) // nolint: safesql } if err != nil { return []error{}, errors.New(fmt.Sprintln("Error running query on database: ", namespace, err)) } defer rows.Close() // nolint: errcheck var columnNames []string columnNames, err = rows.Columns() if err != nil { return []error{}, errors.New(fmt.Sprintln("Error retrieving column list for: ", namespace, err)) } // Make a lookup map for the column indices var columnIdx = make(map[string]int, len(columnNames)) for i, n := range columnNames { columnIdx[n] = i } var columnData = make([]interface{}, len(columnNames)) var scanArgs = make([]interface{}, len(columnNames)) for i := range columnData { scanArgs[i] = &columnData[i] } nonfatalErrors := []error{} for rows.Next() { err = rows.Scan(scanArgs...) if err != nil { return []error{}, errors.New(fmt.Sprintln("Error retrieving rows:", namespace, err)) } // Get the label values for this row var labels = make([]string, len(mapping.Labels)) for idx, columnName := range mapping.Labels { labels[idx], _ = dbconv.DbToString(columnData[columnIdx[columnName]]) } // Loop over column names, and match to scan data. Unknown columns // will be filled with an untyped metric number *if* they can be // converted to float64s. NULLs are allowed and treated as NaN. for idx, columnName := range columnNames { if metricMapping, ok := mapping.ColumnMappings[columnName]; ok { // Is this a metricy metric? if metricMapping.Discard { continue } value, ok := dbconv.DbToFloat64(columnData[idx]) if !ok { nonfatalErrors = append(nonfatalErrors, errors.New(fmt.Sprintln("Unexpected error parsing column: ", namespace, columnName, columnData[idx]))) continue } // Generate the metric ch <- prometheus.MustNewConstMetric(metricMapping.Desc, metricMapping.Vtype, value, labels...) } else { // Unknown metric. Report as untyped if scan to float64 works, else note an error too. metricLabel := fmt.Sprintf("%s_%s", namespace, columnName) desc := prometheus.NewDesc(metricLabel, fmt.Sprintf("Unknown metric from %s", namespace), mapping.Labels, nil) // Its not an error to fail here, since the values are // unexpected anyway. value, ok := dbconv.DbToFloat64(columnData[idx]) if !ok { nonfatalErrors = append(nonfatalErrors, errors.New(fmt.Sprintln("Unparseable column type - discarding: ", namespace, columnName, err))) continue } ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, value, labels...) } } } return nonfatalErrors, nil } // Iterate through all the namespace mappings in the exporter and run their // queries. func queryNamespaceMappings(ch chan<- prometheus.Metric, db *sql.DB, metricMap map[string]metricmaps.MetricMapNamespace, queryOverrides map[string]string) map[string]error { // Return a map of namespace -> errors namespaceErrors := make(map[string]error) for namespace, mapping := range metricMap { log.Debugln("Querying namespace: ", namespace) nonFatalErrors, err := queryNamespaceMapping(ch, db, namespace, mapping, queryOverrides) // Serious error - a namespace disappeared if err != nil { namespaceErrors[namespace] = err log.Infoln(err) } // Non-serious errors - likely version or parsing problems. if len(nonFatalErrors) > 0 { for _, err := range nonFatalErrors { log.Infoln(err.Error()) } } } return namespaceErrors }