Skip to content

Commit 629486e

Browse files
committed
nexus: Remove TimeseriesKey from timeseries output
Our external API timeseries endpoints currently return data as a `BTreeMap<TimeseriesKey, Timeseries>`, which results in JSON output with arbitrary numeric keys that have no meaning to API consumers: ```json { "timeseries": { "2352746367989923131": { ... }, "3940108470521992408": { ... } } } ``` This commit introduces a new `TableOutput` struct that presents timeseries data as an array instead of a map. The `TableOutput` type is converted from the internal `Table` representation at the API boundary, preserving the ordering from the original `BTreeMap` while providing a cleaner JSON structure: ```json { "timeseries": [ { ... }, { ... } ] } ``` Closes #8108
1 parent fc12607 commit 629486e

File tree

7 files changed

+81
-14
lines changed

7 files changed

+81
-14
lines changed

nexus/src/external_api/http_entrypoints.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6571,7 +6571,11 @@ impl NexusExternalApi for NexusExternalApiImpl {
65716571
nexus
65726572
.timeseries_query(&opctx, &query)
65736573
.await
6574-
.map(|tables| HttpResponseOk(views::OxqlQueryResult { tables }))
6574+
.map(|tables| {
6575+
HttpResponseOk(views::OxqlQueryResult {
6576+
tables: tables.into_iter().map(Into::into).collect(),
6577+
})
6578+
})
65756579
.map_err(HttpError::from)
65766580
};
65776581
apictx
@@ -6598,7 +6602,11 @@ impl NexusExternalApi for NexusExternalApiImpl {
65986602
nexus
65996603
.timeseries_query_project(&opctx, &project_lookup, &query)
66006604
.await
6601-
.map(|tables| HttpResponseOk(views::OxqlQueryResult { tables }))
6605+
.map(|tables| {
6606+
HttpResponseOk(views::OxqlQueryResult {
6607+
tables: tables.into_iter().map(Into::into).collect(),
6608+
})
6609+
})
66026610
.map_err(HttpError::from)
66036611
};
66046612
apictx

