Skip to content

Commit

Permalink
Add separate metrics for non-numeric fields
Browse files Browse the repository at this point in the history
Every report group (PV, VG, LV) had its own info metric where
non-numeric fields like `pv_attr` were reported as a label. Every unique
combination of labels produces a new time series. From
https://prometheus.io/docs/practices/naming/#labels:

> Remember that every unique combination of key-value label pairs
> represents a new time series, which can dramatically increase the
> amount of data stored. Do not use labels to store dimensions with high
> cardinality (many different label values), such as user IDs, email
> addresses, or other unbounded sets of values.

With this change the labels on the info metrics are reduced to names
(e.g. `pv_name` and `lv_full_name`). Everything else is moved to
separate metrics.

A command line flag, enabled by default, retains the labels on the info
metric.

Related to issue #31.
  • Loading branch information
hansmi committed Dec 10, 2024
1 parent 64c30dd commit ed6ce99
Show file tree
Hide file tree
Showing 26 changed files with 5,050 additions and 76 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ directly using [Go][golang] or [GoReleaser][goreleaser].
## Example metrics

```
lvm_pv_info{pv_fmt="lvm2",pv_name="/dev/sda1",pv_uuid="yc1zVe-…"} 1
lvm_pv_info{pv_fmt="lvm2",pv_name="/dev/sdb1",pv_uuid="WVIH97-…"} 1
lvm_pv_info{pv_name="/dev/sda1",pv_uuid="yc1zVe-…"} 1
lvm_pv_info{pv_name="/dev/sdb1",pv_uuid="WVIH97-…"} 1
lvm_pv_fmt{pv_fmt="lvm2",pv_uuid="yc1zVe-…"} 1
lvm_pv_fmt{pv_fmt="lvm2",pv_uuid="WVIH97-…"} 1
lvm_pv_free_bytes{pv_uuid="WVIH97-…"} 9.14358272e+08
lvm_pv_free_bytes{pv_uuid="yc1zVe-…"} 1.040187392e+09
Expand Down
6 changes: 5 additions & 1 deletion collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ func TestCollector(t *testing.T) {
{name: "issue30-lockargs"},
} {
t.Run(tc.name, func(t *testing.T) {
for _, enableLegacyInfoLabels := range []bool{false} {
for _, enableLegacyInfoLabels := range []bool{false, true} {
expectedName := tc.name

if enableLegacyInfoLabels {
expectedName += "-legacy"
}

t.Run(strconv.FormatBool(enableLegacyInfoLabels), func(t *testing.T) {
c := newCollector(enableLegacyInfoLabels)
c.load = func(ctx context.Context) (*lvmreport.ReportData, error) {
Expand Down
9 changes: 9 additions & 0 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ func fromNumeric(value string) (float64, error) {
return strconv.ParseFloat(value, 64)
}

type fieldFlag uint

const (
// Whether the field should be included as a label on an info metric.
asInfoLabel fieldFlag = 1 << iota
)

type field interface {
Name() string
MetricName() string
Expand All @@ -20,6 +27,8 @@ type textField struct {
fieldName string
desc string

flags fieldFlag

metricName string
}

Expand Down
73 changes: 57 additions & 16 deletions groupcollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,40 @@ func (f *groupField) collect(ch chan<- prometheus.Metric, rawValue string, keyVa
return nil
}

type groupTextField struct {
metricDesc *prometheus.Desc
}

func (f *groupTextField) collect(ch chan<- prometheus.Metric, rawValue string, keyValues []string) error {
labels := defaultStringSlicePool.get()
defer defaultStringSlicePool.put(labels)

labels = append(labels, keyValues...)
labels = append(labels, rawValue)

ch <- prometheus.MustNewConstMetric(f.metricDesc, prometheus.GaugeValue, 1, labels...)

return nil
}

type groupCollector struct {
name lvmreport.GroupName

infoDesc *prometheus.Desc
unknownDesc *prometheus.Desc

keyFields []string
textFields []string
numericFields map[string]*groupField
knownFields map[string]struct{}
keyFields []string
textFields map[string]*groupTextField
numericFields map[string]*groupField
infoLabelFields []string
knownFields map[string]struct{}
}

func newGroupCollector(enableLegacyInfoLabels bool, g *group) *groupCollector {
c := &groupCollector{
name: g.name,
unknownDesc: prometheus.NewDesc("unknown_field_count", "Fields reported by LVM not recognized by exporter", []string{"group", "details"}, nil),
textFields: map[string]*groupTextField{},
numericFields: map[string]*groupField{},
knownFields: map[string]struct{}{},
}
Expand All @@ -55,15 +73,18 @@ func newGroupCollector(enableLegacyInfoLabels bool, g *group) *groupCollector {
keyLabelNames = append(keyLabelNames, f.metricName)
}

infoLabelNames := slices.Clone(keyLabelNames)

for _, f := range g.textFields {
c.textFields = append(c.textFields, f.fieldName)
c.knownFields[f.fieldName] = struct{}{}
infoLabelNames = append(infoLabelNames, f.metricName)
}

c.infoDesc = prometheus.NewDesc(g.infoMetricName, "", infoLabelNames, nil)
if f.flags&asInfoLabel == 0 {
textLabelNames := slices.Clone(keyLabelNames)
textLabelNames = append(textLabelNames, f.metricName)

c.textFields[f.fieldName] = &groupTextField{
metricDesc: prometheus.NewDesc(f.metricName, f.desc, textLabelNames, nil),
}
}
}

for _, f := range g.numericFields {
info := &groupField{
Expand All @@ -77,13 +98,28 @@ func newGroupCollector(enableLegacyInfoLabels bool, g *group) *groupCollector {
c.knownFields[f.fieldName] = struct{}{}
}

infoLabelNames := slices.Clone(keyLabelNames)

for _, f := range g.textFields {
if enableLegacyInfoLabels || f.flags&asInfoLabel != 0 {
c.infoLabelFields = append(c.infoLabelFields, f.fieldName)
infoLabelNames = append(infoLabelNames, f.metricName)
}
}

c.infoDesc = prometheus.NewDesc(g.infoMetricName, "", infoLabelNames, nil)

return c
}

func (c *groupCollector) describe(ch chan<- *prometheus.Desc) {
ch <- c.infoDesc
ch <- c.unknownDesc

for _, info := range c.textFields {
ch <- info.metricDesc
}

for _, info := range c.numericFields {
ch <- info.metricDesc
}
Expand All @@ -103,26 +139,31 @@ func (c *groupCollector) collect(ch chan<- prometheus.Metric, data *lvmreport.Re

infoValues := slices.Clone(keyValues)

for _, name := range c.textFields {
for _, name := range c.infoLabelFields {
infoValues = append(infoValues, row[name])
}

ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1, infoValues...)

for fieldName, rawValue := range row {
if rawValue == "" {
continue
var collector interface {
collect(chan<- prometheus.Metric, string, []string) error
}

info, ok := c.numericFields[fieldName]
if !ok {
if info, ok := c.textFields[fieldName]; ok {
collector = info
} else if rawValue == "" {
continue
} else if info, ok := c.numericFields[fieldName]; ok {
collector = info
} else {
if _, ok := c.knownFields[fieldName]; !ok {
unknown[fieldName] = struct{}{}
}
continue
}

if err := info.collect(ch, rawValue, keyValues); err != nil {
if err := collector.collect(ch, rawValue, keyValues); err != nil {
allErrors.Append(fmt.Errorf("field %s: %w", fieldName, err))
continue
}
Expand Down
2 changes: 2 additions & 0 deletions lv.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ var lvGroup = &group{
{
fieldName: "lv_full_name",
metricName: "lv_full_name",
flags: asInfoLabel,
desc: "Full name of LV including its VG, namely VG/LV",
},
{
Expand Down Expand Up @@ -132,6 +133,7 @@ var lvGroup = &group{
{
fieldName: "lv_name",
metricName: "lv_name",
flags: asInfoLabel,
desc: "Name; LVs created for internal use are enclosed in brackets",
},
{
Expand Down
37 changes: 37 additions & 0 deletions pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"sync"
)

var defaultStringSlicePool = newStringSlicePool()

type stringSlicePool struct {
pool sync.Pool
}

func newStringSlicePool() *stringSlicePool {
return &stringSlicePool{
pool: sync.Pool{
New: func() any {
return []string(nil)
},
},
}
}

// get returns an empty string slice. The caller has ownership of the slice
// until the slice is put back into the pool.
func (p *stringSlicePool) get() []string {
return p.pool.Get().([]string)[:0]
}

func (p *stringSlicePool) put(s []string) {
// All elements must be accessible.
s = s[:cap(s)]

// Remove references to values.
clear(s)

p.pool.Put(s)
}
41 changes: 41 additions & 0 deletions pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"math/rand/v2"
"slices"
"strconv"
"testing"
)

func TestStringSlicePool(t *testing.T) {
p := newStringSlicePool()

for range 100 {
s := p.get()

if len(s) > 0 {
t.Errorf("get() returned non-empty slice: %q", s)
}

count := rand.IntN(100)

for i := range count {
s = append(s, strconv.Itoa(i))
}

switch rand.IntN(3) {
case 1:
s = s[:0]
case 2:
s = s[:count/2]
}

p.put(s)

if idx := slices.IndexFunc(s[:count], func(value string) bool {
return value != ""
}); idx != -1 {
t.Errorf("Slice was not cleared: %q", s[:count])
}
}
}
7 changes: 6 additions & 1 deletion pv.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ var pvGroup = &group{
textFields: []*textField{
{fieldName: "pv_attr", metricName: "pv_attr"},
{fieldName: "pv_fmt", metricName: "pv_fmt", desc: "Type of metadata"},
{fieldName: "pv_name", metricName: "pv_name", desc: "Name"},
{
fieldName: "pv_name",
metricName: "pv_name",
flags: asInfoLabel,
desc: "Name",
},
{fieldName: "pv_tags", metricName: "pv_tags"},
},
numericFields: []*numericField{
Expand Down
3 changes: 3 additions & 0 deletions testdata/group-collector-with-error.golden
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# TYPE lvm_m_count gauge
lvm_m_count{m_key1="2"} 22
lvm_m_count{m_key1="3"} 11.5
# HELP lvm_m_info1
# TYPE lvm_m_info1 gauge
lvm_m_info1{m_info1="info:2",m_key1="2"} 1
# HELP lvm_test_info
# TYPE lvm_test_info gauge
lvm_test_info{m_info1="",m_key1="1"} 1
Expand Down
48 changes: 48 additions & 0 deletions testdata/issue29-vdo-online-legacy.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# HELP lvm_lv_info
# TYPE lvm_lv_info gauge
lvm_lv_info{lv_active="",lv_allocation_policy="",lv_ancestors="",lv_attr="",lv_convert_lv="",lv_convert_lv_uuid="",lv_data_lv="",lv_data_lv_uuid="",lv_descendants="",lv_dm_path="",lv_full_ancestors="",lv_full_descendants="",lv_full_name="",lv_health_status="",lv_host="",lv_kernel_cache_policy="",lv_kernel_cache_settings="",lv_kernel_discards="",lv_kernel_metadata_format="",lv_kernel_read_ahead_bytes="",lv_layout="",lv_lockargs="",lv_metadata_lv="",lv_metadata_lv_uuid="",lv_mirror_log="",lv_mirror_log_uuid="",lv_modules="",lv_move_pv="",lv_move_pv_uuid="",lv_name="[vpool0_vdata]",lv_origin="",lv_origin_uuid="",lv_parent="",lv_path="",lv_permissions="",lv_pool_lv="",lv_pool_lv_uuid="",lv_raid_sync_action="",lv_raidintegritymode="",lv_role="",lv_tags="",lv_uuid="tHnHR1-bjg8-ycJX-eyhR-heEp-L4qs-S510dO",lv_vdo_compression_state="",lv_vdo_index_state="",lv_vdo_operating_mode="",lv_when_full=""} 1
lvm_lv_info{lv_active="",lv_allocation_policy="",lv_ancestors="",lv_attr="",lv_convert_lv="",lv_convert_lv_uuid="",lv_data_lv="",lv_data_lv_uuid="",lv_descendants="",lv_dm_path="",lv_full_ancestors="",lv_full_descendants="",lv_full_name="",lv_health_status="",lv_host="",lv_kernel_cache_policy="",lv_kernel_cache_settings="",lv_kernel_discards="",lv_kernel_metadata_format="",lv_kernel_read_ahead_bytes="",lv_layout="",lv_lockargs="",lv_metadata_lv="",lv_metadata_lv_uuid="",lv_mirror_log="",lv_mirror_log_uuid="",lv_modules="",lv_move_pv="",lv_move_pv_uuid="",lv_name="lv_data",lv_origin="",lv_origin_uuid="",lv_parent="",lv_path="",lv_permissions="",lv_pool_lv="",lv_pool_lv_uuid="",lv_raid_sync_action="",lv_raidintegritymode="",lv_role="",lv_tags="",lv_uuid="5UyOrx-zG6u-w7cF-G2Ny-HmVd-0lsb-4J6lG2",lv_vdo_compression_state="",lv_vdo_index_state="",lv_vdo_operating_mode="",lv_when_full=""} 1
lvm_lv_info{lv_active="",lv_allocation_policy="",lv_ancestors="",lv_attr="",lv_convert_lv="",lv_convert_lv_uuid="",lv_data_lv="",lv_data_lv_uuid="",lv_descendants="",lv_dm_path="",lv_full_ancestors="",lv_full_descendants="",lv_full_name="",lv_health_status="",lv_host="",lv_kernel_cache_policy="",lv_kernel_cache_settings="",lv_kernel_discards="",lv_kernel_metadata_format="",lv_kernel_read_ahead_bytes="",lv_layout="",lv_lockargs="",lv_metadata_lv="",lv_metadata_lv_uuid="",lv_mirror_log="",lv_mirror_log_uuid="",lv_modules="",lv_move_pv="",lv_move_pv_uuid="",lv_name="lv_db_backup",lv_origin="",lv_origin_uuid="",lv_parent="",lv_path="",lv_permissions="",lv_pool_lv="",lv_pool_lv_uuid="",lv_raid_sync_action="",lv_raidintegritymode="",lv_role="",lv_tags="",lv_uuid="r7Btja-nL3P-Co38-X2K4-DLWS-LMhT-LajcQa",lv_vdo_compression_state="online",lv_vdo_index_state="online",lv_vdo_operating_mode="normal",lv_when_full=""} 1
lvm_lv_info{lv_active="",lv_allocation_policy="",lv_ancestors="",lv_attr="",lv_convert_lv="",lv_convert_lv_uuid="",lv_data_lv="",lv_data_lv_uuid="",lv_descendants="",lv_dm_path="",lv_full_ancestors="",lv_full_descendants="",lv_full_name="",lv_health_status="",lv_host="",lv_kernel_cache_policy="",lv_kernel_cache_settings="",lv_kernel_discards="",lv_kernel_metadata_format="",lv_kernel_read_ahead_bytes="",lv_layout="",lv_lockargs="",lv_metadata_lv="",lv_metadata_lv_uuid="",lv_mirror_log="",lv_mirror_log_uuid="",lv_modules="",lv_move_pv="",lv_move_pv_uuid="",lv_name="root",lv_origin="",lv_origin_uuid="",lv_parent="",lv_path="",lv_permissions="",lv_pool_lv="",lv_pool_lv_uuid="",lv_raid_sync_action="",lv_raidintegritymode="",lv_role="",lv_tags="",lv_uuid="cqZnVQ-FUPG-btWz-3fff-cMX5-wViK-8MM3QY",lv_vdo_compression_state="",lv_vdo_index_state="",lv_vdo_operating_mode="",lv_when_full=""} 1
lvm_lv_info{lv_active="",lv_allocation_policy="",lv_ancestors="",lv_attr="",lv_convert_lv="",lv_convert_lv_uuid="",lv_data_lv="",lv_data_lv_uuid="",lv_descendants="",lv_dm_path="",lv_full_ancestors="",lv_full_descendants="",lv_full_name="",lv_health_status="",lv_host="",lv_kernel_cache_policy="",lv_kernel_cache_settings="",lv_kernel_discards="",lv_kernel_metadata_format="",lv_kernel_read_ahead_bytes="",lv_layout="",lv_lockargs="",lv_metadata_lv="",lv_metadata_lv_uuid="",lv_mirror_log="",lv_mirror_log_uuid="",lv_modules="",lv_move_pv="",lv_move_pv_uuid="",lv_name="swap",lv_origin="",lv_origin_uuid="",lv_parent="",lv_path="",lv_permissions="",lv_pool_lv="",lv_pool_lv_uuid="",lv_raid_sync_action="",lv_raidintegritymode="",lv_role="",lv_tags="",lv_uuid="lGs7Tn-1Fyv-jMHN-aniN-r8qk-duEI-B9Xl9e",lv_vdo_compression_state="",lv_vdo_index_state="",lv_vdo_operating_mode="",lv_when_full=""} 1
lvm_lv_info{lv_active="",lv_allocation_policy="",lv_ancestors="",lv_attr="",lv_convert_lv="",lv_convert_lv_uuid="",lv_data_lv="",lv_data_lv_uuid="",lv_descendants="",lv_dm_path="",lv_full_ancestors="",lv_full_descendants="",lv_full_name="",lv_health_status="",lv_host="",lv_kernel_cache_policy="",lv_kernel_cache_settings="",lv_kernel_discards="",lv_kernel_metadata_format="",lv_kernel_read_ahead_bytes="",lv_layout="",lv_lockargs="",lv_metadata_lv="",lv_metadata_lv_uuid="",lv_mirror_log="",lv_mirror_log_uuid="",lv_modules="",lv_move_pv="",lv_move_pv_uuid="",lv_name="vpool0",lv_origin="",lv_origin_uuid="",lv_parent="",lv_path="",lv_permissions="",lv_pool_lv="",lv_pool_lv_uuid="",lv_raid_sync_action="",lv_raidintegritymode="",lv_role="",lv_tags="",lv_uuid="dylifl-81AF-n2mG-kFKo-Ntxe-mnNQ-3eB7fF",lv_vdo_compression_state="online",lv_vdo_index_state="online",lv_vdo_operating_mode="normal",lv_when_full=""} 1
# HELP lvm_lv_vdo_compression_state For vdo pools, whether compression is running
# TYPE lvm_lv_vdo_compression_state gauge
lvm_lv_vdo_compression_state{lv_uuid="5UyOrx-zG6u-w7cF-G2Ny-HmVd-0lsb-4J6lG2",lv_vdo_compression_state=""} 1
lvm_lv_vdo_compression_state{lv_uuid="cqZnVQ-FUPG-btWz-3fff-cMX5-wViK-8MM3QY",lv_vdo_compression_state=""} 1
lvm_lv_vdo_compression_state{lv_uuid="dylifl-81AF-n2mG-kFKo-Ntxe-mnNQ-3eB7fF",lv_vdo_compression_state="online"} 1
lvm_lv_vdo_compression_state{lv_uuid="lGs7Tn-1Fyv-jMHN-aniN-r8qk-duEI-B9Xl9e",lv_vdo_compression_state=""} 1
lvm_lv_vdo_compression_state{lv_uuid="r7Btja-nL3P-Co38-X2K4-DLWS-LMhT-LajcQa",lv_vdo_compression_state="online"} 1
lvm_lv_vdo_compression_state{lv_uuid="tHnHR1-bjg8-ycJX-eyhR-heEp-L4qs-S510dO",lv_vdo_compression_state=""} 1
# HELP lvm_lv_vdo_index_state For vdo pools, state of index for deduplication
# TYPE lvm_lv_vdo_index_state gauge
lvm_lv_vdo_index_state{lv_uuid="5UyOrx-zG6u-w7cF-G2Ny-HmVd-0lsb-4J6lG2",lv_vdo_index_state=""} 1
lvm_lv_vdo_index_state{lv_uuid="cqZnVQ-FUPG-btWz-3fff-cMX5-wViK-8MM3QY",lv_vdo_index_state=""} 1
lvm_lv_vdo_index_state{lv_uuid="dylifl-81AF-n2mG-kFKo-Ntxe-mnNQ-3eB7fF",lv_vdo_index_state="online"} 1
lvm_lv_vdo_index_state{lv_uuid="lGs7Tn-1Fyv-jMHN-aniN-r8qk-duEI-B9Xl9e",lv_vdo_index_state=""} 1
lvm_lv_vdo_index_state{lv_uuid="r7Btja-nL3P-Co38-X2K4-DLWS-LMhT-LajcQa",lv_vdo_index_state="online"} 1
lvm_lv_vdo_index_state{lv_uuid="tHnHR1-bjg8-ycJX-eyhR-heEp-L4qs-S510dO",lv_vdo_index_state=""} 1
# HELP lvm_lv_vdo_operating_mode For vdo pools, its current operating mode
# TYPE lvm_lv_vdo_operating_mode gauge
lvm_lv_vdo_operating_mode{lv_uuid="5UyOrx-zG6u-w7cF-G2Ny-HmVd-0lsb-4J6lG2",lv_vdo_operating_mode=""} 1
lvm_lv_vdo_operating_mode{lv_uuid="cqZnVQ-FUPG-btWz-3fff-cMX5-wViK-8MM3QY",lv_vdo_operating_mode=""} 1
lvm_lv_vdo_operating_mode{lv_uuid="dylifl-81AF-n2mG-kFKo-Ntxe-mnNQ-3eB7fF",lv_vdo_operating_mode="normal"} 1
lvm_lv_vdo_operating_mode{lv_uuid="lGs7Tn-1Fyv-jMHN-aniN-r8qk-duEI-B9Xl9e",lv_vdo_operating_mode=""} 1
lvm_lv_vdo_operating_mode{lv_uuid="r7Btja-nL3P-Co38-X2K4-DLWS-LMhT-LajcQa",lv_vdo_operating_mode="normal"} 1
lvm_lv_vdo_operating_mode{lv_uuid="tHnHR1-bjg8-ycJX-eyhR-heEp-L4qs-S510dO",lv_vdo_operating_mode=""} 1
# HELP lvm_lv_vdo_saving_percent For vdo pools, percentage of saved space
# TYPE lvm_lv_vdo_saving_percent gauge
lvm_lv_vdo_saving_percent{lv_uuid="dylifl-81AF-n2mG-kFKo-Ntxe-mnNQ-3eB7fF"} 55.37
lvm_lv_vdo_saving_percent{lv_uuid="r7Btja-nL3P-Co38-X2K4-DLWS-LMhT-LajcQa"} 55.37
# HELP lvm_lv_vdo_used_size_bytes For vdo pools, currently used space
# TYPE lvm_lv_vdo_used_size_bytes gauge
lvm_lv_vdo_used_size_bytes{lv_uuid="dylifl-81AF-n2mG-kFKo-Ntxe-mnNQ-3eB7fF"} 2.78899963904e+12
lvm_lv_vdo_used_size_bytes{lv_uuid="r7Btja-nL3P-Co38-X2K4-DLWS-LMhT-LajcQa"} 2.78899963904e+12
# HELP lvm_unknown_field_count Fields reported by LVM not recognized by exporter
# TYPE lvm_unknown_field_count gauge
lvm_unknown_field_count{details="",group="lv"} 0
lvm_unknown_field_count{details="",group="pv"} 0
lvm_unknown_field_count{details="",group="vg"} 0
# HELP lvm_up Whether scrape was successful
# TYPE lvm_up gauge
lvm_up{status=""} 1
Loading

0 comments on commit ed6ce99

Please sign in to comment.