Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nexus] Project-scoped OxQL endpoint #6873

Merged
merged 9 commits into from
Dec 3, 2024
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 @@ -30,6 +30,7 @@ probe_create POST /experimental/v1/probes
probe_delete DELETE /experimental/v1/probes/{probe}
probe_list GET /experimental/v1/probes
probe_view GET /experimental/v1/probes/{probe}
timeseries_query POST /v1/timeseries/query

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

// TODO: list endpoint for project-scoped schemas is blocked on
// https://github.com/oxidecomputer/omicron/issues/5942: the authz scope for
// each schema is not stored in Clickhouse yet.

/// Run project-scoped timeseries query
///
/// 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",
tags = ["hidden"],
}]
async fn timeseries_query(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::ProjectSelector>,
body: TypedBody<params::TimeseriesQuery>,
) -> Result<HttpResponseOk<views::OxqlQueryResult>, HttpError>;

// Updates

/// Upload TUF repository
Expand Down
70 changes: 47 additions & 23 deletions nexus/src/app/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,28 +140,52 @@ impl super::Nexus {
self.timeseries_client
.oxql_query(query)
.await
.map(|result| {
// TODO-observability: The query method returns information
// about the duration of the OxQL query and the database
// resource usage for each contained SQL query. We should
// publish this as a timeseries itself, so that we can track
// improvements to query processing.
//
// For now, simply return the tables alone.
result.tables
})
.map_err(|e| match e {
oximeter_db::Error::DatabaseUnavailable(_)
| oximeter_db::Error::Connection(_) => {
Error::ServiceUnavailable {
internal_message: e.to_string(),
}
}
oximeter_db::Error::Oxql(_)
| oximeter_db::Error::TimeseriesNotFound(_) => {
Error::invalid_request(e.to_string())
}
_ => Error::InternalError { internal_message: e.to_string() },
})
// TODO-observability: The query method returns information
// about the duration of the OxQL query and the database
// resource usage for each contained SQL query. We should
// publish this as a timeseries itself, so that we can track
// improvements to query processing.
//
// For now, simply return the tables alone.
.map(|result| result.tables)
.map_err(map_timeseries_err)
}

/// Run an OxQL query against the timeseries database, scoped to a specific project.
pub(crate) async fn timeseries_query_project(
&self,
_opctx: &OpContext,
project_lookup: &lookup::Project<'_>,
query: impl AsRef<str>,
) -> Result<Vec<oxql_types::Table>, Error> {
// Ensure the user has read access to the project
let (authz_silo, authz_project) =
project_lookup.lookup_for(authz::Action::Read).await?;

// Ensure the query only refers to the project
let filtered_query = format!(
"{} | filter silo_id == \"{}\" && project_id == \"{}\"",
query.as_ref(),
authz_silo.id(),
authz_project.id()
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the crux of the PR.


self.timeseries_client
.oxql_query(filtered_query)
.await
.map(|result| result.tables)
.map_err(map_timeseries_err)
}
}

fn map_timeseries_err(e: oximeter_db::Error) -> Error {
match e {
oximeter_db::Error::DatabaseUnavailable(_)
| oximeter_db::Error::Connection(_) => Error::unavail(&e.to_string()),
oximeter_db::Error::Oxql(_)
| oximeter_db::Error::TimeseriesNotFound(_) => {
Error::invalid_request(e.to_string())
}
_ => Error::internal_error(&e.to_string()),
}
}
27 changes: 27 additions & 0 deletions 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_query(
rqctx: RequestContext<ApiContext>,
query_params: Query<params::ProjectSelector>,
body: TypedBody<params::TimeseriesQuery>,
) -> Result<HttpResponseOk<views::OxqlQueryResult>, 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 project_selector = query_params.into_inner();
let query = body.into_inner().query;
let project_lookup =
nexus.project_lookup(&opctx, project_selector)?;
nexus
.timeseries_query_project(&opctx, &project_lookup, &query)
.await
.map(|tables| HttpResponseOk(views::OxqlQueryResult { tables }))
.map_err(HttpError::from)
};
apictx
.context
.external_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

// Updates

async fn system_update_put_repository(
Expand Down
23 changes: 19 additions & 4 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,10 +948,14 @@ pub static DEMO_SILO_METRICS_URL: Lazy<String> = Lazy::new(|| {
)
});

pub static TIMESERIES_LIST_URL: Lazy<String> =
pub static TIMESERIES_QUERY_URL: Lazy<String> = Lazy::new(|| {
format!("/v1/timeseries/query?project={}", *DEMO_PROJECT_NAME)
});

pub static SYSTEM_TIMESERIES_LIST_URL: Lazy<String> =
Lazy::new(|| String::from("/v1/system/timeseries/schemas"));

pub static TIMESERIES_QUERY_URL: Lazy<String> =
pub static SYSTEM_TIMESERIES_QUERY_URL: Lazy<String> =
Lazy::new(|| String::from("/v1/system/timeseries/query"));

pub static DEMO_TIMESERIES_QUERY: Lazy<params::TimeseriesQuery> =
Expand Down Expand Up @@ -2208,7 +2212,18 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
},

VerifyEndpoint {
url: &TIMESERIES_LIST_URL,
url: &TIMESERIES_QUERY_URL,
visibility: Visibility::Protected,
unprivileged_access: UnprivilegedAccess::None,
allowed_methods: vec![
AllowedMethod::Post(
serde_json::to_value(&*DEMO_TIMESERIES_QUERY).unwrap()
),
],
},

VerifyEndpoint {
url: &SYSTEM_TIMESERIES_LIST_URL,
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::None,
allowed_methods: vec![
Expand All @@ -2217,7 +2232,7 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
},

VerifyEndpoint {
url: &TIMESERIES_QUERY_URL,
url: &SYSTEM_TIMESERIES_QUERY_URL,
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::None,
allowed_methods: vec![
Expand Down
Loading
Loading