nexus/tests/integration_tests/metrics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ async fn test_instance_watcher_metrics(
270270

271271
#[track_caller]
272272
fn count_state(
273-
table: &oxql_types::Table,
273+
table: &oxql_types::TableOutput,
274274
instance_id: InstanceUuid,
275275
state: &'static str,
276276
) -> Result<i64, MetricsNotYet> {

nexus/tests/integration_tests/metrics_querier.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ impl<'a, N> MetricsQuerier<'a, N> {
9393
cond: F,
9494
) -> T
9595
where
96-
F: Fn(Vec<oxql_types::Table>) -> Result<T, MetricsNotYet>,
96+
F: Fn(Vec<oxql_types::TableOutput>) -> Result<T, MetricsNotYet>,
9797
{
9898
self.timeseries_query_until("/v1/system/timeseries/query", query, cond)
9999
.await
@@ -108,7 +108,7 @@ impl<'a, N> MetricsQuerier<'a, N> {
108108
cond: F,
109109
) -> T
110110
where
111-
F: Fn(Vec<oxql_types::Table>) -> Result<T, MetricsNotYet>,
111+
F: Fn(Vec<oxql_types::TableOutput>) -> Result<T, MetricsNotYet>,
112112
{
113113
self.timeseries_query_until(
114114
&format!("/v1/timeseries/query?project={project}"),
@@ -128,7 +128,7 @@ impl<'a, N> MetricsQuerier<'a, N> {
128128
&self,
129129
project: &str,
130130
query: &str,
131-
) -> Vec<oxql_types::Table> {
131+
) -> Vec<oxql_types::TableOutput> {
132132
self.project_timeseries_query_until(project, query, |tables| Ok(tables))
133133
.await
134134
}
@@ -270,7 +270,7 @@ impl<'a, N> MetricsQuerier<'a, N> {
270270
cond: F,
271271
) -> T
272272
where
273-
F: Fn(Vec<oxql_types::Table>) -> Result<T, MetricsNotYet>,
273+
F: Fn(Vec<oxql_types::TableOutput>) -> Result<T, MetricsNotYet>,
274274
{
275275
let result = wait_for_condition(
276276
|| async {
@@ -389,5 +389,5 @@ impl<'a, N> MetricsQuerier<'a, N> {
389389

390390
enum TimeseriesQueryResult {
391391
TimeseriesNotFound,
392-
Ok(Vec<oxql_types::Table>),
392+
Ok(Vec<oxql_types::TableOutput>),
393393
}

nexus/types/src/external_api/views.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ pub struct AllowList {
11171117
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
11181118
pub struct OxqlQueryResult {
11191119
/// Tables resulting from the query, each containing timeseries.
1120-
pub tables: Vec<oxql_types::Table>,
1120+
pub tables: Vec<oxql_types::TableOutput>,
11211121
}
11221122

11231123
// ALERTS

openapi/nexus.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22275,7 +22275,7 @@
2227522275
"description": "Tables resulting from the query, each containing timeseries.",
2227622276
"type": "array",
2227722277
"items": {
22278-
"$ref": "#/components/schemas/Table"
22278+
"$ref": "#/components/schemas/TableOutput"
2227922279
}
2228022280
}
2228122281
},
@@ -25523,16 +25523,16 @@
2552325523
"vlan_id"
2552425524
]
2552525525
},
25526-
"Table": {
25527-
"description": "A table represents one or more timeseries with the same schema.\n\nA table is the result of an OxQL query. It contains a name, usually the name of the timeseries schema from which the data is derived, and any number of timeseries, which contain the actual data.",
25526+
"TableOutput": {
25527+
"description": "A table represents one or more timeseries with the same schema.\n\nA table is the result of an OxQL query. It contains a name, usually the name of the timeseries schema from which the data is derived, and any number of timeseries, which contain the actual data. A table representation for external API responses.",
2552825528
"type": "object",
2552925529
"properties": {
2553025530
"name": {
2553125531
"type": "string"
2553225532
},
2553325533
"timeseries": {
25534-
"type": "object",
25535-
"additionalProperties": {
25534+
"type": "array",
25535+
"items": {
2553625536
"$ref": "#/components/schemas/Timeseries"
2553725537
}
2553825538
}

oximeter/oxql-types/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod point;
1111
pub mod table;
1212

1313
pub use self::table::Table;
14+
pub use self::table::TableOutput;
1415
pub use self::table::Timeseries;
1516

1617
/// Describes the time alignment for an OxQL query.

oximeter/oxql-types/src/table.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,61 @@ impl Table {
360360
}
361361
}
362362
}
363+
364+
/// A table represents one or more timeseries with the same schema.
365+
///
366+
/// A table is the result of an OxQL query. It contains a name, usually the name
367+
/// of the timeseries schema from which the data is derived, and any number of
368+
/// timeseries, which contain the actual data.
369+
//
370+
// # Motivation
371+
//
372+
// This struct is derived from [`Table`] but presents timeseries data as a `Vec`
373+
// rather than a map keyed by [`TimeseriesKey`]. This provides a cleaner JSON
374+
// representation for external consumers, as these numeric keys are ephemeral
375+
// identifiers that have no meaning to API consumers. Key ordering is retained
376+
// as this is contructed from the already sorted values present in [`Table`].
377+
//
378+
// When serializing a [`Table`] to JSON, the `BTreeMap<TimeseriesKey, Timeseries>`
379+
// structure produces output with numeric keys like:
380+
// ```json
381+
// {
382+
// "timeseries": {
383+
// "2352746367989923131": { ... },
384+
// "3940108470521992408": { ... }
385+
// }
386+
// }
387+
// ```
388+
//
389+
// `TableOutput` instead serializes timeseries as an array:
390+
// ```json
391+
// {
392+
// "timeseries": [ { ... }, { ... } ]
393+
// }
394+
// ```
395+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
396+
pub struct TableOutput {
397+
// The name of the table.
398+
pub name: String,
399+
// The set of timeseries in the table, ordered by key.
400+
timeseries: Vec<Timeseries>,
401+
}
402+
403+
impl From<Table> for TableOutput {
404+
fn from(table: Table) -> Self {
405+
let timeseries: Vec<_> = table.timeseries.into_values().collect();
406+
TableOutput { name: table.name, timeseries }
407+
}
408+
}
409+
410+
impl TableOutput {
411+
/// Return the name of the table.
412+
pub fn name(&self) -> &str {
413+
self.name.as_str()
414+
}
415+
416+
/// Return the list of timeseries in this table, ordered by key.
417+
pub fn timeseries(&self) -> impl ExactSizeIterator<Item = &Timeseries> {
418+
self.timeseries.iter()
419+
}
420+
}

0 commit comments

Comments
 (0)