Skip to content

Commit eb98559

Browse files
authored
feat: export per-node cpu and memory (#8)
1 parent 41ad30f commit eb98559

3 files changed

Lines changed: 74 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ The export creates a **zip file** containing the following files:
203203
- **`crdb_internal.transaction_statistics.csv`** - Transaction execution stats
204204
- **`crdb_internal.transaction_contention_events.csv`** - Lock contention events
205205
- **`crdb_internal.gossip_nodes.csv`** - Node information and topology
206+
- **`crdb_internal.node_cpu_mem.csv`** - Per-node vCPU count and total memory (derived from `kv_node_status`)
206207
- **`crdb_internal.table_indexes.csv`** - Table and index descriptor IDs across all databases
207208
- **`system.table_statistics.csv`** - Optimizer table statistics (column-level stats used by the query planner)
208209

pkg/export/exporter.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ type Table struct {
113113
RedactColumn string
114114
// RedactedKeys is the set of RedactKeyColumn values for which RedactColumn is redacted.
115115
RedactedKeys []string
116+
// Query overrides the default SELECT for this table. When set, the query is used as-is
117+
// for both column discovery and data export. TimeColumn, RedactKeyColumn, RedactColumn,
118+
// and RedactedKeys are ignored when Query is set. The output filename is still derived
119+
// from Database and Name.
120+
Query string
116121
}
117122

118123
// sensitiveClusterSettings is the list of cluster setting names whose values are
@@ -140,6 +145,19 @@ var exportTables = []Table{
140145
{Database: "crdb_internal", Name: "transaction_statistics", TimeColumn: "aggregated_ts", Scope: TenantScopeMain},
141146
{Database: "crdb_internal", Name: "transaction_contention_events", TimeColumn: "collection_ts", Scope: TenantScopeMain},
142147
{Database: "crdb_internal", Name: "gossip_nodes", TimeColumn: "", Optional: true, Scope: TenantScopeSystem},
148+
{
149+
Database: "crdb_internal",
150+
Name: "node_cpu_mem",
151+
Optional: true,
152+
Scope: TenantScopeSystem,
153+
Query: `SELECT node_id, address,` +
154+
` ROUND(` +
155+
`((metrics->>'sys.cpu.user.percent')::FLOAT + (metrics->>'sys.cpu.sys.percent')::FLOAT)` +
156+
` / NULLIF((metrics->>'sys.cpu.combined.percent-normalized')::FLOAT, 0)` +
157+
`)::INT AS num_vcpus,` +
158+
` ROUND((metrics->>'sys.totalmem')::FLOAT / 1073741824, 1) AS total_mem_gib` +
159+
` FROM crdb_internal.kv_node_status`,
160+
},
143161
{Database: "", Name: "crdb_internal.table_indexes", TimeColumn: "", Scope: TenantScopeMain}, // Use "" to query across all databases
144162
{Database: "system", Name: "table_statistics", TimeColumn: "", Scope: TenantScopeMain},
145163
{
@@ -235,7 +253,7 @@ func (exporter *Exporter) Close() error {
235253
// - Cluster metadata (version, ID, name, organization, settings)
236254
// - Database schemas (CREATE statements for all user databases)
237255
// - Zone configurations
238-
// - Statistics tables (statement_statistics, transaction_statistics, transaction_contention_events, gossip_nodes, table_indexes across all databases, system.table_statistics)
256+
// - Statistics tables (statement_statistics, transaction_statistics, transaction_contention_events, gossip_nodes, node_cpu_mem, table_indexes across all databases, system.table_statistics)
239257
// - Cluster settings (crdb_internal.cluster_settings, system.settings) with sensitive values redacted
240258
//
241259
// The statistics tables are filtered by the TimeRange specified in Config.
@@ -599,7 +617,13 @@ func (exporter *Exporter) doExportTable(ctx context.Context, dir string, table T
599617
}
600618

601619
// Get column names
602-
rows, err := conn.Query(ctx, fmt.Sprintf("SELECT * FROM %s LIMIT 0", tableRef))
620+
var colProbeSQL string
621+
if table.Query != "" {
622+
colProbeSQL = fmt.Sprintf("SELECT * FROM (%s) AS q LIMIT 0", table.Query)
623+
} else {
624+
colProbeSQL = fmt.Sprintf("SELECT * FROM %s LIMIT 0", tableRef)
625+
}
626+
rows, err := conn.Query(ctx, colProbeSQL)
603627
if err != nil {
604628
return err
605629
}
@@ -618,22 +642,28 @@ func (exporter *Exporter) doExportTable(ctx context.Context, dir string, table T
618642
return err
619643
}
620644

621-
// Use a SQL query to export data in CSV format
622-
var where string
623-
if table.TimeColumn != "" {
624-
where = fmt.Sprintf("WHERE %s BETWEEN '%s' and '%s'",
625-
pgx.Identifier{table.TimeColumn}.Sanitize(),
626-
startTime(exporter.Config.TimeRange.Start).Format("2006-01-02 15:04:05"), // offset for aggregation interval -- TODO
627-
endTime(exporter.Config.TimeRange.End).Format("2006-01-02 15:04:05"),
628-
)
629-
}
645+
// Build and run the COPY query.
646+
var copyQuery string
647+
if table.Query != "" {
648+
copyQuery = fmt.Sprintf("COPY (%s) TO STDOUT WITH CSV", table.Query)
649+
} else {
650+
// Use a SQL query to export data in CSV format
651+
var where string
652+
if table.TimeColumn != "" {
653+
where = fmt.Sprintf("WHERE %s BETWEEN '%s' and '%s'",
654+
pgx.Identifier{table.TimeColumn}.Sanitize(),
655+
startTime(exporter.Config.TimeRange.Start).Format("2006-01-02 15:04:05"), // offset for aggregation interval -- TODO
656+
endTime(exporter.Config.TimeRange.End).Format("2006-01-02 15:04:05"),
657+
)
658+
}
630659

631-
// Build SELECT expression, applying column-level redaction when configured.
632-
selectExpr := buildSelectExpr(headers, table)
660+
// Build SELECT expression, applying column-level redaction when configured.
661+
selectExpr := buildSelectExpr(headers, table)
633662

634-
copyQuery := fmt.Sprintf(
635-
"COPY (SELECT %s FROM %s %s) TO STDOUT WITH CSV",
636-
selectExpr, tableRef, where)
663+
copyQuery = fmt.Sprintf(
664+
"COPY (SELECT %s FROM %s %s) TO STDOUT WITH CSV",
665+
selectExpr, tableRef, where)
666+
}
637667
logrus.Info(copyQuery)
638668
_, err = conn.PgConn().CopyTo(ctx, file, copyQuery)
639669
if err != nil {

pkg/export/exporter_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,33 @@ func TestExportTables(t *testing.T) {
195195
}
196196
}
197197

198+
func TestExportTablesIncludesNodeCPUMem(t *testing.T) {
199+
found := false
200+
for _, table := range exportTables {
201+
if table.Database == "crdb_internal" && table.Name == "node_cpu_mem" {
202+
found = true
203+
if table.Scope != TenantScopeSystem {
204+
t.Errorf("crdb_internal.node_cpu_mem should have Scope TenantScopeSystem, got %q", table.Scope)
205+
}
206+
if !table.Optional {
207+
t.Error("crdb_internal.node_cpu_mem should be Optional")
208+
}
209+
if table.Query == "" {
210+
t.Error("crdb_internal.node_cpu_mem should have a custom Query")
211+
}
212+
if !strings.Contains(table.Query, "num_vcpus") {
213+
t.Error("crdb_internal.node_cpu_mem Query should select num_vcpus")
214+
}
215+
if !strings.Contains(table.Query, "total_mem_gib") {
216+
t.Error("crdb_internal.node_cpu_mem Query should select total_mem_gib")
217+
}
218+
}
219+
}
220+
if !found {
221+
t.Error("exportTables should contain crdb_internal.node_cpu_mem")
222+
}
223+
}
224+
198225
func TestExportTablesIncludesClusterSettings(t *testing.T) {
199226
found := false
200227
for _, table := range exportTables {

0 commit comments

Comments
 (0)