Skip to content

feat(BA-7317): render prometheus query preset templates with Jinja - #13675

Open
seedspirit wants to merge 8 commits into
mainfrom
feat/BA-7317
Open

feat(BA-7317): render prometheus query preset templates with Jinja#13675
seedspirit wants to merge 8 commits into
mainfrom
feat/BA-7317

Conversation

@seedspirit

@seedspirit seedspirit commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace the str.format-based query_template engine with a sandboxed Jinja environment (ImmutableSandboxedEnvironment + StrictUndefined); placeholders are now {{ labels }}, {{ window }}, {{ group_by }}.
  • Validate templates with a parser-based AST whitelist (literal text + variable substitution only — statements, filters, and attribute access are rejected) and reject the legacy {placeholder} syntax at the API boundary with a guidance message.
  • Add data migration 4b8e2f7a91d3 that rewrites all stored presets (including seeded defaults) to the Jinja form; the conversion is idempotent and the legacy parsing logic survives only inside the migration. The example fixture JSON is converted as well.

Test plan

  • pants test on the changed targets and their direct dependents (26 targets, including component tests) passes
  • Migration verified against a live PostgreSQL per alembic/AGENTS.md: representative legacy rows (seeded + user-authored patterns) convert correctly; downgrade → re-upgrade is idempotent
  • Live verification via ./bai: preview/execute succeed with Jinja templates, legacy syntax is rejected client- and server-side with a clear error
  • Fixed live-stat/container-metric templates verified against a real Prometheus (all 7 rendered queries accepted; user_utilization_metric returns real data for GAUGE/RATE/DIFF paths)

Resolves BA-7317

🤖 Generated with Claude Code


📚 Documentation preview 📚: https://sorna--13675.org.readthedocs.build/en/13675/


📚 Documentation preview 📚: https://sorna-ko--13675.org.readthedocs.build/ko/13675/

@github-actions github-actions Bot added size:XL 500~ LoC comp:manager Related to Manager component comp:common Related to Common component require:db-migration Automatically set when alembic migrations are added or updated area:docs Documentations labels Aug 10, 2026
seedspirit added a commit that referenced this pull request Aug 11, 2026
seedspirit added a commit that referenced this pull request Aug 12, 2026
@seedspirit
seedspirit marked this pull request as ready for review August 12, 2026 07:11
@seedspirit
seedspirit requested a review from a team as a code owner August 12, 2026 07:11
Copilot AI balanced review requested due to automatic review settings August 12, 2026 07:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Migrates Prometheus query presets from str.format placeholders to restricted, sandboxed Jinja templates.

Changes:

  • Adds Jinja rendering and AST-based validation.
  • Migrates stored presets and fixtures.
  • Updates APIs, documentation, and tests.

Reviewed changes

Copilot reviewed 20 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/manager/services/utilization_metric/test_container_metric.py Updates rendered-query expectations.
tests/unit/manager/services/prometheus_query_preset/test_prometheus_query_preset_service.py Updates service fixtures.
tests/unit/manager/services/idle_checker/test_service.py Updates idle-checker template.
tests/unit/manager/repositories/prometheus_query_preset/test_prometheus_query_preset_repository.py Updates repository templates.
tests/unit/manager/repositories/prometheus_query_preset/test_prometheus_query_preset_options.py Updates seed defaults.
tests/unit/manager/repositories/metric/test_session_utilization.py Updates utilization template.
tests/unit/manager/clients/prometheus/test_preset.py Tests Jinja rendering and validation.
tests/unit/manager/clients/prometheus/test_client.py Updates client rendering tests.
tests/unit/common/dto/manager/v2/prometheus_query_preset/test_request.py Tests request validation.
tests/component/prometheus_query_preset/test_prometheus_query_preset_preview.py Updates preview scenarios.
tests/component/manager/clients/prometheus/test_sd_relabel.py Updates relabel fixture.
tests/component/manager/clients/prometheus/test_client_integration.py Updates integration fixture.
src/ai/backend/manager/models/alembic/versions/4b8e2f7a91d3_convert_prometheus_query_preset_templates_to_jinja.py Migrates stored templates.
src/ai/backend/manager/clients/prometheus/preset.py Implements cached Jinja rendering.
src/ai/backend/manager/clients/prometheus/fixed_query_builder.py Converts built-in templates.
src/ai/backend/manager/api/gql/prometheus_query_preset/types/inputs.py Documents GraphQL syntax.
src/ai/backend/common/dto/manager/v2/prometheus_query_preset/validators.py Adds sandbox and AST validation.
src/ai/backend/common/dto/manager/v2/prometheus_query_preset/request.py Documents request fields.
src/ai/backend/common/data/idle_checker/types.py Updates placeholder descriptions.
fixtures/manager/example-prometheus-query-presets.json Converts example presets.
docs/manager/rest-reference/openapi.json Updates REST reference.
docs/manager/graphql-reference/v2-schema.graphql Updates GraphQL schema reference.
docs/manager/graphql-reference/supergraph.graphql Updates supergraph reference.
changes/13675.feature.md Adds release-note fragment.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread changes/13675.breaking.md
@@ -0,0 +1 @@
Switch Prometheus query preset templates to sandboxed Jinja syntax ({{ labels }}, {{ window }}, {{ group_by }}) with automatic migration of stored presets; the legacy str.format placeholder syntax is no longer accepted
Comment on lines +70 to +72
try:
jinja2.Environment().parse(template)
return template

