Skip to content

Commit

Permalink
silo-scoped endpoint to list project-scoped schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
david-crespo committed Nov 13, 2024
1 parent 5d69621 commit b707fd0
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 4 deletions.
1 change: 1 addition & 0 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ API operations found with tag "metrics"
OPERATION ID METHOD URL PATH
silo_metric GET /v1/metrics/{metric_name}
timeseries_query POST /v1/timeseries/query
timeseries_schema_list GET /v1/timeseries/schema

API operations found with tag "policy"
OPERATION ID METHOD URL PATH
Expand Down
20 changes: 19 additions & 1 deletion nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2567,9 +2567,27 @@ pub trait NexusExternalApi {
body: TypedBody<params::TimeseriesQuery>,
) -> Result<HttpResponseOk<views::OxqlQueryResult>, HttpError>;

/// List project-scoped timeseries schemas
///
/// List schemas that can be queried through the `/v1/timeseries/query` endpoint.
#[endpoint {
method = GET,
path = "/v1/timeseries/schema",
tags = ["metrics"],
}]
async fn timeseries_schema_list(
rqctx: RequestContext<Self::Context>,
pag_params: Query<TimeseriesSchemaPaginationParams>,
) -> Result<
HttpResponseOk<ResultsPage<oximeter_types::TimeseriesSchema>>,
HttpError,
>;

/// Run project-scoped timeseries query
///
/// Queries are written in OxQL. Queries can only refer to timeseries data from the specified project.
/// Queries are written in OxQL. Project must be specified by name or ID in
/// URL query parameter. The OxQL query will only return timeseries data
/// from the specified project.
#[endpoint {
method = POST,
path = "/v1/timeseries/query",
Expand Down
44 changes: 43 additions & 1 deletion nexus/src/app/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use nexus_db_queries::{
use nexus_external_api::TimeseriesSchemaPaginationParams;
use nexus_types::external_api::params::SystemMetricName;
use omicron_common::api::external::{Error, InternalContext};
use oximeter::AuthzScope;
use oximeter_db::{Measurement, TimeseriesSchema};
use std::num::NonZeroU32;

Expand Down Expand Up @@ -166,7 +167,7 @@ impl super::Nexus {
}

/// Run an OxQL query against the timeseries database, scoped to a specific project.
pub(crate) async fn timeseries_query_project(
pub(crate) async fn project_timeseries_query(
&self,
_opctx: &OpContext,
project_lookup: &lookup::Project<'_>,
Expand Down Expand Up @@ -201,4 +202,45 @@ impl super::Nexus {
_ => Error::InternalError { internal_message: e.to_string() },
})
}

/// List available project-scoped timeseries schema
pub(crate) async fn project_timeseries_schema_list(
&self,
opctx: &OpContext,
pagination: &TimeseriesSchemaPaginationParams,
limit: NonZeroU32,
) -> Result<dropshot::ResultsPage<TimeseriesSchema>, Error> {
// any authenticated user should be able to do this
let authz_silo = opctx
.authn
.silo_required()
.internal_context("listing project-scoped timeseries schemas")?;
opctx.authorize(authz::Action::ListChildren, &authz_silo).await?;

self.timeseries_client
.timeseries_schema_list(&pagination.page, limit)
.await
.and_then(|schemas| {
let filtered = schemas
.items
.into_iter()
.filter(|schema| schema.authz_scope == AuthzScope::Project)
.collect();
dropshot::ResultsPage::new(
filtered,
&dropshot::EmptyScanParams {},
|schema, _| schema.timeseries_name.clone(),
)
.map_err(|err| oximeter_db::Error::Database(err.to_string()))
})
.map_err(|e| match e {
oximeter_db::Error::DatabaseUnavailable(_)
| oximeter_db::Error::Connection(_) => {
Error::ServiceUnavailable {
internal_message: e.to_string(),
}
}
_ => Error::InternalError { internal_message: e.to_string() },
})
}
}
29 changes: 28 additions & 1 deletion nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5544,6 +5544,33 @@ impl NexusExternalApi for NexusExternalApiImpl {
.await
}

async fn timeseries_schema_list(
rqctx: RequestContext<ApiContext>,
pag_params: Query<TimeseriesSchemaPaginationParams>,
) -> Result<
HttpResponseOk<ResultsPage<oximeter_db::TimeseriesSchema>>,
HttpError,
> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;
let opctx =
crate::context::op_context_for_external_api(&rqctx).await?;
let pagination = pag_params.into_inner();
let limit = rqctx.page_limit(&pagination)?;
nexus
.project_timeseries_schema_list(&opctx, &pagination, limit)
.await
.map(HttpResponseOk)
.map_err(HttpError::from)
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn timeseries_query(
rqctx: RequestContext<ApiContext>,
query_params: Query<params::ProjectSelector>,
Expand All @@ -5559,7 +5586,7 @@ impl NexusExternalApi for NexusExternalApiImpl {
let project_lookup =
nexus.project_lookup(&opctx, project_selector)?;
nexus
.timeseries_query_project(&opctx, &project_lookup, &query)
.project_timeseries_query(&opctx, &project_lookup, &query)
.await
.map(|tables| HttpResponseOk(views::OxqlQueryResult { tables }))
.map_err(HttpError::from)
Expand Down
11 changes: 11 additions & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,8 @@ pub static DEMO_SILO_METRICS_URL: Lazy<String> = Lazy::new(|| {
pub static TIMESERIES_QUERY_URL: Lazy<String> = Lazy::new(|| {
format!("/v1/timeseries/query?project={}", *DEMO_PROJECT_NAME)
});
pub static TIMESERIES_LIST_URL: Lazy<String> =
Lazy::new(|| String::from("/v1/timeseries/schema"));

pub static SYSTEM_TIMESERIES_LIST_URL: Lazy<String> =
Lazy::new(|| String::from("/v1/system/timeseries/schema"));
Expand Down Expand Up @@ -2209,6 +2211,15 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
],
},

VerifyEndpoint {
url: &TIMESERIES_LIST_URL,
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::None,
allowed_methods: vec![
AllowedMethod::GetVolatile,
],
},

VerifyEndpoint {
url: &TIMESERIES_QUERY_URL,
visibility: Visibility::Protected,
Expand Down
27 changes: 27 additions & 0 deletions nexus/tests/integration_tests/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,33 @@ async fn test_timeseries_schema_list(
let nexus_id = cptestctx.server.server_context().nexus.id();
wait_for_producer(&cptestctx.oximeter, nexus_id).await;

// We should be able to fetch the list of timeseries, and it should include
// Nexus's HTTP latency distribution. This is defined in Nexus itself, and
// should always exist after we've registered as a producer and start
// producing data. Force a collection to ensure that happens.
cptestctx.oximeter.force_collect().await;
let client = &cptestctx.external_client;
let url = "/v1/timeseries/schema";
let schema =
objects_list_page_authz::<TimeseriesSchema>(client, &url).await;
// request latency metric that shows up in the system endpoint is filtered out here
assert!(schema.items.is_empty());

// TODO: add a project-scoped metric and fetch again
// TODO: I think even unprivileged user should be able to list these
}

/// Test that we can correctly list some timeseries schema.
#[nexus_test]
async fn test_system_timeseries_schema_list(
cptestctx: &ControlPlaneTestContext<omicron_nexus::Server>,
) {
// Nexus registers itself as a metric producer on startup, with its own UUID
// as the producer ID. Wait for this to show up in the registered lists of
// producers.
let nexus_id = cptestctx.server.server_context().nexus.id();
wait_for_producer(&cptestctx.oximeter, nexus_id).await;

// We should be able to fetch the list of timeseries, and it should include
// Nexus's HTTP latency distribution. This is defined in Nexus itself, and
// should always exist after we've registered as a producer and start
Expand Down
55 changes: 54 additions & 1 deletion openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -8896,7 +8896,7 @@
"metrics"
],
"summary": "Run project-scoped timeseries query",
"description": "Queries are written in OxQL. Queries can only refer to timeseries data from the specified project.",
"description": "Queries are written in OxQL. Project must be specified by name or ID in URL query parameter. The OxQL query will only return timeseries data from the specified project.",
"operationId": "timeseries_query",
"parameters": [
{
Expand Down Expand Up @@ -8939,6 +8939,59 @@
}
}
},
"/v1/timeseries/schema": {
"get": {
"tags": [
"metrics"
],
"summary": "List project-scoped timeseries schemas",
"description": "List schemas that can be queried through the `/v1/timeseries/query` endpoint.",
"operationId": "timeseries_schema_list",
"parameters": [
{
"in": "query",
"name": "limit",
"description": "Maximum number of items returned by a single call",
"schema": {
"nullable": true,
"type": "integer",
"format": "uint32",
"minimum": 1
}
},
{
"in": "query",
"name": "page_token",
"description": "Token returned by previous call to retrieve the subsequent page",
"schema": {
"nullable": true,
"type": "string"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TimeseriesSchemaResultsPage"
}
}
}
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"x-dropshot-pagination": {
"required": []
}
}
},
"/v1/users": {
"get": {
"tags": [
Expand Down

0 comments on commit b707fd0

Please sign in to comment.