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
10 changes: 10 additions & 0 deletions cmd/openstack-database-exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ var (
"placement.database-url",
"Placement database connection URL (oslo.db format)",
).Envar("PLACEMENT_DATABASE_URL").String()
novaDatabaseURL = kingpin.Flag(
"nova.database-url",
"Nova database connection URL (oslo.db format)",
).Envar("NOVA_DATABASE_URL").String()
novaAPIDatabaseURL = kingpin.Flag(
"nova-api.database-url",
"Nova API database connection URL (oslo.db format)",
).Envar("NOVA_API_DATABASE_URL").String()
)

func main() {
Expand All @@ -79,6 +87,8 @@ func main() {
NeutronDatabaseURL: *neutronDatabaseURL,
OctaviaDatabaseURL: *octaviaDatabaseURL,
PlacementDatabaseURL: *placementDatabaseURL,
NovaDatabaseURL: *novaDatabaseURL,
NovaAPIDatabaseURL: *novaAPIDatabaseURL,
}, logger)

http.Handle(*metricsPath, promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
Expand Down
4 changes: 4 additions & 0 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/vexxhost/openstack_database_exporter/internal/collector/magnum"
"github.com/vexxhost/openstack_database_exporter/internal/collector/manila"
"github.com/vexxhost/openstack_database_exporter/internal/collector/neutron"
"github.com/vexxhost/openstack_database_exporter/internal/collector/nova"
"github.com/vexxhost/openstack_database_exporter/internal/collector/octavia"
"github.com/vexxhost/openstack_database_exporter/internal/collector/placement"
)
Expand All @@ -28,6 +29,8 @@ type Config struct {
NeutronDatabaseURL string
OctaviaDatabaseURL string
PlacementDatabaseURL string
NovaDatabaseURL string
NovaAPIDatabaseURL string
}

func NewRegistry(cfg Config, logger *slog.Logger) *prometheus.Registry {
Expand All @@ -39,6 +42,7 @@ func NewRegistry(cfg Config, logger *slog.Logger) *prometheus.Registry {
magnum.RegisterCollectors(reg, cfg.MagnumDatabaseURL, logger)
manila.RegisterCollectors(reg, cfg.ManilaDatabaseURL, logger)
neutron.RegisterCollectors(reg, cfg.NeutronDatabaseURL, logger)
nova.RegisterCollectors(reg, cfg.NovaDatabaseURL, cfg.NovaAPIDatabaseURL, cfg.PlacementDatabaseURL, logger)
octavia.RegisterCollectors(reg, cfg.OctaviaDatabaseURL, logger)
placement.RegisterCollectors(reg, cfg.PlacementDatabaseURL, logger)

Expand Down
113 changes: 113 additions & 0 deletions internal/collector/nova/compute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package nova

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

"github.com/prometheus/client_golang/prometheus"
novadb "github.com/vexxhost/openstack_database_exporter/internal/db/nova"
novaapidb "github.com/vexxhost/openstack_database_exporter/internal/db/nova_api"
placementdb "github.com/vexxhost/openstack_database_exporter/internal/db/placement"
)

var (
novaUpDesc = prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "up"),
"up",
nil,
nil,
)
)

type ComputeCollector struct {
novaDB *sql.DB
novaApiDB *sql.DB
placementDB *sql.DB
logger *slog.Logger
servicesCollector *ServicesCollector
flavorsCollector *FlavorsCollector
quotasCollector *QuotasCollector
limitsCollector *LimitsCollector
computeNodesCollector *ComputeNodesCollector
serverCollector *ServerCollector
}

func NewComputeCollector(novaDB, novaApiDB, placementDB *sql.DB, logger *slog.Logger) *ComputeCollector {
novaQueries := novadb.New(novaDB)
novaApiQueries := novaapidb.New(novaApiDB)

var placementQueries *placementdb.Queries
if placementDB != nil {
placementQueries = placementdb.New(placementDB)
}

return &ComputeCollector{
novaDB: novaDB,
novaApiDB: novaApiDB,
placementDB: placementDB,
logger: logger,
servicesCollector: NewServicesCollector(logger, novaQueries, novaApiQueries),
flavorsCollector: NewFlavorsCollector(logger, novaQueries, novaApiQueries),
quotasCollector: NewQuotasCollector(logger, novaQueries, novaApiQueries, placementQueries),
limitsCollector: NewLimitsCollector(logger, novaQueries, novaApiQueries, placementQueries),
computeNodesCollector: NewComputeNodesCollector(logger, novaQueries, novaApiQueries),
serverCollector: NewServerCollector(logger, novaQueries, novaApiQueries),
}
}

func (c *ComputeCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- novaUpDesc
c.servicesCollector.Describe(ch)
c.flavorsCollector.Describe(ch)
c.quotasCollector.Describe(ch)
c.limitsCollector.Describe(ch)
c.computeNodesCollector.Describe(ch)
c.serverCollector.Describe(ch)
}

func (c *ComputeCollector) Collect(ch chan<- prometheus.Metric) {
// Track if any sub-collector fails
var hasError bool

// Collect metrics from all sub-collectors
if err := c.servicesCollector.Collect(ch); err != nil {
c.logger.Error("Services collector failed", "error", err)
hasError = true
}

if err := c.flavorsCollector.Collect(ch); err != nil {
c.logger.Error("Flavors collector failed", "error", err)
hasError = true
}

if err := c.quotasCollector.Collect(ch); err != nil {
c.logger.Error("Quotas collector failed", "error", err)
hasError = true
}

if err := c.limitsCollector.Collect(ch); err != nil {
c.logger.Error("Limits collector failed", "error", err)
hasError = true
}

if err := c.computeNodesCollector.Collect(ch); err != nil {
c.logger.Error("Compute nodes collector failed", "error", err)
hasError = true
}

if err := c.serverCollector.Collect(ch); err != nil {
c.logger.Error("Server collector failed", "error", err)
hasError = true
}

// Emit single up metric based on overall success/failure
upValue := float64(1)
if hasError {
upValue = 0
}
ch <- prometheus.MustNewConstMetric(
novaUpDesc,
prometheus.GaugeValue,
upValue,
)
}
202 changes: 202 additions & 0 deletions internal/collector/nova/compute_nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package nova

import (
"context"
"log/slog"
"strings"

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

// ComputeNodesCollector collects metrics about Nova compute nodes
type ComputeNodesCollector struct {
logger *slog.Logger
novaDB *nova.Queries
novaAPIDB *nova_api.Queries
computeNodeMetrics map[string]*prometheus.Desc
}

// NewComputeNodesCollector creates a new compute nodes collector
func NewComputeNodesCollector(logger *slog.Logger, novaDB *nova.Queries, novaAPIDB *nova_api.Queries) *ComputeNodesCollector {
return &ComputeNodesCollector{
logger: logger.With(
"namespace", Namespace,
"subsystem", Subsystem,
"collector", "compute_nodes",
),
novaDB: novaDB,
novaAPIDB: novaAPIDB,
computeNodeMetrics: map[string]*prometheus.Desc{
"current_workload": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "current_workload"),
"current_workload",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"free_disk_bytes": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "free_disk_bytes"),
"free_disk_bytes",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"local_storage_available_bytes": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "local_storage_available_bytes"),
"local_storage_available_bytes",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"local_storage_used_bytes": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "local_storage_used_bytes"),
"local_storage_used_bytes",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"memory_available_bytes": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "memory_available_bytes"),
"memory_available_bytes",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"memory_used_bytes": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "memory_used_bytes"),
"memory_used_bytes",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"running_vms": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "running_vms"),
"running_vms",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"vcpus_available": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "vcpus_available"),
"vcpus_available",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
"vcpus_used": prometheus.NewDesc(
prometheus.BuildFQName(Namespace, Subsystem, "vcpus_used"),
"vcpus_used",
[]string{"aggregates", "availability_zone", "hostname"},
nil,
),
},
}
}

// Describe implements the prometheus.Collector interface
func (c *ComputeNodesCollector) Describe(ch chan<- *prometheus.Desc) {
for _, desc := range c.computeNodeMetrics {
ch <- desc
}
}

// Collect implements the prometheus.Collector interface
func (c *ComputeNodesCollector) Collect(ch chan<- prometheus.Metric) error {
return c.collectComputeNodeMetrics(ch)
}

func (c *ComputeNodesCollector) collectComputeNodeMetrics(ch chan<- prometheus.Metric) error {
computeNodes, err := c.novaDB.GetComputeNodes(context.Background())
if err != nil {
return err
}

// Get aggregates info for compute nodes
aggregates, err := c.novaAPIDB.GetAggregateHosts(context.Background())
if err != nil {
c.logger.Error("Failed to get aggregate hosts", "error", err)
}

// Build a map of hostname -> aggregates
hostAggregates := make(map[string][]string)
for _, agg := range aggregates {
hostname := agg.Host.String
if hostname != "" {
hostAggregates[hostname] = append(hostAggregates[hostname], agg.AggregateName.String)
}
}

for _, node := range computeNodes {
hostname := node.HypervisorHostname.String
if hostname == "" {
continue
}

// Get aggregates for this host
var aggregatesStr string
if aggList, exists := hostAggregates[hostname]; exists {
aggregatesStr = strings.Join(aggList, ",")
}

availabilityZone := "" // Compute nodes don't have direct AZ assignment

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["current_workload"],
prometheus.GaugeValue,
float64(node.CurrentWorkload.Int32),
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["free_disk_bytes"],
prometheus.GaugeValue,
float64(node.FreeDiskGb.Int32)*1024*1024*1024,
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["local_storage_available_bytes"],
prometheus.GaugeValue,
float64(node.LocalGb-node.LocalGbUsed)*1024*1024*1024,
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["local_storage_used_bytes"],
prometheus.GaugeValue,
float64(node.LocalGbUsed)*1024*1024*1024,
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["memory_available_bytes"],
prometheus.GaugeValue,
float64(node.MemoryMb-node.MemoryMbUsed)*1024*1024,
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["memory_used_bytes"],
prometheus.GaugeValue,
float64(node.MemoryMbUsed)*1024*1024,
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["running_vms"],
prometheus.GaugeValue,
float64(node.RunningVms.Int32),
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["vcpus_available"],
prometheus.GaugeValue,
float64(node.Vcpus-node.VcpusUsed),
aggregatesStr, availabilityZone, hostname,
)

ch <- prometheus.MustNewConstMetric(
c.computeNodeMetrics["vcpus_used"],
prometheus.GaugeValue,
float64(node.VcpusUsed),
aggregatesStr, availabilityZone, hostname,
)
}

return nil
}
Loading
Loading