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
102 changes: 102 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Release

on:
push:
tags: ['v*']

permissions:
contents: read

jobs:
image:
name: Container image
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write
outputs:
image: ${{ steps.meta.outputs.tags }}
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha

- id: build
uses: docker/build-push-action@v7
with:
context: detector
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: true
sbom: true
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Generate SBOM (Syft)
uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: spdx-json
artifact-name: promanomaly.spdx.json
upload-artifact: true

- name: Install Trivy
run: |
mkdir -p "$HOME/.local/bin"
curl -sSfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Trivy scan (release)
# SARIF upload intentionally not wired up — requires GitHub Advanced
# Security on private repos. Findings surface in the job log.
run: |
trivy image \
--format table \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--exit-code 0 \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}

github-release:
name: GitHub Release
needs: [image]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Create GitHub release with auto-generated notes
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
fail_on_unmatched_files: false
body: |
Container image: `${{ needs.image.outputs.image }}`
Digest: `${{ needs.image.outputs.digest }}`
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ helm install promanomaly promanomaly/promanomaly-stack
This installs VictoriaMetrics, the detector, and example dashboards.
For an existing TSDB, install only `promanomaly/promanomaly`.

For a complete day-one starter next to node-exporter, point the detector
at [`examples/configs/node-exporter.yaml`](examples/configs/node-exporter.yaml) —
a ready-made multi-group config watching the host golden signals (CPU,
memory, filesystem fill + fill-rate, disk I/O, network errors, load) with
detectors already matched to each signal's shape. Sibling starters cover
[kube-state-metrics](examples/configs/kube-state-metrics.yaml),
[cAdvisor](examples/configs/cadvisor.yaml),
[kubelet](examples/configs/kubelet.yaml), and
[application/middleware](examples/configs/applications.yaml) signals, and
the [`docs/cookbook/`](docs/cookbook/) recipes explain which detector fits
which signal. Once running, `promanomaly top --target <url>` ranks what is
anomalous right now by a normalised severity signal — see
[`docs/triage.md`](docs/triage.md).

