Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit d9c0134

Browse files
committed
Gatherer + metric: add support for extension based metric SQL overrides
To cover corner-cases like mentioned in #276. In short now there's another dimention to defining metrics - the extension dependencies. As this was originally not intended it's a bit hacky: if a metric has a special "pointer" attribute / structure named "extension_version_based_overrides" defined, then we look at installed extension version and "import" metric definition SQL-s from some other metrics. The latter should be defined with "is_private" column attribute though, so that they're never collected separately but only through such replacing.
1 parent eece9c1 commit d9c0134

File tree

7 files changed

+164
-31
lines changed

7 files changed

+164
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
# override the SQL definition from $metric_name if below defined extensions and versions match
3+
# SQL config equivalent:
4+
# {"extension_version_based_overrides": [{"target_metric": "reco_add_index_ext_qualstats_2.0", "expected_extension_versions": [{"ext_name": "pg_qualstats", "ext_min_version": "2.0"}] }]}
5+
extension_version_based_overrides:
6+
- target_metric: reco_add_index_ext_qualstats_2.0
7+
expected_extension_versions:
8+
- ext_min_version: 2.0
9+
ext_name: pg_qualstats
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* assumes the pg_qualstats extension and superuser or select grant on pg_qualstats_index_advisor() function */
2+
SELECT
3+
epoch_ns,
4+
tag_reco_topic,
5+
tag_object_name,
6+
recommendation,
7+
case when exists (select * from pg_inherits
8+
where inhrelid = regclass(tag_object_name)
9+
) then 'NB! Partitioned table, create the index on parent' else extra_info
10+
end as extra_info
11+
FROM (
12+
SELECT (extract(epoch from now()) * 1e9)::int8 as epoch_ns,
13+
'create_index'::text as tag_reco_topic,
14+
(regexp_matches(v::text, E'ON (.*?) '))[1] as tag_object_name,
15+
v::text as recommendation,
16+
'' as extra_info
17+
FROM json_array_elements(
18+
pg_qualstats_index_advisor() -> 'indexes') v
19+
) x
20+
ORDER BY tag_object_name;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
# not an "independent" regular metric, but only used for overriding SQL definitions of other metrics, "pointing" to the private ones
3+
# via the "extension_version_based_overrides.target_metric_name" attribute currently
4+
is_private: true

pgwatch2/pgwatch2.go

+114-27
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ type MetricColumnAttrs struct {
104104
}
105105

106106
type MetricAttrs struct {
107-
IsInstanceLevel bool `yaml:"is_instance_level"`
108-
MetricStorageName string `yaml:"metric_storage_name"`
107+
IsInstanceLevel bool `yaml:"is_instance_level"`
108+
MetricStorageName string `yaml:"metric_storage_name"`
109+
ExtensionVersionOverrides []ExtensionOverrides `yaml:"extension_version_based_overrides"`
110+
IsPrivate bool `yaml:"is_private"` // used only for extension overrides currently and ignored otherwise
109111
}
110112

