Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ make chart-lint # helm lint + kubeconform
[detector/src/promanomaly/detectors/](detector/src/promanomaly/detectors/),
`pyproject.toml` entry points, and
[docs/detectors.md](docs/detectors.md). CI checks all three.
- Adding a Helm value? Regenerate `values.schema.json` (`make chart-schema`).
- Adding a Helm value? Update [charts/promanomaly/values.schema.json](charts/promanomaly/values.schema.json) by hand to match. The schema is hand-authored alongside `values.yaml` so it can express constraints (enums, ranges, opt-in defaults) a generator can't infer from a sample — see the comment block above the `chart-lint` target in the [Makefile](Makefile).
- Adding or changing a contracted label? Coordinate a PR pair against
[LABELS_CONTRACT.md](LABELS_CONTRACT.md) here and in promforecast — CI
diffs the two files and will fail if they drift.
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,18 @@ operational guidance — including [`docs/patterns.md`](docs/patterns.md),
the composition cookbook of how anomaly scores plug into the rest of
the Prometheus ecosystem (rate-of-change, group rollups, multi-window
monitoring, deploy-time silencing, cross-tool joins with promforecast).
[`docs/when-to-use.md`](docs/when-to-use.md) walks through the
"promforecast, promanomaly, or both?" decision tree, and
[`docs/config-schema.md`](docs/config-schema.md) documents the v1
schema stability commitments.

A self-monitoring example config is bundled at
[`examples/configs/self-monitoring.yaml`](examples/configs/self-monitoring.yaml),
and a starter `PrometheusRule` at
[`examples/alerts/promanomaly-rules.yaml`](examples/alerts/promanomaly-rules.yaml).
Full multi-group production reference configs land in a later release.
The full multi-group production reference deployment — values, NetworkPolicy
egress, ArgoCD `Application`, and Flux `HelmRepository`/`HelmRelease` —
lives at [`examples/production/`](examples/production/).

## Relationship with promforecast

Expand All @@ -83,9 +90,17 @@ rare events, and per-instance outliers.

## Status

Early development. Output metric names are part of the public API and
follow Prometheus conventions strictly (no colons — those are reserved
for recording rules).
The configuration schema is stable as of the v1.0 release:
`apiVersion: promanomaly.io/v1` and the `values.schema.json` `$id`
both move from alpha to v1. The previous `promanomaly.io/v1alpha1`
alias keeps loading as a byte-identical form with a deprecation
warning. See [`docs/config-schema.md`](docs/config-schema.md) for the
stability commitments, including which changes are breaking and
which are not.

Output metric names and label sets are part of the public API and
follow Prometheus conventions strictly (no colons — those are
reserved for recording rules).

## Security

Expand Down
7 changes: 5 additions & 2 deletions charts/promanomaly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ Where ``my-groups.yaml`` carries detector groups under ``groups:``.
See ``values.yaml`` and the schema in ``values.schema.json``. The
notable bits:

- ``replicaCount`` is pinned to ``1`` — single-replica is the only
supported topology today. HA mode arrives in a later release.
- ``replicaCount`` defaults to ``1``. Single-replica is the simplest
topology; HA mode (``highAvailability.enabled: true``) is supported
and adds Lease-based leader election + a Redis-backed snapshot
cache so multiple replicas serve the same ``/metrics`` snapshot.
See ``examples/configs/ha.yaml`` and ``examples/production/``.
- ``existingConfigMap`` lets GitOps tooling (Argo CD, Flux) manage the
detector configuration out-of-band; the chart then skips its own
ConfigMap rendering.
Expand Down
14 changes: 14 additions & 0 deletions charts/promanomaly/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,19 @@ hot-reloads on change (validation failure rolls back automatically).
Datasource:
URL: {{ .Values.datasource.url }}

Config schema: this chart renders ``apiVersion: promanomaly.io/v1``
(stable since the v1.0 release). The legacy ``promanomaly.io/v1alpha1``
alias still loads as a byte-identical form but emits a deprecation
warning at boot and is scheduled for removal after v2.0.
{{- if .Values.existingConfigMap }}

NOTE: ``existingConfigMap={{ .Values.existingConfigMap }}`` is set, so the
chart did NOT render its own ConfigMap. If that ConfigMap still declares
``apiVersion: promanomaly.io/v1alpha1`` the pod will boot but will log
``config.api_version_deprecated`` on every restart — bump it to
``promanomaly.io/v1`` (no other changes required). See
docs/config-schema.md for the stability commitment.
{{- end }}

For a quick-start that includes VictoriaMetrics, install
``promanomaly-stack`` instead of this chart.
2 changes: 1 addition & 1 deletion charts/promanomaly/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ metadata:
labels: {{- include "promanomaly.labels" . | nindent 4 }}
data:
config.yaml: |
apiVersion: promanomaly.io/v1alpha1
apiVersion: promanomaly.io/v1
datasource:
url: {{ .Values.datasource.url | quote }}
timeout: {{ .Values.datasource.timeout | quote }}
Expand Down
2 changes: 2 additions & 0 deletions charts/promanomaly/values.schema.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://promanomaly.io/schemas/v1/values.schema.json",
"title": "promanomaly chart values",
"description": "Stable v1 schema. A future breaking change ships under a fresh $id (e.g. .../v2/values.schema.json) alongside a new apiVersion; this $id remains pinned to the v1 surface for the entire v1.x line.",
"type": "object",
"additionalProperties": true,
"required": ["image", "datasource"],
Expand Down
5 changes: 3 additions & 2 deletions charts/promanomaly/values.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Default values for the promanomaly detector chart.
# Single-replica only — HA mode lands in a later release and adds
# leader election. See README for the operational story.
# Single-replica by default; HA mode (Lease-based leader election +
# Redis snapshot cache) is opt-in via ``highAvailability.enabled``.
# See README + examples/production/ for the HA topology.

image:
repository: ghcr.io/esops-dev/promanomaly
Expand Down
29 changes: 27 additions & 2 deletions detector/src/promanomaly/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@
model_validator,
)

CURRENT_API_VERSION = "promanomaly.io/v1alpha1"
SUPPORTED_API_VERSIONS = frozenset({CURRENT_API_VERSION})
# Graduated from ``v1alpha1`` to ``v1`` in the v1.0 release. The previous
# alpha alias remains valid (byte-identical schema) but loads with a
# ``DeprecationWarning`` so operators get a nudge to bump their configs
# without their pods CrashLoopBackOff'ing on the next image pull. A future
# breaking change ships under a fresh apiVersion (``v2alpha1`` → ``v2``);
# v1 stays the long-term stable surface for the entire v1.x line.
CURRENT_API_VERSION = "promanomaly.io/v1"
DEPRECATED_API_VERSIONS = frozenset({"promanomaly.io/v1alpha1"})
SUPPORTED_API_VERSIONS = frozenset({CURRENT_API_VERSION}) | DEPRECATED_API_VERSIONS

# Prometheus metric/label name. ``id`` and ``group`` are emitted as label
# values but must additionally pass the same character rules as metric names
Expand Down Expand Up @@ -657,6 +664,23 @@ def _validate_api_version(cls, value: str) -> str:
if value not in SUPPORTED_API_VERSIONS:
supported = sorted(SUPPORTED_API_VERSIONS)
raise ValueError(f"unsupported apiVersion {value!r}; expected one of {supported}")
if value in DEPRECATED_API_VERSIONS:
# Byte-identical schema; nudge operators toward ``v1`` so the
# eventual ``v2`` deprecation window doesn't catch them holding
# an alpha alias. ``DeprecationWarning`` here is caught by
# pytest and visible under CLI ``-W default``; the structured
# boot log line is re-emitted from ``serve()`` AFTER
# ``configure_logging`` has wired up the JSON pipeline (see
# ``main.serve``), so operators reading the normal log stream
# see it as a regular JSON warning record.
import warnings

warnings.warn(
f"apiVersion {value!r} is deprecated; bump configs to "
f"{CURRENT_API_VERSION!r}. The schema is byte-identical.",
DeprecationWarning,
stacklevel=2,
)
return value

@field_validator("groups")
Expand Down Expand Up @@ -730,6 +754,7 @@ def _validate_detector_params(cfg: Config) -> None:

__all__ = [
"CURRENT_API_VERSION",
"DEPRECATED_API_VERSIONS",
"SUPPORTED_API_VERSIONS",
"AlertThresholds",
"AuthConfig",
Expand Down
23 changes: 22 additions & 1 deletion detector/src/promanomaly/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from fastapi import FastAPI

from .cache import RedisQueryCache, TTLCache
from .config import Config, load_config
from .config import CURRENT_API_VERSION, DEPRECATED_API_VERSIONS, Config, load_config
from .exporter import Exporter, OperationalMetrics
from .ha import HAComponents, build_ha_components, run_follower_loop, stop_ha
from .leader import resolve_identity
Expand Down Expand Up @@ -197,6 +197,17 @@ async def reload(self) -> tuple[bool, str]:
# changed, mirroring the boot-time first-run pattern.
for group in new_config.groups:
self._spawn(self._safe_group_run(group.name))
if new_config.apiVersion in DEPRECATED_API_VERSIONS:
# Mirror the boot-time emission so a hot-reload of a
# legacy-aliased ConfigMap surfaces the same JSON log
# line in the operator's stream — silence after the
# first boot would hide the issue from anyone who
# joined the on-call rotation later.
logger.warning(
"config.api_version_deprecated",
api_version=new_config.apiVersion,
replacement=CURRENT_API_VERSION,
)
logger.info("reload_complete")
return True, "ok"

Expand Down Expand Up @@ -494,6 +505,16 @@ def serve(config_path: str | Path) -> None:
groups=[g.name for g in config.groups],
refresh_interval_seconds=config.server.refresh_interval_seconds,
)
if config.apiVersion in DEPRECATED_API_VERSIONS:
# Emitted here (not from the pydantic validator) so the warning
# lands in the JSON log stream operators are already tailing.
# The validator additionally fires a ``DeprecationWarning`` for
# CI / pytest visibility.
logger.warning(
"config.api_version_deprecated",
api_version=config.apiVersion,
replacement=CURRENT_API_VERSION,
)
if config.aggressive_refresh:
logger.warning(
"aggressive_refresh_interval",
Expand Down
17 changes: 17 additions & 0 deletions detector/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ def test_unknown_api_version_rejected() -> None:
Config.model_validate(bad)


def test_v1alpha1_api_version_still_loads_with_deprecation_warning() -> None:
# The v1.0 graduation keeps ``v1alpha1`` accepted as a byte-identical
# alias so existing GitOps repos don't CrashLoopBackOff on the image
# bump; the loader nudges operators with a DeprecationWarning.
legacy = {**MIN_CONFIG, "apiVersion": "promanomaly.io/v1alpha1"}
with pytest.warns(DeprecationWarning, match="apiVersion 'promanomaly.io/v1alpha1'"):
cfg = Config.model_validate(legacy)
assert cfg.apiVersion == "promanomaly.io/v1alpha1"


def test_current_api_version_is_v1() -> None:
# Pin the graduation: anything other than ``promanomaly.io/v1`` here
# means a release accidentally regressed the schema to alpha or
# leapt to v2 without a deprecation cycle.
assert CURRENT_API_VERSION == "promanomaly.io/v1"


@pytest.mark.parametrize("bad_id", ["with:colon", "spaces here", "1leading_digit", ""])
def test_invalid_query_id_rejected(bad_id: str) -> None:
from pydantic import ValidationError
Expand Down
2 changes: 1 addition & 1 deletion docker/dev-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# scores are produced minutes after the stack starts (instead of after
# an hour of warm-up). Not intended for any non-dev use.

apiVersion: promanomaly.io/v1alpha1
apiVersion: promanomaly.io/v1

datasource:
url: http://victoriametrics:8428/
Expand Down
58 changes: 58 additions & 0 deletions docs/config-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Configuration Schema Stability

Starting with **v1.0**, promanomaly’s configuration schema is stable. This page explains exactly what that promise means for operators.

## Versioned Surfaces

| Surface | Identifier | Current value |
|--------------------------|-------------------------------------|---------------|
| Config API | `apiVersion:` (in YAML) | `promanomaly.io/v1` |
| Helm values schema | `$id` in `values.schema.json` | `https://promanomaly.io/schemas/v1/values.schema.json` |

A breaking change bumps both versions together.

## What v1 Guarantees

**Breaking changes** (require a major version bump + deprecation period):
- Removing or renaming config fields
- Changing default values of fields or detector parameters
- Adding labels to existing output metrics
- Renaming output metrics or labels
- Tightening validation rules

**Non-breaking changes**:
- Adding new config fields or detector parameters (with sensible defaults)
- Adding new output metrics
- Adding **opt-in** labels (only emitted when the feature is explicitly enabled)
- Adding new detectors
- Loosening validation rules

**Rule of thumb**: If your existing config still produces the same output metrics with the same labels and the same values after an upgrade, the change is non-breaking.

## Deprecation Policy

- Deprecated features keep working for **at least one full minor release**.
- The detector emits clear warnings at startup (structured log + `DeprecationWarning`).
- Helm charts show a NOTES warning during install.
- Every deprecation is listed in `CHANGELOG.md`.

## Legacy `v1alpha1` Alias

```yaml
apiVersion: promanomaly.io/v1alpha1 # still works (emits warning)
apiVersion: promanomaly.io/v1 # recommended
```

The alias is kept for backward compatibility and will be removed after v2.0.

## Helm Chart Schema

The JSON Schema `$id` is stable for the entire v1.x line. Any `# yaml-language-server: $schema=...` pins you have in your `values.yaml` files remain valid across all patch and minor releases.

## Coordination with promforecast

promforecast follows the exact same stability rules. Shared concepts (`id`, `group`, and other label contracts) are kept in sync via `LABELS_CONTRACT.md`.

---

This is the complete, operator-friendly reference. Upgrading inside v1.x never requires editing your config files.
Loading
Loading