From 545418007993799f5fd8124724b8dd4938498179 Mon Sep 17 00:00:00 2001 From: Freerk-Ole Zakfeld Date: Tue, 14 Jan 2025 18:47:23 +0100 Subject: [PATCH 1/2] Add Nova Trait exporter --- TODO.md | 9 +++-- exporters/nova_trait.go | 78 ++++++++++++++++++++++++++++++++++++ exporters/nova_trait_test.go | 47 ++++++++++++++++++++++ main.go | 19 ++++++--- 4 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 exporters/nova_trait.go create mode 100644 exporters/nova_trait_test.go diff --git a/TODO.md b/TODO.md index 7c5ac83..773f002 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,6 @@ -Neutron (number of Provider Network Fixed IPs) -Swift/S3 (Storage in GiB) -Manila (Storage in GiB) +Volume types + +``` +select vl.project_id, vt.name, sum(vl.size) from volumes vl left join volume_types vt on vl.volume_type_id = vt.id group by project_id, volume_type_id; +openstack_project_volume_size_gb{volume_type="SSD"} +``` diff --git a/exporters/nova_trait.go b/exporters/nova_trait.go new file mode 100644 index 0000000..8485a04 --- /dev/null +++ b/exporters/nova_trait.go @@ -0,0 +1,78 @@ +package exporters + +import ( + "database/sql" + "log" + "strings" + + "github.com/prometheus/client_golang/prometheus" +) + +type NovaTraitUsageExporter struct { + db *sql.DB + trait string + vcpus *prometheus.Desc + instances *prometheus.Desc +} + +func NewNovaTraitUsageExporter(db *sql.DB, trait string) (*NovaTraitUsageExporter, error) { + return &NovaTraitUsageExporter{ + db: db, + trait: trait, + vcpus: prometheus.NewDesc( + "openstack_project_vcpus_trait__"+strings.ToLower(trait), + "Total number of vcpus per OpenStack project for instances with image trait "+trait, + []string{"project_id"}, nil, + ), + instances: prometheus.NewDesc( + "openstack_project_instances_trait__"+strings.ToLower(trait), + "Total number of instances per OpenStack project with image trait "+trait, + []string{"project_id"}, nil, + ), + }, nil +} + +func (e *NovaTraitUsageExporter) Describe(ch chan<- *prometheus.Desc) { + ch <- e.vcpus + ch <- e.instances +} + +func (e *NovaTraitUsageExporter) Collect(ch chan<- prometheus.Metric) { + e.collectMetrics(ch) +} + +func (e *NovaTraitUsageExporter) collectMetrics(ch chan<- prometheus.Metric) { + rows, err := e.db.Query("SELECT i.project_id AS project_id, COUNT(i.id) AS total_instances, SUM(vcpus) AS total_vcpus FROM instances i INNER JOIN instance_system_metadata m on i.uuid = m.instance_uuid WHERE deleted = 0 AND m.key = 'image_trait:?' and m.value = 'required' GROUP BY project_id", e.trait) + if err != nil { + log.Println("Error querying Nova database:", err) + return + } + defer rows.Close() + + for rows.Next() { + var projectID string + var totalVcpus float64 + var totalInstances float64 + if err := rows.Scan(&projectID, &totalVcpus, &totalInstances); err != nil { + log.Println("Error scanning Nova row:", err) + continue + } + + ch <- prometheus.MustNewConstMetric( + e.vcpus, + prometheus.GaugeValue, + totalVcpus, + projectID, + ) + + ch <- prometheus.MustNewConstMetric( + e.instances, + prometheus.GaugeValue, + totalInstances, + projectID, + ) + } + if err := rows.Err(); err != nil { + log.Println("Error in Nova result set:", err) + } +} diff --git a/exporters/nova_trait_test.go b/exporters/nova_trait_test.go new file mode 100644 index 0000000..2795039 --- /dev/null +++ b/exporters/nova_trait_test.go @@ -0,0 +1,47 @@ +package exporters + +import ( + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +func TestNovaTraitUsageExporter(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + rows := sqlmock.NewRows([]string{"project_id", "total_instances", "total_vcpus"}). + AddRow("c352b0ed-30ca-4634-9c2d-1947efc29096", 0, 0). + AddRow("6ee08ba2-2ca1-4c91-b139-4bf0dbaa4096", 4, 5) + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + exporter, err := NewNovaTraitUsageExporter(db, "CUSTOM_TRAIT") + if err != nil { + t.Fatalf("Failed to create NewNovaTraitUsageExporter: %v", err) + } + + expectedMetrics := ` + # HELP openstack_project_instances_trait__custom_trait Total number of instances per OpenStack project with image trait CUSTOM_TRAIT + # TYPE openstack_project_instances_trait__custom_trait gauge + openstack_project_instances_trait__custom_trait{project_id="6ee08ba2-2ca1-4c91-b139-4bf0dbaa4096"} 5 + openstack_project_instances_trait__custom_trait{project_id="c352b0ed-30ca-4634-9c2d-1947efc29096"} 0 + # HELP openstack_project_vcpus_trait__custom_trait Total number of vcpus per OpenStack project for instances with image trait CUSTOM_TRAIT + # TYPE openstack_project_vcpus_trait__custom_trait gauge + openstack_project_vcpus_trait__custom_trait{project_id="6ee08ba2-2ca1-4c91-b139-4bf0dbaa4096"} 4 + openstack_project_vcpus_trait__custom_trait{project_id="c352b0ed-30ca-4634-9c2d-1947efc29096"} 0 + + ` + + if err := testutil.CollectAndCompare(exporter, strings.NewReader(expectedMetrics)); err != nil { + t.Errorf("unexpected collecting result:\n%s", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("There were unfulfilled expectations: %s", err) + } +} diff --git a/main.go b/main.go index b4d3b4a..4f93fc0 100644 --- a/main.go +++ b/main.go @@ -34,12 +34,13 @@ func main() { } enabledExporters := map[string]bool{ - "cinder": GetBoolEnv("CINDER_ENABLED", true), - "nova": GetBoolEnv("NOVA_ENABLED", true), - "neutron": GetBoolEnv("NEUTRON_ENABLED", true), - "designate": GetBoolEnv("DESIGNATE_ENABLED", true), - "octavia": GetBoolEnv("OCTAVIA_ENABLED", true), - "manila": GetBoolEnv("MANILA_ENABLED", false), + "cinder": GetBoolEnv("CINDER_ENABLED", true), + "nova": GetBoolEnv("NOVA_ENABLED", true), + "nova-trait": GetBoolEnv("NOVA_TRAIT_ENABLED", false), + "neutron": GetBoolEnv("NEUTRON_ENABLED", true), + "designate": GetBoolEnv("DESIGNATE_ENABLED", true), + "octavia": GetBoolEnv("OCTAVIA_ENABLED", true), + "manila": GetBoolEnv("MANILA_ENABLED", false), } for name, enabled := range enabledExporters { @@ -61,6 +62,12 @@ func main() { exporter, err = exporters.NewCinderUsageExporter(db) case "nova": exporter, err = exporters.NewNovaUsageExporter(db) + case "nova-trait": + trait, exists := os.LookupEnv("NOVA_TRAIT") + if !exists { + log.Fatalf("NOVA_TRAIT not set") + } + exporter, err = exporters.NewNovaTraitUsageExporter(db, trait) case "neutron": exporter, err = exporters.NewNeutronUsageExporter(db) case "designate": From c0e435adeb3ab5ffadef6b6f22d71a2ac940b81b Mon Sep 17 00:00:00 2001 From: Freerk-Ole Zakfeld Date: Tue, 14 Jan 2025 21:36:02 +0100 Subject: [PATCH 2/2] Fix SQL Query for Nova Traits Query Signed-off-by: Freerk-Ole Zakfeld --- exporters/nova_trait.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/nova_trait.go b/exporters/nova_trait.go index 8485a04..c30a76f 100644 --- a/exporters/nova_trait.go +++ b/exporters/nova_trait.go @@ -42,7 +42,7 @@ func (e *NovaTraitUsageExporter) Collect(ch chan<- prometheus.Metric) { } func (e *NovaTraitUsageExporter) collectMetrics(ch chan<- prometheus.Metric) { - rows, err := e.db.Query("SELECT i.project_id AS project_id, COUNT(i.id) AS total_instances, SUM(vcpus) AS total_vcpus FROM instances i INNER JOIN instance_system_metadata m on i.uuid = m.instance_uuid WHERE deleted = 0 AND m.key = 'image_trait:?' and m.value = 'required' GROUP BY project_id", e.trait) + rows, err := e.db.Query("SELECT i.project_id AS project_id, COUNT(i.id) AS total_instances, SUM(vcpus) AS total_vcpus FROM instances i INNER JOIN instance_system_metadata m on i.uuid = m.instance_uuid WHERE i.deleted = 0 AND m.key = ? and m.value = 'required' GROUP BY project_id", "image_trait:"+e.trait) if err != nil { log.Println("Error querying Nova database:", err) return diff --git a/main.go b/main.go index 4f93fc0..e834263 100644 --- a/main.go +++ b/main.go @@ -48,7 +48,7 @@ func main() { continue } - dsn := baseDSN + "/" + name + dsn := baseDSN + "/" + strings.Split(name, "-")[0] db, err := sql.Open("mysql", dsn) if err != nil {