Skip to content

Commit 215aca7

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 215aca7

File tree

6 files changed

+72
-9
lines changed

6 files changed

+72
-9
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

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

0 commit comments

Comments
 (0)