Skip to content

Add arbitrary labels on a per-database basis #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,12 @@ databases:
## Oracle Database Connection pool minimum size
# poolMinConnections: 15
## Arbitrary labels to add to each metric scraped from this database
# labels:
# label_name1: label_value1
# label_name2: label_value2
metrics:
## How often to scrape metrics. If not provided, metrics will be scraped on request.
# scrapeInterval: 15s
Expand Down Expand Up @@ -791,6 +797,16 @@ databases:
# poolMaxConnections: 15
## Oracle Database Connection pool minimum size
# poolMinConnections: 15
### Arbitrary labels to add to each metric scraped from this database
## Any labels configured for one database will be added to metrics from
## every database, because the same metric names must always have the same
## full labelset. If the label isn't set for a particular database, then it
## will just be set to an empty string.
# labels:
# label_name1: label_value1
# label_name2: label_value2
db2:
## Database username
username: ${DB2_USERNAME}
Expand Down Expand Up @@ -828,6 +844,15 @@ databases:
## Oracle Database Connection pool minimum size
# poolMinConnections: 15
### Arbitrary labels to add to each metric scraped from this database
## Any labels configured for one database will be added to metrics from
## every database, because the same metric names must always have the same
## full labelset. If the label isn't set for a particular database, then it
## will just be set to an empty string.
# labels:
# label_name1: label_value1
# label_name2: label_value2
metrics:
## How often to scrape metrics. If not provided, metrics will be scraped on request.
# scrapeInterval: 15s
Expand Down
37 changes: 33 additions & 4 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"io"
"log/slog"
"os"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -50,6 +51,20 @@ func maskDsn(dsn string) string {
func NewExporter(logger *slog.Logger, m *MetricsConfiguration) *Exporter {
var databases []*Database
wg := &sync.WaitGroup{}

var allConstLabels []string
// All the metrics of the same name need to have the same set of labels
// If a label is set for a particular database, it must be included also
// in the same metrics collected from other databases. It will just be
// set to a blank value.
for _, dbconfig := range m.Databases {
for label, _ := range dbconfig.Labels {
if (!slices.Contains(allConstLabels, label)) {
allConstLabels = append(allConstLabels, label)
}
}
}

for dbname, dbconfig := range m.Databases {
logger.Info("Initializing database", "database", dbname)
database := NewDatabase(logger, dbname, dbconfig)
Expand Down Expand Up @@ -91,12 +106,25 @@ func NewExporter(logger *slog.Logger, m *MetricsConfiguration) *Exporter {
MetricsConfiguration: m,
databases: databases,
lastScraped: map[string]*time.Time{},
allConstLabels: allConstLabels,
}
e.metricsToScrape = e.DefaultMetrics()

return e
}

func (e *Exporter) constLabels() map[string]string {
// All the metrics of the same name need to have the same labels
// If a label is set for a particular database, it must be included also
// in the same metrics collected from other databases. It will just be
// set to a blank value.
labels := map[string]string{}
for _, label := range e.allConstLabels {
labels[label] = ""
}
return labels
}

// Describe describes all the metrics exported by the Oracle DB exporter.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
// We cannot know in advance what metrics the exporter will generate
Expand Down Expand Up @@ -148,8 +176,8 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
ch <- e.error
e.scrapeErrors.Collect(ch)
for _, db := range e.databases {
ch <- db.DBTypeMetric()
ch <- db.UpMetric()
ch <- db.DBTypeMetric(e.constLabels())
ch <- db.UpMetric(e.constLabels())
}
}

Expand Down Expand Up @@ -203,7 +231,7 @@ func (e *Exporter) scheduledScrape(tick *time.Time) {
metricCh <- e.error
e.scrapeErrors.Collect(metricCh)
for _, db := range e.databases {
metricCh <- db.UpMetric()
metricCh <- db.UpMetric(e.constLabels())
}
close(metricCh)
wg.Wait()
Expand Down Expand Up @@ -399,7 +427,8 @@ func (e *Exporter) scrapeGenericValues(d *Database, ch chan<- prometheus.Metric,
metricsDesc map[string]string, metricsType map[string]string, metricsBuckets map[string]map[string]string,
fieldToAppend string, ignoreZeroResult bool, request string, queryTimeout time.Duration) error {
metricsCount := 0
constLabels := d.constLabels()
constLabels := d.constLabels(e.constLabels())
e.logger.Debug("labels", constLabels)
genericParser := func(row map[string]string) error {
// Construct labels value
labelsValues := []string{}
Expand Down
1 change: 1 addition & 0 deletions collector/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type DatabaseConfig struct {
URL string `yaml:"url"`
ConnectConfig `yaml:",inline"`
Vault *VaultConfig `yaml:"vault,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
}

type ConnectConfig struct {
Expand Down
28 changes: 16 additions & 12 deletions collector/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@ import (
"time"
)

func (d *Database) UpMetric() prometheus.Metric {
func (d *Database) UpMetric(exporterLabels map[string]string) prometheus.Metric {
desc := prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Whether the Oracle database server is up.",
nil,
d.constLabels(),
d.constLabels(exporterLabels),
)
return prometheus.MustNewConstMetric(desc,
prometheus.GaugeValue,
d.Up,
)
}

func (d *Database) DBTypeMetric() prometheus.Metric {
func (d *Database) DBTypeMetric(exporterLabels map[string]string) prometheus.Metric {
desc := prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "dbtype"),
"Type of database the exporter is connected to (0=non-CDB, 1=CDB, >1=PDB).",
nil,
d.constLabels(),
d.constLabels(exporterLabels),
)
return prometheus.MustNewConstMetric(desc,
prometheus.GaugeValue,
Expand Down Expand Up @@ -65,20 +65,24 @@ func (d *Database) ping(logger *slog.Logger) error {
return nil
}

func (d *Database) constLabels() map[string]string {
return map[string]string{
Copy link
Member

@anders-swanson anders-swanson Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be a function of the exporter now that it involves dynamic labels beyond what a given database has

"database": d.Name,
func (d *Database) constLabels(labels map[string]string) map[string]string {
labels["database"] = d.Name

// configured per-database labels added to constLabels
for label, value := range d.Config.Labels {
labels[label] = value
}
return labels
}

func NewDatabase(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) *Database {
db, dbtype := connect(logger, dbname, dbconfig)
return &Database{
Name: dbname,
Up: 0,
Session: db,
Type: dbtype,
Config: dbconfig,
Name: dbname,
Up: 0,
Session: db,
Type: dbtype,
Config: dbconfig,
}
}

Expand Down
11 changes: 6 additions & 5 deletions collector/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ type Exporter struct {
databases []*Database
logger *slog.Logger
lastScraped map[string]*time.Time
allConstLabels []string
}

type Database struct {
Name string
Up float64
Session *sql.DB
Type float64
Config DatabaseConfig
Name string
Up float64
Session *sql.DB
Type float64
Config DatabaseConfig
}

type Config struct {
Expand Down
11 changes: 11 additions & 0 deletions example-config-multi-database.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ databases:
# poolMaxConnections: 15
## Oracle Database Connection pool minimum size
# poolMinConnections: 15

## Arbitrary labels to add to each metric scraped from this database
# labels:
# label_name1: label_value1
# label_name2: label_value2

db2:
## Database username
username: ${DB2_USERNAME}
Expand Down Expand Up @@ -82,6 +88,11 @@ databases:
## Oracle Database Connection pool minimum size
# poolMinConnections: 15

## Arbitrary labels to add to each metric scraped from this database
# labels:
# label_name1: label_value1
# label_name2: label_value2

metrics:
## How often to scrape metrics. If not provided, metrics will be scraped on request.
# scrapeInterval: 15s
Expand Down