111113
type MetricVersionProperties struct {
@@ -164,13 +166,24 @@ type DBVersionMapEntry struct {
164166
RealDbname string
165167
SystemIdentifier string
166168
IsSuperuser bool // if true and no helpers are installed, use superuser SQL version of metric if available
169+
Extensions map[string]decimal.Decimal
167170
}
168171

169172
type ExistingPartitionInfo struct {
170173
StartTime time.Time
171174
EndTime time.Time
172175
}
173176

177+
type ExtensionOverrides struct {
178+
TargetMetric string `yaml:"target_metric"`
179+
ExpectedExtensionVersions []ExtensionInfo `yaml:"expected_extension_versions"`
180+
}
181+
182+
type ExtensionInfo struct {
183+
ExtName string `yaml:"ext_name"`
184+
ExtMinVersion decimal.Decimal `yaml:"ext_min_version"`
185+
}
186+
174187
const EPOCH_COLUMN_NAME string = "epoch_ns" // this column (epoch in nanoseconds) is expected in every metric query
175188
const TAG_PREFIX string = "tag_"
176189
const METRIC_DEFINITION_REFRESH_TIME int64 = 120 // min time before checking for new/changed metric definitions
@@ -220,6 +233,7 @@ var metric_def_map_lock = sync.RWMutex{}
220233
var host_metric_interval_map = make(map[string]float64) // [db1_metric] = 30
221234
var db_pg_version_map = make(map[string]DBVersionMapEntry)
222235
var db_pg_version_map_lock = sync.RWMutex{}
236+
var db_get_pg_version_map_lock = make(map[string]sync.RWMutex) // synchronize initial PG version detection to 1 instance for each defined host
223237
var monitored_db_cache map[string]MonitoredDatabase
224238
var monitored_db_cache_lock sync.RWMutex
225239
var monitored_db_conn_cache map[string]*sqlx.DB = make(map[string]*sqlx.DB)
@@ -262,6 +276,7 @@ var instanceMetricCache = make(map[string]([]map[string]interface{})) // [db
262276
var instanceMetricCacheLock = sync.RWMutex{}
263277
var instanceMetricCacheTimestamp = make(map[string]time.Time) // [dbUnique+metric]last_fetch_time
264278
var instanceMetricCacheTimestampLock = sync.RWMutex{}
279+
var MinExtensionInfoAvailable, _ = decimal.NewFromString("9.1")
265280

266281
func IsPostgresDBType(dbType string) bool {
267282
if dbType == DBTYPE_BOUNCER {
@@ -1763,6 +1778,7 @@ func MetricsPersister(data_store string, storage_ch <-chan []MetricStoreMessage)
17631778

17641779
func DBGetPGVersion(dbUnique string, dbType string, noCache bool) (DBVersionMapEntry, error) {
17651780
var ver DBVersionMapEntry
1781+
var verNew DBVersionMapEntry
17661782
var ok bool
17671783
sql := `
17681784
select /* pgwatch2_generated */ (regexp_matches(
@@ -1776,35 +1792,46 @@ func DBGetPGVersion(dbUnique string, dbType string, noCache bool) (DBVersionMapE
17761792
join pg_catalog.pg_roles b on (m.roleid = b.oid)
17771793
where m.member = r.oid and b.rolname = 'rds_superuser') as rolsuper
17781794
from pg_roles r where rolname = session_user;`
1795+
sql_extensions := `select extname::text, (regexp_matches(extversion, $$\d+\.?\d+?$$))[1]::text as extversion from pg_extension order by 1;`
17791796

17801797
db_pg_version_map_lock.RLock()
1798+
get_ver_lock, ok := db_get_pg_version_map_lock[dbUnique]
1799+
if !ok {
1800+
log.Fatal("db_get_pg_version_map_lock uninitialized")
1801+
}
17811802
ver, ok = db_pg_version_map[dbUnique]
17821803
db_pg_version_map_lock.RUnlock()
17831804

17841805
if !noCache && ok && ver.LastCheckedOn.After(time.Now().Add(time.Minute*-2)) { // use cached version for 2 min
17851806
//log.Debugf("using cached postgres version %s for %s", ver.Version.String(), dbUnique)
17861807
return ver, nil
17871808
} else {
1788-
log.Debug("determining DB version for", dbUnique)
1809+
get_ver_lock.Lock() // limit to 1 concurrent version info fetch per DB
1810+
defer get_ver_lock.Unlock()
1811+
log.Debugf("[%s] determining DB version and recovery status...", dbUnique)
1812+
1813+
if verNew.Extensions == nil {
1814+
verNew.Extensions = make(map[string]decimal.Decimal)
1815+
}
17891816

17901817
if dbType == DBTYPE_BOUNCER {
17911818
data, err, _ := DBExecReadByDbUniqueName(dbUnique, "", false, "show version")
17921819
if err != nil {
1793-
return ver, err
1820+
return verNew, err
17941821
}
17951822
if len(data) == 0 {
17961823
// surprisingly pgbouncer 'show version' outputs in pre v1.12 is emitted as 'NOTICE' which cannot be accessed from Go lib/pg
1797-
ver.Version, _ = decimal.NewFromString("0")
1798-
ver.VersionStr = "0"
1824+
verNew.Version, _ = decimal.NewFromString("0")
1825+
verNew.VersionStr = "0"
17991826
} else {
18001827
rPBVer := regexp.MustCompile("\\d+\\.+\\d+") // "PgBouncer 1.12.0"
18011828
matches := rPBVer.FindStringSubmatch(data[0]["version"].(string))
18021829
if len(matches) != 1 {
18031830
log.Errorf("Unexpected PgBouncer version input: %s", data[0]["version"].(string))
18041831
return ver, errors.New(fmt.Sprintf("Unexpected PgBouncer version input: %s", data[0]["version"].(string)))
18051832
}
1806-
ver.VersionStr = matches[0]
1807-
ver.Version, _ = decimal.NewFromString(matches[0])
1833+
verNew.VersionStr = matches[0]
1834+
verNew.Version, _ = decimal.NewFromString(matches[0])
18081835
}
18091836
} else {
18101837
data, err, _ := DBExecReadByDbUniqueName(dbUnique, "", useConnPooling, sql)
@@ -1816,33 +1843,51 @@ func DBGetPGVersion(dbUnique string, dbType string, noCache bool) (DBVersionMapE
18161843
return ver, nil
18171844
}
18181845
}
1819-
ver.Version, _ = decimal.NewFromString(data[0]["ver"].(string))
1820-
ver.VersionStr = data[0]["ver"].(string)
1821-
ver.IsInRecovery = data[0]["pg_is_in_recovery"].(bool)
1822-
ver.RealDbname = data[0]["current_database"].(string)
1846+
verNew.Version, _ = decimal.NewFromString(data[0]["ver"].(string))
1847+
verNew.VersionStr = data[0]["ver"].(string)
1848+
verNew.IsInRecovery = data[0]["pg_is_in_recovery"].(bool)
1849+
verNew.RealDbname = data[0]["current_database"].(string)
18231850

1824-
if ver.Version.GreaterThanOrEqual(decimal.NewFromFloat(10)) && addSystemIdentifier {
1825-
log.Debugf("determining system identifier version for %s (ver: %v)", dbUnique, ver.VersionStr)
1851+
if verNew.Version.GreaterThanOrEqual(decimal.NewFromFloat(10)) && addSystemIdentifier {
1852+
log.Debugf("[%s] determining system identifier version (pg ver: %v)", dbUnique, verNew.VersionStr)
18261853
data, err, _ := DBExecReadByDbUniqueName(dbUnique, "", useConnPooling, sql_sysid)
18271854
if err == nil && len(data) > 0 {
1828-
ver.SystemIdentifier = data[0]["system_identifier"].(string)
1855+
verNew.SystemIdentifier = data[0]["system_identifier"].(string)
18291856
}
18301857
}
18311858

1832-
log.Debugf("determining if monitoring user is a superuser for %s", dbUnique)
1859+
log.Debugf("[%s] determining if monitoring user is a superuser...", dbUnique)
18331860
data, err, _ = DBExecReadByDbUniqueName(dbUnique, "", useConnPooling, sql_su)
18341861
if err == nil {
1835-
ver.IsSuperuser = data[0]["rolsuper"].(bool)
1862+
verNew.IsSuperuser = data[0]["rolsuper"].(bool)
1863+
}
1864+
log.Debugf("[%s] superuser=%v", dbUnique, verNew.IsSuperuser)
1865+
1866+
if verNew.Version.GreaterThanOrEqual(MinExtensionInfoAvailable) {
1867+
//log.Debugf("[%s] determining installed extensions info...", dbUnique)
1868+
data, err, _ = DBExecReadByDbUniqueName(dbUnique, "", useConnPooling, sql_extensions)
1869+
if err != nil {
1870+
log.Errorf("[%s] failed to determine installed extensions info: %v", dbUnique, err)
1871+
} else {
1872+
for _, dr := range data {
1873+
extver, err := decimal.NewFromString(dr["extversion"].(string))
1874+
if err != nil {
1875+
log.Errorf("[%s] failed to determine extension version info for extension %s: %v", dbUnique, dr["extname"], err)
1876+
continue
1877+
}
1878+
verNew.Extensions[dr["extname"].(string)] = extver
1879+
}
1880+
log.Debugf("[%s] installed extensions: %+v", dbUnique, verNew.Extensions)
1881+
}
18361882
}
1837-
log.Debugf("superuser=%v", ver.IsSuperuser)
18381883
}
18391884

1840-
ver.LastCheckedOn = time.Now()
1885+
verNew.LastCheckedOn = time.Now()
18411886
db_pg_version_map_lock.Lock()
1842-
db_pg_version_map[dbUnique] = ver
1887+
db_pg_version_map[dbUnique] = verNew
18431888
db_pg_version_map_lock.Unlock()
18441889
}
1845-
return ver, nil
1890+
return verNew, nil
18461891
}
18471892

18481893
// Need to define a sort interface as Go doesn't have support for Numeric/Decimal
@@ -1897,7 +1942,43 @@ func GetMetricVersionProperties(metric string, vme DBVersionMapEntry, metricDefM
18971942
return MetricVersionProperties{}, errors.New(fmt.Sprintf("no suitable SQL found for metric \"%s\", version \"%s\"", metric, vme.VersionStr))
18981943
}
18991944

1900-
return mdm[metric][best_ver], nil
1945+
ret, _ := mdm[metric][best_ver]
1946+
1947+
// check if SQL def. override defined for some specific extension version and replace the metric SQL-s if so
1948+
if ret.MetricAttrs.ExtensionVersionOverrides != nil && len(ret.MetricAttrs.ExtensionVersionOverrides) > 0 {
1949+
if vme.Extensions != nil && len(vme.Extensions) > 0 {
1950+
log.Debugf("[%s] extension version based override request found: %+v", metric, ret.MetricAttrs.ExtensionVersionOverrides)
1951+
for _, extOverride := range ret.MetricAttrs.ExtensionVersionOverrides {
1952+
var matching = true
1953+
for _, extVer := range extOverride.ExpectedExtensionVersions { // "natural" sorting of metric definition assumed
1954+
installedExtVer, ok := vme.Extensions[extVer.ExtName]
1955+
if !ok || !installedExtVer.GreaterThanOrEqual(extVer.ExtMinVersion) {
1956+
matching = false
1957+
}
1958+
}
1959+
if matching { // all defined extensions / versions (if many) need to match
1960+
_, ok := mdm[extOverride.TargetMetric]
1961+
if !ok {
1962+
log.Warningf("extension based override metric not found for metric %s. substitute metric name: %s", metric, extOverride.TargetMetric)
1963+
continue
1964+
}
1965+
mvp, err := GetMetricVersionProperties(extOverride.TargetMetric, vme, mdm, )
1966+
if err != nil {
1967+
log.Warningf("undefined extension based override for metric %s, substitute metric name: %s, version: %s not found", metric, extOverride.TargetMetric, best_ver)
1968+
continue
1969+
}
1970+
log.Debugf("overriding metric %s based on the extension_version_based_overrides metric attribute with %s:%s", metric, extOverride.TargetMetric, best_ver)
1971+
if mvp.Sql != "" {
1972+
ret.Sql = mvp.Sql
1973+
}
1974+
if mvp.SqlSU != "" {
1975+
ret.SqlSU = mvp.SqlSU
1976+
}
1977+
}
1978+
}
1979+
}
1980+
}
1981+
return ret, nil
19011982
}
19021983

19031984
func DetectSprocChanges(dbUnique string, vme DBVersionMapEntry, storage_ch chan<- []MetricStoreMessage, host_state map[string]map[string]string) ChangeDetectionResults {
@@ -2219,7 +2300,7 @@ func GetAllRecoMetricsForVersion(vme DBVersionMapEntry) map[string]MetricVersion
22192300
mvp, err := GetMetricVersionProperties(m, vme, metric_def_map)
22202301
if err != nil {
22212302
log.Warningf("Could not get SQL definition for metric \"%s\", PG %s", m, vme.VersionStr)
2222-
} else {
2303+
} else if !mvp.MetricAttrs.IsPrivate {
22232304
mvp_map[m] = mvp
22242305
}
22252306
}
@@ -2233,7 +2314,7 @@ func CheckForRecommendationsAndStore(dbUnique string, vme DBVersionMapEntry) ([]
22332314
start_time_epoch_ns := time.Now().UnixNano()
22342315

22352316
reco_metrics := GetAllRecoMetricsForVersion(vme)
2236-
log.Infof("Processing %d recommendation metrics for \"%s\"", len(reco_metrics), dbUnique)
2317+
log.Debugf("Processing %d recommendation metrics for \"%s\"", len(reco_metrics), dbUnique)
22372318

22382319
for m, mvp := range reco_metrics {
22392320
data, err, duration := DBExecReadByDbUniqueName(dbUnique, m, useConnPooling, mvp.Sql)
@@ -2775,7 +2856,9 @@ func ReadMetricDefinitionMapFromPostgres(failOnError bool) (map[string]map[decim
27752856
left join
27762857
pgwatch2.metric_attribute on (ma_metric_name = m_name)
27772858
where
2778-
m_is_active`
2859+
m_is_active
2860+
order by
2861+
1, 2`
27792862

27802863
log.Info("updating metrics definitons from ConfigDB...")
27812864
data, err := DBExecRead(configDb, CONFIGDB_IDENT, sql)
@@ -3125,7 +3208,7 @@ func ParseMetricAttrsFromString(jsonAttrs string) MetricAttrs {
31253208
func ReadMetricsFromFolder(folder string, failOnError bool) (map[string]map[decimal.Decimal]MetricVersionProperties, error) {
31263209
metrics_map := make(map[string]map[decimal.Decimal]MetricVersionProperties)
31273210
rIsDigitOrPunctuation := regexp.MustCompile("^[\\d\\.]+$")
3128-
metricNamePattern := "^[a-z0-9_]+$"
3211+
metricNamePattern := "^[a-z0-9_\\.]+$"
31293212
rMetricNameFilter := regexp.MustCompile(metricNamePattern)
31303213

31313214
log.Infof("Searching for metrics from path %s ...", folder)
@@ -4022,7 +4105,7 @@ func main() {
40224105
_, exists := db_conn_limiting_channel[db_unique]
40234106
db_conn_limiting_channel_lock.RUnlock()
40244107

4025-
if !exists { // initialize DB connection limiting structure
4108+
if !exists { // new host, initialize DB connection limiting structure
40264109
db_conn_limiting_channel_lock.Lock()
40274110
db_conn_limiting_channel[db_unique] = make(chan bool, MAX_PG_CONNECTIONS_PER_MONITORED_DB)
40284111
i := 0
@@ -4032,6 +4115,10 @@ func main() {
40324115
i++
40334116
}
40344117
db_conn_limiting_channel_lock.Unlock()
4118+
4119+
db_pg_version_map_lock.Lock()
4120+
db_get_pg_version_map_lock[db_unique] = sync.RWMutex{}
4121+
db_pg_version_map_lock.Unlock()
40354122
}
40364123

40374124
_, connectFailedSoFar := failedInitialConnectHosts[db_unique]

pgwatch2/sql/config_store/config_store.sql

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ create table metric (
7676

7777
unique (m_name, m_pg_version_from, m_standby_only),
7878
check (not (m_master_only and m_standby_only)),
79-
check (m_name ~ '^[a-z0-9_]+$')
79+
check (m_name ~ E'^[a-z0-9_\\.]+$')
8080
);
8181

8282
create table metric_attribute (
8383
ma_metric_name text not null primary key,
8484
ma_last_modified_on timestamptz not null default now(),
8585
ma_metric_attrs jsonb not null,
8686

87-
check (ma_metric_name ~ '^[a-z0-9_]+$')
87+
check (ma_metric_name ~ E'^[a-z0-9_\\.]+$')
8888
);
8989

9090

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
begin;
2+
3+
alter table pgwatch2.metric
4+
drop constraint metric_m_name_check,
5+
add constraint metric_m_name_check check (m_name ~ E'^[a-z0-9_\\.]+$');
6+
7+
alter table pgwatch2.metric_attribute
8+
drop constraint metric_attribute_ma_metric_name_check,
9+
add constraint metric_attribute_ma_metric_name_check check (ma_metric_name ~ E'^[a-z0-9_\\.]+$');
10+
11+
insert into pgwatch2.schema_version (sv_tag) values ('1.8.0');
12+
13+
commit;

webpy/templates/metrics.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ <h2 title="Inactive at the bottom">Metric definitions</h2>
151151
<form action="/metrics" method="post">
152152
<td>
153153
<input type="hidden" name="m_id" value="{{row['m_id']}}">
154-
<input type="text" size="18" name="m_name" value="{{row['m_name']|e}}" title="Metric name. Lowercase alphanumerics and underscores allowed." pattern="[a-z0-9_]+">
154+
<input type="text" size="18" name="m_name" value="{{row['m_name']|e}}" title="Metric name. Lowercase alphanumerics and underscores allowed." pattern="[a-z0-9_\.]+">
155155
</td>
156156
<td><input type="text" size="3" name="m_pg_version_from" value="{{row['m_pg_version_from']}}" title="Version from"></td>
157157
<td><textarea name="m_sql" cols="35" title="SQL for metric">{{row['m_sql']}}</textarea></td>
@@ -171,7 +171,7 @@ <h2 title="Inactive at the bottom">Metric definitions</h2>
171171
{% endfor %}
172172
<tr>
173173
<form action="/metrics" method="post">
174-
<td><input type="text" size="18" name="m_name" value="" title="Metric name. Lowercase alphanumerics and underscores allowed." pattern="[a-z0-9_]+"></td>
174+
<td><input type="text" size="18" name="m_name" value="" title="Metric name. Lowercase alphanumerics and underscores allowed." pattern="[a-z0-9_\.]+"></td>
175175
<td><input type="text" size="3" name="m_pg_version_from" value="9.0" title="Version from"></td>
176176
<td><textarea name="m_sql" cols="35" title="SQL for metric">SELECT (extract(epoch from now()) * 1e9)::int8 as epoch_ns, ...</textarea></td>
177177
<td><textarea name="m_sql_su" cols="35" title="Privileged (superuser or pg_monitor grant) SQL for metric"></textarea></td>

0 commit comments

Comments
 (0)