@fregataa fregataa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's add breaking changelog

seedspirit added a commit that referenced this pull request Aug 14, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
seedspirit and others added 7 commits August 14, 2026 11:02
Replace the str.format-based query_template engine with a sandboxed Jinja
environment. Templates now use {{ labels }}, {{ window }}, {{ group_by }};
the legacy {placeholder} syntax is rejected at the API boundary with a
guidance message, and a data migration rewrites all stored presets
(including seeded defaults) to the Jinja form. Validation is parser-based:
an AST whitelist permits only literal text and variable substitution, and
StrictUndefined rejects unknown variables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: octodog <mu001@lablup.com>
…igration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… head

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… head

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +38 to +60
def validate_query_template(template: str) -> None:
"""Validate a Jinja PromQL template; raises ``InvalidMetricPresetTemplate``."""
if not template.strip():
raise InvalidMetricPresetTemplate("Template must not be empty.")
unsupported_vars = _UNSUPPORTED_TEMPLATE_VAR_RE.findall(template)
if unsupported_vars:
placeholders = ", ".join(f"{{{name}}}" for name in sorted(PLACEHOLDER_NAMES))
placeholders = ", ".join(f"{{{{ {name} }}}}" for name in sorted(PLACEHOLDER_NAMES))
raise InvalidMetricPresetTemplate(
f"Unsupported template variables: {unsupported_vars}. "
f"Use placeholders {placeholders} or literal PromQL values."
)
if _LEGACY_TEMPLATE_RE.search(template):
raise InvalidMetricPresetTemplate(
"Legacy str.format template syntax is no longer supported; "
f"use {{{{ labels }}}}, {{{{ window }}}}, {{{{ group_by }}}}: {template!r}"
)
try:
ast = PROMQL_TEMPLATE_ENV.parse(template)
except TemplateSyntaxError as e:
raise InvalidMetricPresetTemplate(f"Invalid template syntax ({e}): {template!r}") from e
for node in _walk(ast):
if not isinstance(node, _ALLOWED_NODE_TYPES):
raise InvalidMetricPresetTemplate(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is this implementation here? + Since the implementation places jinja in the global scope, the location of the implementation seems off—please adjust it properly. Even in the Notification Center and other places, values for the implementation are provided in fields, so please do not place variables in the global scope.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The DTO package should not contain any business logic.

Comment on lines +58 to +72
def _to_jinja(template: str) -> str:
"""Rewrite a legacy ``str.format`` template as Jinja; other templates unchanged."""
try:
parsed = list(string.Formatter().parse(_escape_non_placeholders(template)))
except ValueError:
return template
has_placeholder = False
for _literal, field, _spec, _conv in parsed:
if field in ("labels", "window", "group_by"):
has_placeholder = True
break
if not has_placeholder:
try:
jinja2.Environment().parse(template)
return template

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure if I need iLogic for this—can't I just run the format command and replace the values with {{...}}?

@seedspirit seedspirit Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These helpers are a frozen copy of the removed legacy renderer (escape_non_placeholders(template).format(...)).

Plain substitution breaks on legacy-valid templates like metric{{mode!="idle",{labels}}}: replacing only {labels} leaves {{mode!="idle" behind, which Jinja rejects — the legacy {{/}} escapes must also be unfolded into single braces. string.Formatter().parse() does exactly that, with the same parser the legacy renderer used.

Verified the converted output renders identically to the legacy renderer for all seeded presets and the shape above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentations comp:common Related to Common component comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants