netdata/netdata

View on GitHub
src/go/collectors/go.d.plugin/modules/postgres/charts.go

Summary

Maintainability
F
5 days
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package postgres

import (
    "fmt"
    "strings"
    "time"

    "github.com/netdata/netdata/go/go.d.plugin/agent/module"
)

const (
    prioConnectionsUtilization = module.Priority + iota
    prioConnectionsUsage
    prioConnectionsStateCount
    prioDBConnectionsUtilization
    prioDBConnectionsCount

    prioTransactionsDuration
    prioDBTransactionsRatio
    prioDBTransactionsRate

    prioQueriesDuration

    prioDBOpsFetchedRowsRatio
    prioDBOpsReadRowsRate
    prioDBOpsWriteRowsRate
    prioDBTempFilesCreatedRate
    prioDBTempFilesIORate
    prioTableOpsRowsRate
    prioTableOpsRowsHOTRatio
    prioTableOpsRowsHOTRate
    prioTableScansRate
    prioTableScansRowsRate

    prioDBCacheIORatio
    prioDBIORate
    prioTableCacheIORatio
    prioTableIORate
    prioTableIndexCacheIORatio
    prioTableIndexIORate
    prioTableToastCacheIORatio
    prioTableToastIORate
    prioTableToastIndexCacheIORatio
    prioTableToastIndexIORate

    prioDBSize
    prioTableTotalSize
    prioIndexSize

    prioTableBloatSizePerc
    prioTableBloatSize
    prioIndexBloatSizePerc
    prioIndexBloatSize

    prioLocksUtilization
    prioDBLocksHeldCount
    prioDBLocksAwaitedCount
    prioDBDeadlocksRate

    prioAutovacuumWorkersCount
    prioTableAutovacuumSinceTime
    prioTableVacuumSinceTime
    prioTableAutoAnalyzeSinceTime
    prioTableLastAnalyzeAgo

    prioCheckpointsRate
    prioCheckpointsTime
    prioBGWriterHaltsRate
    prioBuffersIORate
    prioBuffersBackendFsyncRate
    prioBuffersAllocRate
    prioTXIDExhaustionTowardsAutovacuumPerc
    prioTXIDExhaustionPerc
    prioTXIDExhaustionOldestTXIDNum
    prioTableRowsDeadRatio
    prioTableRowsCount
    prioTableNullColumns
    prioIndexUsageStatus

    prioReplicationAppWALLagSize
    prioReplicationAppWALLagTime
    prioReplicationSlotFilesCount
    prioDBConflictsRate
    prioDBConflictsReasonRate

    prioWALIORate
    prioWALFilesCount
    prioWALArchivingFilesCount

    prioDatabasesCount
    prioCatalogRelationsCount
    prioCatalogRelationsSize

    prioUptime
)

var baseCharts = module.Charts{
    serverConnectionsUtilizationChart.Copy(),
    serverConnectionsUsageChart.Copy(),
    serverConnectionsStateCount.Copy(),
    locksUtilization.Copy(),
    checkpointsChart.Copy(),
    checkpointWriteChart.Copy(),
    buffersIORateChart.Copy(),
    buffersAllocRateChart.Copy(),
    bgWriterHaltsRateChart.Copy(),
    buffersBackendFsyncRateChart.Copy(),
    walIORateChart.Copy(),
    autovacuumWorkersCountChart.Copy(),
    txidExhaustionTowardsAutovacuumPercChart.Copy(),
    txidExhaustionPercChart.Copy(),
    txidExhaustionOldestTXIDNumChart.Copy(),

    catalogRelationSCountChart.Copy(),
    catalogRelationsSizeChart.Copy(),
    serverUptimeChart.Copy(),
    databasesCountChart.Copy(),
}

var walFilesCharts = module.Charts{
    walFilesCountChart.Copy(),
    walArchivingFilesCountChart.Copy(),
}

func (p *Postgres) addWALFilesCharts() {
    charts := walFilesCharts.Copy()

    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

var (
    serverConnectionsUtilizationChart = module.Chart{
        ID:       "connections_utilization",
        Title:    "Connections utilization",
        Units:    "percentage",
        Fam:      "connections",
        Ctx:      "postgres.connections_utilization",
        Priority: prioConnectionsUtilization,
        Dims: module.Dims{
            {ID: "server_connections_utilization", Name: "used"},
        },
    }
    serverConnectionsUsageChart = module.Chart{
        ID:       "connections_usage",
        Title:    "Connections usage",
        Units:    "connections",
        Fam:      "connections",
        Ctx:      "postgres.connections_usage",
        Priority: prioConnectionsUsage,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "server_connections_available", Name: "available"},
            {ID: "server_connections_used", Name: "used"},
        },
    }
    serverConnectionsStateCount = module.Chart{
        ID:       "connections_state",
        Title:    "Connections in each state",
        Units:    "connections",
        Fam:      "connections",
        Ctx:      "postgres.connections_state_count",
        Priority: prioConnectionsStateCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "server_connections_state_active", Name: "active"},
            {ID: "server_connections_state_idle", Name: "idle"},
            {ID: "server_connections_state_idle_in_transaction", Name: "idle_in_transaction"},
            {ID: "server_connections_state_idle_in_transaction_aborted", Name: "idle_in_transaction_aborted"},
            {ID: "server_connections_state_fastpath_function_call", Name: "fastpath_function_call"},
            {ID: "server_connections_state_disabled", Name: "disabled"},
        },
    }

    locksUtilization = module.Chart{
        ID:       "locks_utilization",
        Title:    "Acquired locks utilization",
        Units:    "percentage",
        Fam:      "locks",
        Ctx:      "postgres.locks_utilization",
        Priority: prioLocksUtilization,
        Dims: module.Dims{
            {ID: "locks_utilization", Name: "used"},
        },
    }

    checkpointsChart = module.Chart{
        ID:       "checkpoints_rate",
        Title:    "Checkpoints",
        Units:    "checkpoints/s",
        Fam:      "maintenance",
        Ctx:      "postgres.checkpoints_rate",
        Priority: prioCheckpointsRate,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "checkpoints_timed", Name: "scheduled", Algo: module.Incremental},
            {ID: "checkpoints_req", Name: "requested", Algo: module.Incremental},
        },
    }
    // TODO: should be seconds, also it is units/s when using incremental...
    checkpointWriteChart = module.Chart{
        ID:       "checkpoints_time",
        Title:    "Checkpoint time",
        Units:    "milliseconds",
        Fam:      "maintenance",
        Ctx:      "postgres.checkpoints_time",
        Priority: prioCheckpointsTime,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "checkpoint_write_time", Name: "write", Algo: module.Incremental},
            {ID: "checkpoint_sync_time", Name: "sync", Algo: module.Incremental},
        },
    }
    bgWriterHaltsRateChart = module.Chart{
        ID:       "bgwriter_halts_rate",
        Title:    "Background writer scan halts",
        Units:    "halts/s",
        Fam:      "maintenance",
        Ctx:      "postgres.bgwriter_halts_rate",
        Priority: prioBGWriterHaltsRate,
        Dims: module.Dims{
            {ID: "maxwritten_clean", Name: "maxwritten", Algo: module.Incremental},
        },
    }

    buffersIORateChart = module.Chart{
        ID:       "buffers_io_rate",
        Title:    "Buffers written rate",
        Units:    "B/s",
        Fam:      "maintenance",
        Ctx:      "postgres.buffers_io_rate",
        Priority: prioBuffersIORate,
        Type:     module.Area,
        Dims: module.Dims{
            {ID: "buffers_checkpoint", Name: "checkpoint", Algo: module.Incremental},
            {ID: "buffers_backend", Name: "backend", Algo: module.Incremental},
            {ID: "buffers_clean", Name: "bgwriter", Algo: module.Incremental},
        },
    }
    buffersBackendFsyncRateChart = module.Chart{
        ID:       "buffers_backend_fsync_rate",
        Title:    "Backend fsync calls",
        Units:    "calls/s",
        Fam:      "maintenance",
        Ctx:      "postgres.buffers_backend_fsync_rate",
        Priority: prioBuffersBackendFsyncRate,
        Dims: module.Dims{
            {ID: "buffers_backend_fsync", Name: "fsync", Algo: module.Incremental},
        },
    }
    buffersAllocRateChart = module.Chart{
        ID:       "buffers_alloc_rate",
        Title:    "Buffers allocated",
        Units:    "B/s",
        Fam:      "maintenance",
        Ctx:      "postgres.buffers_allocated_rate",
        Priority: prioBuffersAllocRate,
        Dims: module.Dims{
            {ID: "buffers_alloc", Name: "allocated", Algo: module.Incremental},
        },
    }

    walIORateChart = module.Chart{
        ID:       "wal_io_rate",
        Title:    "Write-Ahead Log writes",
        Units:    "B/s",
        Fam:      "wal",
        Ctx:      "postgres.wal_io_rate",
        Priority: prioWALIORate,
        Dims: module.Dims{
            {ID: "wal_writes", Name: "written", Algo: module.Incremental},
        },
    }
    walFilesCountChart = module.Chart{
        ID:       "wal_files_count",
        Title:    "Write-Ahead Log files",
        Units:    "files",
        Fam:      "wal",
        Ctx:      "postgres.wal_files_count",
        Priority: prioWALFilesCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "wal_written_files", Name: "written"},
            {ID: "wal_recycled_files", Name: "recycled"},
        },
    }

    walArchivingFilesCountChart = module.Chart{
        ID:       "wal_archiving_files_count",
        Title:    "Write-Ahead Log archived files",
        Units:    "files/s",
        Fam:      "wal",
        Ctx:      "postgres.wal_archiving_files_count",
        Priority: prioWALArchivingFilesCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "wal_archive_files_ready_count", Name: "ready"},
            {ID: "wal_archive_files_done_count", Name: "done"},
        },
    }

    autovacuumWorkersCountChart = module.Chart{
        ID:       "autovacuum_workers_count",
        Title:    "Autovacuum workers",
        Units:    "workers",
        Fam:      "vacuum and analyze",
        Ctx:      "postgres.autovacuum_workers_count",
        Priority: prioAutovacuumWorkersCount,
        Dims: module.Dims{
            {ID: "autovacuum_analyze", Name: "analyze"},
            {ID: "autovacuum_vacuum_analyze", Name: "vacuum_analyze"},
            {ID: "autovacuum_vacuum", Name: "vacuum"},
            {ID: "autovacuum_vacuum_freeze", Name: "vacuum_freeze"},
            {ID: "autovacuum_brin_summarize", Name: "brin_summarize"},
        },
    }

    txidExhaustionTowardsAutovacuumPercChart = module.Chart{
        ID:       "txid_exhaustion_towards_autovacuum_perc",
        Title:    "Percent towards emergency autovacuum",
        Units:    "percentage",
        Fam:      "maintenance",
        Ctx:      "postgres.txid_exhaustion_towards_autovacuum_perc",
        Priority: prioTXIDExhaustionTowardsAutovacuumPerc,
        Dims: module.Dims{
            {ID: "percent_towards_emergency_autovacuum", Name: "emergency_autovacuum"},
        },
    }
    txidExhaustionPercChart = module.Chart{
        ID:       "txid_exhaustion_perc",
        Title:    "Percent towards transaction ID wraparound",
        Units:    "percentage",
        Fam:      "maintenance",
        Ctx:      "postgres.txid_exhaustion_perc",
        Priority: prioTXIDExhaustionPerc,
        Dims: module.Dims{
            {ID: "percent_towards_wraparound", Name: "txid_exhaustion"},
        },
    }
    txidExhaustionOldestTXIDNumChart = module.Chart{
        ID:       "txid_exhaustion_oldest_txid_num",
        Title:    "Oldest transaction XID",
        Units:    "xid",
        Fam:      "maintenance",
        Ctx:      "postgres.txid_exhaustion_oldest_txid_num",
        Priority: prioTXIDExhaustionOldestTXIDNum,
        Dims: module.Dims{
            {ID: "oldest_current_xid", Name: "xid"},
        },
    }

    catalogRelationSCountChart = module.Chart{
        ID:       "catalog_relations_count",
        Title:    "Relation count",
        Units:    "relations",
        Fam:      "catalog",
        Ctx:      "postgres.catalog_relations_count",
        Priority: prioCatalogRelationsCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "catalog_relkind_r_count", Name: "ordinary_table"},
            {ID: "catalog_relkind_i_count", Name: "index"},
            {ID: "catalog_relkind_S_count", Name: "sequence"},
            {ID: "catalog_relkind_t_count", Name: "toast_table"},
            {ID: "catalog_relkind_v_count", Name: "view"},
            {ID: "catalog_relkind_m_count", Name: "materialized_view"},
            {ID: "catalog_relkind_c_count", Name: "composite_type"},
            {ID: "catalog_relkind_f_count", Name: "foreign_table"},
            {ID: "catalog_relkind_p_count", Name: "partitioned_table"},
            {ID: "catalog_relkind_I_count", Name: "partitioned_index"},
        },
    }
    catalogRelationsSizeChart = module.Chart{
        ID:       "catalog_relations_size",
        Title:    "Relation size",
        Units:    "B",
        Fam:      "catalog",
        Ctx:      "postgres.catalog_relations_size",
        Priority: prioCatalogRelationsSize,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "catalog_relkind_r_size", Name: "ordinary_table"},
            {ID: "catalog_relkind_i_size", Name: "index"},
            {ID: "catalog_relkind_S_size", Name: "sequence"},
            {ID: "catalog_relkind_t_size", Name: "toast_table"},
            {ID: "catalog_relkind_v_size", Name: "view"},
            {ID: "catalog_relkind_m_size", Name: "materialized_view"},
            {ID: "catalog_relkind_c_size", Name: "composite_type"},
            {ID: "catalog_relkind_f_size", Name: "foreign_table"},
            {ID: "catalog_relkind_p_size", Name: "partitioned_table"},
            {ID: "catalog_relkind_I_size", Name: "partitioned_index"},
        },
    }

    serverUptimeChart = module.Chart{
        ID:       "server_uptime",
        Title:    "Uptime",
        Units:    "seconds",
        Fam:      "uptime",
        Ctx:      "postgres.uptime",
        Priority: prioUptime,
        Dims: module.Dims{
            {ID: "server_uptime", Name: "uptime"},
        },
    }

    databasesCountChart = module.Chart{
        ID:       "databases_count",
        Title:    "Number of databases",
        Units:    "databases",
        Fam:      "catalog",
        Ctx:      "postgres.databases_count",
        Priority: prioDatabasesCount,
        Dims: module.Dims{
            {ID: "databases_count", Name: "databases"},
        },
    }

    transactionsDurationChartTmpl = module.Chart{
        ID:       "transactions_duration",
        Title:    "Observed transactions time",
        Units:    "transactions/s",
        Fam:      "transactions",
        Ctx:      "postgres.transactions_duration",
        Priority: prioTransactionsDuration,
        Type:     module.Stacked,
    }
    queriesDurationChartTmpl = module.Chart{
        ID:       "queries_duration",
        Title:    "Observed active queries time",
        Units:    "queries/s",
        Fam:      "queries",
        Ctx:      "postgres.queries_duration",
        Priority: prioQueriesDuration,
        Type:     module.Stacked,
    }
)

func newRunningTimeHistogramChart(tmpl module.Chart, prefix string, buckets []float64) (*module.Chart, error) {
    chart := tmpl.Copy()

    for i, v := range buckets {
        dim := &module.Dim{
            ID:   fmt.Sprintf("%s_hist_bucket_%d", prefix, i+1),
            Name: time.Duration(v * float64(time.Second)).String(),
            Algo: module.Incremental,
        }
        if err := chart.AddDim(dim); err != nil {
            return nil, err
        }
    }

    dim := &module.Dim{
        ID:   fmt.Sprintf("%s_hist_bucket_inf", prefix),
        Name: "+Inf",
        Algo: module.Incremental,
    }
    if err := chart.AddDim(dim); err != nil {
        return nil, err
    }

    return chart, nil
}

func (p *Postgres) addTransactionsRunTimeHistogramChart() {
    chart, err := newRunningTimeHistogramChart(
        transactionsDurationChartTmpl,
        "transaction_running_time",
        p.XactTimeHistogram,
    )
    if err != nil {
        p.Warning(err)
        return
    }
    if err := p.Charts().Add(chart); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addQueriesRunTimeHistogramChart() {
    chart, err := newRunningTimeHistogramChart(
        queriesDurationChartTmpl,
        "query_running_time",
        p.QueryTimeHistogram,
    )
    if err != nil {
        p.Warning(err)
        return
    }
    if err := p.Charts().Add(chart); err != nil {
        p.Warning(err)
    }
}

var (
    replicationStandbyAppCharts = module.Charts{
        replicationAppWALLagSizeChartTmpl.Copy(),
        replicationAppWALLagTimeChartTmpl.Copy(),
    }
    replicationAppWALLagSizeChartTmpl = module.Chart{
        ID:       "replication_app_%s_wal_lag_size",
        Title:    "Standby application WAL lag size",
        Units:    "B",
        Fam:      "replication",
        Ctx:      "postgres.replication_app_wal_lag_size",
        Priority: prioReplicationAppWALLagSize,
        Dims: module.Dims{
            {ID: "repl_standby_app_%s_wal_sent_lag_size", Name: "sent_lag"},
            {ID: "repl_standby_app_%s_wal_write_lag_size", Name: "write_lag"},
            {ID: "repl_standby_app_%s_wal_flush_lag_size", Name: "flush_lag"},
            {ID: "repl_standby_app_%s_wal_replay_lag_size", Name: "replay_lag"},
        },
    }
    replicationAppWALLagTimeChartTmpl = module.Chart{
        ID:       "replication_app_%s_wal_lag_time",
        Title:    "Standby application WAL lag time",
        Units:    "seconds",
        Fam:      "replication",
        Ctx:      "postgres.replication_app_wal_lag_time",
        Priority: prioReplicationAppWALLagTime,
        Dims: module.Dims{
            {ID: "repl_standby_app_%s_wal_write_lag_time", Name: "write_lag"},
            {ID: "repl_standby_app_%s_wal_flush_lag_time", Name: "flush_lag"},
            {ID: "repl_standby_app_%s_wal_replay_lag_time", Name: "replay_lag"},
        },
    }
)

func newReplicationStandbyAppCharts(app string) *module.Charts {
    charts := replicationStandbyAppCharts.Copy()
    for _, c := range *charts {
        c.ID = fmt.Sprintf(c.ID, app)
        c.Labels = []module.Label{
            {Key: "application", Value: app},
        }
        for _, d := range c.Dims {
            d.ID = fmt.Sprintf(d.ID, app)
        }
    }
    return charts
}

func (p *Postgres) addNewReplicationStandbyAppCharts(app string) {
    charts := newReplicationStandbyAppCharts(app)
    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) removeReplicationStandbyAppCharts(app string) {
    prefix := fmt.Sprintf("replication_standby_app_%s_", app)
    for _, c := range *p.Charts() {
        if strings.HasPrefix(c.ID, prefix) {
            c.MarkRemove()
            c.MarkNotCreated()
        }
    }
}

var (
    replicationSlotCharts = module.Charts{
        replicationSlotFilesCountChartTmpl.Copy(),
    }
    replicationSlotFilesCountChartTmpl = module.Chart{
        ID:       "replication_slot_%s_files_count",
        Title:    "Replication slot files",
        Units:    "files",
        Fam:      "replication",
        Ctx:      "postgres.replication_slot_files_count",
        Priority: prioReplicationSlotFilesCount,
        Dims: module.Dims{
            {ID: "repl_slot_%s_replslot_wal_keep", Name: "wal_keep"},
            {ID: "repl_slot_%s_replslot_files", Name: "pg_replslot_files"},
        },
    }
)

func newReplicationSlotCharts(slot string) *module.Charts {
    charts := replicationSlotCharts.Copy()
    for _, c := range *charts {
        c.ID = fmt.Sprintf(c.ID, slot)
        c.Labels = []module.Label{
            {Key: "slot", Value: slot},
        }
        for _, d := range c.Dims {
            d.ID = fmt.Sprintf(d.ID, slot)
        }
    }
    return charts
}

func (p *Postgres) addNewReplicationSlotCharts(slot string) {
    charts := newReplicationSlotCharts(slot)
    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) removeReplicationSlotCharts(slot string) {
    prefix := fmt.Sprintf("replication_slot_%s_", slot)
    for _, c := range *p.Charts() {
        if strings.HasPrefix(c.ID, prefix) {
            c.MarkRemove()
            c.MarkNotCreated()
        }
    }
}

var (
    dbChartsTmpl = module.Charts{
        dbTransactionsRatioChartTmpl.Copy(),
        dbTransactionsRateChartTmpl.Copy(),
        dbConnectionsUtilizationChartTmpl.Copy(),
        dbConnectionsCountChartTmpl.Copy(),
        dbCacheIORatioChartTmpl.Copy(),
        dbIORateChartTmpl.Copy(),
        dbOpsFetchedRowsRatioChartTmpl.Copy(),
        dbOpsReadRowsRateChartTmpl.Copy(),
        dbOpsWriteRowsRateChartTmpl.Copy(),
        dbDeadlocksRateChartTmpl.Copy(),
        dbLocksHeldCountChartTmpl.Copy(),
        dbLocksAwaitedCountChartTmpl.Copy(),
        dbTempFilesCreatedRateChartTmpl.Copy(),
        dbTempFilesIORateChartTmpl.Copy(),
        dbSizeChartTmpl.Copy(),
    }
    dbTransactionsRatioChartTmpl = module.Chart{
        ID:       "db_%s_transactions_ratio",
        Title:    "Database transactions ratio",
        Units:    "percentage",
        Fam:      "transactions",
        Ctx:      "postgres.db_transactions_ratio",
        Priority: prioDBTransactionsRatio,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "db_%s_xact_commit", Name: "committed", Algo: module.PercentOfIncremental},
            {ID: "db_%s_xact_rollback", Name: "rollback", Algo: module.PercentOfIncremental},
        },
    }
    dbTransactionsRateChartTmpl = module.Chart{
        ID:       "db_%s_transactions_rate",
        Title:    "Database transactions",
        Units:    "transactions/s",
        Fam:      "transactions",
        Ctx:      "postgres.db_transactions_rate",
        Priority: prioDBTransactionsRate,
        Dims: module.Dims{
            {ID: "db_%s_xact_commit", Name: "committed", Algo: module.Incremental},
            {ID: "db_%s_xact_rollback", Name: "rollback", Algo: module.Incremental},
        },
    }
    dbConnectionsUtilizationChartTmpl = module.Chart{
        ID:       "db_%s_connections_utilization",
        Title:    "Database connections utilization",
        Units:    "percentage",
        Fam:      "connections",
        Ctx:      "postgres.db_connections_utilization",
        Priority: prioDBConnectionsUtilization,
        Dims: module.Dims{
            {ID: "db_%s_numbackends_utilization", Name: "used"},
        },
    }
    dbConnectionsCountChartTmpl = module.Chart{
        ID:       "db_%s_connections",
        Title:    "Database connections",
        Units:    "connections",
        Fam:      "connections",
        Ctx:      "postgres.db_connections_count",
        Priority: prioDBConnectionsCount,
        Dims: module.Dims{
            {ID: "db_%s_numbackends", Name: "connections"},
        },
    }
    dbCacheIORatioChartTmpl = module.Chart{
        ID:       "db_%s_cache_io_ratio",
        Title:    "Database buffer cache miss ratio",
        Units:    "percentage",
        Fam:      "cache",
        Ctx:      "postgres.db_cache_io_ratio",
        Priority: prioDBCacheIORatio,
        Dims: module.Dims{
            {ID: "db_%s_blks_read_perc", Name: "miss"},
        },
    }
    dbIORateChartTmpl = module.Chart{
        ID:       "db_%s_io_rate",
        Title:    "Database reads",
        Units:    "B/s",
        Fam:      "cache",
        Ctx:      "postgres.db_io_rate",
        Priority: prioDBIORate,
        Type:     module.Area,
        Dims: module.Dims{
            {ID: "db_%s_blks_hit", Name: "memory", Algo: module.Incremental},
            {ID: "db_%s_blks_read", Name: "disk", Algo: module.Incremental},
        },
    }
    dbOpsFetchedRowsRatioChartTmpl = module.Chart{
        ID:       "db_%s_db_ops_fetched_rows_ratio",
        Title:    "Database rows fetched ratio",
        Units:    "percentage",
        Fam:      "throughput",
        Ctx:      "postgres.db_ops_fetched_rows_ratio",
        Priority: prioDBOpsFetchedRowsRatio,
        Dims: module.Dims{
            {ID: "db_%s_tup_fetched_perc", Name: "fetched"},
        },
    }
    dbOpsReadRowsRateChartTmpl = module.Chart{
        ID:       "db_%s_ops_read_rows_rate",
        Title:    "Database rows read",
        Units:    "rows/s",
        Fam:      "throughput",
        Ctx:      "postgres.db_ops_read_rows_rate",
        Priority: prioDBOpsReadRowsRate,
        Dims: module.Dims{
            {ID: "db_%s_tup_returned", Name: "returned", Algo: module.Incremental},
            {ID: "db_%s_tup_fetched", Name: "fetched", Algo: module.Incremental},
        },
    }
    dbOpsWriteRowsRateChartTmpl = module.Chart{
        ID:       "db_%s_ops_write_rows_rate",
        Title:    "Database rows written",
        Units:    "rows/s",
        Fam:      "throughput",
        Ctx:      "postgres.db_ops_write_rows_rate",
        Priority: prioDBOpsWriteRowsRate,
        Dims: module.Dims{
            {ID: "db_%s_tup_inserted", Name: "inserted", Algo: module.Incremental},
            {ID: "db_%s_tup_deleted", Name: "deleted", Algo: module.Incremental},
            {ID: "db_%s_tup_updated", Name: "updated", Algo: module.Incremental},
        },
    }
    dbConflictsRateChartTmpl = module.Chart{
        ID:       "db_%s_conflicts_rate",
        Title:    "Database canceled queries",
        Units:    "queries/s",
        Fam:      "replication",
        Ctx:      "postgres.db_conflicts_rate",
        Priority: prioDBConflictsRate,
        Dims: module.Dims{
            {ID: "db_%s_conflicts", Name: "conflicts", Algo: module.Incremental},
        },
    }
    dbConflictsReasonRateChartTmpl = module.Chart{
        ID:       "db_%s_conflicts_reason_rate",
        Title:    "Database canceled queries by reason",
        Units:    "queries/s",
        Fam:      "replication",
        Ctx:      "postgres.db_conflicts_reason_rate",
        Priority: prioDBConflictsReasonRate,
        Dims: module.Dims{
            {ID: "db_%s_confl_tablespace", Name: "tablespace", Algo: module.Incremental},
            {ID: "db_%s_confl_lock", Name: "lock", Algo: module.Incremental},
            {ID: "db_%s_confl_snapshot", Name: "snapshot", Algo: module.Incremental},
            {ID: "db_%s_confl_bufferpin", Name: "bufferpin", Algo: module.Incremental},
            {ID: "db_%s_confl_deadlock", Name: "deadlock", Algo: module.Incremental},
        },
    }
    dbDeadlocksRateChartTmpl = module.Chart{
        ID:       "db_%s_deadlocks_rate",
        Title:    "Database deadlocks",
        Units:    "deadlocks/s",
        Fam:      "locks",
        Ctx:      "postgres.db_deadlocks_rate",
        Priority: prioDBDeadlocksRate,
        Dims: module.Dims{
            {ID: "db_%s_deadlocks", Name: "deadlocks", Algo: module.Incremental},
        },
    }
    dbLocksHeldCountChartTmpl = module.Chart{
        ID:       "db_%s_locks_held",
        Title:    "Database locks held",
        Units:    "locks",
        Fam:      "locks",
        Ctx:      "postgres.db_locks_held_count",
        Priority: prioDBLocksHeldCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "db_%s_lock_mode_AccessShareLock_held", Name: "access_share"},
            {ID: "db_%s_lock_mode_RowShareLock_held", Name: "row_share"},
            {ID: "db_%s_lock_mode_RowExclusiveLock_held", Name: "row_exclusive"},
            {ID: "db_%s_lock_mode_ShareUpdateExclusiveLock_held", Name: "share_update"},
            {ID: "db_%s_lock_mode_ShareLock_held", Name: "share"},
            {ID: "db_%s_lock_mode_ShareRowExclusiveLock_held", Name: "share_row_exclusive"},
            {ID: "db_%s_lock_mode_ExclusiveLock_held", Name: "exclusive"},
            {ID: "db_%s_lock_mode_AccessExclusiveLock_held", Name: "access_exclusive"},
        },
    }
    dbLocksAwaitedCountChartTmpl = module.Chart{
        ID:       "db_%s_locks_awaited_count",
        Title:    "Database locks awaited",
        Units:    "locks",
        Fam:      "locks",
        Ctx:      "postgres.db_locks_awaited_count",
        Priority: prioDBLocksAwaitedCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "db_%s_lock_mode_AccessShareLock_awaited", Name: "access_share"},
            {ID: "db_%s_lock_mode_RowShareLock_awaited", Name: "row_share"},
            {ID: "db_%s_lock_mode_RowExclusiveLock_awaited", Name: "row_exclusive"},
            {ID: "db_%s_lock_mode_ShareUpdateExclusiveLock_awaited", Name: "share_update"},
            {ID: "db_%s_lock_mode_ShareLock_awaited", Name: "share"},
            {ID: "db_%s_lock_mode_ShareRowExclusiveLock_awaited", Name: "share_row_exclusive"},
            {ID: "db_%s_lock_mode_ExclusiveLock_awaited", Name: "exclusive"},
            {ID: "db_%s_lock_mode_AccessExclusiveLock_awaited", Name: "access_exclusive"},
        },
    }
    dbTempFilesCreatedRateChartTmpl = module.Chart{
        ID:       "db_%s_temp_files_files_created_rate",
        Title:    "Database created temporary files",
        Units:    "files/s",
        Fam:      "throughput",
        Ctx:      "postgres.db_temp_files_created_rate",
        Priority: prioDBTempFilesCreatedRate,
        Dims: module.Dims{
            {ID: "db_%s_temp_files", Name: "created", Algo: module.Incremental},
        },
    }
    dbTempFilesIORateChartTmpl = module.Chart{
        ID:       "db_%s_temp_files_io_rate",
        Title:    "Database temporary files data written to disk",
        Units:    "B/s",
        Fam:      "throughput",
        Ctx:      "postgres.db_temp_files_io_rate",
        Priority: prioDBTempFilesIORate,
        Dims: module.Dims{
            {ID: "db_%s_temp_bytes", Name: "written", Algo: module.Incremental},
        },
    }
    dbSizeChartTmpl = module.Chart{
        ID:       "db_%s_size",
        Title:    "Database size",
        Units:    "B",
        Fam:      "size",
        Ctx:      "postgres.db_size",
        Priority: prioDBSize,
        Dims: module.Dims{
            {ID: "db_%s_size", Name: "size"},
        },
    }
)

