feat(BA-4667): Add GraphQL API for prometheus query preset operations#9643
feat(BA-4667): Add GraphQL API for prometheus query preset operations#9643seedspirit wants to merge 2 commits intomainfrom
Conversation
Add Strawberry GraphQL types, resolvers, and fetchers for prometheus query preset admin CRUD and execute query following BEP-1050 spec: - Admin queries: adminPrometheusQueryPreset, adminPrometheusQueryPresets with Node/Connection pagination, filtering, and ordering - Admin mutations: adminCreatePrometheusQueryPreset, adminModifyPrometheusQueryPreset, adminDeletePrometheusQueryPreset - Execute query: prometheusQueryPresetResult (all authenticated users) resolves preset by name and returns PrometheusQueryResultGQL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a Strawberry GraphQL surface for Prometheus query preset CRUD/listing (admin-only) and preset execution (authenticated users), including Relay Node/Connection pagination and typed Prometheus result payloads.
Changes:
- Introduces new GraphQL types (Node/Connection, inputs, filters/order-by, payloads) for Prometheus query presets and query results.
- Adds query/mutation resolvers and fetchers, wiring them into the main GraphQL schema.
- Extends repository query conditions to support created_at time filtering.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ai/backend/manager/repositories/prometheus_query_preset/options.py | Adds created_at before/after query conditions used by GQL filters. |
| src/ai/backend/manager/api/gql/schema.py | Registers new Prometheus query preset queries/mutations in the root schema. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/types/payloads.py | Adds GraphQL result/payload types for Prometheus execution and delete payload. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/types/node.py | Adds Relay Node/Connection and create/modify payload types for presets. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/types/inputs.py | Adds inputs for CRUD and execution (labels, time range), mapping to service-layer specs. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/types/filters.py | Adds filter + order-by inputs for list queries, mapping to repository conditions/orders. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/types/init.py | Exports the new GQL types for package consumers. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/resolver/query.py | Implements admin get/list queries and the authenticated execution query by preset name. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/resolver/mutation.py | Implements admin create/modify/delete mutations. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/resolver/init.py | Re-exports resolvers for package wiring. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/fetcher/preset.py | Implements fetchers for list/get and execution, assembling Relay connections and result types. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/fetcher/init.py | Exports fetchers. |
| src/ai/backend/manager/api/gql/prometheus_query_preset/init.py | Package exports for schema wiring and external imports. |
| changes/9643.feature.md | Documents the new GraphQL API feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if self.OR: | ||
| or_sub_conditions: list[QueryCondition] = [] | ||
| for sub_filter in self.OR: | ||
| or_sub_conditions.extend(sub_filter.build_conditions()) | ||
| if or_sub_conditions: | ||
| conditions.append(combine_conditions_or(or_sub_conditions)) | ||
|
|
||
| if self.NOT: | ||
| not_sub_conditions: list[QueryCondition] = [] | ||
| for sub_filter in self.NOT: | ||
| not_sub_conditions.extend(sub_filter.build_conditions()) | ||
| if not_sub_conditions: | ||
| conditions.append(negate_conditions(not_sub_conditions)) |
There was a problem hiding this comment.
The OR/NOT implementations flatten each sub-filter’s (implicitly AND-ed) conditions into a single list, which changes boolean semantics. Example: OR([{a,b},{c,d}]) becomes (a OR b OR c OR d) instead of (a AND b) OR (c AND d). Fix by first combining each sub_filter.build_conditions() into a single grouped condition (AND within the sub-filter), then OR those grouped conditions; similarly, NOT should negate grouped sub-filters (or apply De Morgan correctly), not a flattened list.
| class ModifyPrometheusQueryPresetInput: | ||
| name: str | None = strawberry.field(default=UNSET, description="New preset name.") | ||
| metric_name: str | None = strawberry.field(default=UNSET, description="New metric name.") | ||
| query_template: str | None = strawberry.field(default=UNSET, description="New PromQL template.") | ||
| time_window: str | None = strawberry.field( | ||
| default=UNSET, description="New default time window." | ||
| ) | ||
| options: PrometheusPresetOptionsInput | None = strawberry.field( | ||
| default=UNSET, description="New preset options." | ||
| ) | ||
|
|
||
| def to_updater(self, preset_id: UUID) -> Updater[PrometheusQueryPresetRow]: | ||
| spec = PrometheusQueryPresetUpdaterSpec() | ||
|
|
||
| if self.name is not UNSET and self.name is not None: | ||
| spec.name = OptionalState.update(self.name) | ||
|
|
||
| if self.metric_name is not UNSET and self.metric_name is not None: | ||
| spec.metric_name = OptionalState.update(self.metric_name) | ||
|
|
||
| if self.query_template is not UNSET and self.query_template is not None: | ||
| spec.query_template = OptionalState.update(self.query_template) |
There was a problem hiding this comment.
Modify input semantics for explicit null are inconsistent: time_window treats null as a deliberate nullify, but name/metric_name/query_template treat null as a no-op. For a TriState/OptionalState API this is surprising for clients. Consider either (a) making these fields str with default=UNSET (disallow null), or (b) supporting explicit nullification consistently (e.g., using TriState for these fields too).
| def from_data(cls, data: PrometheusQueryPresetData) -> Self: | ||
| return cls( | ||
| id=ID(str(data.id)), | ||
| name=data.name, |
There was a problem hiding this comment.
PrometheusQueryPresetGQL.id is declared as NodeID[str], but from_data() assigns it using strawberry.ID. Even if both are string-like at runtime, mixing Relay NodeID and plain ID is easy to misread and can cause subtle issues with Relay/global-id expectations. Prefer assigning the internal NodeID value directly (e.g., a UUID string as the NodeID type) rather than wrapping it as ID.
Summary
prometheusQueryPresetResult) available to all authenticated users, resolving presets by name and returning structured Prometheus resultsPrometheusQueryPresetGQL,PrometheusPresetOptionsGQL,MetricLabelEntryGQL,MetricResultGQL,PrometheusQueryResultGQL, and corresponding input/filter/payload typesTest plan
Resolves BA-4667