From 90d84b1cef6f17bc8d5b1dfccd44fbcdb4018e46 Mon Sep 17 00:00:00 2001 From: Tadas Sutkaitis Date: Tue, 16 Dec 2025 12:03:35 +0200 Subject: [PATCH 1/3] feat: add heat metrics Signed-off-by: Tadas Sutkaitis --- internal/collector/collector.go | 3 + internal/collector/heat/heat.go | 30 ++++ internal/collector/heat/orchestration.go | 53 ++++++ internal/collector/heat/orchestration_test.go | 85 +++++++++ internal/collector/heat/stacks.go | 162 ++++++++++++++++++ internal/collector/heat/stacks_test.go | 138 +++++++++++++++ internal/db/heat/db.go | 31 ++++ internal/db/heat/models.go | 35 ++++ internal/db/heat/queries.sql.go | 75 ++++++++ sql/heat/queries.sql | 15 ++ sql/heat/schema.sql | 33 ++++ sqlc.yaml | 8 + 12 files changed, 668 insertions(+) create mode 100644 internal/collector/heat/heat.go create mode 100644 internal/collector/heat/orchestration.go create mode 100644 internal/collector/heat/orchestration_test.go create mode 100644 internal/collector/heat/stacks.go create mode 100644 internal/collector/heat/stacks_test.go create mode 100644 internal/db/heat/db.go create mode 100644 internal/db/heat/models.go create mode 100644 internal/db/heat/queries.sql.go create mode 100644 sql/heat/queries.sql create mode 100644 sql/heat/schema.sql diff --git a/internal/collector/collector.go b/internal/collector/collector.go index e05c652..3cb4afb 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -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/heat" "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" @@ -22,6 +23,7 @@ const ( type Config struct { CinderDatabaseURL string GlanceDatabaseURL string + HeatDatabaseURL string KeystoneDatabaseURL string MagnumDatabaseURL string ManilaDatabaseURL string @@ -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) + heat.RegisterCollectors(reg, cfg.HeatDatabaseURL, logger) keystone.RegisterCollectors(reg, cfg.KeystoneDatabaseURL, logger) magnum.RegisterCollectors(reg, cfg.MagnumDatabaseURL, logger) manila.RegisterCollectors(reg, cfg.ManilaDatabaseURL, logger) diff --git a/internal/collector/heat/heat.go b/internal/collector/heat/heat.go new file mode 100644 index 0000000..135d47e --- /dev/null +++ b/internal/collector/heat/heat.go @@ -0,0 +1,30 @@ +package heat + +import ( + "log/slog" + + "github.com/prometheus/client_golang/prometheus" + "github.com/vexxhost/openstack_database_exporter/internal/db" +) + +const ( + Namespace = "openstack" + Subsystem = "orchestration" +) + +func RegisterCollectors(registry *prometheus.Registry, databaseURL string, logger *slog.Logger) { + if databaseURL == "" { + logger.Info("Collector not loaded", "service", "heat", "reason", "database URL not configured") + return + } + + conn, err := db.Connect(databaseURL) + if err != nil { + logger.Error("Failed to connect to database", "service", "heat", "error", err) + return + } + + registry.MustRegister(NewOrchestrationCollector(conn, logger)) + + logger.Info("Registered collectors", "service", "heat") +} \ No newline at end of file diff --git a/internal/collector/heat/orchestration.go b/internal/collector/heat/orchestration.go new file mode 100644 index 0000000..2d16353 --- /dev/null +++ b/internal/collector/heat/orchestration.go @@ -0,0 +1,53 @@ +package heat + +import ( + "database/sql" + "log/slog" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + orchestrationUpDesc = prometheus.NewDesc( + prometheus.BuildFQName(Namespace, Subsystem, "up"), + "up", + nil, + nil, + ) +) + +type OrchestrationCollector struct { + db *sql.DB + logger *slog.Logger + stacksCollector *StacksCollector +} + +func NewOrchestrationCollector(db *sql.DB, logger *slog.Logger) *OrchestrationCollector { + return &OrchestrationCollector{ + db: db, + logger: logger, + stacksCollector: NewStacksCollector(db, logger), + } +} + +func (c *OrchestrationCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- orchestrationUpDesc + c.stacksCollector.Describe(ch) +} + +func (c *OrchestrationCollector) Collect(ch chan<- prometheus.Metric) { + var hasError bool + + // Collect metrics from stacks collector and track errors + if err := c.stacksCollector.Collect(ch); err != nil { + c.logger.Error("stacks collector failed", "error", err) + hasError = true + } + + // Emit single up metric based on overall success + upValue := float64(1) + if hasError { + upValue = float64(0) + } + ch <- prometheus.MustNewConstMetric(orchestrationUpDesc, prometheus.GaugeValue, upValue) +} diff --git a/internal/collector/heat/orchestration_test.go b/internal/collector/heat/orchestration_test.go new file mode 100644 index 0000000..b244361 --- /dev/null +++ b/internal/collector/heat/orchestration_test.go @@ -0,0 +1,85 @@ +package heat + +import ( + "database/sql" + "log/slog" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + heatdb "github.com/vexxhost/openstack_database_exporter/internal/db/heat" + "github.com/vexxhost/openstack_database_exporter/internal/testutil" +) + +func TestOrchestrationCollector(t *testing.T) { + tests := []testutil.CollectorTestCase{ + { + Name: "successful collection with stacks working", + SetupMock: func(mock sqlmock.Sqlmock) { + rows := sqlmock.NewRows([]string{ + "id", "name", "status", "action", "tenant", "created_at", "updated_at", "deleted_at", "nested_depth", "disable_rollback", + }).AddRow( + "stack-1", "test-stack", "CREATE_COMPLETE", "CREATE", "project-1", nil, nil, nil, 0, false, + ) + + mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnRows(rows) + }, + ExpectedMetrics: `# HELP openstack_orchestration_stack_status stack_status +# TYPE openstack_orchestration_stack_status gauge +openstack_orchestration_stack_status{action="CREATE",id="stack-1",name="test-stack",project_id="project-1",status="CREATE_COMPLETE"} 5 +# HELP openstack_orchestration_stack_status_counter stack_status_counter +# TYPE openstack_orchestration_stack_status_counter gauge +openstack_orchestration_stack_status_counter{status="ADOPT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="ADOPT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="ADOPT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="CREATE_COMPLETE"} 1 +openstack_orchestration_stack_status_counter{status="CREATE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="CREATE_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="INIT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="INIT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="INIT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_IN_PROGRESS"} 0 +# HELP openstack_orchestration_total_stacks total_stacks +# TYPE openstack_orchestration_total_stacks gauge +openstack_orchestration_total_stacks 1 +# HELP openstack_orchestration_up up +# TYPE openstack_orchestration_up gauge +openstack_orchestration_up 1 +`, + }, + { + Name: "stacks collector fails, up metric should be 0", + SetupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnError(sql.ErrConnDone) + }, + ExpectedMetrics: `# HELP openstack_orchestration_up up +# TYPE openstack_orchestration_up gauge +openstack_orchestration_up 0 +`, + }, + } + + testutil.RunCollectorTests(t, tests, func(db *sql.DB, logger *slog.Logger) *OrchestrationCollector { + return NewOrchestrationCollector(db, logger) + }) +} \ No newline at end of file diff --git a/internal/collector/heat/stacks.go b/internal/collector/heat/stacks.go new file mode 100644 index 0000000..bca36da --- /dev/null +++ b/internal/collector/heat/stacks.go @@ -0,0 +1,162 @@ +package heat + +import ( + "context" + "database/sql" + "log/slog" + + "github.com/prometheus/client_golang/prometheus" + heatdb "github.com/vexxhost/openstack_database_exporter/internal/db/heat" +) + +var ( + // Known stack statuses from the original openstack-exporter + knownStackStatuses = []string{ + "INIT_IN_PROGRESS", + "INIT_FAILED", + "INIT_COMPLETE", + "CREATE_IN_PROGRESS", + "CREATE_FAILED", + "CREATE_COMPLETE", + "DELETE_IN_PROGRESS", + "DELETE_FAILED", + "DELETE_COMPLETE", + "UPDATE_IN_PROGRESS", + "UPDATE_FAILED", + "UPDATE_COMPLETE", + "ROLLBACK_IN_PROGRESS", + "ROLLBACK_FAILED", + "ROLLBACK_COMPLETE", + "SUSPEND_IN_PROGRESS", + "SUSPEND_FAILED", + "SUSPEND_COMPLETE", + "RESUME_IN_PROGRESS", + "RESUME_FAILED", + "RESUME_COMPLETE", + "ADOPT_IN_PROGRESS", + "ADOPT_FAILED", + "ADOPT_COMPLETE", + "SNAPSHOT_IN_PROGRESS", + "SNAPSHOT_FAILED", + "SNAPSHOT_COMPLETE", + "CHECK_IN_PROGRESS", + "CHECK_FAILED", + "CHECK_COMPLETE", + } + + stackStatusDesc = prometheus.NewDesc( + prometheus.BuildFQName(Namespace, Subsystem, "stack_status"), + "stack_status", + []string{ + "id", + "name", + "project_id", + "status", + "action", + }, + nil, + ) + + stackStatusCounterDesc = prometheus.NewDesc( + prometheus.BuildFQName(Namespace, Subsystem, "stack_status_counter"), + "stack_status_counter", + []string{ + "status", + }, + nil, + ) + + totalStacksDesc = prometheus.NewDesc( + prometheus.BuildFQName(Namespace, Subsystem, "total_stacks"), + "total_stacks", + nil, + nil, + ) +) + +type StacksCollector struct { + db *sql.DB + queries *heatdb.Queries + logger *slog.Logger +} + +func NewStacksCollector(db *sql.DB, logger *slog.Logger) *StacksCollector { + return &StacksCollector{ + db: db, + queries: heatdb.New(db), + logger: logger.With( + "namespace", Namespace, + "subsystem", Subsystem, + "collector", "stacks", + ), + } +} + +func (c *StacksCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- stackStatusDesc + ch <- stackStatusCounterDesc + ch <- totalStacksDesc +} + +func (c *StacksCollector) Collect(ch chan<- prometheus.Metric) error { + ctx := context.Background() + + stacks, err := c.queries.GetStackMetrics(ctx) + if err != nil { + c.logger.Error("failed to query stacks", "error", err) + return err + } + + // Initialize status counters + stackStatusCounter := make(map[string]int, len(knownStackStatuses)) + for _, status := range knownStackStatuses { + stackStatusCounter[status] = 0 + } + + // total_stacks count + ch <- prometheus.MustNewConstMetric( + totalStacksDesc, + prometheus.GaugeValue, + float64(len(stacks)), + ) + + // Individual stack status metrics and count status occurrences + for _, stack := range stacks { + // Count status occurrences + stackStatusCounter[stack.Status]++ + + // stack_status metric + statusValue := mapStackStatusValue(stack.Status) + ch <- prometheus.MustNewConstMetric( + stackStatusDesc, + prometheus.GaugeValue, + float64(statusValue), + stack.ID, + stack.Name, + stack.Tenant, + stack.Status, + stack.Action, + ) + } + + // Stack status counter metrics + for status, count := range stackStatusCounter { + ch <- prometheus.MustNewConstMetric( + stackStatusCounterDesc, + prometheus.GaugeValue, + float64(count), + status, + ) + } + + return nil +} + +func mapStackStatusValue(status string) int { + for idx, s := range knownStackStatuses { + if status == s { + return idx + } + } + return -1 +} diff --git a/internal/collector/heat/stacks_test.go b/internal/collector/heat/stacks_test.go new file mode 100644 index 0000000..f6df4ee --- /dev/null +++ b/internal/collector/heat/stacks_test.go @@ -0,0 +1,138 @@ +package heat + +import ( + "database/sql" + "log/slog" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/prometheus/client_golang/prometheus" + heatdb "github.com/vexxhost/openstack_database_exporter/internal/db/heat" + "github.com/vexxhost/openstack_database_exporter/internal/testutil" +) + +func TestStacksCollector(t *testing.T) { + tests := []testutil.CollectorTestCase{ + { + Name: "successful collection with stack data", + SetupMock: func(mock sqlmock.Sqlmock) { + rows := sqlmock.NewRows([]string{ + "id", "name", "status", "action", "tenant", "created_at", "updated_at", "deleted_at", "nested_depth", "disable_rollback", + }).AddRow( + "stack-1", "test-stack", "CREATE_COMPLETE", "CREATE", "project-1", nil, nil, nil, 0, false, + ).AddRow( + "stack-2", "test-stack-2", "CREATE_FAILED", "CREATE", "project-1", nil, nil, nil, 0, false, + ) + + mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnRows(rows) + }, + ExpectedMetrics: `# HELP openstack_orchestration_stack_status stack_status +# TYPE openstack_orchestration_stack_status gauge +openstack_orchestration_stack_status{action="CREATE",id="stack-1",name="test-stack",project_id="project-1",status="CREATE_COMPLETE"} 5 +openstack_orchestration_stack_status{action="CREATE",id="stack-2",name="test-stack-2",project_id="project-1",status="CREATE_FAILED"} 4 +# HELP openstack_orchestration_stack_status_counter stack_status_counter +# TYPE openstack_orchestration_stack_status_counter gauge +openstack_orchestration_stack_status_counter{status="ADOPT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="ADOPT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="ADOPT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="CREATE_COMPLETE"} 1 +openstack_orchestration_stack_status_counter{status="CREATE_FAILED"} 1 +openstack_orchestration_stack_status_counter{status="CREATE_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="INIT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="INIT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="INIT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_IN_PROGRESS"} 0 +# HELP openstack_orchestration_total_stacks total_stacks +# TYPE openstack_orchestration_total_stacks gauge +openstack_orchestration_total_stacks 2 +`, + }, + { + Name: "empty results", + SetupMock: func(mock sqlmock.Sqlmock) { + rows := sqlmock.NewRows([]string{ + "id", "name", "status", "action", "tenant", "created_at", "updated_at", "deleted_at", "nested_depth", "disable_rollback", + }) + + mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnRows(rows) + }, + ExpectedMetrics: `# HELP openstack_orchestration_stack_status_counter stack_status_counter +# TYPE openstack_orchestration_stack_status_counter gauge +openstack_orchestration_stack_status_counter{status="ADOPT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="ADOPT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="ADOPT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="CHECK_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="CREATE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="CREATE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="CREATE_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="DELETE_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="INIT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="INIT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="INIT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="RESUME_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="ROLLBACK_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="SNAPSHOT_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="SUSPEND_IN_PROGRESS"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_COMPLETE"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_FAILED"} 0 +openstack_orchestration_stack_status_counter{status="UPDATE_IN_PROGRESS"} 0 +# HELP openstack_orchestration_total_stacks total_stacks +# TYPE openstack_orchestration_total_stacks gauge +openstack_orchestration_total_stacks 0 +`, + }, + { + Name: "database query error", + SetupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnError(sql.ErrConnDone) + }, + ExpectedMetrics: ``, + }, + } + + testutil.RunCollectorTests(t, tests, func(db *sql.DB, logger *slog.Logger) prometheus.Collector { + return &testStacksCollector{NewStacksCollector(db, logger)} + }) +} + +// testStacksCollector wraps StacksCollector to be compatible with prometheus.Collector for testing +type testStacksCollector struct { + *StacksCollector +} + +func (t *testStacksCollector) Collect(ch chan<- prometheus.Metric) { + _ = t.StacksCollector.Collect(ch) +} diff --git a/internal/db/heat/db.go b/internal/db/heat/db.go new file mode 100644 index 0000000..64ffa40 --- /dev/null +++ b/internal/db/heat/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package heat + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/db/heat/models.go b/internal/db/heat/models.go new file mode 100644 index 0000000..4180ece --- /dev/null +++ b/internal/db/heat/models.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package heat + +import ( + "database/sql" +) + +type Stack struct { + ID string + CreatedAt sql.NullTime + UpdatedAt sql.NullTime + DeletedAt sql.NullTime + Name sql.NullString + RawTemplateID int32 + PrevRawTemplateID sql.NullInt32 + UserCredsID sql.NullInt32 + Username sql.NullString + OwnerID sql.NullString + Action sql.NullString + Status sql.NullString + StatusReason sql.NullString + Timeout sql.NullInt32 + Tenant sql.NullString + DisableRollback bool + StackUserProjectID sql.NullString + Backup sql.NullBool + NestedDepth sql.NullInt32 + Convergence sql.NullBool + CurrentTraversal sql.NullString + CurrentDeps sql.NullString + ParentResourceName sql.NullString +} diff --git a/internal/db/heat/queries.sql.go b/internal/db/heat/queries.sql.go new file mode 100644 index 0000000..56ffd6b --- /dev/null +++ b/internal/db/heat/queries.sql.go @@ -0,0 +1,75 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: queries.sql + +package heat + +import ( + "context" + "database/sql" +) + +const GetStackMetrics = `-- name: GetStackMetrics :many +SELECT + s.id, + COALESCE(s.name, '') as name, + COALESCE(s.status, '') as status, + COALESCE(s.action, '') as action, + COALESCE(s.tenant, '') as tenant, + s.created_at, + s.updated_at, + s.deleted_at, + COALESCE(s.nested_depth, 0) as nested_depth, + COALESCE(s.disable_rollback, false) as disable_rollback +FROM stack s +WHERE s.deleted_at IS NULL +ORDER BY s.created_at DESC +` + +type GetStackMetricsRow struct { + ID string + Name string + Status string + Action string + Tenant string + CreatedAt sql.NullTime + UpdatedAt sql.NullTime + DeletedAt sql.NullTime + NestedDepth int32 + DisableRollback bool +} + +func (q *Queries) GetStackMetrics(ctx context.Context) ([]GetStackMetricsRow, error) { + rows, err := q.db.QueryContext(ctx, GetStackMetrics) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetStackMetricsRow + for rows.Next() { + var i GetStackMetricsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Status, + &i.Action, + &i.Tenant, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + &i.NestedDepth, + &i.DisableRollback, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/sql/heat/queries.sql b/sql/heat/queries.sql new file mode 100644 index 0000000..ebfefe6 --- /dev/null +++ b/sql/heat/queries.sql @@ -0,0 +1,15 @@ +-- name: GetStackMetrics :many +SELECT + s.id, + COALESCE(s.name, '') as name, + COALESCE(s.status, '') as status, + COALESCE(s.action, '') as action, + COALESCE(s.tenant, '') as tenant, + s.created_at, + s.updated_at, + s.deleted_at, + COALESCE(s.nested_depth, 0) as nested_depth, + COALESCE(s.disable_rollback, false) as disable_rollback +FROM stack s +WHERE s.deleted_at IS NULL +ORDER BY s.created_at DESC; diff --git a/sql/heat/schema.sql b/sql/heat/schema.sql new file mode 100644 index 0000000..f6d29f8 --- /dev/null +++ b/sql/heat/schema.sql @@ -0,0 +1,33 @@ +CREATE TABLE + `stack` ( + `id` varchar(36) NOT NULL, + `created_at` datetime DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + `deleted_at` datetime DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `raw_template_id` int NOT NULL, + `prev_raw_template_id` int DEFAULT NULL, + `user_creds_id` int DEFAULT NULL, + `username` varchar(256) DEFAULT NULL, + `owner_id` varchar(36) DEFAULT NULL, + `action` varchar(255) DEFAULT NULL, + `status` varchar(255) DEFAULT NULL, + `status_reason` text, + `timeout` int DEFAULT NULL, + `tenant` varchar(256) DEFAULT NULL, + `disable_rollback` tinyint(1) NOT NULL, + `stack_user_project_id` varchar(64) DEFAULT NULL, + `backup` tinyint(1) DEFAULT NULL, + `nested_depth` int DEFAULT NULL, + `convergence` tinyint(1) DEFAULT NULL, + `current_traversal` varchar(36) DEFAULT NULL, + `current_deps` longtext, + `parent_resource_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `prev_raw_template_id` (`prev_raw_template_id`), + KEY `raw_template_id` (`raw_template_id`), + KEY `user_creds_id` (`user_creds_id`), + KEY `ix_stack_name` (`name`), + KEY `ix_stack_tenant` (`tenant`(255)), + KEY `ix_stack_owner_id` (`owner_id`) + ); diff --git a/sqlc.yaml b/sqlc.yaml index 70b81a9..b57e28d 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -65,3 +65,11 @@ sql: package: "placement" out: "internal/db/placement" emit_exported_queries: true + - engine: "mysql" + schema: "sql/heat/schema.sql" + queries: "sql/heat/queries.sql" + gen: + go: + package: "heat" + out: "internal/db/heat" + emit_exported_queries: true From cdcf51b701d7e41bb4cee3dae2f871fa577da3b5 Mon Sep 17 00:00:00 2001 From: Tadas Sutkaitis Date: Tue, 16 Dec 2025 12:06:19 +0200 Subject: [PATCH 2/3] fix: end lines Signed-off-by: Tadas Sutkaitis --- internal/collector/heat/heat.go | 2 +- internal/collector/heat/orchestration_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/collector/heat/heat.go b/internal/collector/heat/heat.go index 135d47e..59eee1b 100644 --- a/internal/collector/heat/heat.go +++ b/internal/collector/heat/heat.go @@ -27,4 +27,4 @@ func RegisterCollectors(registry *prometheus.Registry, databaseURL string, logge registry.MustRegister(NewOrchestrationCollector(conn, logger)) logger.Info("Registered collectors", "service", "heat") -} \ No newline at end of file +} diff --git a/internal/collector/heat/orchestration_test.go b/internal/collector/heat/orchestration_test.go index b244361..8a1ce1d 100644 --- a/internal/collector/heat/orchestration_test.go +++ b/internal/collector/heat/orchestration_test.go @@ -82,4 +82,4 @@ openstack_orchestration_up 0 testutil.RunCollectorTests(t, tests, func(db *sql.DB, logger *slog.Logger) *OrchestrationCollector { return NewOrchestrationCollector(db, logger) }) -} \ No newline at end of file +} From b7652643d6bad5ac247432ba4fde84b2554066d1 Mon Sep 17 00:00:00 2001 From: Tadas Sutkaitis Date: Tue, 16 Dec 2025 16:20:30 +0200 Subject: [PATCH 3/3] fix: add missing heatDatabaseURL flag Signed-off-by: Tadas Sutkaitis --- cmd/openstack-database-exporter/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/openstack-database-exporter/main.go b/cmd/openstack-database-exporter/main.go index 9fbd997..86bfdf1 100644 --- a/cmd/openstack-database-exporter/main.go +++ b/cmd/openstack-database-exporter/main.go @@ -31,6 +31,10 @@ var ( "glance.database-url", "Glance database connection URL (oslo.db format)", ).Envar("GLANCE_DATABASE_URL").String() + heatDatabaseURL = kingpin.Flag( + "heat.database-url", + "Heat database connection URL (oslo.db format)", + ).Envar("HEAT_DATABASE_URL").String() keystoneDatabaseURL = kingpin.Flag( "keystone.database-url", "Keystone database connection URL (oslo.db format)", @@ -73,6 +77,7 @@ func main() { reg := collector.NewRegistry(collector.Config{ CinderDatabaseURL: *cinderDatabaseURL, GlanceDatabaseURL: *glanceDatabaseURL, + HeatDatabaseURL: *heatDatabaseURL, KeystoneDatabaseURL: *keystoneDatabaseURL, MagnumDatabaseURL: *magnumDatabaseURL, ManilaDatabaseURL: *manilaDatabaseURL,