func (p *Postgres) addDBConflictsCharts(db *dbMetrics) {
    tmpl := module.Charts{
        dbConflictsRateChartTmpl.Copy(),
        dbConflictsReasonRateChartTmpl.Copy(),
    }
    charts := newDatabaseCharts(tmpl.Copy(), db)

    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

func newDatabaseCharts(tmpl *module.Charts, db *dbMetrics) *module.Charts {
    charts := tmpl.Copy()
    for _, c := range *charts {
        c.ID = fmt.Sprintf(c.ID, db.name)
        c.Labels = []module.Label{
            {Key: "database", Value: db.name},
        }
        for _, d := range c.Dims {
            d.ID = fmt.Sprintf(d.ID, db.name)
        }
    }
    return charts
}

func (p *Postgres) addNewDatabaseCharts(db *dbMetrics) {
    charts := newDatabaseCharts(dbChartsTmpl.Copy(), db)

    if db.size == nil {
        _ = charts.Remove(fmt.Sprintf(dbSizeChartTmpl.ID, db.name))
    }

    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) removeDatabaseCharts(db *dbMetrics) {
    prefix := fmt.Sprintf("db_%s_", db.name)
    for _, c := range *p.Charts() {
        if strings.HasPrefix(c.ID, prefix) {
            c.MarkRemove()
            c.MarkNotCreated()
        }
    }
}

var (
    tableChartsTmpl = module.Charts{
        tableRowsCountChartTmpl.Copy(),
        tableDeadRowsDeadRatioChartTmpl.Copy(),
        tableOpsRowsRateChartTmpl.Copy(),
        tableOpsRowsHOTRatioChartTmpl.Copy(),
        tableOpsRowsHOTRateChartTmpl.Copy(),
        tableScansRateChartTmpl.Copy(),
        tableScansRowsRateChartTmpl.Copy(),
        tableNullColumnsCountChartTmpl.Copy(),
        tableTotalSizeChartTmpl.Copy(),
        tableBloatSizePercChartTmpl.Copy(),
        tableBloatSizeChartTmpl.Copy(),
    }

    tableDeadRowsDeadRatioChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_rows_dead_ratio",
        Title:    "Table dead rows",
        Units:    "%",
        Fam:      "maintenance",
        Ctx:      "postgres.table_rows_dead_ratio",
        Priority: prioTableRowsDeadRatio,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_n_dead_tup_perc", Name: "dead"},
        },
    }
    tableRowsCountChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_rows_count",
        Title:    "Table total rows",
        Units:    "rows",
        Fam:      "maintenance",
        Ctx:      "postgres.table_rows_count",
        Priority: prioTableRowsCount,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_n_live_tup", Name: "live"},
            {ID: "table_%s_db_%s_schema_%s_n_dead_tup", Name: "dead"},
        },
    }
    tableOpsRowsRateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_ops_rows_rate",
        Title:    "Table throughput",
        Units:    "rows/s",
        Fam:      "throughput",
        Ctx:      "postgres.table_ops_rows_rate",
        Priority: prioTableOpsRowsRate,
        Type:     module.Stacked,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_n_tup_ins", Name: "inserted", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_n_tup_del", Name: "deleted", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_n_tup_upd", Name: "updated", Algo: module.Incremental},
        },
    }
    tableOpsRowsHOTRatioChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_ops_rows_hot_ratio",
        Title:    "Table HOT updates ratio",
        Units:    "percentage",
        Fam:      "throughput",
        Ctx:      "postgres.table_ops_rows_hot_ratio",
        Priority: prioTableOpsRowsHOTRatio,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_n_tup_hot_upd_perc", Name: "hot"},
        },
    }
    tableOpsRowsHOTRateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_ops_rows_hot_rate",
        Title:    "Table HOT updates",
        Units:    "rows/s",
        Fam:      "throughput",
        Ctx:      "postgres.table_ops_rows_hot_rate",
        Priority: prioTableOpsRowsHOTRate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_n_tup_hot_upd", Name: "hot", Algo: module.Incremental},
        },
    }
    tableCacheIORatioChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_cache_io_ratio",
        Title:    "Table I/O cache miss ratio",
        Units:    "percentage",
        Fam:      "cache",
        Ctx:      "postgres.table_cache_io_ratio",
        Priority: prioTableCacheIORatio,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_heap_blks_read_perc", Name: "miss"},
        },
    }
    tableIORateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_io_rate",
        Title:    "Table I/O",
        Units:    "B/s",
        Fam:      "cache",
        Ctx:      "postgres.table_io_rate",
        Priority: prioTableIORate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_heap_blks_hit", Name: "memory", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_heap_blks_read", Name: "disk", Algo: module.Incremental},
        },
    }
    tableIndexCacheIORatioChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_index_cache_io_ratio",
        Title:    "Table index I/O cache miss ratio",
        Units:    "percentage",
        Fam:      "cache",
        Ctx:      "postgres.table_index_cache_io_ratio",
        Priority: prioTableIndexCacheIORatio,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_idx_blks_read_perc", Name: "miss", Algo: module.Incremental},
        },
    }
    tableIndexIORateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_index_io_rate",
        Title:    "Table index I/O",
        Units:    "B/s",
        Fam:      "cache",
        Ctx:      "postgres.table_index_io_rate",
        Priority: prioTableIndexIORate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_idx_blks_hit", Name: "memory", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_idx_blks_read", Name: "disk", Algo: module.Incremental},
        },
    }
    tableTOASCacheIORatioChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_toast_cache_io_ratio",
        Title:    "Table TOAST I/O cache miss ratio",
        Units:    "percentage",
        Fam:      "cache",
        Ctx:      "postgres.table_toast_cache_io_ratio",
        Priority: prioTableToastCacheIORatio,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_toast_blks_read_perc", Name: "miss", Algo: module.Incremental},
        },
    }
    tableTOASTIORateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_toast_io_rate",
        Title:    "Table TOAST I/O",
        Units:    "B/s",
        Fam:      "cache",
        Ctx:      "postgres.table_toast_io_rate",
        Priority: prioTableToastIORate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_toast_blks_hit", Name: "memory", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_toast_blks_read", Name: "disk", Algo: module.Incremental},
        },
    }
    tableTOASTIndexCacheIORatioChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_toast_index_cache_io_ratio",
        Title:    "Table TOAST index I/O cache miss ratio",
        Units:    "percentage",
        Fam:      "cache",
        Ctx:      "postgres.table_toast_index_cache_io_ratio",
        Priority: prioTableToastIndexCacheIORatio,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_tidx_blks_read_perc", Name: "miss", Algo: module.Incremental},
        },
    }
    tableTOASTIndexIORateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_toast_index_io_rate",
        Title:    "Table TOAST index I/O",
        Units:    "B/s",
        Fam:      "cache",
        Ctx:      "postgres.table_toast_index_io_rate",
        Priority: prioTableToastIndexIORate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_tidx_blks_hit", Name: "memory", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_tidx_blks_read", Name: "disk", Algo: module.Incremental},
        },
    }
    tableScansRateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_scans_rate",
        Title:    "Table scans",
        Units:    "scans/s",
        Fam:      "throughput",
        Ctx:      "postgres.table_scans_rate",
        Priority: prioTableScansRate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_idx_scan", Name: "index", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_seq_scan", Name: "sequential", Algo: module.Incremental},
        },
    }
    tableScansRowsRateChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_scans_rows_rate",
        Title:    "Table live rows fetched by scans",
        Units:    "rows/s",
        Fam:      "throughput",
        Ctx:      "postgres.table_scans_rows_rate",
        Priority: prioTableScansRowsRate,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_idx_tup_fetch", Name: "index", Algo: module.Incremental},
            {ID: "table_%s_db_%s_schema_%s_seq_tup_read", Name: "sequential", Algo: module.Incremental},
        },
    }
    tableAutoVacuumSinceTimeChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_autovacuum_since_time",
        Title:    "Table time since last auto VACUUM",
        Units:    "seconds",
        Fam:      "vacuum and analyze",
        Ctx:      "postgres.table_autovacuum_since_time",
        Priority: prioTableAutovacuumSinceTime,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_last_autovacuum_ago", Name: "time"},
        },
    }
    tableVacuumSinceTimeChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_vacuum_since_time",
        Title:    "Table time since last manual VACUUM",
        Units:    "seconds",
        Fam:      "vacuum and analyze",
        Ctx:      "postgres.table_vacuum_since_time",
        Priority: prioTableVacuumSinceTime,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_last_vacuum_ago", Name: "time"},
        },
    }
    tableAutoAnalyzeSinceTimeChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_autoanalyze_since_time",
        Title:    "Table time since last auto ANALYZE",
        Units:    "seconds",
        Fam:      "vacuum and analyze",
        Ctx:      "postgres.table_autoanalyze_since_time",
        Priority: prioTableAutoAnalyzeSinceTime,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_last_autoanalyze_ago", Name: "time"},
        },
    }
    tableAnalyzeSinceTimeChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_analyze_since_time",
        Title:    "Table time since last manual ANALYZE",
        Units:    "seconds",
        Fam:      "vacuum and analyze",
        Ctx:      "postgres.table_analyze_since_time",
        Priority: prioTableLastAnalyzeAgo,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_last_analyze_ago", Name: "time"},
        },
    }
    tableNullColumnsCountChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_null_columns_count",
        Title:    "Table null columns",
        Units:    "columns",
        Fam:      "maintenance",
        Ctx:      "postgres.table_null_columns_count",
        Priority: prioTableNullColumns,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_null_columns", Name: "null"},
        },
    }
    tableTotalSizeChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_total_size",
        Title:    "Table total size",
        Units:    "B",
        Fam:      "size",
        Ctx:      "postgres.table_total_size",
        Priority: prioTableTotalSize,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_total_size", Name: "size"},
        },
    }
    tableBloatSizePercChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_bloat_size_perc",
        Title:    "Table bloat size percentage",
        Units:    "percentage",
        Fam:      "bloat",
        Ctx:      "postgres.table_bloat_size_perc",
        Priority: prioTableBloatSizePerc,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_bloat_size_perc", Name: "bloat"},
        },
        Vars: module.Vars{
            {ID: "table_%s_db_%s_schema_%s_total_size", Name: "table_size"},
        },
    }
    tableBloatSizeChartTmpl = module.Chart{
        ID:       "table_%s_db_%s_schema_%s_bloat_size",
        Title:    "Table bloat size",
        Units:    "B",
        Fam:      "bloat",
        Ctx:      "postgres.table_bloat_size",
        Priority: prioTableBloatSize,
        Dims: module.Dims{
            {ID: "table_%s_db_%s_schema_%s_bloat_size", Name: "bloat"},
        },
    }
)

func newTableCharts(tbl *tableMetrics) *module.Charts {
    charts := tableChartsTmpl.Copy()

    if tbl.bloatSize == nil {
        _ = charts.Remove(tableBloatSizeChartTmpl.ID)
        _ = charts.Remove(tableBloatSizePercChartTmpl.ID)
    }

    for i, chart := range *charts {
        (*charts)[i] = newTableChart(chart, tbl)
    }

    return charts
}

func newTableChart(chart *module.Chart, tbl *tableMetrics) *module.Chart {
    chart = chart.Copy()
    chart.ID = fmt.Sprintf(chart.ID, tbl.name, tbl.db, tbl.schema)
    chart.Labels = []module.Label{
        {Key: "database", Value: tbl.db},
        {Key: "schema", Value: tbl.schema},
        {Key: "table", Value: tbl.name},
        {Key: "parent_table", Value: tbl.parentName},
    }
    for _, d := range chart.Dims {
        d.ID = fmt.Sprintf(d.ID, tbl.name, tbl.db, tbl.schema)
    }
    for _, v := range chart.Vars {
        v.ID = fmt.Sprintf(v.ID, tbl.name, tbl.db, tbl.schema)
    }
    return chart
}

func (p *Postgres) addNewTableCharts(tbl *tableMetrics) {
    charts := newTableCharts(tbl)
    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableLastAutoVacuumAgoChart(tbl *tableMetrics) {
    chart := newTableChart(tableAutoVacuumSinceTimeChartTmpl.Copy(), tbl)

    if err := p.Charts().Add(chart); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableLastVacuumAgoChart(tbl *tableMetrics) {
    chart := newTableChart(tableVacuumSinceTimeChartTmpl.Copy(), tbl)

    if err := p.Charts().Add(chart); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableLastAutoAnalyzeAgoChart(tbl *tableMetrics) {
    chart := newTableChart(tableAutoAnalyzeSinceTimeChartTmpl.Copy(), tbl)

    if err := p.Charts().Add(chart); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableLastAnalyzeAgoChart(tbl *tableMetrics) {
    chart := newTableChart(tableAnalyzeSinceTimeChartTmpl.Copy(), tbl)

    if err := p.Charts().Add(chart); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableIOChartsCharts(tbl *tableMetrics) {
    charts := module.Charts{
        newTableChart(tableCacheIORatioChartTmpl.Copy(), tbl),
        newTableChart(tableIORateChartTmpl.Copy(), tbl),
    }

    if err := p.Charts().Add(charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableIndexIOCharts(tbl *tableMetrics) {
    charts := module.Charts{
        newTableChart(tableIndexCacheIORatioChartTmpl.Copy(), tbl),
        newTableChart(tableIndexIORateChartTmpl.Copy(), tbl),
    }

    if err := p.Charts().Add(charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableTOASTIOCharts(tbl *tableMetrics) {
    charts := module.Charts{
        newTableChart(tableTOASCacheIORatioChartTmpl.Copy(), tbl),
        newTableChart(tableTOASTIORateChartTmpl.Copy(), tbl),
    }

    if err := p.Charts().Add(charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) addTableTOASTIndexIOCharts(tbl *tableMetrics) {
    charts := module.Charts{
        newTableChart(tableTOASTIndexCacheIORatioChartTmpl.Copy(), tbl),
        newTableChart(tableTOASTIndexIORateChartTmpl.Copy(), tbl),
    }

    if err := p.Charts().Add(charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) removeTableCharts(tbl *tableMetrics) {
    prefix := fmt.Sprintf("table_%s_db_%s_schema_%s", tbl.name, tbl.db, tbl.schema)
    for _, c := range *p.Charts() {
        if strings.HasPrefix(c.ID, prefix) {
            c.MarkRemove()
            c.MarkNotCreated()
        }
    }
}

var (
    indexChartsTmpl = module.Charts{
        indexSizeChartTmpl.Copy(),
        indexBloatSizePercChartTmpl.Copy(),
        indexBloatSizeChartTmpl.Copy(),
        indexUsageStatusChartTmpl.Copy(),
    }
    indexSizeChartTmpl = module.Chart{
        ID:       "index_%s_table_%s_db_%s_schema_%s_size",
        Title:    "Index size",
        Units:    "B",
        Fam:      "size",
        Ctx:      "postgres.index_size",
        Priority: prioIndexSize,
        Dims: module.Dims{
            {ID: "index_%s_table_%s_db_%s_schema_%s_size", Name: "size"},
        },
    }
    indexBloatSizePercChartTmpl = module.Chart{
        ID:       "index_%s_table_%s_db_%s_schema_%s_bloat_size_perc",
        Title:    "Index bloat size percentage",
        Units:    "percentage",
        Fam:      "bloat",
        Ctx:      "postgres.index_bloat_size_perc",
        Priority: prioIndexBloatSizePerc,
        Dims: module.Dims{
            {ID: "index_%s_table_%s_db_%s_schema_%s_bloat_size_perc", Name: "bloat"},
        },
        Vars: module.Vars{
            {ID: "index_%s_table_%s_db_%s_schema_%s_size", Name: "index_size"},
        },
    }
    indexBloatSizeChartTmpl = module.Chart{
        ID:       "index_%s_table_%s_db_%s_schema_%s_bloat_size",
        Title:    "Index bloat size",
        Units:    "B",
        Fam:      "bloat",
        Ctx:      "postgres.index_bloat_size",
        Priority: prioIndexBloatSize,
        Dims: module.Dims{
            {ID: "index_%s_table_%s_db_%s_schema_%s_bloat_size", Name: "bloat"},
        },
    }
    indexUsageStatusChartTmpl = module.Chart{
        ID:       "index_%s_table_%s_db_%s_schema_%s_usage_status",
        Title:    "Index usage status",
        Units:    "status",
        Fam:      "maintenance",
        Ctx:      "postgres.index_usage_status",
        Priority: prioIndexUsageStatus,
        Dims: module.Dims{
            {ID: "index_%s_table_%s_db_%s_schema_%s_usage_status_used", Name: "used"},
            {ID: "index_%s_table_%s_db_%s_schema_%s_usage_status_unused", Name: "unused"},
        },
    }
)

func (p *Postgres) addNewIndexCharts(idx *indexMetrics) {
    charts := indexChartsTmpl.Copy()

    if idx.bloatSize == nil {
        _ = charts.Remove(indexBloatSizeChartTmpl.ID)
        _ = charts.Remove(indexBloatSizePercChartTmpl.ID)
    }

    for _, chart := range *charts {
        chart.ID = fmt.Sprintf(chart.ID, idx.name, idx.table, idx.db, idx.schema)
        chart.Labels = []module.Label{
            {Key: "database", Value: idx.db},
            {Key: "schema", Value: idx.schema},
            {Key: "table", Value: idx.table},
            {Key: "parent_table", Value: idx.parentTable},
            {Key: "index", Value: idx.name},
        }
        for _, d := range chart.Dims {
            d.ID = fmt.Sprintf(d.ID, idx.name, idx.table, idx.db, idx.schema)
        }
        for _, v := range chart.Vars {
            v.ID = fmt.Sprintf(v.ID, idx.name, idx.table, idx.db, idx.schema)
        }
    }

    if err := p.Charts().Add(*charts...); err != nil {
        p.Warning(err)
    }
}

func (p *Postgres) removeIndexCharts(idx *indexMetrics) {
    prefix := fmt.Sprintf("index_%s_table_%s_db_%s_schema_%s", idx.name, idx.table, idx.db, idx.schema)
    for _, c := range *p.Charts() {
        if strings.HasPrefix(c.ID, prefix) {
            c.MarkRemove()
            c.MarkNotCreated()
        }
    }
}