From 90d84b1cef6f17bc8d5b1dfccd44fbcdb4018e46 Mon Sep 17 00:00:00 2001 From: Tadas Sutkaitis Date: Tue, 16 Dec 2025 12:03:35 +0200 Subject: [PATCH 1/5] 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/5] 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/5] 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, From e2273c37fdd7df2de2f96a2d237d58fec6f3d408 Mon Sep 17 00:00:00 2001 From: Tadas Sutkaitis Date: Thu, 12 Feb 2026 16:07:39 +0200 Subject: [PATCH 4/5] feat: add more optimization Signed-off-by: Tadas Sutkaitis --- internal/collector/heat/orchestration.go | 2 -- internal/collector/heat/stacks.go | 33 +++++++++++++++--------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/internal/collector/heat/orchestration.go b/internal/collector/heat/orchestration.go index 2d16353..b9e0806 100644 --- a/internal/collector/heat/orchestration.go +++ b/internal/collector/heat/orchestration.go @@ -17,14 +17,12 @@ var ( ) 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), } diff --git a/internal/collector/heat/stacks.go b/internal/collector/heat/stacks.go index bca36da..510487c 100644 --- a/internal/collector/heat/stacks.go +++ b/internal/collector/heat/stacks.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "log/slog" + "time" "github.com/prometheus/client_golang/prometheus" heatdb "github.com/vexxhost/openstack_database_exporter/internal/db/heat" @@ -44,6 +45,15 @@ var ( "CHECK_COMPLETE", } + // stackStatusValueMap provides O(1) lookup for status -> index mapping. + stackStatusValueMap = func() map[string]int { + m := make(map[string]int, len(knownStackStatuses)) + for idx, s := range knownStackStatuses { + m[s] = idx + } + return m + }() + stackStatusDesc = prometheus.NewDesc( prometheus.BuildFQName(Namespace, Subsystem, "stack_status"), "stack_status", @@ -75,14 +85,12 @@ var ( ) 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, @@ -99,7 +107,8 @@ func (c *StacksCollector) Describe(ch chan<- *prometheus.Desc) { } func (c *StacksCollector) Collect(ch chan<- prometheus.Metric) error { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() stacks, err := c.queries.GetStackMetrics(ctx) if err != nil { @@ -122,8 +131,10 @@ func (c *StacksCollector) Collect(ch chan<- prometheus.Metric) error { // Individual stack status metrics and count status occurrences for _, stack := range stacks { - // Count status occurrences - stackStatusCounter[stack.Status]++ + // Count status occurrences only for known statuses + if _, ok := stackStatusCounter[stack.Status]; ok { + stackStatusCounter[stack.Status]++ + } // stack_status metric statusValue := mapStackStatusValue(stack.Status) @@ -139,12 +150,12 @@ func (c *StacksCollector) Collect(ch chan<- prometheus.Metric) error { ) } - // Stack status counter metrics - for status, count := range stackStatusCounter { + // Stack status counter metrics in stable order + for _, status := range knownStackStatuses { ch <- prometheus.MustNewConstMetric( stackStatusCounterDesc, prometheus.GaugeValue, - float64(count), + float64(stackStatusCounter[status]), status, ) } @@ -153,10 +164,8 @@ func (c *StacksCollector) Collect(ch chan<- prometheus.Metric) error { } func mapStackStatusValue(status string) int { - for idx, s := range knownStackStatuses { - if status == s { - return idx - } + if v, ok := stackStatusValueMap[status]; ok { + return v } return -1 } From 00f653a15985e044a54dc4ed8f6fd8d11d2a1e33 Mon Sep 17 00:00:00 2001 From: Tadas Sutkaitis Date: Thu, 12 Feb 2026 16:27:05 +0200 Subject: [PATCH 5/5] feat: add more optimizations Signed-off-by: Tadas Sutkaitis --- internal/collector/heat/orchestration.go | 7 ++- internal/collector/heat/orchestration_test.go | 4 +- internal/collector/heat/stacks.go | 41 ++++++++++--- internal/collector/heat/stacks_test.go | 59 +++++++++++++++++-- internal/db/heat/queries.sql.go | 29 ++------- sql/heat/queries.sql | 10 +--- 6 files changed, 104 insertions(+), 46 deletions(-) diff --git a/internal/collector/heat/orchestration.go b/internal/collector/heat/orchestration.go index b9e0806..c629b4d 100644 --- a/internal/collector/heat/orchestration.go +++ b/internal/collector/heat/orchestration.go @@ -23,7 +23,11 @@ type OrchestrationCollector struct { func NewOrchestrationCollector(db *sql.DB, logger *slog.Logger) *OrchestrationCollector { return &OrchestrationCollector{ - logger: logger, + logger: logger.With( + "namespace", Namespace, + "subsystem", Subsystem, + "collector", "orchestration", + ), stacksCollector: NewStacksCollector(db, logger), } } @@ -38,7 +42,6 @@ func (c *OrchestrationCollector) Collect(ch chan<- prometheus.Metric) { // 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 } diff --git a/internal/collector/heat/orchestration_test.go b/internal/collector/heat/orchestration_test.go index 8a1ce1d..be6bc03 100644 --- a/internal/collector/heat/orchestration_test.go +++ b/internal/collector/heat/orchestration_test.go @@ -17,9 +17,9 @@ func TestOrchestrationCollector(t *testing.T) { 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", + "id", "name", "status", "action", "tenant", }).AddRow( - "stack-1", "test-stack", "CREATE_COMPLETE", "CREATE", "project-1", nil, nil, nil, 0, false, + "stack-1", "test-stack", "CREATE_COMPLETE", "CREATE", "project-1", ) mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnRows(rows) diff --git a/internal/collector/heat/stacks.go b/internal/collector/heat/stacks.go index 510487c..9cee160 100644 --- a/internal/collector/heat/stacks.go +++ b/internal/collector/heat/stacks.go @@ -46,13 +46,40 @@ var ( } // stackStatusValueMap provides O(1) lookup for status -> index mapping. - stackStatusValueMap = func() map[string]int { - m := make(map[string]int, len(knownStackStatuses)) - for idx, s := range knownStackStatuses { - m[s] = idx - } - return m - }() + // Values are explicitly assigned to keep metric encodings stable even + // if knownStackStatuses is reordered or modified. + stackStatusValueMap = map[string]int{ + "INIT_IN_PROGRESS": 0, + "INIT_FAILED": 1, + "INIT_COMPLETE": 2, + "CREATE_IN_PROGRESS": 3, + "CREATE_FAILED": 4, + "CREATE_COMPLETE": 5, + "DELETE_IN_PROGRESS": 6, + "DELETE_FAILED": 7, + "DELETE_COMPLETE": 8, + "UPDATE_IN_PROGRESS": 9, + "UPDATE_FAILED": 10, + "UPDATE_COMPLETE": 11, + "ROLLBACK_IN_PROGRESS": 12, + "ROLLBACK_FAILED": 13, + "ROLLBACK_COMPLETE": 14, + "SUSPEND_IN_PROGRESS": 15, + "SUSPEND_FAILED": 16, + "SUSPEND_COMPLETE": 17, + "RESUME_IN_PROGRESS": 18, + "RESUME_FAILED": 19, + "RESUME_COMPLETE": 20, + "ADOPT_IN_PROGRESS": 21, + "ADOPT_FAILED": 22, + "ADOPT_COMPLETE": 23, + "SNAPSHOT_IN_PROGRESS": 24, + "SNAPSHOT_FAILED": 25, + "SNAPSHOT_COMPLETE": 26, + "CHECK_IN_PROGRESS": 27, + "CHECK_FAILED": 28, + "CHECK_COMPLETE": 29, + } stackStatusDesc = prometheus.NewDesc( prometheus.BuildFQName(Namespace, Subsystem, "stack_status"), diff --git a/internal/collector/heat/stacks_test.go b/internal/collector/heat/stacks_test.go index f6df4ee..218d728 100644 --- a/internal/collector/heat/stacks_test.go +++ b/internal/collector/heat/stacks_test.go @@ -18,11 +18,11 @@ func TestStacksCollector(t *testing.T) { 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", + "id", "name", "status", "action", "tenant", }).AddRow( - "stack-1", "test-stack", "CREATE_COMPLETE", "CREATE", "project-1", nil, nil, nil, 0, false, + "stack-1", "test-stack", "CREATE_COMPLETE", "CREATE", "project-1", ).AddRow( - "stack-2", "test-stack-2", "CREATE_FAILED", "CREATE", "project-1", nil, nil, nil, 0, false, + "stack-2", "test-stack-2", "CREATE_FAILED", "CREATE", "project-1", ) mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnRows(rows) @@ -72,7 +72,7 @@ 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", + "id", "name", "status", "action", "tenant", }) mock.ExpectQuery(regexp.QuoteMeta(heatdb.GetStackMetrics)).WillReturnRows(rows) @@ -121,6 +121,57 @@ openstack_orchestration_total_stacks 0 }, ExpectedMetrics: ``, }, + { + Name: "unknown status gets -1 value and does not affect counters", + SetupMock: func(mock sqlmock.Sqlmock) { + rows := sqlmock.NewRows([]string{ + "id", "name", "status", "action", "tenant", + }).AddRow( + "stack-1", "test-stack", "UNKNOWN_STATUS", "CREATE", "project-1", + ) + + 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="UNKNOWN_STATUS"} -1 +# 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 1 +`, + }, } testutil.RunCollectorTests(t, tests, func(db *sql.DB, logger *slog.Logger) prometheus.Collector { diff --git a/internal/db/heat/queries.sql.go b/internal/db/heat/queries.sql.go index 56ffd6b..ed262d3 100644 --- a/internal/db/heat/queries.sql.go +++ b/internal/db/heat/queries.sql.go @@ -7,7 +7,6 @@ package heat import ( "context" - "database/sql" ) const GetStackMetrics = `-- name: GetStackMetrics :many @@ -16,28 +15,17 @@ SELECT 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 + COALESCE(s.tenant, '') as tenant 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 + ID string + Name string + Status string + Action string + Tenant string } func (q *Queries) GetStackMetrics(ctx context.Context) ([]GetStackMetricsRow, error) { @@ -55,11 +43,6 @@ func (q *Queries) GetStackMetrics(ctx context.Context) ([]GetStackMetricsRow, er &i.Status, &i.Action, &i.Tenant, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.NestedDepth, - &i.DisableRollback, ); err != nil { return nil, err } diff --git a/sql/heat/queries.sql b/sql/heat/queries.sql index ebfefe6..a8d2301 100644 --- a/sql/heat/queries.sql +++ b/sql/heat/queries.sql @@ -4,12 +4,6 @@ SELECT 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 + COALESCE(s.tenant, '') as tenant FROM stack s -WHERE s.deleted_at IS NULL -ORDER BY s.created_at DESC; +WHERE s.deleted_at IS NULL;