Editing the YAML config does **not** require a pod restart: the detector
watches the mounted ConfigMap, validates the new config in-place, and
rolls back on validation failure (`kubectl apply` to the ConfigMap is
Expand Down Expand Up @@ -90,7 +104,7 @@ rare events, and per-instance outliers.

## Status

The configuration schema is stable as of the v1.0 release:
The configuration schema is stable as of the first stable 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
Expand Down
31 changes: 31 additions & 0 deletions charts/promanomaly/templates/prometheusrule.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ spec:
series for at least the configured grace runs.
{{- end }}
{{- end }}
{{- with $rules.anomalyHighSeverity }}
{{- if .enabled }}
- alert: AnomalyHighSeverity
expr: anomaly_severity >= {{ .threshold }}
for: {{ .for }}
labels:
severity: {{ .severity }}
annotations:
summary: "High-severity anomaly on {{ "{{" }} $labels.id {{ "}}" }} ({{ "{{" }} $labels.group {{ "}}" }})"
description: |
Detector {{ "{{" }} $labels.detector {{ "}}" }} reports
anomaly_severity >= {{ .threshold }} for at least {{ .for }}.
Use `promanomaly top` to rank against other current firings.
{{- end }}
{{- end }}
{{- with $rules.anomalyConfidenceLow }}
{{- if .enabled }}
- alert: AnomalyConfidenceLow
Expand Down Expand Up @@ -165,6 +180,22 @@ spec:
last good snapshot — scores are stale, not fresh.
{{- end }}
{{- end }}
{{- with $rules.fleetAnomalyDensityHigh }}
{{- if .enabled }}
- alert: FleetAnomalyDensityHigh
expr: anomaly_density{detector="", by=""} > {{ .threshold }}
for: {{ .for }}
labels:
severity: {{ .severity }}
annotations:
summary: "High anomaly density in group {{ "{{" }} $labels.group {{ "}}" }}"
description: |
More than {{ .threshold }} (fraction) of series in group
{{ "{{" }} $labels.group {{ "}}" }} are currently anomalous. Use
`promanomaly top --group {{ "{{" }} $labels.group {{ "}}" }}` to see
the worst series ranked by severity.
{{- end }}
{{- end }}
{{- with $rules.anomalyNotReady }}
{{- if .enabled }}
- alert: AnomalyNotReady
Expand Down
17 changes: 16 additions & 1 deletion charts/promanomaly/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ safety:
key_prefix: promanomaly
timeout: 2s

# Dynamic series discovery (v0.4). Bounds the in-memory tracker so
# Dynamic series discovery. Bounds the in-memory tracker so
# workloads with ephemeral identities (transient pod names, replica
# IDs that never recycle) don't grow the discovery dict without end.
# Default of 1000 runs is ~16 hours at the 1m refresh — plenty of
Expand Down Expand Up @@ -261,6 +261,13 @@ prometheusRule:
threshold: 0.5
for: 10m
severity: warning
# Normalised 0-1 severity gate. Pure signal enrichment ranked across
# detectors with incomparable raw-score units. See docs/severity.md.
anomalyHighSeverity:
enabled: true
threshold: 0.7
for: 5m
severity: warning
anomalyStale:
enabled: true
maxStaleSeconds: 600
Expand All @@ -281,3 +288,11 @@ prometheusRule:
enabled: true
for: 5m
severity: critical
# Fleet anomaly-density rollup gate ("is anything weird right now").
# Bounded-cardinality — fires once per group, not per series. Tune
# the threshold (fraction outside) per fleet size and SLO.
fleetAnomalyDensityHigh:
enabled: true
threshold: 0.1
for: 5m
severity: warning
118 changes: 118 additions & 0 deletions dashboards/grafana/promanomaly-fleet-density.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"annotations": {"list": []},
"editable": true,
"schemaVersion": 38,
"title": "promanomaly fleet density",
"description": "Is anything weird right now? Bounded-cardinality fleet rollups driven by anomaly_density / anomaly_active_series / anomaly_severity_density.",
"tags": ["promanomaly", "anomaly-detection", "triage"],
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"label": "Datasource",
"query": "prometheus",
"current": {"text": "VictoriaMetrics", "value": "victoriametrics"}
},
{
"name": "group",
"type": "query",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"query": "label_values(anomaly_density, group)",
"refresh": 2,
"multi": true,
"includeAll": true
}
]
},
"panels": [
{
"id": 1,
"type": "stat",
"title": "Anomaly density by group",
"description": "Fraction of each group currently outside threshold (group rollup only; detector/by partitions excluded).",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1}},
"targets": [
{
"expr": "anomaly_density{group=~\"$group\", detector=\"\", by=\"\"}",
"legendFormat": "{{group}}",
"instant": true
}
],
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 0}
},
{
"id": 2,
"type": "stat",
"title": "Active (firing) series by group",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"targets": [
{
"expr": "anomaly_active_series{group=~\"$group\"}",
"legendFormat": "{{group}}",
"instant": true
}
],
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 0}
},
{
"id": 3,
"type": "timeseries",
"title": "Anomaly density over time",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1}},
"targets": [
{
"expr": "anomaly_density{group=~\"$group\", detector=\"\", by=\"\"}",
"legendFormat": "{{group}}"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 6}
},
{
"id": 4,
"type": "timeseries",
"title": "Severity-weighted density (how bad, not just how many)",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1}},
"targets": [
{
"expr": "anomaly_severity_density{group=~\"$group\", by=\"\"}",
"legendFormat": "{{group}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 14}
},
{
"id": 5,
"type": "timeseries",
"title": "Density by slice (defaults.density_by)",
"description": "Per-slice partition — e.g. by namespace. Only present when defaults.density_by is configured.",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1}},
"targets": [
{
"expr": "anomaly_density{group=~\"$group\", by!=\"\"}",
"legendFormat": "{{group}} {{by}}={{namespace}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 14}
},
{
"id": 6,
"type": "table",
"title": "Most severe firings right now",
"description": "Mirrors `promanomaly top` — the currently-firing series ranked by severity.",
"datasource": {"type": "prometheus", "uid": "${datasource}"},
"targets": [
{
"expr": "topk(20, anomaly_severity{group=~\"$group\"} > 0)",
"instant": true,
"format": "table"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 22}
}
]
}
Loading
Loading