Skip to content

Commit 1a5a2e5

Browse files
committed
stub out project-scoped timeseries endpoint
1 parent cf4b8df commit 1a5a2e5

File tree

6 files changed

+192
-1
lines changed

6 files changed

+192
-1
lines changed

nexus/external-api/output/nexus_tags.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ API operations found with tag "metrics"
7474
OPERATION ID METHOD URL PATH
7575
silo_metric GET /v1/metrics/{metric_name}
7676
timeseries_query POST /v1/timeseries/query
77+
timeseries_query_project POST /v1/timeseries/query/project/{project}
7778
timeseries_schema_list GET /v1/timeseries/schema
7879

7980
API operations found with tag "policy"

nexus/external-api/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2567,6 +2567,20 @@ pub trait NexusExternalApi {
25672567
body: TypedBody<params::TimeseriesQuery>,
25682568
) -> Result<HttpResponseOk<views::OxqlQueryResult>, HttpError>;
25692569

2570+
/// Run project-scoped timeseries query
2571+
///
2572+
/// Queries are written in OxQL. Queries can only refer to timeseries data from the specified project.
2573+
#[endpoint {
2574+
method = POST,
2575+
path = "/v1/timeseries/query/project/{project}",
2576+
tags = ["metrics"],
2577+
}]
2578+
async fn timeseries_query_project(
2579+
rqctx: RequestContext<Self::Context>,
2580+
path_params: Path<params::ProjectPath>,
2581+
body: TypedBody<params::TimeseriesQuery>,
2582+
) -> Result<HttpResponseOk<views::OxqlQueryResult>, HttpError>;
2583+
25702584
// Updates
25712585

25722586
/// Upload TUF repository

nexus/src/app/metrics.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,46 @@ impl super::Nexus {
178178
_ => Error::InternalError { internal_message: e.to_string() },
179179
})
180180
}
181+
182+
/// Run an OxQL query against the timeseries database, scoped to a specific project.
183+
pub(crate) async fn timeseries_query_project(
184+
&self,
185+
_opctx: &OpContext,
186+
project_lookup: &lookup::Project<'_>,
187+
query: impl AsRef<str>,
188+
) -> Result<Vec<oxql_types::Table>, Error> {
189+
// Ensure the user has read access to the project
190+
let (.., authz_project) =
191+
project_lookup.lookup_for(authz::Action::Read).await?;
192+
193+
let parsed_query = oximeter_db::oxql::Query::new(query.as_ref())
194+
.map_err(|e| Error::invalid_request(e.to_string()))?;
195+
196+
// Check that the query only refers to the project
197+
198+
self.timeseries_client
199+
.get()
200+
.await
201+
.map_err(|e| {
202+
Error::internal_error(&format!(
203+
"Cannot access timeseries DB: {}",
204+
e
205+
))
206+
})?
207+
.oxql_query(query)
208+
.await
209+
.map(|result| result.tables)
210+
.map_err(|e| match e {
211+
oximeter_db::Error::DatabaseUnavailable(_) => {
212+
Error::ServiceUnavailable {
213+
internal_message: e.to_string(),
214+
}
215+
}
216+
oximeter_db::Error::Oxql(_)
217+
| oximeter_db::Error::TimeseriesNotFound(_) => {
218+
Error::invalid_request(e.to_string())
219+
}
220+
_ => Error::InternalError { internal_message: e.to_string() },
221+
})
222+
}
181223
}

nexus/src/external_api/http_entrypoints.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5544,6 +5544,35 @@ impl NexusExternalApi for NexusExternalApiImpl {
55445544
.await
55455545
}
55465546

5547+
async fn timeseries_query_project(
5548+
rqctx: RequestContext<ApiContext>,
5549+
path_params: Path<params::ProjectPath>,
5550+
body: TypedBody<params::TimeseriesQuery>,
5551+
) -> Result<HttpResponseOk<views::OxqlQueryResult>, HttpError> {
5552+
let apictx = rqctx.context();
5553+
let handler = async {
5554+
let nexus = &apictx.context.nexus;
5555+
let opctx =
5556+
crate::context::op_context_for_external_api(&rqctx).await?;
5557+
let project_path = path_params.into_inner();
5558+
let query = body.into_inner().query;
5559+
let project_lookup = nexus.project_lookup(
5560+
&opctx,
5561+
params::ProjectSelector { project: project_path.project },
5562+
)?;
5563+
nexus
5564+
.timeseries_query_project(&opctx, &project_lookup, &query)
5565+
.await
5566+
.map(|tables| HttpResponseOk(views::OxqlQueryResult { tables }))
5567+
.map_err(HttpError::from)
5568+
};
5569+
apictx
5570+
.context
5571+
.external_latencies
5572+
.instrument_dropshot_handler(&rqctx, handler)
5573+
.await
5574+
}
5575+
55475576
// Updates
55485577

55495578
async fn system_update_put_repository(

nexus/tests/integration_tests/metrics.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,27 @@ async fn test_timeseries_schema_list(
287287
pub async fn timeseries_query(
288288
cptestctx: &ControlPlaneTestContext<omicron_nexus::Server>,
289289
query: impl ToString,
290+
) -> Vec<oxql_types::Table> {
291+
execute_timeseries_query(cptestctx, "/v1/timeseries/query", query).await
292+
}
293+
294+
pub async fn project_timeseries_query(
295+
cptestctx: &ControlPlaneTestContext<omicron_nexus::Server>,
296+
project: &str,
297+
query: impl ToString,
298+
) -> Vec<oxql_types::Table> {
299+
execute_timeseries_query(
300+
cptestctx,
301+
&format!("/v1/timeseries/query/projects/{}", project),
302+
query,
303+
)
304+
.await
305+
}
306+
307+
async fn execute_timeseries_query(
308+
cptestctx: &ControlPlaneTestContext<omicron_nexus::Server>,
309+
endpoint: &str,
310+
query: impl ToString,
290311
) -> Vec<oxql_types::Table> {
291312
// first, make sure the latest timeseries have been collected.
292313
cptestctx.oximeter.force_collect().await;
@@ -300,7 +321,7 @@ pub async fn timeseries_query(
300321
nexus_test_utils::http_testing::RequestBuilder::new(
301322
&cptestctx.external_client,
302323
http::Method::POST,
303-
"/v1/timeseries/query",
324+
endpoint,
304325
)
305326
.body(Some(&body)),
306327
)
@@ -527,6 +548,41 @@ async fn test_instance_watcher_metrics(
527548
assert_gte!(ts2_running, 2);
528549
}
529550

551+
#[nexus_test]
552+
async fn test_project_timeseries_query(
553+
cptestctx: &ControlPlaneTestContext<omicron_nexus::Server>,
554+
) {
555+
let client = &cptestctx.external_client;
556+
557+
// Create two projects
558+
let project1 = create_project(&client, "project1").await;
559+
let project2 = create_project(&client, "project2").await;
560+
561+
// Create resources in each project
562+
create_instance(&client, "project1", "instance1").await;
563+
create_instance(&client, "project2", "instance2").await;
564+
565+
// Force a metrics collection
566+
cptestctx.oximeter.force_collect().await;
567+
568+
// Query for project1
569+
let query1 = "get virtual_machine:check"; // TODO: add project to query
570+
let result1 =
571+
project_timeseries_query(&cptestctx, "project1", query1).await;
572+
573+
// shouldn't work
574+
let result1 =
575+
project_timeseries_query(&cptestctx, "project2", query1).await;
576+
577+
// Query for project2
578+
let query2 = "get virtual_machine:check";
579+
let result2 =
580+
project_timeseries_query(&cptestctx, "project2", query2).await;
581+
582+
// Query with no project specified
583+
// Query with nonexistent project
584+
}
585+
530586
#[nexus_test]
531587
async fn test_mgs_metrics(
532588
cptestctx: &ControlPlaneTestContext<omicron_nexus::Server>,

openapi/nexus.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8838,6 +8838,55 @@
88388838
}
88398839
}
88408840
},
8841+
"/v1/timeseries/query/project/{project}": {
8842+
"post": {
8843+
"tags": [
8844+
"metrics"
8845+
],
8846+
"summary": "Run project-scoped timeseries query",
8847+
"description": "Queries are written in OxQL. Queries can only refer to timeseries data from the specified project.",
8848+
"operationId": "timeseries_query_project",
8849+
"parameters": [
8850+
{
8851+
"in": "path",
8852+
"name": "project",
8853+
"description": "Name or ID of the project",
8854+
"required": true,
8855+
"schema": {
8856+
"$ref": "#/components/schemas/NameOrId"
8857+
}
8858+
}
8859+
],
8860+
"requestBody": {
8861+
"content": {
8862+
"application/json": {
8863+
"schema": {
8864+
"$ref": "#/components/schemas/TimeseriesQuery"
8865+
}
8866+
}
8867+
},
8868+
"required": true
8869+
},
8870+
"responses": {
8871+
"200": {
8872+
"description": "successful operation",
8873+
"content": {
8874+
"application/json": {
8875+
"schema": {
8876+
"$ref": "#/components/schemas/OxqlQueryResult"
8877+
}
8878+
}
8879+
}
8880+
},
8881+
"4XX": {
8882+
"$ref": "#/components/responses/Error"
8883+
},
8884+
"5XX": {
8885+
"$ref": "#/components/responses/Error"
8886+
}
8887+
}
8888+
}
8889+
},
88418890
"/v1/timeseries/schema": {
88428891
"get": {
88438892
"tags": [

0 commit comments

Comments
 (0)