-
Notifications
You must be signed in to change notification settings - Fork 173
feat(BA-4702): Implement prometheus query preset repository layer #9470
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
Changes from all commits
9867a29
3634e3b
85e914b
a386d1e
f6d2e51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Implement prometheus query preset repository layer. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,11 @@ | ||
| from .types import PrometheusQueryPresetData | ||
| from .types import ( | ||
| PrometheusQueryPresetData, | ||
| PrometheusQueryPresetListResult, | ||
| PrometheusQueryPresetModifier, | ||
| ) | ||
|
|
||
| __all__ = ("PrometheusQueryPresetData",) | ||
| __all__ = ( | ||
| "PrometheusQueryPresetData", | ||
| "PrometheusQueryPresetListResult", | ||
| "PrometheusQueryPresetModifier", | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,10 +2,13 @@ | |||||||||||
|
|
||||||||||||
| from dataclasses import dataclass, field | ||||||||||||
| from datetime import datetime | ||||||||||||
| from typing import Any, override | ||||||||||||
| from uuid import UUID | ||||||||||||
|
|
||||||||||||
| from ai.backend.manager.types import OptionalState, PartialModifier, TriState | ||||||||||||
|
|
||||||||||||
| @dataclass | ||||||||||||
|
|
||||||||||||
| @dataclass(frozen=True) | ||||||||||||
| class PrometheusQueryPresetData: | ||||||||||||
| """Domain model data for prometheus query preset.""" | ||||||||||||
|
|
||||||||||||
|
|
@@ -18,3 +21,36 @@ class PrometheusQueryPresetData: | |||||||||||
| group_labels: list[str] | ||||||||||||
| created_at: datetime = field(compare=False) | ||||||||||||
| updated_at: datetime = field(compare=False) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @dataclass | ||||||||||||
| class PrometheusQueryPresetModifier(PartialModifier): | ||||||||||||
| """Modifier for prometheus query preset.""" | ||||||||||||
|
|
||||||||||||
| name: OptionalState[str] = field(default_factory=OptionalState[str].nop) | ||||||||||||
| metric_name: OptionalState[str] = field(default_factory=OptionalState[str].nop) | ||||||||||||
| query_template: OptionalState[str] = field(default_factory=OptionalState[str].nop) | ||||||||||||
| time_window: TriState[str] = field(default_factory=TriState[str].nop) | ||||||||||||
| filter_labels: OptionalState[list[str]] = field(default_factory=OptionalState[list[str]].nop) | ||||||||||||
| group_labels: OptionalState[list[str]] = field(default_factory=OptionalState[list[str]].nop) | ||||||||||||
|
|
||||||||||||
| @override | ||||||||||||
| def fields_to_update(self) -> dict[str, Any]: | ||||||||||||
| to_update: dict[str, Any] = {} | ||||||||||||
| self.name.update_dict(to_update, "name") | ||||||||||||
| self.metric_name.update_dict(to_update, "metric_name") | ||||||||||||
| self.query_template.update_dict(to_update, "query_template") | ||||||||||||
| self.time_window.update_dict(to_update, "time_window") | ||||||||||||
| self.filter_labels.update_dict(to_update, "filter_labels") | ||||||||||||
| self.group_labels.update_dict(to_update, "group_labels") | ||||||||||||
|
Comment on lines
+44
to
+45
|
||||||||||||
| self.filter_labels.update_dict(to_update, "filter_labels") | |
| self.group_labels.update_dict(to_update, "group_labels") | |
| # NOTE: filter_labels and group_labels are stored under a JSONB "options" | |
| # column in the underlying row type, so they must be handled by a dedicated | |
| # updater/merger rather than as top-level columns here. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from ai.backend.manager.data.prometheus_query_preset import ( | ||
| PrometheusQueryPresetData, | ||
| PrometheusQueryPresetListResult, | ||
| PrometheusQueryPresetModifier, | ||
| ) | ||
|
|
||
| from .creators import PrometheusQueryPresetCreatorSpec | ||
| from .repositories import PrometheusQueryPresetRepositories | ||
| from .repository import PrometheusQueryPresetRepository | ||
|
|
||
| __all__ = ( | ||
| "PrometheusQueryPresetCreatorSpec", | ||
| "PrometheusQueryPresetData", | ||
| "PrometheusQueryPresetListResult", | ||
| "PrometheusQueryPresetModifier", | ||
| "PrometheusQueryPresetRepositories", | ||
| "PrometheusQueryPresetRepository", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| """CreatorSpec implementations for prometheus query preset repository.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
| from typing import override | ||
|
|
||
| from ai.backend.manager.models.prometheus_query_preset.row import ( | ||
| PresetOptions, | ||
| PrometheusQueryPresetRow, | ||
| ) | ||
| from ai.backend.manager.repositories.base import CreatorSpec | ||
|
|
||
|
|
||
| @dataclass | ||
| class PrometheusQueryPresetCreatorSpec(CreatorSpec[PrometheusQueryPresetRow]): | ||
| """CreatorSpec for prometheus query preset.""" | ||
|
|
||
| name: str | ||
| metric_name: str | ||
| query_template: str | ||
| time_window: str | None | ||
| filter_labels: list[str] | ||
| group_labels: list[str] | ||
|
|
||
| @override | ||
| def build_row(self) -> PrometheusQueryPresetRow: | ||
| return PrometheusQueryPresetRow( | ||
| name=self.name, | ||
| metric_name=self.metric_name, | ||
| query_template=self.query_template, | ||
| time_window=self.time_window, | ||
| options=PresetOptions( | ||
| filter_labels=self.filter_labels, | ||
| group_labels=self.group_labels, | ||
| ), | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .db_source import PrometheusQueryPresetDBSource | ||
|
|
||
| __all__ = ("PrometheusQueryPresetDBSource",) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """Database source for prometheus query preset repository operations.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Any, cast | ||
| from uuid import UUID | ||
|
|
||
| import sqlalchemy as sa | ||
| from sqlalchemy.engine import CursorResult | ||
|
|
||
| from ai.backend.common.exception import PrometheusQueryPresetNotFound | ||
| from ai.backend.manager.data.prometheus_query_preset import ( | ||
| PrometheusQueryPresetData, | ||
| PrometheusQueryPresetListResult, | ||
| ) | ||
| from ai.backend.manager.models.prometheus_query_preset import PrometheusQueryPresetRow | ||
| from ai.backend.manager.repositories.base import ( | ||
| BatchQuerier, | ||
| Creator, | ||
| execute_batch_querier, | ||
| execute_creator, | ||
| ) | ||
| from ai.backend.manager.repositories.base.updater import Updater, execute_updater | ||
| from ai.backend.manager.repositories.prometheus_query_preset.updaters import ( | ||
| PrometheusQueryPresetUpdaterSpec, | ||
| ) | ||
| from ai.backend.manager.types import OptionalState | ||
|
|
||
| if TYPE_CHECKING: | ||
| from sqlalchemy.ext.asyncio import AsyncSession as SASession | ||
|
|
||
| from ai.backend.manager.models.utils import ExtendedAsyncSAEngine | ||
|
|
||
|
|
||
| __all__ = ("PrometheusQueryPresetDBSource",) | ||
|
|
||
|
|
||
| class PrometheusQueryPresetDBSource: | ||
| """Database source for prometheus query preset operations.""" | ||
|
|
||
| _db: ExtendedAsyncSAEngine | ||
|
|
||
| def __init__(self, db: ExtendedAsyncSAEngine) -> None: | ||
| self._db = db | ||
|
|
||
| async def create( | ||
| self, | ||
| creator: Creator[PrometheusQueryPresetRow], | ||
| ) -> PrometheusQueryPresetData: | ||
| """Creates a new prometheus query preset.""" | ||
| async with self._db.begin_session() as db_sess: | ||
| result = await execute_creator(db_sess, creator) | ||
| return result.row.to_data() | ||
|
|
||
| async def _merge_partial_options( | ||
| self, | ||
| db_sess: SASession, | ||
| updater: Updater[PrometheusQueryPresetRow], | ||
| ) -> Updater[PrometheusQueryPresetRow]: | ||
| """When only one of filter_labels/group_labels is being updated, | ||
| fetch the current options to preserve the other field.""" | ||
| updater.spec = cast(PrometheusQueryPresetUpdaterSpec, updater.spec) | ||
| filter_updating = updater.spec.filter_labels.optional_value() is not None | ||
| group_updating = updater.spec.group_labels.optional_value() is not None | ||
|
|
||
| # If both are being updated or both are not being updated, no need to merge | ||
| if filter_updating == group_updating: | ||
| return updater | ||
|
|
||
| stmt = sa.select(PrometheusQueryPresetRow.options).where( | ||
| PrometheusQueryPresetRow.id == updater.pk_value | ||
| ) | ||
| current_options = (await db_sess.execute(stmt)).scalar_one_or_none() | ||
| if current_options is None: | ||
| raise PrometheusQueryPresetNotFound( | ||
| f"Prometheus query preset {updater.pk_value} not found" | ||
| ) | ||
|
|
||
| if filter_updating: | ||
| updater.spec.group_labels = OptionalState[list[str]].update( | ||
| list(current_options.group_labels) | ||
| ) | ||
| if group_updating: | ||
| updater.spec.filter_labels = OptionalState[list[str]].update( | ||
| list(current_options.filter_labels) | ||
| ) | ||
| return updater | ||
|
|
||
| async def update( | ||
| self, | ||
| updater: Updater[PrometheusQueryPresetRow], | ||
| ) -> PrometheusQueryPresetData: | ||
| """Updates an existing prometheus query preset.""" | ||
| async with self._db.begin_session() as db_sess: | ||
| updater = await self._merge_partial_options(db_sess, updater) | ||
| result = await execute_updater(db_sess, updater) | ||
| if result is None: | ||
| raise PrometheusQueryPresetNotFound( | ||
| f"Prometheus query preset {updater.pk_value} not found" | ||
| ) | ||
| return result.row.to_data() | ||
|
|
||
| async def delete(self, preset_id: UUID) -> bool: | ||
| """Deletes a prometheus query preset.""" | ||
| async with self._db.begin_session() as db_sess: | ||
| stmt = sa.delete(PrometheusQueryPresetRow).where( | ||
| PrometheusQueryPresetRow.id == preset_id | ||
| ) | ||
| result = await db_sess.execute(stmt) | ||
| return cast(CursorResult[Any], result).rowcount > 0 | ||
|
|
||
| async def get_by_id(self, preset_id: UUID) -> PrometheusQueryPresetData: | ||
| """Retrieves a prometheus query preset by ID.""" | ||
| async with self._db.begin_readonly_session_read_committed() as db_sess: | ||
| row = await db_sess.get(PrometheusQueryPresetRow, preset_id) | ||
| if row is None: | ||
| raise PrometheusQueryPresetNotFound( | ||
| f"Prometheus query preset {preset_id} not found" | ||
| ) | ||
| return row.to_data() | ||
|
|
||
| async def search( | ||
| self, | ||
| querier: BatchQuerier, | ||
| ) -> PrometheusQueryPresetListResult: | ||
| """Searches prometheus query presets with total count.""" | ||
| async with self._db.begin_readonly_session() as db_sess: | ||
| query = sa.select(PrometheusQueryPresetRow) | ||
|
|
||
| result = await execute_batch_querier( | ||
| db_sess, | ||
| query, | ||
| querier, | ||
| ) | ||
|
|
||
| items = [row.PrometheusQueryPresetRow.to_data() for row in result.rows] | ||
|
seedspirit marked this conversation as resolved.
|
||
|
|
||
| return PrometheusQueryPresetListResult( | ||
| items=items, | ||
| total_count=result.total_count, | ||
| has_next_page=result.has_next_page, | ||
| has_previous_page=result.has_previous_page, | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.