diff --git a/cmd/postgres_exporter/postgres_exporter.go b/cmd/postgres_exporter/postgres_exporter.go index e78c9b87..63de6840 100644 --- a/cmd/postgres_exporter/postgres_exporter.go +++ b/cmd/postgres_exporter/postgres_exporter.go @@ -85,6 +85,26 @@ func (cu *ColumnUsage) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// MappingOptions is a copy of ColumnMapping used only for parsing +type MappingOptions struct { + Usage string `yaml:"usage"` + Description string `yaml:"description"` + Mapping map[string]float64 `yaml:"metric_mapping"` // Optional column mapping for MAPPEDMETRIC + SupportedVersions semver.Range `yaml:"pg_version"` // Semantic version ranges which are supported. Unsupported columns are not queried (internally converted to DISCARD). +} + +// nolint: golint +type Mapping map[string]MappingOptions + +// nolint: golint +type UserQuery struct { + Query string `yaml:"query"` + Metrics []Mapping `yaml:"metrics"` +} + +// nolint: golint +type UserQueries map[string]UserQuery + // Regex used to get the "short-version" from the postgres version field. var versionRegex = regexp.MustCompile(`^\w+ ((\d+)(\.\d+)?(\.\d+)?)`) var lowestSupportedVersion = semver.MustParse("9.1.0") @@ -392,6 +412,45 @@ func makeQueryOverrideMap(pgVersion semver.Version, queryOverrides map[string][] return resultMap } +func parseUserQueries(content []byte) (map[string]map[string]ColumnMapping, map[string]string, error) { + var userQueries UserQueries + + err := yaml.Unmarshal(content, &userQueries) + if err != nil { + return nil, nil, err + } + + // Stores the loaded map representation + metricMaps := make(map[string]map[string]ColumnMapping) + newQueryOverrides := make(map[string]string) + + for metric, specs := range userQueries { + log.Debugln("New user metric namespace from YAML:", metric) + newQueryOverrides[metric] = specs.Query + metricMap, ok := metricMaps[metric] + if !ok { + // Namespace for metric not found - add it. + metricMap = make(map[string]ColumnMapping) + metricMaps[metric] = metricMap + } + for _, metric := range specs.Metrics { + for name, mappingOption := range metric { + var columnMapping ColumnMapping + tmpUsage, _ := stringToColumnUsage(mappingOption.Usage) + columnMapping.usage = tmpUsage + columnMapping.description = mappingOption.Description + + // TODO: we should support cu + columnMapping.mapping = nil + // Should we support this for users? + columnMapping.supportedVersions = nil + metricMap[name] = columnMapping + } + } + } + return metricMaps, newQueryOverrides, nil +} + // Add queries to the builtinMetricMaps and queryOverrides maps. Added queries do not // respect version requirements, because it is assumed that the user knows // what they are doing with their version of postgres. @@ -399,71 +458,12 @@ func makeQueryOverrideMap(pgVersion semver.Version, queryOverrides map[string][] // This function modifies metricMap and queryOverrideMap to contain the new // queries. // TODO: test code for all cu. -// TODO: use proper struct type system // TODO: the YAML this supports is "non-standard" - we should move away from it. func addQueries(content []byte, pgVersion semver.Version, server *Server) error { - var extra map[string]interface{} - - err := yaml.Unmarshal(content, &extra) + metricMaps, newQueryOverrides, err := parseUserQueries(content) if err != nil { - return err + return nil } - - // Stores the loaded map representation - metricMaps := make(map[string]map[string]ColumnMapping) - newQueryOverrides := make(map[string]string) - - for metric, specs := range extra { - log.Debugln("New user metric namespace from YAML:", metric) - for key, value := range specs.(map[interface{}]interface{}) { - switch key.(string) { - case "query": - query := value.(string) - newQueryOverrides[metric] = query - - case "metrics": - for _, c := range value.([]interface{}) { - column := c.(map[interface{}]interface{}) - - for n, a := range column { - var columnMapping ColumnMapping - - // Fetch the metric map we want to work on. - metricMap, ok := metricMaps[metric] - if !ok { - // Namespace for metric not found - add it. - metricMap = make(map[string]ColumnMapping) - metricMaps[metric] = metricMap - } - - // Get name. - name := n.(string) - - for attrKey, attrVal := range a.(map[interface{}]interface{}) { - switch attrKey.(string) { - case "usage": - usage, err := stringToColumnUsage(attrVal.(string)) - if err != nil { - return err - } - columnMapping.usage = usage - case "description": - columnMapping.description = attrVal.(string) - } - } - - // TODO: we should support cu - columnMapping.mapping = nil - // Should we support this for users? - columnMapping.supportedVersions = nil - - metricMap[name] = columnMapping - } - } - } - } - } - // Convert the loaded metric map into exporter representation partialExporterMap := makeDescMap(pgVersion, server.labels, metricMaps) @@ -488,7 +488,6 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error } server.queryOverrides[k] = v } - return nil } diff --git a/cmd/postgres_exporter/postgres_exporter_test.go b/cmd/postgres_exporter/postgres_exporter_test.go index 5a0529b6..d63826d0 100644 --- a/cmd/postgres_exporter/postgres_exporter_test.go +++ b/cmd/postgres_exporter/postgres_exporter_test.go @@ -3,6 +3,7 @@ package main import ( + "io/ioutil" "os" "reflect" "testing" @@ -305,3 +306,17 @@ func (s *FunctionalSuite) TestBooleanConversionToValueAndString(c *C) { c.Assert(ok, Equals, cs.expectedOK) } } + +func (s *FunctionalSuite) TestParseUserQueries(c *C) { + userQueriesData, err := ioutil.ReadFile("./tests/user_queries_ok.yaml") + if err == nil { + metricMaps, newQueryOverrides, err := parseUserQueries(userQueriesData) + c.Assert(err, Equals, nil) + c.Assert(metricMaps, NotNil) + c.Assert(newQueryOverrides, NotNil) + + if len(metricMaps) != 2 { + c.Errorf("Expected 2 metrics from user file, got %d", len(metricMaps)) + } + } +} diff --git a/cmd/postgres_exporter/tests/user_queries_ok.yaml b/cmd/postgres_exporter/tests/user_queries_ok.yaml new file mode 100644 index 00000000..e5ecec94 --- /dev/null +++ b/cmd/postgres_exporter/tests/user_queries_ok.yaml @@ -0,0 +1,23 @@ +pg_locks_mode: + query: "WITH q_locks AS (select * from pg_locks where pid != pg_backend_pid() and database = (select oid from pg_database where datname = current_database())) SELECT (select current_database()) as datname, + lockmodes AS tag_lockmode, coalesce((select count(*) FROM q_locks WHERE mode = lockmodes), 0) AS count FROM + unnest('{AccessShareLock, ExclusiveLock, RowShareLock, RowExclusiveLock, ShareLock, ShareRowExclusiveLock, AccessExclusiveLock, ShareUpdateExclusiveLock}'::text[]) lockmodes;" + metrics: + - datname: + usage: "LABEL" + description: "Database name" + - tag_lockmode: + usage: "LABEL" + description: "Lock type" + - count: + usage: "GAUGE" + description: "Number of lock" +pg_wal: + query: "select current_database() as datname, case when pg_is_in_recovery() = false then pg_xlog_location_diff(pg_current_xlog_location(), '0/0')::int8 else pg_xlog_location_diff(pg_last_xlog_replay_location(), '0/0')::int8 end as xlog_location_b;" + metrics: + - datname: + usage: "LABEL" + description: "Database name" + - xlog_location_b: + usage: "COUNTER" + description: "current transaction log write location"