Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/9640.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add REST API endpoints for prometheus query preset CRUD and execution (BEP-1050).
2 changes: 2 additions & 0 deletions src/ai/backend/common/dto/clients/prometheus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .defs import PROMETHEUS_DURATION_PATTERN
from .request import QueryTimeRange
from .response import (
LabelValueResponse,
Expand All @@ -9,6 +10,7 @@
)

__all__ = [
"PROMETHEUS_DURATION_PATTERN",
"QueryTimeRange",
"LabelValueResponse",
"MetricResponse",
Expand Down
4 changes: 4 additions & 0 deletions src/ai/backend/common/dto/clients/prometheus/defs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Constants for Prometheus DTOs."""

# Prometheus duration format: e.g. "5m", "1h30m", "10s", "1d"
PROMETHEUS_DURATION_PATTERN = r"^(?:\d+(?:ms|s|m|h|d|w|y))+$"
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Prometheus Query Definition DTOs for Manager API.
"""

from .path import (
QueryDefinitionIdPathParam,
)
from .request import (
CreateQueryDefinitionOptionsRequest,
CreateQueryDefinitionRequest,
ExecuteQueryDefinitionOptionsRequest,
ExecuteQueryDefinitionRequest,
MetricLabelEntry,
ModifyQueryDefinitionOptionsRequest,
ModifyQueryDefinitionRequest,
QueryDefinitionFilter,
SearchQueryDefinitionsRequest,
)
from .response import (
CreateQueryDefinitionResponse,
DeleteQueryDefinitionResponse,
ExecuteQueryDefinitionResponse,
GetQueryDefinitionResponse,
MetricLabelEntryDTO,
MetricValueDTO,
ModifyQueryDefinitionResponse,
PaginationInfo,
QueryDefinitionDTO,
QueryDefinitionExecuteData,
QueryDefinitionMetricResult,
QueryDefinitionOptionsDTO,
SearchQueryDefinitionsResponse,
)
from .types import (
OrderDirection,
QueryDefinitionOrder,
QueryDefinitionOrderField,
)

__all__ = (
# Path DTOs
"QueryDefinitionIdPathParam",
# Request DTOs
"CreateQueryDefinitionOptionsRequest",
"CreateQueryDefinitionRequest",
"ExecuteQueryDefinitionOptionsRequest",
"ExecuteQueryDefinitionRequest",
"MetricLabelEntry",
"ModifyQueryDefinitionOptionsRequest",
"ModifyQueryDefinitionRequest",
"QueryDefinitionFilter",
"SearchQueryDefinitionsRequest",
# Response DTOs
"CreateQueryDefinitionResponse",
"DeleteQueryDefinitionResponse",
"ExecuteQueryDefinitionResponse",
"GetQueryDefinitionResponse",
"MetricLabelEntryDTO",
"MetricValueDTO",
"ModifyQueryDefinitionResponse",
"PaginationInfo",
"QueryDefinitionDTO",
"QueryDefinitionExecuteData",
"QueryDefinitionMetricResult",
"QueryDefinitionOptionsDTO",
"SearchQueryDefinitionsResponse",
# Types
"OrderDirection",
"QueryDefinitionOrder",
"QueryDefinitionOrderField",
)
20 changes: 20 additions & 0 deletions src/ai/backend/common/dto/manager/prometheus_query_preset/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Path parameter DTOs for Prometheus Query Definition API endpoints.
Shared between Client SDK and Manager API.
"""

from __future__ import annotations

from uuid import UUID

from pydantic import Field

from ai.backend.common.api_handlers import BaseRequestModel

__all__ = ("QueryDefinitionIdPathParam",)


class QueryDefinitionIdPathParam(BaseRequestModel):
"""Path parameter for query definition ID."""

id: UUID = Field(description="The query definition ID")
142 changes: 142 additions & 0 deletions src/ai/backend/common/dto/manager/prometheus_query_preset/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
Request DTOs for Prometheus Query Definition API endpoints.
Shared between Client SDK and Manager API.
"""

from __future__ import annotations

import re

from pydantic import Field, field_validator

from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel
from ai.backend.common.dto.clients.prometheus.defs import PROMETHEUS_DURATION_PATTERN
from ai.backend.common.dto.clients.prometheus.request import QueryTimeRange
from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT
from ai.backend.common.dto.manager.query import StringFilter

from .types import QueryDefinitionOrder

__all__ = (
"CreateQueryDefinitionOptionsRequest",
"CreateQueryDefinitionRequest",
"ExecuteQueryDefinitionOptionsRequest",
"ExecuteQueryDefinitionRequest",
"MetricLabelEntry",
"ModifyQueryDefinitionOptionsRequest",
"ModifyQueryDefinitionRequest",
"QueryDefinitionFilter",
"SearchQueryDefinitionsRequest",
)


class CreateQueryDefinitionOptionsRequest(BaseRequestModel):
"""Options for a prometheus query definition."""

filter_labels: list[str] = Field(description="Allowed filter label keys")
group_labels: list[str] = Field(description="Allowed group-by label keys")


class CreateQueryDefinitionRequest(BaseRequestModel):
"""Request to create a prometheus query definition."""

name: str = Field(description="Human-readable name")
metric_name: str = Field(description="Prometheus metric name")
query_template: str = Field(description="PromQL template with placeholders")
time_window: str | None = Field(
default=None, pattern=PROMETHEUS_DURATION_PATTERN, description="Default time window"
)
options: CreateQueryDefinitionOptionsRequest = Field(description="Query definition options")


class ModifyQueryDefinitionOptionsRequest(BaseRequestModel):
"""Options for modifying a prometheus query definition.

Each field is optional — only provided fields are updated.
"""

filter_labels: list[str] | None = Field(default=None, description="Allowed filter label keys")
group_labels: list[str] | None = Field(default=None, description="Allowed group-by label keys")


class ModifyQueryDefinitionRequest(BaseRequestModel):
"""Request to modify a prometheus query definition.

Only ``time_window`` uses ``Sentinel`` because it is the only nullable DB column;
all other fields are non-nullable, so ``None`` simply means "do not update".
"""

name: str | None = Field(default=None, description="Human-readable name")
metric_name: str | None = Field(default=None, description="Prometheus metric name")
query_template: str | None = Field(
default=None, description="PromQL template with placeholders"
)
time_window: str | Sentinel | None = Field(default=SENTINEL, description="Default time window")
options: ModifyQueryDefinitionOptionsRequest | None = Field(
default=None, description="Query definition options"
)

@field_validator("time_window", mode="after")
@classmethod
def _validate_time_window(cls, v: str | Sentinel | None) -> str | Sentinel | None:
if isinstance(v, str) and not re.match(PROMETHEUS_DURATION_PATTERN, v):
raise ValueError(f"Invalid Prometheus duration format: {v!r}")
return v


class QueryDefinitionFilter(BaseRequestModel):
"""Filter for prometheus query definition search."""

name: StringFilter | None = Field(default=None, description="Filter by name")


class SearchQueryDefinitionsRequest(BaseRequestModel):
"""Request body for searching prometheus query definitions with filters, orders, and pagination."""

filter: QueryDefinitionFilter | None = Field(default=None, description="Filter conditions")
order: list[QueryDefinitionOrder] | None = Field(
default=None, description="Order specifications"
)
offset: int = Field(default=0, ge=0, description="Number of items to skip")
limit: int = Field(
default=DEFAULT_PAGE_LIMIT,
ge=1,
le=MAX_PAGE_LIMIT,
description="Maximum items to return",
)


class MetricLabelEntry(BaseRequestModel):
"""A key-value label entry for executing a query definition."""

key: str = Field(description="Label key")
value: str = Field(description="Label value")


class ExecuteQueryDefinitionOptionsRequest(BaseRequestModel):
"""Execution options for a prometheus query definition."""

filter_labels: list[MetricLabelEntry] = Field(
default_factory=list,
description="Filter labels as key-value pairs",
)
group_labels: list[str] = Field(
default_factory=list,
description="Group-by labels",
)


class ExecuteQueryDefinitionRequest(BaseRequestModel):
"""Request to execute a prometheus query definition."""

options: ExecuteQueryDefinitionOptionsRequest = Field(
default_factory=ExecuteQueryDefinitionOptionsRequest,
description="Execution options (filter and group labels)",
)
window: str | None = Field(
default=None, pattern=PROMETHEUS_DURATION_PATTERN, description="Time window override"
)
time_range: QueryTimeRange | None = Field(
default=None,
description="Time range for the query; if omitted, executes an instant query at the current time",
)
123 changes: 123 additions & 0 deletions src/ai/backend/common/dto/manager/prometheus_query_preset/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
Response DTOs for Prometheus Query Definition API endpoints.
Shared between Client SDK and Manager API.
"""

from __future__ import annotations

from datetime import datetime
from uuid import UUID

from pydantic import BaseModel, Field

from ai.backend.common.api_handlers import BaseResponseModel

__all__ = (
"CreateQueryDefinitionResponse",
"DeleteQueryDefinitionResponse",
"ExecuteQueryDefinitionResponse",
"GetQueryDefinitionResponse",
"MetricLabelEntryDTO",
"MetricValueDTO",
"ModifyQueryDefinitionResponse",
"PaginationInfo",
"QueryDefinitionDTO",
"QueryDefinitionExecuteData",
"QueryDefinitionMetricResult",
"QueryDefinitionOptionsDTO",
"SearchQueryDefinitionsResponse",
)


class QueryDefinitionOptionsDTO(BaseModel):
"""Options DTO for a prometheus query definition."""

filter_labels: list[str] = Field(description="Allowed filter label keys")
group_labels: list[str] = Field(description="Allowed group-by label keys")


class QueryDefinitionDTO(BaseModel):
"""DTO for prometheus query definition data."""

id: UUID = Field(description="Query definition ID")
name: str = Field(description="Human-readable name")
metric_name: str = Field(description="Prometheus metric name")
query_template: str = Field(description="PromQL template")
time_window: str | None = Field(default=None, description="Default time window")
options: QueryDefinitionOptionsDTO = Field(description="Query definition options")
created_at: datetime = Field(description="Creation timestamp")
updated_at: datetime = Field(description="Last update timestamp")


class PaginationInfo(BaseModel):
"""Pagination information."""

total: int = Field(description="Total count of items")
offset: int = Field(description="Current offset")
limit: int = Field(description="Current limit")


class CreateQueryDefinitionResponse(BaseResponseModel):
"""Response for creating a query definition."""

item: QueryDefinitionDTO


class GetQueryDefinitionResponse(BaseResponseModel):
"""Response for getting a query definition by ID."""

item: QueryDefinitionDTO


class SearchQueryDefinitionsResponse(BaseResponseModel):
"""Response for searching query definitions."""

items: list[QueryDefinitionDTO]
pagination: PaginationInfo


class ModifyQueryDefinitionResponse(BaseResponseModel):
"""Response for modifying a query definition."""

item: QueryDefinitionDTO


class DeleteQueryDefinitionResponse(BaseResponseModel):
"""Response for deleting a query definition."""

id: UUID = Field(description="Deleted query definition ID")


class MetricLabelEntryDTO(BaseModel):
"""A key-value label entry in the execute response."""

key: str
value: str


class MetricValueDTO(BaseModel):
"""A single (timestamp, value) data point from Prometheus."""

timestamp: float
value: str


class QueryDefinitionMetricResult(BaseModel):
"""A single metric result from query definition execution."""

metric: list[MetricLabelEntryDTO]
values: list[MetricValueDTO]


class QueryDefinitionExecuteData(BaseModel):
"""Data field of the execute response."""

result_type: str
result: list[QueryDefinitionMetricResult]


class ExecuteQueryDefinitionResponse(BaseResponseModel):
"""Response for executing a query definition."""

status: str
data: QueryDefinitionExecuteData
Loading
Loading