Skip to content
Open
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
5 changes: 5 additions & 0 deletions cmd/openstack-database-exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ var (
"glance.database-url",
"Glance database connection URL (oslo.db format)",
).Envar("GLANCE_DATABASE_URL").String()
ironicDatabaseURL = kingpin.Flag(
"ironic.database-url",
"Ironic database connection URL (oslo.db format)",
).Envar("IRONIC_DATABASE_URL").String()
keystoneDatabaseURL = kingpin.Flag(
"keystone.database-url",
"Keystone database connection URL (oslo.db format)",
Expand Down Expand Up @@ -73,6 +77,7 @@ func main() {
reg := collector.NewRegistry(collector.Config{
CinderDatabaseURL: *cinderDatabaseURL,
GlanceDatabaseURL: *glanceDatabaseURL,
IronicDatabaseURL: *ironicDatabaseURL,
KeystoneDatabaseURL: *keystoneDatabaseURL,
MagnumDatabaseURL: *magnumDatabaseURL,
ManilaDatabaseURL: *manilaDatabaseURL,
Expand Down
3 changes: 3 additions & 0 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/vexxhost/openstack_database_exporter/internal/collector/cinder"
"github.com/vexxhost/openstack_database_exporter/internal/collector/glance"
"github.com/vexxhost/openstack_database_exporter/internal/collector/ironic"
"github.com/vexxhost/openstack_database_exporter/internal/collector/keystone"
"github.com/vexxhost/openstack_database_exporter/internal/collector/magnum"
"github.com/vexxhost/openstack_database_exporter/internal/collector/manila"
Expand All @@ -22,6 +23,7 @@ const (
type Config struct {
CinderDatabaseURL string
GlanceDatabaseURL string
IronicDatabaseURL string
KeystoneDatabaseURL string
MagnumDatabaseURL string
ManilaDatabaseURL string
Expand All @@ -35,6 +37,7 @@ func NewRegistry(cfg Config, logger *slog.Logger) *prometheus.Registry {

cinder.RegisterCollectors(reg, cfg.CinderDatabaseURL, logger)
glance.RegisterCollectors(reg, cfg.GlanceDatabaseURL, logger)
ironic.RegisterCollectors(reg, cfg.IronicDatabaseURL, logger)
keystone.RegisterCollectors(reg, cfg.KeystoneDatabaseURL, logger)
magnum.RegisterCollectors(reg, cfg.MagnumDatabaseURL, logger)
manila.RegisterCollectors(reg, cfg.ManilaDatabaseURL, logger)
Expand Down
86 changes: 86 additions & 0 deletions internal/collector/ironic/baremetal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package ironic

import (
"context"
"database/sql"
"log/slog"

"github.com/prometheus/client_golang/prometheus"

ironicdb "github.com/vexxhost/openstack_database_exporter/internal/db/ironic"
)

// BaremetalCollector is the umbrella collector for Ironic baremetal metrics
type BaremetalCollector struct {
db *sql.DB
queries *ironicdb.Queries
logger *slog.Logger

// Single up metric for baremetal service
upMetric *prometheus.Desc

// Sub-collectors
nodesCollector *NodesCollector
}

// NewBaremetalCollector creates a new umbrella collector for Ironic baremetal service
func NewBaremetalCollector(db *sql.DB, logger *slog.Logger) *BaremetalCollector {
return &BaremetalCollector{
db: db,
queries: ironicdb.New(db),
logger: logger.With(
"namespace", Namespace,
"subsystem", Subsystem,
"collector", "baremetal",
),

upMetric: prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "up"),
"Whether the Ironic baremetal service is up",
nil,
nil,
),

// Initialize sub-collectors
nodesCollector: NewNodesCollector(db, logger),
}
}

// Describe implements prometheus.Collector
func (c *BaremetalCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.upMetric
c.nodesCollector.Describe(ch)
}

// Collect implements prometheus.Collector
func (c *BaremetalCollector) Collect(ch chan<- prometheus.Metric) {
ctx := context.Background()
up := float64(1)

// Test database connectivity by running a simple query
_, err := c.queries.GetNodeMetrics(ctx)
if err != nil {
c.logger.Error("failed to query Ironic database", "error", err)
up = 0
}

// Emit up metric
upMetric, err := prometheus.NewConstMetric(
c.upMetric,
prometheus.GaugeValue,
up,
)
if err != nil {
c.logger.Error("failed to create up metric", "error", err)
} else {
ch <- upMetric
}

// Only collect from sub-collectors if we're up
if up == 1 {
// Collect nodes metrics
if err := c.nodesCollector.Collect(ch); err != nil {
c.logger.Error("nodes collector failed", "error", err)
}
}
}
78 changes: 78 additions & 0 deletions internal/collector/ironic/baremetal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package ironic

import (
"database/sql"
"regexp"
"testing"

"github.com/DATA-DOG/go-sqlmock"
ironicdb "github.com/vexxhost/openstack_database_exporter/internal/db/ironic"
"github.com/vexxhost/openstack_database_exporter/internal/testutil"
)

func TestBaremetalCollector(t *testing.T) {
tests := []testutil.CollectorTestCase{
{
Name: "successful collection",
SetupMock: func(mock sqlmock.Sqlmock) {
// Mock the up check query
rows := sqlmock.NewRows([]string{
"uuid", "name", "power_state", "provision_state", "maintenance",
"resource_class", "console_enabled", "retired", "retired_reason",
}).AddRow(
"550e8400-e29b-41d4-a716-446655440000", "node-1", "power on", "active", false,
"baremetal", true, false, "",
)
mock.ExpectQuery(regexp.QuoteMeta(ironicdb.GetNodeMetrics)).WillReturnRows(rows)

// Mock the nodes collector query
rows2 := sqlmock.NewRows([]string{
"uuid", "name", "power_state", "provision_state", "maintenance",
"resource_class", "console_enabled", "retired", "retired_reason",
}).AddRow(
"550e8400-e29b-41d4-a716-446655440000", "node-1", "power on", "active", false,
"baremetal", true, false, "",
)
mock.ExpectQuery(regexp.QuoteMeta(ironicdb.GetNodeMetrics)).WillReturnRows(rows2)
},
ExpectedMetrics: `# HELP openstack_ironic_node Ironic node status
# TYPE openstack_ironic_node gauge
openstack_ironic_node{console_enabled="true",id="550e8400-e29b-41d4-a716-446655440000",maintenance="false",name="node-1",power_state="power on",provision_state="active",resource_class="baremetal",retired="false",retired_reason=""} 1
# HELP openstack_ironic_up Whether the Ironic baremetal service is up
# TYPE openstack_ironic_up gauge
openstack_ironic_up 1
`,
},
{
Name: "database connection failure",
SetupMock: func(mock sqlmock.Sqlmock) {
// Mock the up check query to fail
mock.ExpectQuery(regexp.QuoteMeta(ironicdb.GetNodeMetrics)).WillReturnError(sql.ErrConnDone)
},
ExpectedMetrics: `# HELP openstack_ironic_up Whether the Ironic baremetal service is up
# TYPE openstack_ironic_up gauge
openstack_ironic_up 0
`,
},
{
Name: "nodes collector failure but service up",
SetupMock: func(mock sqlmock.Sqlmock) {
// Mock the up check query to succeed
rows := sqlmock.NewRows([]string{
"uuid", "name", "power_state", "provision_state", "maintenance",
"resource_class", "console_enabled", "retired", "retired_reason",
})
mock.ExpectQuery(regexp.QuoteMeta(ironicdb.GetNodeMetrics)).WillReturnRows(rows)

// Mock the nodes collector query to fail
mock.ExpectQuery(regexp.QuoteMeta(ironicdb.GetNodeMetrics)).WillReturnError(sql.ErrConnDone)
},
ExpectedMetrics: `# HELP openstack_ironic_up Whether the Ironic baremetal service is up
# TYPE openstack_ironic_up gauge
openstack_ironic_up 1
`,
},
}

testutil.RunCollectorTests(t, tests, NewBaremetalCollector)
}
32 changes: 32 additions & 0 deletions internal/collector/ironic/ironic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ironic

import (
"log/slog"

"github.com/prometheus/client_golang/prometheus"
"github.com/vexxhost/openstack_database_exporter/internal/db"
)

// Namespace and subsystem constants
const (
Namespace = "openstack"
Subsystem = "ironic"
)

// RegisterCollectors registers all Ironic collectors with the given database and logger
func RegisterCollectors(registry *prometheus.Registry, databaseURL string, logger *slog.Logger) {
if databaseURL == "" {
logger.Info("Collector not loaded", "service", "ironic", "reason", "database URL not configured")
return
}

conn, err := db.Connect(databaseURL)
if err != nil {
logger.Error("Failed to connect to database", "service", "ironic", "error", err)
return
}

registry.MustRegister(NewBaremetalCollector(conn, logger))

logger.Info("Registered collectors", "service", "ironic")
}
119 changes: 119 additions & 0 deletions internal/collector/ironic/nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package ironic

import (
"context"
"database/sql"
"log/slog"

"github.com/prometheus/client_golang/prometheus"
ironicdb "github.com/vexxhost/openstack_database_exporter/internal/db/ironic"
)

// NodesCollector collects metrics about Ironic nodes
type NodesCollector struct {
db *sql.DB
queries *ironicdb.Queries
logger *slog.Logger

// Individual node metrics
nodeMetric *prometheus.Desc
}

// NewNodesCollector creates a new NodesCollector
func NewNodesCollector(db *sql.DB, logger *slog.Logger) *NodesCollector {
return &NodesCollector{
db: db,
queries: ironicdb.New(db),
logger: logger.With(
"namespace", Namespace,
"subsystem", Subsystem,
"collector", "nodes",
),

nodeMetric: prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "node"),
"Ironic node status",
[]string{
"id", "name", "power_state", "provision_state",
"resource_class", "maintenance", "console_enabled", "retired", "retired_reason",
},
nil,
),
}
}

// Describe implements prometheus.Collector
func (c *NodesCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.nodeMetric
}

// Collect implements prometheus.Collector
func (c *NodesCollector) Collect(ch chan<- prometheus.Metric) error {
ctx := context.Background()

nodes, err := c.queries.GetNodeMetrics(ctx)
if err != nil {
c.logger.Error("failed to get node metrics", "error", err)
return err
}

for _, node := range nodes {
// Individual node status metric
maintenance := "false"
if node.Maintenance.Valid && node.Maintenance.Bool {
maintenance = "true"
}

powerState := "unknown"
if node.PowerState.Valid {
powerState = node.PowerState.String
}

provisionState := "unknown"
if node.ProvisionState.Valid {
provisionState = node.ProvisionState.String
}

resourceClass := "unknown"
if node.ResourceClass.Valid {
resourceClass = node.ResourceClass.String
}

consoleEnabled := "false"
if node.ConsoleEnabled.Valid && node.ConsoleEnabled.Bool {
consoleEnabled = "true"
}

retired := "false"
if node.Retired.Valid && node.Retired.Bool {
retired = "true"
}

retiredReason := node.RetiredReason

name := ""
if node.Name.Valid {
name = node.Name.String
}

nodeUUID := ""
if node.Uuid.Valid {
nodeUUID = node.Uuid.String
}

// Emit individual node metric
metric, err := prometheus.NewConstMetric(
c.nodeMetric,
prometheus.GaugeValue,
1,
nodeUUID, name, powerState, provisionState, resourceClass, maintenance, consoleEnabled, retired, retiredReason,
)
if err != nil {
c.logger.Error("failed to create node metric", "error", err)
continue
}
ch <- metric
}

return nil
}
31 changes: 31 additions & 0 deletions internal/db/ironic/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading