diff --git a/.gitignore b/.gitignore index e99898d6..a5e69a40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .build postgres_exporter +postgres_exporter_integration_test *.tar.gz *.test *-stamp diff --git a/Makefile b/Makefile index 204b40fd..1c41ac49 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,10 @@ all: vet test postgres_exporter postgres_exporter: $(GO_SRC) CGO_ENABLED=0 go build -a -ldflags "-extldflags '-static' -X main.Version=git:$(shell git rev-parse HEAD)" -o postgres_exporter . +postgres_exporter_integration_test: $(GO_SRC) + CGO_ENABLED=0 go test -c -tags integration \ + -a -ldflags "-extldflags '-static' -X main.Version=git:$(shell git rev-parse HEAD)" -o postgres_exporter_integration_test . + # Take a go build and turn it into a minimal container docker: postgres_exporter docker build -t $(CONTAINER_NAME) . @@ -19,8 +23,8 @@ vet: test: go test -v . -test-integration: postgres_exporter - tests/test-smoke ./postgres_exporter +test-integration: postgres_exporter postgres_exporter_integration_test + tests/test-smoke ./postgres_exporter ./postgres_exporter_integration_test # Do a self-contained docker build - we pull the official upstream container # and do a self-contained build. diff --git a/postgres_exporter.go b/postgres_exporter.go index ab452de2..301f5357 100644 --- a/postgres_exporter.go +++ b/postgres_exporter.go @@ -592,9 +592,11 @@ func newDesc(subsystem, name, help string) *prometheus.Desc { // Query the SHOW variables from the query map // TODO: make this more functional -func (e *Exporter) queryShowVariables(ch chan<- prometheus.Metric, db *sql.DB) { +func queryShowVariables(ch chan<- prometheus.Metric, db *sql.DB, variableMap map[string]MetricMapNamespace) []error { log.Debugln("Querying SHOW variables") - for _, mapping := range e.variableMap { + nonFatalErrors := []error{} + + for _, mapping := range variableMap { for columnName, columnMapping := range mapping.columnMappings { // Check for a discard request on this value if columnMapping.discard { @@ -607,23 +609,26 @@ func (e *Exporter) queryShowVariables(ch chan<- prometheus.Metric, db *sql.DB) { var val interface{} err := row.Scan(&val) if err != nil { - log.Errorln("Error scanning runtime variable:", columnName, err) + nonFatalErrors = append(nonFatalErrors, errors.New(fmt.Sprintln("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) + nonFatalErrors = append(nonFatalErrors, errors.New(fmt.Sprintln("Unexpected error parsing column: ", namespace, columnName, val))) continue } ch <- prometheus.MustNewConstMetric(columnMapping.desc, columnMapping.vtype, fval) } } + + return nonFatalErrors } -func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace string, mapping MetricMapNamespace) error { +// 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 MetricMapNamespace) ([]error, error) { query, er := queryOverrides[namespace] if er == false { query = fmt.Sprintf("SELECT * FROM %s;", namespace) @@ -632,14 +637,14 @@ func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace st // Don't fail on a bad scrape of one metric rows, err := db.Query(query) if err != nil { - return errors.New(fmt.Sprintln("Error running query on database: ", namespace, err)) + return []error{}, errors.New(fmt.Sprintln("Error running query on database: ", namespace, err)) } defer rows.Close() var columnNames []string columnNames, err = rows.Columns() if err != nil { - return errors.New(fmt.Sprintln("Error retrieving column list for: ", namespace, err)) + return []error{}, errors.New(fmt.Sprintln("Error retrieving column list for: ", namespace, err)) } // Make a lookup map for the column indices @@ -654,10 +659,12 @@ func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace st scanArgs[i] = &columnData[i] } + nonfatalErrors := []error{} + for rows.Next() { err = rows.Scan(scanArgs...) if err != nil { - return errors.New(fmt.Sprintln("Error retrieving rows:", namespace, err)) + return []error{}, errors.New(fmt.Sprintln("Error retrieving rows:", namespace, err)) } // Get the label values for this row @@ -678,7 +685,7 @@ func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace st value, ok := dbToFloat64(columnData[idx]) if !ok { - log.Errorln("Unexpected error parsing column: ", namespace, columnName, columnData[idx]) + nonfatalErrors = append(nonfatalErrors, errors.New(fmt.Sprintln("Unexpected error parsing column: ", namespace, columnName, columnData[idx]))) continue } @@ -692,7 +699,7 @@ func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace st // unexpected anyway. value, ok := dbToFloat64(columnData[idx]) if !ok { - log.Warnln("Unparseable column type - discarding: ", namespace, columnName, err) + nonfatalErrors = append(nonfatalErrors, errors.New(fmt.Sprintln("Unparseable column type - discarding: ", namespace, columnName, err))) continue } @@ -700,7 +707,32 @@ func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace st } } } - return nil + 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]MetricMapNamespace) 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) + // Serious error - a namespace disappeard + 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 } func (e *Exporter) scrape(ch chan<- prometheus.Metric) { @@ -734,15 +766,14 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(versionDesc, prometheus.UntypedValue, 1, versionString, shortVersion) // Handle querying the show variables - e.queryShowVariables(ch, db) + nonFatalErrors := queryShowVariables(ch, db, e.variableMap) + if len(nonFatalErrors) > 0 { + e.error.Set(1) + } - for namespace, mapping := range e.metricMap { - log.Debugln("Querying namespace: ", namespace) - err = queryNamespaceMapping(ch, db, namespace, mapping) - if err != nil { - log.Infoln(err) - e.error.Set(1) - } + errMap := queryNamespaceMappings(ch, db, e.metricMap) + if len(errMap) > 0 { + e.error.Set(1) } } diff --git a/postgres_exporter_integration_test.go b/postgres_exporter_integration_test.go new file mode 100644 index 00000000..db8efb7e --- /dev/null +++ b/postgres_exporter_integration_test.go @@ -0,0 +1,60 @@ +// These are specialized integration tests. We only build them when we're doing +// a lot of additional work to keep the external docker environment they require +// working. +// +build integration + +package main + +import ( + "os" + "testing" + + . "gopkg.in/check.v1" + + "github.com/prometheus/client_golang/prometheus" + "database/sql" + _ "github.com/lib/pq" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { TestingT(t) } + +type IntegrationSuite struct{ + e *Exporter +} + +var _ = Suite(&IntegrationSuite{}) + +func (s *IntegrationSuite) SetUpSuite(c *C) { + dsn := os.Getenv("DATA_SOURCE_NAME") + c.Assert(dsn, Not(Equals), "") + + exporter := NewExporter(dsn) + c.Assert(exporter, NotNil) + // Assign the exporter to the suite + s.e = exporter + + prometheus.MustRegister(exporter) +} + +func (s *IntegrationSuite) TestAllNamespacesReturnResults(c *C) { + // Setup a dummy channel to consume metrics + ch := make(chan prometheus.Metric, 100) + go func() { + for _ = range ch {} + }() + + // Open a database connection + db, err := sql.Open("postgres", s.e.dsn) + c.Assert(db, NotNil) + c.Assert(err, IsNil) + defer db.Close() + + // Check the show variables work + nonFatalErrors := queryShowVariables(ch, db, s.e.variableMap) + c.Check(len(nonFatalErrors), Equals, 0) + + // This should never happen in our test cases. + errMap := queryNamespaceMappings(ch, db, s.e.metricMap) + c.Check(len(errMap), Equals, 0) +} \ No newline at end of file