feat/astro migration (#538)

* feat: migrate website from Jekyll to Astro

Rebuilds the site using Astro (SSG) with Tailwind CSS v4, replacing the
Jekyll/Cayman theme. Key changes:

- Splits the monolithic /rules page into 110 statically-generated pages
  (92 per-service + 13 group index + homepage + guide pages) for SEO
- URL structure: /rules/[group-slug]/[service-slug]/ with backward-
  compatibility redirect map for old anchor-based URLs (/rules#redis)
- Modern UI: Prometheus-orange accent, dark mode (system + toggle),
  sticky sidebar, responsive layout, copy-to-clipboard per rule/section
- SEO: per-page <title>, <meta description>, Open Graph, Twitter Card,
  canonical URLs, sitemap.xml via @astrojs/sitemap
- GEO: FAQPage JSON-LD schema on each service page (rules as Q&A pairs
  for AI search engines), TechArticle schema, BreadcrumbList
- Search: Pagefind (build-time index, lazy-loaded, ~200KB)
- Zero JS by default; copy buttons and theme toggle use inline scripts
- New CI: .github/workflows/deploy.yml builds Astro + Pagefind and
  deploys to GitHub Pages via actions/deploy-pages
- Existing dist.yml and test.yml workflows are untouched
- _data/rules.yml remains the single source of truth

Note: GitHub Pages source must be changed from "Build from branch"
(Jekyll) to "GitHub Actions" in repository settings.

* doc: new website based on astro

* refactor: remove previous website

* chore: add npm dependabot for Astro site + scope CI to _data changes

* Update site/astro.config.mjs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update site/src/components/CopyButton.astro

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* oops

* fix: strip trailing slash from BASE_URL to prevent double slashes in URLs

Agent-Logs-Url: https://github.com/samber/awesome-prometheus-alerts/sessions/c85937ba-1855-4b8a-a72b-847eab1c8639

Co-authored-by: samber <2951285+samber@users.noreply.github.com>

* fix: resolve Astro build errors in astro.config.mjs

- Remove assetsInclude yml which caused Vite to treat YAML files as static assets instead of running them through the custom YAML transform plugin; data.groups was undefined at runtime because the import resolved to a URL rather than parsed content
- Deduplicate old-path redirects: emit only the slash-less variant per service to avoid Astro router collision warnings (trailing-slash variant is handled automatically)

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samber <2951285+samber@users.noreply.github.com>
This commit is contained in:
Samuel Berthe 2026-04-10 21:08:06 +02:00 committed by GitHub
parent 0d148832d3
commit 79afa21610
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 11076 additions and 1080 deletions

View file

@ -5,3 +5,8 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "monthly" interval: "monthly"
- package-ecosystem: "npm"
directory: "/site"
schedule:
interval: "monthly"

62
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Deploy Astro site to GitHub Pages
on:
push:
branches: [master]
workflow_dispatch:
# Only allow one concurrent deployment
concurrency:
group: pages
cancel-in-progress: false
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: site/package-lock.json
- name: Install dependencies
working-directory: site
run: npm ci
- name: Build Astro site
working-directory: site
env:
ASTRO_TELEMETRY_DISABLED: "1"
run: npm run build
- name: Build Pagefind search index
working-directory: site
run: npx pagefind --site dist
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site/dist
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -2,9 +2,13 @@ name: Promtool check
on: on:
pull_request: pull_request:
paths:
- _data/**
push: push:
branches: branches:
- master - master
paths:
- _data/**
jobs: jobs:
promtool-check: promtool-check:

12
.gitignore vendored
View file

@ -1,8 +1,12 @@
_site/ # Generated data
.sass-cache/
.jekyll-cache/
.jekyll-metadata
_data/rules.json _data/rules.json
test/rules/ test/rules/
# Node / Astro
/node_modules /node_modules
site/node_modules/
site/dist/
site/.astro/
# Misc
.worktrees/ .worktrees/

View file

@ -6,17 +6,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
A curated collection of ~940 Prometheus alerting rules covering 90+ services across 100+ exporters, organized in categories: basic resource monitoring (Prometheus, host/hardware, SMART, Docker, Blackbox, Windows, VMware, Netdata), databases (MySQL, PostgreSQL, Redis, MongoDB, Elasticsearch, Cassandra, Clickhouse, CouchDB, etc.), message brokers (RabbitMQ, Kafka, Pulsar, Nats, Zookeeper), proxies/load balancers/service meshes (Nginx, Apache, HaProxy, Traefik, Caddy, Linkerd, Istio), runtimes (PHP-FPM, JVM, Sidekiq), data engineering (Apache Flink, Apache Spark, Hadoop), orchestrators (Kubernetes, Nomad, Consul, Etcd, OpenStack), CI/CD (Jenkins, ArgoCD, FluxCD, GitLab CI, Spinnaker), network and security (SSL/TLS, CoreDNS, Vault, Cloudflare, Cilium, eBPF), storage (Ceph, ZFS, OpenEBS, Minio), cloud providers (AWS, Azure, DigitalOcean), observability (Thanos, Loki, Cortex, OpenTelemetry Collector, Grafana Tempo/Mimir/Alloy, Jaeger), and other (APC UPS, Graph Node). A curated collection of ~940 Prometheus alerting rules covering 90+ services across 100+ exporters, organized in categories: basic resource monitoring (Prometheus, host/hardware, SMART, Docker, Blackbox, Windows, VMware, Netdata), databases (MySQL, PostgreSQL, Redis, MongoDB, Elasticsearch, Cassandra, Clickhouse, CouchDB, etc.), message brokers (RabbitMQ, Kafka, Pulsar, Nats, Zookeeper), proxies/load balancers/service meshes (Nginx, Apache, HaProxy, Traefik, Caddy, Linkerd, Istio), runtimes (PHP-FPM, JVM, Sidekiq), data engineering (Apache Flink, Apache Spark, Hadoop), orchestrators (Kubernetes, Nomad, Consul, Etcd, OpenStack), CI/CD (Jenkins, ArgoCD, FluxCD, GitLab CI, Spinnaker), network and security (SSL/TLS, CoreDNS, Vault, Cloudflare, Cilium, eBPF), storage (Ceph, ZFS, OpenEBS, Minio), cloud providers (AWS, Azure, DigitalOcean), observability (Thanos, Loki, Cortex, OpenTelemetry Collector, Grafana Tempo/Mimir/Alloy, Jaeger), and other (APC UPS, Graph Node).
All rules are stored in a single YAML data file (`_data/rules.yml`) and rendered as a Jekyll-based GitHub Pages site at https://samber.github.io/awesome-prometheus-alerts. The site provides copy-pasteable Prometheus alert snippets and downloadable rule files per exporter. All rules are stored in a single YAML data file (`_data/rules.yml`) and rendered as a static site built with Astro + TypeScript (located in `site/`). The site provides copy-pasteable Prometheus alert snippets and downloadable rule files per exporter.
The project is community-driven. Most contributions are PRs adding or updating rules in `_data/rules.yml`. Files in `dist/rules/` are auto-generated on merge — never edit them manually. The project is community-driven. Most contributions are PRs adding or updating rules in `_data/rules.yml`. Files in `dist/rules/` are auto-generated on merge — never edit them manually.
## Architecture ## Architecture
- **`_data/rules.yml`** — The single source of truth for all alerting rules. This is the main file contributors edit. It is NOT a valid Prometheus config; the site renders each rule into copy-pasteable Prometheus alert format. - **`_data/rules.yml`** — The single source of truth for all alerting rules. This is the main file contributors edit. It is NOT a valid Prometheus config; the site renders each rule into copy-pasteable Prometheus alert format.
- **`rules.md`** — Jekyll template that iterates over `_data/rules.yml` and renders the rules page with copy buttons and formatted YAML blocks. - **`site/`** — Astro + TypeScript static site. Run `npm run dev` inside this directory to develop locally.
- **`alertmanager.md`** — Static page with Prometheus/AlertManager configuration examples. - **`site/src/data/rules.ts`** — Typed wrappers and helper functions over `_data/rules.yml`.
- **`_layouts/default.html`** — Site layout (Jekyll theme: cayman). - **`site/src/data/site.ts`** — Shared site metadata constants (URLs, author, schema objects).
- **`_config.yml`** — Jekyll configuration. - **`site/src/pages/`** — Astro page routes: `index.astro` (homepage), `rules/[group]/[service].astro` (per-service rule pages), `alertmanager.astro`, `blackbox-exporter.astro`, `sleep-peacefully.astro` (guides).
- **`site/src/layouts/BaseLayout.astro`** — Root HTML layout (SEO, GA, dark mode).
- **`site/src/layouts/GuideLayout.astro`** — Layout for guide pages (TOC, hero, related guides).
- **`site/src/components/`** — Shared Astro components (Header, Footer, Sidebar, RuleCard, ExporterSection, etc.).
- **`site/astro.config.mjs`** — Astro configuration (sitemap, Vite YAML plugin, base URL).
- **`dist/rules/`** — Pre-built downloadable rule files organized by service/exporter (referenced in the site for `wget` commands). - **`dist/rules/`** — Pre-built downloadable rule files organized by service/exporter (referenced in the site for `wget` commands).
## Rules YAML Structure ## Rules YAML Structure
@ -50,19 +54,20 @@ Services are grouped in category. If you are not sure about the classification,
## Running Locally ## Running Locally
```bash ```bash
# With Ruby/Bundler cd site
gem install bundler npm install
bundle install npm run dev
jekyll serve
# With Docker Compose
docker compose up -d
# With Docker directly
docker run --rm -it -p 4000:4000 -v $(pwd):/srv/jekyll jekyll/jekyll jekyll serve
``` ```
Site serves at http://localhost:4000/awesome-prometheus-alerts. Site serves at http://localhost:4321/awesome-prometheus-alerts.
To build for production:
```bash
cd site
npm run build
npm run preview
```
## Contributing Rules ## Contributing Rules

View file

@ -16,24 +16,16 @@ Please ensure your pull request adheres to the following guidelines:
- Description must be factual (the "what?") and should provide root cause suggestions (the "why?"), for faster resolution. - Description must be factual (the "what?") and should provide root cause suggestions (the "why?"), for faster resolution.
- Queries must be tested on latest exporter version. - Queries must be tested on latest exporter version.
## Improving Github page ## Improving the website
The site is built with Astro + TypeScript, located in `site/`.
### Run locally ### Run locally
``` ```
gem install bundler cd site
bundle install npm install
jekyll serve npm run dev
``` ```
Or with Docker: Site serves at http://localhost:4321/awesome-prometheus-alerts.
```
docker run --rm -it -p 4000:4000 -v $(pwd):/srv/jekyll jekyll/jekyll jekyll serve
```
Or with Docker Compose:
```
docker compose up -d
```

View file

@ -1,3 +0,0 @@
source 'https://rubygems.org'
gem 'github-pages', '>= 232', group: :jekyll_plugins
gem 'webrick', '~> 1.8'

View file

@ -1,293 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (7.2.3.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.9)
public_suffix (>= 2.0.2, < 8.0)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.0.1)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
colorator (1.1.0)
commonmarker (0.23.12)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
csv (3.3.5)
dnsruby (1.73.1)
base64 (>= 0.2)
logger (~> 1.6)
simpleidn (~> 0.2.1)
drb (2.2.3)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
ethon (0.18.0)
ffi (>= 1.15.0)
logger
eventmachine (1.2.7)
execjs (2.10.0)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.3)
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
forwardable-extended (2.6.0)
gemoji (4.1.0)
github-pages (232)
github-pages-health-check (= 1.18.2)
jekyll (= 3.10.0)
jekyll-avatar (= 0.8.0)
jekyll-coffeescript (= 1.2.2)
jekyll-commonmark-ghpages (= 0.5.1)
jekyll-default-layout (= 0.1.5)
jekyll-feed (= 0.17.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.16.1)
jekyll-include-cache (= 0.2.1)
jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.8.0)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.2.0)
jekyll-theme-cayman (= 0.2.0)
jekyll-theme-dinky (= 0.2.0)
jekyll-theme-hacker (= 0.2.0)
jekyll-theme-leap-day (= 0.2.0)
jekyll-theme-merlot (= 0.2.0)
jekyll-theme-midnight (= 0.2.0)
jekyll-theme-minimal (= 0.2.0)
jekyll-theme-modernist (= 0.2.0)
jekyll-theme-primer (= 0.6.0)
jekyll-theme-slate (= 0.2.0)
jekyll-theme-tactile (= 0.2.0)
jekyll-theme-time-machine (= 0.2.0)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.13.0)
kramdown (= 2.4.0)
kramdown-parser-gfm (= 1.1.0)
liquid (= 4.0.4)
mercenary (~> 0.3)
minima (= 2.5.1)
nokogiri (>= 1.16.2, < 2.0)
rouge (= 3.30.0)
terminal-table (~> 1.4)
webrick (~> 1.8)
github-pages-health-check (1.18.2)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (>= 4, < 8)
public_suffix (>= 3.0, < 6.0)
typhoeus (~> 1.3)
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.8.1)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
jekyll (3.10.0)
addressable (~> 2.4)
colorator (~> 1.0)
csv (~> 3.0)
em-websocket (~> 0.5)
i18n (>= 0.7, < 2)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
webrick (>= 1.0)
jekyll-avatar (0.8.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.2.2)
coffee-script (~> 2.2)
coffee-script-source (~> 1.12)
jekyll-commonmark (1.4.0)
commonmarker (~> 0.22)
jekyll-commonmark-ghpages (0.5.1)
commonmarker (>= 0.23.7, < 1.1.0)
jekyll (>= 3.9, < 4.0)
jekyll-commonmark (~> 1.4.0)
rouge (>= 2.0, < 5.0)
jekyll-default-layout (0.1.5)
jekyll (>= 3.0, < 5.0)
jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.16.1)
jekyll (>= 3.4, < 5.0)
octokit (>= 4, < 7, != 4.4.0)
jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0)
jekyll-mentions (1.6.0)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.3)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.6.0)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.13.0)
gemoji (>= 3, < 5)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
json (2.19.2)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.10.0)
logger
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
mercenary (0.3.6)
mini_portile2 (2.8.9)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.27.0)
net-http (0.9.1)
uri (>= 0.11.1)
nokogiri (1.19.1)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl)
racc (~> 1.4)
octokit (4.25.1)
faraday (>= 1, < 3)
sawyer (~> 0.9)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.1.1)
racc (1.8.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.4.4)
rouge (3.30.0)
rubyzip (2.4.1)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.9.3)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
securerandom (0.4.1)
simpleidn (0.2.3)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (1.8.0)
uri (1.1.1)
webrick (1.9.2)
PLATFORMS
ruby
x86_64-linux
x86_64-linux-musl
DEPENDENCIES
github-pages (>= 232)
webrick (~> 1.8)
BUNDLED WITH
2.3.25

View file

@ -179,7 +179,7 @@ There are many ways to contribute: writing code, alerting rules, documentation,
## 🏋️ Improvements ## 🏋️ Improvements
- Create an alert rule builder in Jekyll for custom alerts (severity, thresholds, instances...) - Create an alert rule builder for custom alerts (severity, thresholds, instances...)
- Add resolution suggestions to rule descriptions, for faster incident resolution ([#85](https://github.com/samber/awesome-prometheus-alerts/issues/85)). - Add resolution suggestions to rule descriptions, for faster incident resolution ([#85](https://github.com/samber/awesome-prometheus-alerts/issues/85)).
## 💫 Show your support ## 💫 Show your support

View file

@ -1,8 +0,0 @@
theme: jekyll-theme-cayman
title: Awesome Prometheus alerts
description: Collection of alerting rules
repository: samber/awesome-prometheus-alerts
baseurl: /awesome-prometheus-alerts

View file

@ -1,162 +0,0 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="UTF-8">
{% seo %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#157878">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
<link rel="stylesheet" href="{{ '/assets/css/app.css?v=' | append: site.github.build_revision | relative_url }}">
<link rel="icon" type="image/x-icon" href="{{ '/assets/favicon.ico' | relative_url }}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
<script src="{{ '/assets/js/app.js?v=' | append: site.github.build_revision | relative_url }}"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118604063-2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-118604063-2');
</script>
</head>
<body>
<style>
#skip-to-content {
height: 1px;
width: 1px;
position: absolute;
overflow: hidden;
top: -10px;
&:focus {
position: fixed;
top: 10px;
left: 10px;
height: auto;
width: auto;
background: invert($body-link-color);
outline: thick solid invert($body-link-color);
}
}
ul.github-buttons-cta li {
display: inline-block;
height: 20px;
padding: 0px 15px;
}
ul.github-buttons-cta li a {
/* width: 100px; */
text-decoration: none;
}
.fa {
/* padding: 14px;
width: 50px;
height: 50px; */
font-size: 25px;
text-align: center;
text-decoration: none;
border-radius: 50%;
}
.fa:hover {
opacity: 0.8;
}
.fa-twitter,
.fa-linkedin {
/* background: #55ACEE; */
color: white;
}
</style>
<a id="skip-to-content" href="#content">Skip to the content.</a>
<header class="page-header" role="banner">
<h1 class="project-name">
<a href="{{ '/' | relative_url }}" style="color: white">
{{ site.title | default: site.github.repository_name }}
</a>
</h1>
<h2 class="project-tagline">{{ site.description | default: site.github.project_tagline }}</h2>
<a href="{{ '/alertmanager' | relative_url }}" class="btn">Global configuration</a>
<a href="{{ '/rules' | relative_url }}" class="btn">Rules</a>
<a href="{{ '/sleep-peacefully' | relative_url }}" class="btn">Sleep peacefully</a>
<a href="{{ '/blackbox-exporter' | relative_url }}" class="btn">Blackbox</a>
<a href="https://github.com/samber/awesome-prometheus-alerts/blob/master/CONTRIBUTING.md" class="btn">
Contribute on GitHub
</a>
<ul class="github-buttons-cta">
<li>
<a href="https://github.com/samber/awesome-prometheus-alerts">
<img alt="GitHub Repo Watchers" src="https://img.shields.io/github/watchers/samber/awesome-prometheus-alerts?style=social">
</a>
</li>
<li>
<a href="https://github.com/samber/awesome-prometheus-alerts">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/samber/awesome-prometheus-alerts?style=social">
</a>
</li>
<li>
<a href="https://github.com/samber/awesome-prometheus-alerts">
<img alt="GitHub Repo forks" src="https://img.shields.io/github/forks/samber/awesome-prometheus-alerts?style=social">
</a>
</li>
<li>
<a href="https://twitter.com/share?via=samuelberthe&related=samuelberthe&text=🚨 📊 Here is a collection of Awesome Prometheus Alerts&url=https://samber.github.io/awesome-prometheus-alerts"
class="fa fa-twitter" target="_blank"></a>
</li>
<li>
<a href="http://www.linkedin.com/shareArticle?mini=true&url=https://samber.github.io/awesome-prometheus-alerts/"
class="fa fa-linkedin" target="_blank"></a>
</li>
</ul>
<ul id="sponsoring">
<li>
Kindly supported by&nbsp; 👉
</li>
<li>
<a href="https://cast.ai/samuel">
<img width="" src="assets/sponsor-cast-ai.png" />
</a>
</li>
<li>
<a href="https://betterstack.com/">
<img width="" src="assets/sponsor-betterstack.png" />
</a>
</li>
</ul>
</header>
<main id="content" class="main-content" role="main">
{{ content }}
<footer class="site-footer">
{% if site.github.is_project_page %}
<span class="site-footer-owner">
<a href="{{ site.github.repository_url }}">{{ site.title }}</a> is maintained by
<a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a>.
</span>
{% endif %}
</footer>
</main>
</body>
</html>

View file

@ -1,141 +0,0 @@
<h1 style="text-align: center;">
Global configuration
</h1>
If you notice a delay between an event and the first notification, read the following blog post => [https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html](https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html).
## Prometheus configuration
{% highlight yaml %}
# prometheus.yml
global:
scrape_interval: 20s
# A short evaluation_interval will check alerting rules very often.
# It can be costly if you run Prometheus with 100+ alerts.
evaluation_interval: 20s
...
rule_files:
- 'alerts/*.yml'
scrape_configs:
...
{% endhighlight %}
{% highlight yaml %}
# alerts/example-redis.yml
groups:
- name: ExampleRedisGroup
rules:
- alert: ExampleRedisDown
expr: redis_up{} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Redis instance down"
description: "Whatever"
{% endhighlight %}
## AlertManager configuration
{% highlight yaml %}
{% raw %}
# alertmanager.yml
route:
# When a new group of alerts is created by an incoming alert, wait at
# least 'group_wait' to send the initial notification.
# This way ensures that you get multiple alerts for the same group that start
# firing shortly after another are batched together on the first
# notification.
group_wait: 10s
# When the first notification was sent, wait 'group_interval' to send a batch
# of new alerts that started firing for that group.
group_interval: 30s
# If an alert has successfully been sent, wait 'repeat_interval' to
# resend them.
repeat_interval: 30m
# A default receiver
receiver: "slack"
# All the above attributes are inherited by all child routes and can
# overwritten on each.
routes:
- receiver: "slack"
group_wait: 10s
match_re:
severity: critical|warning
continue: true
- receiver: "pager"
group_wait: 10s
match_re:
severity: critical
continue: true
receivers:
- name: "slack"
slack_configs:
- api_url: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxxxxx'
send_resolved: true
channel: 'monitoring'
text: "{{ range .Alerts }}<!channel> {{ .Annotations.summary }}\n{{ .Annotations.description }}\n{{ end }}"
- name: "pager"
webhook_configs:
- url: http://a.b.c.d:8080/send/sms
send_resolved: true
{% endraw %}
{% endhighlight %}
## Reduce Prometheus server load
For expansive or frequent PromQL queries, Prometheus allows to precompute rules.
{% highlight yaml %}
{% raw %}
groups:
# first define the recorded rule
- name: ExampleRecordedGroup
rules:
- record: job:rabbitmq_queue_messages_delivered_total:rate:5m
expr: rate(rabbitmq_queue_messages_delivered_total[5m])
# then use it in alerts
- name: ExampleAlertingGroup
rules:
- alert: ExampleRabbitmqLowMessageDelivery
expr: sum(job:rabbitmq_queue_messages_delivered_total:rate:5m) < 10
for: 2m
labels:
severity: critical
annotations:
summary: "Low delivery rate in Rabbitmq queues"
{% endraw %}
{% endhighlight %}
## Troubleshooting
If the notification takes too much time to be triggered, check the following delays:
- `scrape_interval = 20s` (prometheus.yml)
- `evaluation_interval = 20s` (prometheus.yml)
- `increase(mysql_global_status_slow_queries[1m]) > 0` (alerts/example-mysql.yml)
- `for: 5m` (alerts/example-mysql.yml)
- `group_wait = 10s` (alertmanager.yml)
Also read:
- [https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html](https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html).
- [https://hodovi.cc/blog/creating-awesome-alertmanager-templates-for-slack/](https://hodovi.cc/blog/creating-awesome-alertmanager-templates-for-slack/)
- [https://grafana.com/blog/2024/10/03/how-to-use-prometheus-to-efficiently-detect-anomalies-at-scale/](https://grafana.com/blog/2024/10/03/how-to-use-prometheus-to-efficiently-detect-anomalies-at-scale/)

View file

@ -1,125 +0,0 @@
<h1 style="text-align: center;">
Blackbox exporter
</h1>
## Wordwide probes
<a href="https://github.com/prometheus/blackbox_exporter" target="_blank">Blackbox Exporter</a> gives you the ability to probe endpoints over HTTP, HTTPS, DNS, TCP and ICMP.
You should deploy blackbox exporters in multiple Point of Presence around the globe, to monitor latency. Feel free to use the following endpoints for your own projects:
- https://probe-<b>montreal</b>.cleverapps.io
- https://probe-<b>paris</b>.cleverapps.io
- https://probe-<b>jeddah</b>.cleverapps.io
- https://probe-<b>singapore</b>.cleverapps.io
- https://probe-<b>sydney</b>.cleverapps.io
- https://probe-<b>warsaw</b>.cleverapps.io
☝️ Logs have been disabled. More probes from the community would be appreciated, please contribute <a href="https://github.com/samber/awesome-prometheus-alerts/" target="_blank">here</a>! These blackbox exporters use the following <a href="https://github.com/samber/blackbox_exporter/blob/master/samber.yml" target="_blank">configuration</a>.
## Prometheus Configuration
Blackbox exporters and endpoints must be declared in Prometheus. Here is a simple configuration, inspired by [Hayk Davtyan medium post](https://medium.com/geekculture/single-prometheus-job-for-dozens-of-blackbox-exporters-2a7ba492d6c8):
```yml
# sd/blackbox.yml
- targets:
#
# Montreal
#
# http
- probe-montreal.cleverapps.io:_:http_2xx:_:Montreal:_:f229cy:_:https://api.screeb.app
- probe-montreal.cleverapps.io:_:http_2xx:_:Montreal:_:f229cy:_:https://t.screeb.app/tag.js
# icmp
- probe-montreal.cleverapps.io:_:icmp_ipv4:_:Montreal:_:f229cy:_:api.screeb.app
- probe-montreal.cleverapps.io:_:icmp_ipv4:_:Montreal:_:f229cy:_:t.screeb.app
#
# Paris
#
# http
- probe-paris.cleverapps.io:_:http_2xx:_:Paris:_:u09tgy:_:https://api.screeb.app
- probe-paris.cleverapps.io:_:http_2xx:_:Paris:_:u09tgy:_:https://t.screeb.app/tag.js
# icmp
- probe-paris.cleverapps.io:_:icmp_ipv4:_:Paris:_:u09tgy:_:api.screeb.app
- probe-paris.cleverapps.io:_:icmp_ipv4:_:Paris:_:u09tgy:_:t.screeb.app
#
# Sydney
#
# http
- probe-sydney.cleverapps.io:_:http_2xx:_:Sydney:_:r3gpkn:_:https://api.screeb.app
- probe-sydney.cleverapps.io:_:http_2xx:_:Sydney:_:r3gpkn:_:https://t.screeb.app/tag.js
# icmp
- probe-sydney.cleverapps.io:_:icmp_ipv4:_:Sydney:_:r3gpkn:_:api.screeb.app
- probe-sydney.cleverapps.io:_:icmp_ipv4:_:Sydney:_:r3gpkn:_:t.screeb.app
# ...
```
```yml
# prometheus.yml
global:
# ...
scrape_configs:
- job_name: 'blackbox'
metrics_path: /probe
scrape_interval: 30s
scheme: https
file_sd_configs:
- files:
- /etc/prometheus/sd/blackbox.yml
relabel_configs:
# adds "module" label in the final labelset
- source_labels: [__address__]
regex: '.*:_:(.*):_:.*:_:.*:_:.*'
target_label: module
# adds "geohash" label in the final labelset
- source_labels: [__address__]
regex: '.*:_:.*:_:.*:_:(.*):_:.*'
target_label: geohash
# rewrites "instance" label with corresponding URL
- source_labels: [__address__]
regex: '.*:_:.*:_:.*:_:.*:_:(.*)'
target_label: instance
# rewrites "pop" label with corresponding location name
- source_labels: [__address__]
regex: '.*:_:.*:_:(.*):_:.*:_:.*'
target_label: pop
# passes "module" parameter to Blackbox exporter
- source_labels: [module]
target_label: __param_module
# passes "target" parameter to Blackbox exporter
- source_labels: [instance]
target_label: __param_target
# the Blackbox exporter's real hostname:port
- source_labels: [__address__]
regex: '(.*):_:.*:_:.*:_:.*:_:.*'
target_label: __address__
# ...
```
## Geohash
![](assets/grafana-map-panel.png)
To display nice maps in Grafana, you need to instruct blackbox exporters about the location. Grafana map panel speaks the "geohash" format:
- go to google map
- extract the lat/long from the url
- convert lat/long to geohash here: http://geohash.co
## Grafana
Some great dashboard have been created by the community: https://grafana.com/grafana/dashboards/?search=blackbox
Since Grafana v5.0.0, a map panel is available: https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/

View file

@ -1,11 +0,0 @@
version: '3'
services:
jekyll:
image: jekyll/jekyll:latest
command: jekyll serve
volumes:
- ./:/srv/jekyll
ports:
- 4000:4000

View file

@ -1,54 +0,0 @@
<style>
.center-image
{
margin: 0 auto;
display: block;
}
</style>
![Prometheus logo](/assets/prometheus-logo.png){: .center-image }
<h2>
Hello world
</h2>
<a href="/awesome-prometheus-alerts/alertmanager">
AlertManager configuration
</a>
<a href="/awesome-prometheus-alerts/sleep-peacefully">
Alerting time window
</a>
<h2>
Out of the box prometheus alerting rules
</h2>
<ul>
{% for group in site.data.rules.groups %}
<li style="margin-top: 30px;">
{% assign nbrRules = 0 %}
{% for service in group.services %}
{% for exporter in service.exporters %}
{% for rule in exporter.rules %}
{% assign nbrRules = nbrRules | plus: 1 %}
{% endfor %}
{% endfor %}
{% endfor %}
<h3>{{ group.name }} <small style="margin-left: 20px;">({{ nbrRules }} rules)</small></h3>
<ul>
{% for service in group.services %}
<li>
<a href="/awesome-prometheus-alerts/rules#{{ service.name | replace: " ", "-" | downcase }}">
{{ service.name }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>

141
rules.md
View file

@ -1,141 +0,0 @@
<style>
ul {
list-style: none;
}
</style>
<!-- CAUTIONS -->
<div style="padding: 20px 20px 10px 20px; border: solid grey 1px; border-radius: 10px;">
<h2 style="text-align:center;">⚠️ Caution ⚠️</h2>
<p style="text-align:center;">
Alert thresholds depend on nature of applications.
<br>
Some queries in this page may have arbitrary tolerance threshold.
<br><br>
Building an efficient and battle-tested monitoring platform takes time. 😉
</p>
</div>
<br>
<br>
<h1></h1>
<!-- RULES -->
<ul>
{% for group in site.data.rules.groups %}
{% assign groupIndex = forloop.index %}
{% for service in group.services %}
{% assign serviceIndex = forloop.index %}
{% assign nbrExporters = service.exporters | size %}
{% for exporter in service.exporters %}
{% assign exporterIndex = forloop.index %}
{% assign nbrRules = exporter.rules | size %}
<li>
{% assign serviceId = service.name | replace: " ", "-" | downcase %}
<h2 id="{{ serviceId }}">
<span id="{{ serviceId }}-{{ exporterIndex }}"></span>
<a class="anchor" href="#{{ serviceId }}-{{ exporterIndex }}">#</a>
{{ groupIndex }}.{{ serviceIndex }}.{% if nbrExporters > 1 %}{{ exporterIndex }}.{% endif %}
{{ service.name }}
{% if exporter.name %}:
{% if exporter.doc_url %}
<a href="{{ exporter.doc_url }}">
{{ exporter.name }}
</a>
{% else %}
{{ exporter.name }}
{% endif %}
{% endif %}
{% if nbrRules > 0 %}
<small style="font-size: 60%; vertical-align: middle; margin-left: 10px;">
({{ nbrRules }} rules)
</small>
<span class="clipboard-multiple" data-clipboard-target-id="group-{{ groupIndex }}-service-{{ serviceIndex }}-exporter-{{ exporterIndex }}">[copy section]</span>
{% endif %}
</h2>
{% if nbrRules == 0 %}
{% highlight javascript %}
// @TODO: Please contribute => https://github.com/samber/awesome-prometheus-alerts 👋
{% endhighlight %}
{% else %}
{{ exporter.comments | strip | newline_to_br }}
{% highlight bash %}
$ wget https://raw.githubusercontent.com/samber/awesome-prometheus-alerts/refs/heads/master/dist/rules/{{ service.name | replace: " ", "-" | downcase }}/{{ exporter.slug }}.yml
{% endhighlight %}
{% endif %}
<ul>
{% for rule in exporter.rules %}
{% assign ruleIndex = forloop.index %}
{% assign comments = rule.comments | strip | newline_to_br | split: '<br />' %}
<li>
<h4 id="rule-{{ serviceId }}-{{ exporterIndex }}-{{ ruleIndex }}">
<span id="rule-{{ serviceId }}-{{ ruleIndex }}"></span><!-- @deprecated -->
<a class="anchor" href="#rule-{{ serviceId }}-{{ exporterIndex }}-{{ ruleIndex }}">#</a>
{{ groupIndex}}.{{ serviceIndex }}.{% if nbrExporters > 1 %}{{ exporterIndex }}.{% endif %}{{ ruleIndex }}.
{{ rule.name }}
</h4>
<summary>
{{ rule.description }}
<span class="clipboard-single" data-clipboard-target-id="group-{{ groupIndex }}-service-{{ serviceIndex }}-exporter-{{ exporterIndex }}-rule-{{ ruleIndex }}" onclick="event.preventDefault();">[copy]</span>
</summary>
<div id="group-{{ groupIndex }}-service-{{ serviceIndex }}-exporter-{{ exporterIndex }}-rule-{{ ruleIndex }}">
{% assign ruleName = rule.name | split: ' ' %}
{% capture ruleNameCamelcase %}{% for word in ruleName %}{{ word | capitalize }} {% endfor %}{% endcapture %}
{% highlight yaml %}
{% for comment in comments %}# {{ comment | strip }}
{% endfor %}- alert: {{ ruleNameCamelcase | remove: ' ' }}
expr: {{ rule.query }}
for: {% if rule.for %}{{ rule.for }}{% else %}0m{% endif %}
labels:
severity: {{ rule.severity }}
annotations:
summary: {{ rule.name }} (instance {% raw %}{{ $labels.instance }}{% endraw %})
description: "{{ rule.description | replace: '"', '\"' }}\n VALUE = {% raw %}{{ $value }}{% endraw %}\n LABELS = {% raw %}{{ $labels }}{% endraw %}"
{% endhighlight %}
</div>
<br/>
</li>
{% endfor %}
</ul>
<hr/>
</li>
{% endfor %}
{% endfor %}
{% endfor %}
</ul>
<!-- NAVBAR -->
<div id="rules-navbar" class="affix">
<h3>Menu</h3>
<ul>
{% for group in site.data.rules.groups %}
<li>
<h4>{{ group.name }}</h4>
<ul>
{% for service in group.services %}
<li>
<a href="#{{ service.name | replace: " ", "-" | downcase }}">
👉 {{ service.name }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
<script>
$('#rules-navbar').affix({offset: {top: 750} }).css('display', 'block');
</script>
</div>

105
site/astro.config.mjs Normal file
View file

@ -0,0 +1,105 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
import icon from 'astro-icon';
import { parse as parseYaml } from 'yaml';
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
function normalizeViteId(id) {
const cleanId = id.split('?', 1)[0].split('#', 1)[0];
return cleanId.startsWith('/@fs/') ? cleanId.slice(4) : cleanId;
}
/** Custom Vite plugin that parses YAML files using the 'yaml' package,
* which tolerates duplicate keys (last one wins) unlike js-yaml 4.x. */
function yamlPlugin() {
return {
name: 'vite-plugin-yaml-tolerant',
transform(code, id) {
const normalizedId = normalizeViteId(id);
if (!normalizedId.endsWith('.yml') && !normalizedId.endsWith('.yaml')) return null;
const content = typeof code === 'string' ? code : readFileSync(resolve(normalizedId), 'utf-8');
const data = parseYaml(content, { merge: true, strict: false, uniqueKeys: false });
return {
code: `export default ${JSON.stringify(data)};`,
map: null,
};
},
};
}
const toSlug = (name) =>
name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
/** Build redirect map: old flat /rules/{service} paths → new /rules/{group}/{service}/ paths */
function buildRedirects(base) {
try {
const rulesPath = resolve(__dirname, '../_data/rules.yml');
const raw = readFileSync(rulesPath, 'utf-8');
const { groups } = parseYaml(raw, { merge: true, strict: false, uniqueKeys: false });
const redirects = {};
for (const group of groups) {
const groupSlug = toSlug(group.name);
for (const service of group.services) {
const serviceSlug = toSlug(service.name);
// Old anchor slug (spaces → hyphens only, no other substitutions)
const oldSlug = service.name.replace(/ /g, '-').toLowerCase();
const newPath = `${base}/rules/${groupSlug}/${serviceSlug}/`;
// Redirect from flat old path (without trailing slash; Astro handles the slash variant)
const oldPath = `${base}/rules/${oldSlug}`;
if (oldPath !== newPath && oldPath !== newPath.slice(0, -1)) {
redirects[oldPath] = { destination: newPath, status: 301 };
}
}
}
return redirects;
} catch {
return {};
}
}
const base = '/awesome-prometheus-alerts';
export default defineConfig({
site: 'https://samber.github.io',
base,
redirects: buildRedirects(base),
output: 'static',
integrations: [
tailwind({ applyBaseStyles: false }),
sitemap({
serialize(item) {
const path = new URL(item.url).pathname;
const segments = path.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
// segments[0] = 'awesome-prometheus-alerts', [1] = 'rules'|guide, [2] = group, [3] = service
if (segments.length <= 1) {
// Homepage
return { ...item, changefreq: 'weekly', priority: 1.0, lastmod: new Date() };
}
if (segments.length === 2 && segments[1] === 'rules') {
// /rules/ index
return { ...item, changefreq: 'weekly', priority: 0.9, lastmod: new Date() };
}
if (segments.length === 3 && segments[1] === 'rules') {
// /rules/[group]/ index
return { ...item, changefreq: 'monthly', priority: 0.7, lastmod: new Date() };
}
if (segments.length === 4 && segments[1] === 'rules') {
// /rules/[group]/[service]/ — main content pages
return { ...item, changefreq: 'monthly', priority: 0.8, lastmod: new Date() };
}
// Guide pages and others
return { ...item, changefreq: 'yearly', priority: 0.6, lastmod: new Date() };
},
}),
icon(),
],
vite: {
plugins: [yamlPlugin()],
},
});

7330
site/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

31
site/package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "awesome-prometheus-alerts-site",
"type": "module",
"version": "1.0.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.0.0",
"@astrojs/tailwind": "^5.0.0",
"@iconify-json/lucide": "^1.0.0",
"@rollup/plugin-yaml": "^4.0.0",
"astro": "^5.0.0",
"astro-icon": "^1.0.0",
"js-yaml": "^4.1.0",
"pagefind": "^1.0.0",
"tailwindcss": "^3.4.0",
"yaml": "^2.8.3"
},
"devDependencies": {
"@types/js-yaml": "^4.0.0"
},
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead"
]
}

22
site/public/favicon.svg Normal file
View file

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 40">
<!-- Prometheus flame -->
<path fill="#E6522C" d="
M16 0
C 13 4 9 8 7 12
C 4 17 3 21 3 25
C 3 33.3 8.9 39.5 16 40
C 23.1 39.5 29 33.3 29 25
C 29 21 28 17 25 12
C 23 8 19 4 16 0 Z
"/>
<!-- Inner cutout — circular hole like the official Prometheus flame -->
<circle cx="16" cy="27" r="5.5" fill="white"/>
<!-- Small inner flame pointing upward -->
<path fill="#E6522C" d="
M16 19
C 14.5 21.5 13.5 23.5 13.5 26
C 13.5 27.9 14.6 29.5 16 30
C 17.4 29.5 18.5 27.9 18.5 26
C 18.5 23.5 17.5 21.5 16 19 Z
"/>
</svg>

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

19
site/public/manifest.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "Awesome Prometheus Alerts",
"short_name": "Prom Alerts",
"description": "Collection of copy-pasteable Prometheus alerting rules for 90+ services.",
"start_url": "/awesome-prometheus-alerts/",
"scope": "/awesome-prometheus-alerts/",
"display": "browser",
"background_color": "#0f172a",
"theme_color": "#E6522C",
"lang": "en",
"icons": [
{
"src": "/awesome-prometheus-alerts/favicon.svg",
"type": "image/svg+xml",
"sizes": "any",
"purpose": "any maskable"
}
]
}

28
site/public/robots.txt Normal file
View file

@ -0,0 +1,28 @@
User-agent: *
Allow: /
# AI search bots — explicitly allowed for citation
User-agent: GPTBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: anthropic-ai
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Bingbot
Allow: /
Sitemap: https://samber.github.io/awesome-prometheus-alerts/sitemap-index.xml
LLMs: https://samber.github.io/awesome-prometheus-alerts/llms.txt
LLMs-full: https://samber.github.io/awesome-prometheus-alerts/llms-full.txt

View file

@ -0,0 +1,53 @@
---
interface BreadcrumbItem {
label: string;
href?: string;
}
interface Props {
items: BreadcrumbItem[];
base: string;
}
import { SITE_ORIGIN } from '../data/site';
const { items, base } = Astro.props;
const allItems = [{ label: 'Home', href: `${base}/` }, ...items];
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: allItems.map((item, i) => ({
'@type': 'ListItem',
position: i + 1,
name: item.label,
...(item.href ? { item: `${SITE_ORIGIN}${item.href}` } : {}),
})),
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<nav aria-label="Breadcrumb" class="text-sm text-slate-500 dark:text-slate-400">
<ol class="flex items-center flex-wrap gap-1">
{allItems.map((item, i) => (
<li class="flex items-center gap-1">
{i > 0 && (
<svg class="w-3.5 h-3.5 text-slate-300 dark:text-slate-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
)}
{item.href && i < allItems.length - 1 ? (
<a href={item.href} class="hover:text-brand dark:hover:text-brand-dark transition-colors">
{item.label}
</a>
) : (
<span class={i === allItems.length - 1 ? 'text-slate-700 dark:text-slate-200 font-medium' : ''}>
{item.label}
</span>
)}
</li>
))}
</ol>
</nav>

View file

@ -0,0 +1,12 @@
---
---
<div class="mb-6 p-4 rounded-xl border border-amber-200 dark:border-amber-800/50 bg-amber-50 dark:bg-amber-900/20">
<div class="flex gap-3">
<span class="text-amber-500 flex-shrink-0 mt-0.5">⚠️</span>
<p class="text-sm text-amber-700 dark:text-amber-300">
Alert thresholds depend on the nature of your applications.
Some queries may have arbitrary tolerance thresholds.
Building an efficient monitoring platform takes time. 😉
</p>
</div>
</div>

View file

@ -0,0 +1,90 @@
---
interface Props {
targetId: string;
label?: string;
variant?: 'icon' | 'text';
class?: string;
}
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '' } = Astro.props;
const btnId = `copy-btn-${targetId}`;
---
{variant === 'icon' ? (
<button
id={btnId}
data-copy-target={targetId}
aria-label="Copy to clipboard"
class={`copy-btn inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors ${extraClass}`}
>
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span class="copy-label sr-only">Copy</span>
<span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span>
</button>
) : (
<button
id={btnId}
data-copy-target={targetId}
class={`copy-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:border-brand dark:hover:border-brand-dark hover:text-brand dark:hover:text-brand-dark transition-colors ${extraClass}`}
>
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span class="copy-label">{label}</span>
<span class="copied-label hidden text-green-600 dark:text-green-400">Copied!</span>
</button>
)}
<script define:vars={{ btnId }}>
const btn = document.getElementById(btnId);
if (!(btn instanceof HTMLButtonElement)) return;
if (btn.dataset.copyBound === 'true') return;
btn.dataset.copyBound = 'true';
btn.addEventListener('click', async () => {
const targetId = btn.dataset.copyTarget;
if (!targetId) return;
const target = document.getElementById(targetId);
if (!target) return;
const text = target.textContent ?? '';
try {
await navigator.clipboard.writeText(text.trim());
} catch {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = text.trim();
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
// Visual feedback
const copyIcon = btn.querySelector('.copy-icon');
const checkIcon = btn.querySelector('.check-icon');
const copyLabel = btn.querySelector('.copy-label');
const copiedLabel = btn.querySelector('.copied-label');
copyIcon?.classList.add('hidden');
checkIcon?.classList.remove('hidden');
copyLabel?.classList.add('hidden');
copiedLabel?.classList.remove('hidden');
setTimeout(() => {
copyIcon?.classList.remove('hidden');
checkIcon?.classList.add('hidden');
copyLabel?.classList.remove('hidden');
copiedLabel?.classList.add('hidden');
}, 2000);
});
</script>

View file

@ -0,0 +1,122 @@
---
import type { Exporter, Service } from '../data/rules';
import { formatExporterAsYaml, getDistUrl } from '../data/rules';
import RuleCard from './RuleCard.astro';
import CopyButton from './CopyButton.astro';
interface Props {
exporter: Exporter;
service: Service;
groupIndex: number;
serviceIndex: number;
exporterIndex: number;
showExporterNumber: boolean;
}
const {
exporter,
service,
groupIndex,
serviceIndex,
exporterIndex,
showExporterNumber,
} = Astro.props;
const distUrl = getDistUrl(service.name, exporter.slug);
const allRulesId = `exporter-all-${groupIndex}-${serviceIndex}-${exporterIndex}`;
const allRulesYaml = formatExporterAsYaml(exporter);
const wgetCommand = `wget ${distUrl}`;
const wgetId = `wget-${groupIndex}-${serviceIndex}-${exporterIndex}`;
const exporterPrefix = showExporterNumber
? `${groupIndex}.${serviceIndex}.${exporterIndex}.`
: `${groupIndex}.${serviceIndex}.`;
---
<section class="mb-10 scroll-mt-36" id={`exporter-${exporter.slug}`}>
<!-- Hidden pre for copy-all -->
<pre id={allRulesId} class="hidden">{allRulesYaml}</pre>
<!-- Exporter header -->
<div class="flex items-center justify-between flex-wrap gap-3 mb-3 pb-3 border-b border-slate-100 dark:border-slate-800">
<div class="min-w-0">
{exporter.name ? (
<h2 class="text-base font-semibold text-slate-800 dark:text-slate-100 flex items-center gap-2 flex-wrap">
<span class="text-slate-400 dark:text-slate-500 font-normal text-sm">
{exporterPrefix}
</span>
{exporter.doc_url ? (
<a href={exporter.doc_url} target="_blank" rel="noopener noreferrer" class="text-brand dark:text-brand-dark hover:underline">
{exporter.name}
</a>
) : (
<span>{exporter.name}</span>
)}
<span class="text-xs font-normal text-slate-400 dark:text-slate-500">
({exporter.rules?.length ?? 0} rules)
</span>
</h2>
) : (
<h2 class="text-base font-semibold text-slate-800 dark:text-slate-100">
<span class="text-slate-400 dark:text-slate-500 font-normal text-sm mr-1">{exporterPrefix}</span>
{service.name}
<span class="text-xs font-normal text-slate-400 dark:text-slate-500 ml-2">
({exporter.rules?.length ?? 0} rules)
</span>
</h2>
)}
</div>
<div class="flex items-center gap-2">
<CopyButton targetId={allRulesId} label="Copy all" variant="text" />
</div>
</div>
<!-- Exporter comments -->
{exporter.comments && (
<div class="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 text-sm text-amber-800 dark:text-amber-200">
<pre class="whitespace-pre-wrap font-sans text-sm">{exporter.comments.trim()}</pre>
</div>
)}
<!-- wget download command -->
{(exporter.rules?.length ?? 0) > 0 && (
<div class="mb-4 flex items-center gap-2">
<div class="flex-1 flex items-center gap-2 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-2 min-w-0">
<svg class="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre>
</div>
<CopyButton targetId={wgetId} variant="icon" />
</div>
)}
<!-- Rules list -->
{(exporter.rules?.length ?? 0) === 0 ? (
<div class="rounded-lg border border-dashed border-slate-200 dark:border-slate-700 p-6 text-center">
<p class="text-sm text-slate-400 dark:text-slate-500">
No rules yet —{' '}
<a href="https://github.com/samber/awesome-prometheus-alerts" target="_blank" rel="noopener noreferrer" class="text-brand dark:text-brand-dark hover:underline">
contribute on GitHub 👋
</a>
</p>
</div>
) : (
<div>
{exporter.rules.map((rule, ruleIdx) => {
const anchorId = `rule-${groupIndex}-${serviceIndex}-${exporterIndex}-${ruleIdx + 1}`;
const ruleNumber = showExporterNumber
? `${groupIndex}.${serviceIndex}.${exporterIndex}.${ruleIdx + 1}.`
: `${groupIndex}.${serviceIndex}.${ruleIdx + 1}.`;
return (
<RuleCard
rule={rule}
anchorId={anchorId}
ruleNumber={ruleNumber}
/>
);
})}
</div>
)}
</section>

View file

@ -0,0 +1,96 @@
---
import { sponsors } from '../data/sponsors';
import { getPopularServices, data, getGroupSlug } from '../data/rules';
import { SITE_NAME, SITE_URL, GITHUB_URL, GITHUB_CONTRIBUTING_URL, GITHUB_LICENSE_URL, AUTHOR_NAME, AUTHOR_GITHUB_URL, TWITTER_HANDLE, LICENSE_CC_BY_NAME } from '../data/site';
interface Props {
base: string;
}
const { base } = Astro.props;
const popularServices = getPopularServices();
const featuredGroupSlugs = [
'basic-resource-monitoring',
'databases',
'orchestrators',
'network-and-security',
];
const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(getGroupSlug(g)));
---
<footer class="border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-900 mt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Column 1: About -->
<div>
<div class="flex items-center gap-2 mb-3">
<img src={`${base}/favicon.svg`} alt="Prometheus flame" class="w-5 h-5" aria-hidden="true" />
<span class="font-semibold text-slate-900 dark:text-white text-sm">{SITE_NAME}</span>
</div>
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
A curated collection of copy-pasteable Prometheus alerting rules for 90+ services and exporters.
</p>
<div class="mt-4 flex gap-3">
<a href={`https://twitter.com/share?via=${TWITTER_HANDLE.slice(1)}&text=🚨 ${SITE_NAME}&url=${SITE_URL}`} target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-blue-400 transition-colors" aria-label="Share on Twitter">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
<a href={`http://www.linkedin.com/shareArticle?mini=true&url=${SITE_URL}`} target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-blue-600 transition-colors" aria-label="Share on LinkedIn">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</a>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors" aria-label="GitHub repository">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/></svg>
</a>
</div>
</div>
<!-- Column 2: Quick links -->
<div>
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Quick links</h3>
<ul class="space-y-2">
<li><a href={`${base}/rules/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Browse all rules</a></li>
<li><a href={`${base}/alertmanager/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">AlertManager Config</a></li>
<li><a href={`${base}/blackbox-exporter/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Blackbox Exporter</a></li>
<li><a href={`${base}/sleep-peacefully/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Sleep Peacefully</a></li>
<li><a href={GITHUB_CONTRIBUTING_URL} target="_blank" rel="noopener noreferrer" class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Contributing Guide</a></li>
</ul>
</div>
<!-- Column 3: Categories -->
<div>
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Categories</h3>
<ul class="space-y-2">
{featuredGroups.map((g) => (
<li>
<a href={`${base}/rules/${getGroupSlug(g)}/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">
{g.name}
</a>
</li>
))}
</ul>
</div>
<!-- Column 4: Sponsors -->
<div>
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Sponsors</h3>
<div class="space-y-4">
{sponsors.map((s) => (
<a href={s.url} target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity">
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-6" />
</a>
))}
</div>
</div>
</div>
<div class="mt-10 pt-6 border-t border-slate-200 dark:border-slate-800 flex flex-col sm:flex-row justify-between items-center gap-2 text-xs text-slate-400 dark:text-slate-500">
<span>
<a href={GITHUB_URL} class="hover:text-brand dark:hover:text-brand-dark transition-colors">awesome-prometheus-alerts</a>
{' '}is maintained by{' '}
<a href={AUTHOR_GITHUB_URL} class="hover:text-brand dark:hover:text-brand-dark transition-colors">@{AUTHOR_GITHUB_URL.split('/').pop()}</a>
</span>
<span>Licensed under <a href={GITHUB_LICENSE_URL} class="hover:text-brand dark:hover:text-brand-dark transition-colors">{LICENSE_CC_BY_NAME}</a></span>
</div>
</div>
</footer>

View file

@ -0,0 +1,161 @@
---
import ThemeToggle from './ThemeToggle.astro';
import { sponsors } from '../data/sponsors';
import { SITE_NAME, GITHUB_URL, GITHUB_API_REPO_URL, GITHUB_CONTRIBUTING_URL } from '../data/site';
interface Props {
base: string;
}
const { base } = Astro.props;
const currentPath = Astro.url.pathname;
function isActive(path: string) {
return currentPath.startsWith(`${base}${path}`);
}
let stars = 0;
try {
const res = await fetch(GITHUB_API_REPO_URL, {
headers: { 'Accept': 'application/vnd.github+json' }
});
if (res.ok) {
const data = await res.json();
stars = data.stargazers_count ?? 0;
}
} catch {}
const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
---
<!-- Main header -->
<header class="sticky top-0 z-40 bg-white/95 dark:bg-slate-950/95 backdrop-blur border-b border-slate-200 dark:border-slate-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14">
<!-- Logo -->
<a href={`${base}/`} class="flex items-center gap-2 font-semibold text-slate-900 dark:text-white hover:text-brand dark:hover:text-brand-dark transition-colors flex-shrink-0">
<img src={`${base}/favicon.svg`} alt="Prometheus flame" class="w-6 h-6" aria-hidden="true" />
<span class="hidden sm:block text-sm">{SITE_NAME}</span>
<span class="sm:hidden text-sm">APA</span>
</a>
<!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-6" aria-label="Main navigation">
<a
href={`${base}/rules/`}
class={`text-sm font-medium transition-colors ${isActive('/rules') ? 'text-brand dark:text-brand-dark' : 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white'}`}
>
Rules
</a>
<!-- Guides dropdown -->
<div class="relative group">
<button class={`text-sm font-medium transition-colors flex items-center gap-1 ${isActive('/alertmanager') || isActive('/blackbox') || isActive('/sleep') ? 'text-brand dark:text-brand-dark' : 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white'}`}>
Guides
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div class="absolute top-full right-0 mt-1 w-52 bg-white dark:bg-slate-900 rounded-lg shadow-lg border border-slate-200 dark:border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-150 py-1">
<a href={`${base}/alertmanager/`} class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-brand dark:hover:text-brand-dark">
AlertManager Config
</a>
<a href={`${base}/blackbox-exporter/`} class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-brand dark:hover:text-brand-dark">
Blackbox Exporter
</a>
<a href={`${base}/sleep-peacefully/`} class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-brand dark:hover:text-brand-dark">
Sleep Peacefully
</a>
</div>
</div>
<a
href={GITHUB_CONTRIBUTING_URL}
target="_blank"
rel="noopener noreferrer"
class="text-sm font-medium text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
>
Contribute
</a>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub repository"
class="flex items-center gap-1.5 text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
</svg>
{stars > 0 && (
<span class="text-xs font-medium tabular-nums">{starsLabel}</span>
)}
</a>
</nav>
<div class="flex items-center gap-2">
<ThemeToggle />
<!-- Mobile hamburger -->
<button
id="mobile-menu-btn"
class="md:hidden p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
aria-label="Toggle menu"
aria-expanded="false"
aria-controls="mobile-menu"
>
<svg id="hamburger-icon" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg id="close-icon" class="w-5 h-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Sponsors row -->
<div class="border-t border-slate-100 dark:border-slate-800/60 bg-slate-50/70 dark:bg-slate-900/50 py-2 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto flex items-center justify-center gap-3">
<span class="text-xs font-medium tracking-wider uppercase text-slate-400 dark:text-slate-500">Sponsored by</span>
{sponsors.map((s) => (
<a href={s.url} target="_blank" rel="noopener noreferrer" class="hover:opacity-75 transition-opacity" title={s.name}>
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-10 w-auto" />
</a>
))}
</div>
</div>
<!-- Mobile menu -->
<div
id="mobile-menu"
class="hidden md:hidden border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950"
>
<nav class="px-4 py-3 space-y-1" aria-label="Mobile navigation">
<a href={`${base}/rules/`} class="block px-3 py-2 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800">Rules</a>
<a href={`${base}/alertmanager/`} class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">AlertManager Config</a>
<a href={`${base}/blackbox-exporter/`} class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">Blackbox Exporter</a>
<a href={`${base}/sleep-peacefully/`} class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">Sleep Peacefully</a>
<a href={GITHUB_CONTRIBUTING_URL} target="_blank" rel="noopener noreferrer" class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">Contribute</a>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">GitHub</a>
</nav>
</div>
</header>
<script>
const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu');
const hamburger = document.getElementById('hamburger-icon');
const closeIcon = document.getElementById('close-icon');
btn?.addEventListener('click', () => {
const isOpen = menu?.classList.toggle('hidden') === false;
btn.setAttribute('aria-expanded', String(isOpen));
hamburger?.classList.toggle('hidden', isOpen);
closeIcon?.classList.toggle('hidden', !isOpen);
});
</script>

View file

@ -0,0 +1,56 @@
---
import type { Rule } from '../data/rules';
import { formatRuleAsYaml } from '../data/rules';
import SeverityBadge from './SeverityBadge.astro';
import CopyButton from './CopyButton.astro';
interface Props {
rule: Rule;
anchorId: string;
ruleNumber: string;
}
const { rule, anchorId, ruleNumber } = Astro.props;
const yamlContent = formatRuleAsYaml(rule);
const codeId = `code-${anchorId}`;
---
<article class="group mb-4 rounded-xl border border-slate-200 dark:border-slate-700/60 bg-white dark:bg-slate-900 hover:border-slate-300 dark:hover:border-slate-600 transition-colors scroll-mt-36" id={anchorId}>
<!-- Rule header -->
<div class="flex items-start justify-between gap-3 px-4 pt-3 pb-2">
<div class="flex items-center gap-2 flex-wrap min-w-0">
<SeverityBadge severity={rule.severity} />
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-100">
<span class="text-slate-400 dark:text-slate-500 font-normal text-xs mr-1">{ruleNumber}</span>
{rule.name}
</h3>
</div>
<div class="flex items-center gap-1 flex-shrink-0">
<!-- Permalink -->
<a
href={`#${anchorId}`}
class="p-1.5 rounded text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Permalink to this rule"
title="Permalink"
>
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
</a>
<CopyButton targetId={codeId} variant="icon" />
</div>
</div>
<!-- Description -->
<p class="px-4 pb-2 text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
{rule.description}
</p>
<!-- YAML code block -->
<div class="relative mx-4 mb-4">
<pre
id={codeId}
class="rule-code text-xs leading-relaxed whitespace-pre overflow-x-auto"
>{yamlContent}</pre>
</div>
</article>

View file

@ -0,0 +1,56 @@
---
import { SITE_NAME, AUTHOR_NAME, TWITTER_HANDLE } from '../data/site';
interface Props {
title: string;
description: string;
canonicalUrl: string;
ogImage?: string;
ogType?: string;
keywords?: string;
jsonLd?: object | object[];
base: string;
siteUrl: string;
datePublished?: string;
dateModified?: string;
}
const { title, description, canonicalUrl, jsonLd, keywords, datePublished, dateModified, base, siteUrl } = Astro.props;
const ogType = Astro.props.ogType ?? 'website';
const ogImage = Astro.props.ogImage ?? `${base}/images/prometheus-logo.png`;
const fullOgImage = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`;
const jsonLdArray = jsonLd
? Array.isArray(jsonLd) ? jsonLd : [jsonLd]
: [];
---
<title>{title}</title>
<meta name="description" content={description} />
{keywords && <meta name="keywords" content={keywords} />}
<link rel="canonical" href={canonicalUrl} />
<!-- Open Graph -->
<meta property="og:type" content={ogType} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullOgImage} />
<meta property="og:image:alt" content={`${title} — Prometheus alert rules`} />
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:locale" content="en_US" />
{ogType === 'article' && datePublished && <meta property="article:published_time" content={datePublished} />}
{ogType === 'article' && dateModified && <meta property="article:modified_time" content={dateModified} />}
{ogType === 'article' && <meta property="article:author" content={AUTHOR_NAME} />}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullOgImage} />
<meta name="twitter:site" content={TWITTER_HANDLE} />
<meta name="twitter:creator" content={TWITTER_HANDLE} />
{jsonLdArray.map((schema) => (
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
))}

View file

@ -0,0 +1,45 @@
---
// Pagefind search widget
// The pagefind bundle is generated after `astro build` with: npx pagefind --site dist
interface Props {
class?: string;
}
const { class: extraClass = '' } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
---
<div id="search" class={extraClass}></div>
<link rel="stylesheet" href={`${base}/pagefind/pagefind-ui.css`} />
<script>
// Load Pagefind UI lazily (only when the search div is visible/focused)
function initPagefind() {
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
// @ts-ignore
import(`${base}/pagefind/pagefind-ui.js`).then((module) => {
const PagefindUI = module.PagefindUI;
new PagefindUI({
element: '#search',
showSubResults: true,
highlightParam: 'highlight',
baseUrl: base,
});
});
}
const searchEl = document.getElementById('search');
if (searchEl) {
// Load when visible via Intersection Observer
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
initPagefind();
observer.disconnect();
}
}, { rootMargin: '200px' });
observer.observe(searchEl);
} else {
initPagefind();
}
}
</script>

View file

@ -0,0 +1,41 @@
---
import type { Service, Group } from '../data/rules';
import { getRuleCount, getGroupSlug, getServiceSlug } from '../data/rules';
interface Props {
service: Service;
group: Group;
base: string;
}
const { service, group, base } = Astro.props;
const ruleCount = getRuleCount(service);
const groupSlug = getGroupSlug(group);
const serviceSlug = getServiceSlug(service);
const href = `${base}/rules/${groupSlug}/${serviceSlug}/`;
const exporterNames = service.exporters
.map((e) => e.name)
.filter(Boolean)
.slice(0, 3);
---
<a
href={href}
class="group block p-4 rounded-xl border border-slate-200 dark:border-slate-700/60 bg-white dark:bg-slate-900 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all duration-150"
>
<div class="flex items-start justify-between gap-2 mb-2">
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors leading-snug">
{service.name}
</h3>
<span class="flex-shrink-0 text-xs font-medium px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400">
{ruleCount} rule{ruleCount !== 1 ? 's' : ''}
</span>
</div>
{exporterNames.length > 0 && (
<p class="text-xs text-slate-400 dark:text-slate-500 truncate">
{exporterNames.join(' · ')}{service.exporters.length > 3 ? ` +${service.exporters.length - 3}` : ''}
</p>
)}
</a>

View file

@ -0,0 +1,16 @@
---
interface Props {
severity: 'critical' | 'warning' | 'info';
}
const { severity } = Astro.props;
const classes = {
critical: 'badge-critical',
warning: 'badge-warning',
info: 'badge-info',
}[severity];
---
<span class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${classes}`}>
{severity}
</span>

View file

@ -0,0 +1,72 @@
---
import type { Group, Service } from '../data/rules';
import { getGroupSlug, getServiceSlug } from '../data/rules';
interface Props {
groups: Group[];
currentGroupSlug?: string;
currentServiceSlug?: string;
currentService?: Service;
base: string;
}
const { groups, currentGroupSlug, currentServiceSlug, currentService, base } = Astro.props;
---
<aside class="hidden lg:block w-60 flex-shrink-0" aria-label="Rules navigation">
<nav class="sticky top-20 max-h-[calc(100vh-6rem)] overflow-y-auto pr-2 pb-8">
<ul class="space-y-3">
{groups.map((group, i) => {
const groupSlug = getGroupSlug(group);
const isGroupActive = groupSlug === currentGroupSlug;
return (
<li class={i > 0 ? 'pt-3 border-t border-slate-100 dark:border-slate-800' : ''}>
<a
href={`${base}/rules/${groupSlug}/`}
class={`block text-xs font-bold uppercase tracking-widest mb-1.5 transition-colors ${isGroupActive ? 'text-brand dark:text-brand-dark' : 'text-slate-700 dark:text-slate-300 hover:text-brand dark:hover:text-brand-dark'}`}
>
{group.name}
</a>
<ul class="space-y-0.5">
{group.services.map((service) => {
const serviceSlug = getServiceSlug(service);
const isActive = isGroupActive && serviceSlug === currentServiceSlug;
return (
<li>
<a
href={`${base}/rules/${groupSlug}/${serviceSlug}/`}
class={`block text-sm py-0.5 px-2 rounded transition-colors truncate ${
isActive
? 'text-brand dark:text-brand-dark bg-brand/5 dark:bg-brand-dark/10 font-medium'
: 'text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800/50'
}`}
>
{service.name}
</a>
{/* Exporter sub-level — only on the current service page */}
{isActive && currentService && currentService.exporters.length > 1 && (
<ul class="mt-0.5 ml-3 space-y-0.5 border-l border-slate-200 dark:border-slate-700 pl-2">
{currentService.exporters.map((exp) => (
<li>
<a
href={`#exporter-${exp.slug}`}
class="block text-xs py-0.5 px-1 rounded truncate text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors"
>
{exp.name ?? exp.slug}
</a>
</li>
))}
</ul>
)}
</li>
);
})}
</ul>
</li>
);
})}
</ul>
</nav>
</aside>

View file

@ -0,0 +1,22 @@
---
import { getTotalRuleCount, getTotalExporterCount, data } from '../data/rules';
const totalRules = getTotalRuleCount();
const totalExporters = getTotalExporterCount();
const totalGroups = data.groups.length;
---
<div class="flex flex-wrap justify-center gap-6 sm:gap-10 py-4 text-center">
<div>
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalRules}</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">alert rules</div>
</div>
<div>
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalExporters}</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">exporters</div>
</div>
<div>
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalGroups}</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">categories</div>
</div>
</div>

View file

@ -0,0 +1,25 @@
---
---
<button
id="theme-toggle"
aria-label="Toggle dark mode"
class="p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
>
<!-- Sun icon (shown in dark mode) -->
<svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" class="hidden dark:block w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 7a5 5 0 100 10 5 5 0 000-10z" />
</svg>
<!-- Moon icon (shown in light mode) -->
<svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" class="block dark:hidden w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
<script>
const btn = document.getElementById('theme-toggle');
btn?.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
</script>

443
site/src/data/rules.ts Normal file
View file

@ -0,0 +1,443 @@
// @ts-ignore — Vite YAML plugin provides this at build time
import rulesData from '../../../_data/rules.yml';
export interface Rule {
name: string;
description: string;
query: string;
severity: 'critical' | 'warning' | 'info';
for?: string;
comments?: string;
}
export interface Exporter {
slug: string;
name?: string;
doc_url?: string;
comments?: string;
rules: Rule[];
}
export interface Service {
name: string;
exporters: Exporter[];
}
export interface Group {
name: string;
services: Service[];
}
export interface RulesData {
groups: Group[];
}
export const data: RulesData = rulesData as RulesData;
/** Slugify a name for use in URLs — mirrors the dist/ workflow naming */
export function toSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function getGroupSlug(group: Group): string {
return toSlug(group.name);
}
export function getServiceSlug(service: Service): string {
return toSlug(service.name);
}
/** CamelCase a rule name for the Prometheus alert name field */
export function toCamelCase(name: string): string {
return name
.split(/\s+/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join('');
}
/** Count all rules across a service's exporters */
export function getRuleCount(service: Service): number {
return service.exporters.reduce((sum, exp) => sum + (exp.rules?.length ?? 0), 0);
}
/** Count all rules in the entire dataset */
export function getTotalRuleCount(): number {
return data.groups.reduce(
(sum, group) => sum + group.services.reduce((s, svc) => s + getRuleCount(svc), 0),
0
);
}
/** Count all services */
export function getTotalServiceCount(): number {
return data.groups.reduce((sum, group) => sum + group.services.length, 0);
}
/** Count all exporters */
export function getTotalExporterCount(): number {
return data.groups.reduce(
(sum, group) =>
sum + group.services.reduce((s, svc) => s + svc.exporters.length, 0),
0
);
}
/** Flat list of all services with routing context */
export function getAllServices(): Array<{
group: Group;
service: Service;
groupSlug: string;
serviceSlug: string;
ruleCount: number;
}> {
return data.groups.flatMap((group) =>
group.services.map((service) => ({
group,
service,
groupSlug: getGroupSlug(group),
serviceSlug: getServiceSlug(service),
ruleCount: getRuleCount(service),
}))
);
}
/** Hard-coded map from old Jekyll anchor IDs to new Astro paths.
*
* Old Jekyll site pattern (spaceshyphens, lowercased; dots/slashes preserved):
* - #service-name service section heading
* - #service-name-1 first exporter subsection
* - #service-name-2 second exporter subsection,
*
* New Astro paths use /rules/{group}/{service}/#exporter-{slug} for exporter sections.
*/
const ANCHOR_REDIRECT_MAP: Record<string, string> = {
// Basic resource monitoring
'prometheus-self-monitoring': '/rules/basic-resource-monitoring/prometheus-self-monitoring/',
'prometheus-self-monitoring-1': '/rules/basic-resource-monitoring/prometheus-self-monitoring/#exporter-embedded-exporter',
'host-and-hardware': '/rules/basic-resource-monitoring/host-and-hardware/',
'host-and-hardware-1': '/rules/basic-resource-monitoring/host-and-hardware/#exporter-node-exporter',
's.m.a.r.t-device-monitoring': '/rules/basic-resource-monitoring/s-m-a-r-t-device-monitoring/',
's.m.a.r.t-device-monitoring-1': '/rules/basic-resource-monitoring/s-m-a-r-t-device-monitoring/#exporter-smartctl-exporter',
'ipmi': '/rules/basic-resource-monitoring/ipmi/',
'ipmi-1': '/rules/basic-resource-monitoring/ipmi/#exporter-ipmi-exporter',
'docker-containers': '/rules/basic-resource-monitoring/docker-containers/',
'docker-containers-1': '/rules/basic-resource-monitoring/docker-containers/#exporter-google-cadvisor',
'blackbox': '/rules/basic-resource-monitoring/blackbox/',
'blackbox-1': '/rules/basic-resource-monitoring/blackbox/#exporter-blackbox-exporter',
'windows-server': '/rules/basic-resource-monitoring/windows-server/',
'windows-server-1': '/rules/basic-resource-monitoring/windows-server/#exporter-windows-exporter',
'vmware': '/rules/basic-resource-monitoring/vmware/',
'vmware-1': '/rules/basic-resource-monitoring/vmware/#exporter-pryorda-vmware-exporter',
'proxmox-ve': '/rules/basic-resource-monitoring/proxmox-ve/',
'proxmox-ve-1': '/rules/basic-resource-monitoring/proxmox-ve/#exporter-prometheus-pve-exporter',
'netdata': '/rules/basic-resource-monitoring/netdata/',
'netdata-1': '/rules/basic-resource-monitoring/netdata/#exporter-embedded-exporter',
'ebpf': '/rules/basic-resource-monitoring/ebpf/',
'ebpf-1': '/rules/basic-resource-monitoring/ebpf/#exporter-ebpf-exporter',
'process-exporter': '/rules/basic-resource-monitoring/process-exporter/',
'process-exporter-1': '/rules/basic-resource-monitoring/process-exporter/#exporter-process-exporter',
'systemd': '/rules/basic-resource-monitoring/systemd/',
'systemd-1': '/rules/basic-resource-monitoring/systemd/#exporter-systemd-exporter',
// Databases
'mysql': '/rules/databases/mysql/',
'mysql-1': '/rules/databases/mysql/#exporter-mysqld-exporter',
'postgresql': '/rules/databases/postgresql/',
'postgresql-1': '/rules/databases/postgresql/#exporter-postgres-exporter',
'sql-server': '/rules/databases/sql-server/',
'sql-server-1': '/rules/databases/sql-server/#exporter-ozarklake-mssql-exporter',
'oracle-database': '/rules/databases/oracle-database/',
'oracle-database-1': '/rules/databases/oracle-database/#exporter-iamseth-oracledb-exporter',
'patroni': '/rules/databases/patroni/',
'patroni-1': '/rules/databases/patroni/#exporter-embedded-exporter-patroni',
'pgbouncer': '/rules/databases/pgbouncer/',
'pgbouncer-1': '/rules/databases/pgbouncer/#exporter-spreaker-pgbouncer-exporter',
'redis': '/rules/databases/redis/',
'redis-1': '/rules/databases/redis/#exporter-oliver006-redis-exporter',
'memcached': '/rules/databases/memcached/',
'memcached-1': '/rules/databases/memcached/#exporter-memcached-exporter',
'mongodb': '/rules/databases/mongodb/',
'mongodb-1': '/rules/databases/mongodb/#exporter-percona-mongodb-exporter',
'mongodb-2': '/rules/databases/mongodb/#exporter-dcu-mongodb-exporter',
'mongodb-3': '/rules/databases/mongodb/#exporter-stefanprodan-mgob-exporter',
'elasticsearch': '/rules/databases/elasticsearch/',
'elasticsearch-1': '/rules/databases/elasticsearch/#exporter-prometheus-community-elasticsearch-exporter',
'opensearch': '/rules/databases/opensearch/',
'opensearch-1': '/rules/databases/opensearch/#exporter-opensearch-project-opensearch-prometheus-exporter',
'meilisearch': '/rules/databases/meilisearch/',
'meilisearch-1': '/rules/databases/meilisearch/#exporter-embedded-exporter',
'cassandra': '/rules/databases/cassandra/',
'cassandra-1': '/rules/databases/cassandra/#exporter-instaclustr-cassandra-exporter',
'cassandra-2': '/rules/databases/cassandra/#exporter-criteo-cassandra-exporter',
'clickhouse': '/rules/databases/clickhouse/',
'clickhouse-1': '/rules/databases/clickhouse/#exporter-embedded-exporter',
'couchdb': '/rules/databases/couchdb/',
'couchdb-1': '/rules/databases/couchdb/#exporter-gesellix-couchdb-prometheus-exporter',
'solr': '/rules/databases/solr/',
'solr-1': '/rules/databases/solr/#exporter-embedded-exporter',
// Message brokers
'rabbitmq': '/rules/message-brokers/rabbitmq/',
'rabbitmq-1': '/rules/message-brokers/rabbitmq/#exporter-rabbitmq-exporter',
'rabbitmq-2': '/rules/message-brokers/rabbitmq/#exporter-kbudde-rabbitmq-exporter',
'zookeeper': '/rules/message-brokers/zookeeper/',
'zookeeper-1': '/rules/message-brokers/zookeeper/#exporter-cloudflare-kafka-zookeeper-exporter',
'zookeeper-2': '/rules/message-brokers/zookeeper/#exporter-dabealu-zookeeper-exporter',
'kafka': '/rules/message-brokers/kafka/',
'kafka-1': '/rules/message-brokers/kafka/#exporter-danielqsj-kafka-exporter',
'kafka-2': '/rules/message-brokers/kafka/#exporter-linkedin-kafka-exporter',
'pulsar': '/rules/message-brokers/pulsar/',
'pulsar-1': '/rules/message-brokers/pulsar/#exporter-embedded-exporter',
'nats': '/rules/message-brokers/nats/',
'nats-1': '/rules/message-brokers/nats/#exporter-nats-exporter',
// Proxies, load balancers and service meshes
'nginx': '/rules/proxies-load-balancers-and-service-meshes/nginx/',
'nginx-1': '/rules/proxies-load-balancers-and-service-meshes/nginx/#exporter-knyar-nginx-exporter',
'apache': '/rules/proxies-load-balancers-and-service-meshes/apache/',
'apache-1': '/rules/proxies-load-balancers-and-service-meshes/apache/#exporter-lusitaniae-apache-exporter',
'haproxy': '/rules/proxies-load-balancers-and-service-meshes/haproxy/',
'haproxy-1': '/rules/proxies-load-balancers-and-service-meshes/haproxy/#exporter-embedded-exporter-v2',
'haproxy-2': '/rules/proxies-load-balancers-and-service-meshes/haproxy/#exporter-haproxy-exporter-v1',
'traefik': '/rules/proxies-load-balancers-and-service-meshes/traefik/',
'traefik-1': '/rules/proxies-load-balancers-and-service-meshes/traefik/#exporter-embedded-exporter-v2',
'traefik-2': '/rules/proxies-load-balancers-and-service-meshes/traefik/#exporter-embedded-exporter-v1',
'caddy': '/rules/proxies-load-balancers-and-service-meshes/caddy/',
'caddy-1': '/rules/proxies-load-balancers-and-service-meshes/caddy/#exporter-embedded-exporter',
'envoy': '/rules/proxies-load-balancers-and-service-meshes/envoy/',
'envoy-1': '/rules/proxies-load-balancers-and-service-meshes/envoy/#exporter-embedded-exporter',
'linkerd': '/rules/proxies-load-balancers-and-service-meshes/linkerd/',
'linkerd-1': '/rules/proxies-load-balancers-and-service-meshes/linkerd/#exporter-embedded-exporter',
'istio': '/rules/proxies-load-balancers-and-service-meshes/istio/',
'istio-1': '/rules/proxies-load-balancers-and-service-meshes/istio/#exporter-embedded-exporter',
// Runtimes
'php-fpm': '/rules/runtimes/php-fpm/',
'php-fpm-1': '/rules/runtimes/php-fpm/#exporter-bakins-fpm-exporter',
'jvm': '/rules/runtimes/jvm/',
'jvm-1': '/rules/runtimes/jvm/#exporter-jvm-exporter',
'golang': '/rules/runtimes/golang/',
'golang-1': '/rules/runtimes/golang/#exporter-golang-exporter',
'ruby': '/rules/runtimes/ruby/',
'ruby-1': '/rules/runtimes/ruby/#exporter-ruby-exporter',
'python': '/rules/runtimes/python/',
'python-1': '/rules/runtimes/python/#exporter-python-exporter',
'sidekiq': '/rules/runtimes/sidekiq/',
'sidekiq-1': '/rules/runtimes/sidekiq/#exporter-strech-sidekiq-exporter',
// Data engineering
'apache-flink': '/rules/data-engineering/apache-flink/',
'apache-flink-1': '/rules/data-engineering/apache-flink/#exporter-flink-prometheus-reporter',
'apache-spark': '/rules/data-engineering/apache-spark/',
'apache-spark-1': '/rules/data-engineering/apache-spark/#exporter-spark-prometheus',
'hadoop': '/rules/data-engineering/hadoop/',
'hadoop-1': '/rules/data-engineering/hadoop/#exporter-jmx_exporter',
// Orchestrators
'kubernetes': '/rules/orchestrators/kubernetes/',
'kubernetes-1': '/rules/orchestrators/kubernetes/#exporter-kubestate-exporter',
'nomad': '/rules/orchestrators/nomad/',
'nomad-1': '/rules/orchestrators/nomad/#exporter-embedded-exporter',
'consul': '/rules/orchestrators/consul/',
'consul-1': '/rules/orchestrators/consul/#exporter-consul-exporter',
'etcd': '/rules/orchestrators/etcd/',
'etcd-1': '/rules/orchestrators/etcd/#exporter-embedded-exporter',
'openstack': '/rules/orchestrators/openstack/',
'openstack-1': '/rules/orchestrators/openstack/#exporter-openstack-exporter',
// CI/CD
'jenkins': '/rules/ci-cd/jenkins/',
'jenkins-1': '/rules/ci-cd/jenkins/#exporter-metric-plugin',
'argocd': '/rules/ci-cd/argocd/',
'argocd-1': '/rules/ci-cd/argocd/#exporter-embedded-exporter',
'fluxcd': '/rules/ci-cd/fluxcd/',
'fluxcd-1': '/rules/ci-cd/fluxcd/#exporter-embedded-exporter',
'gitlab-ci': '/rules/ci-cd/gitlab-ci/',
'gitlab-ci-1': '/rules/ci-cd/gitlab-ci/#exporter-gitlab-built-in-exporter',
'gitlab-ci-2': '/rules/ci-cd/gitlab-ci/#exporter-workhorse',
'gitlab-ci-3': '/rules/ci-cd/gitlab-ci/#exporter-gitaly',
'spinnaker': '/rules/ci-cd/spinnaker/',
'spinnaker-1': '/rules/ci-cd/spinnaker/#exporter-embedded-exporter',
// Network and security
'speedtest': '/rules/network-and-security/speedtest/',
'speedtest-1': '/rules/network-and-security/speedtest/#exporter-nlamirault-speedtest-exporter',
'ssl/tls': '/rules/network-and-security/ssl-tls/',
'ssl/tls-1': '/rules/network-and-security/ssl-tls/#exporter-ribbybibby-ssl-exporter',
'cert-manager': '/rules/network-and-security/cert-manager/',
'cert-manager-1': '/rules/network-and-security/cert-manager/#exporter-embedded-exporter',
'juniper': '/rules/network-and-security/juniper/',
'juniper-1': '/rules/network-and-security/juniper/#exporter-czerwonk-junos-exporter',
'coredns': '/rules/network-and-security/coredns/',
'coredns-1': '/rules/network-and-security/coredns/#exporter-embedded-exporter',
'freeswitch': '/rules/network-and-security/freeswitch/',
'freeswitch-1': '/rules/network-and-security/freeswitch/#exporter-znerol-freeswitch-exporter',
'hashicorp-vault': '/rules/network-and-security/hashicorp-vault/',
'hashicorp-vault-1': '/rules/network-and-security/hashicorp-vault/#exporter-embedded-exporter',
'keycloak': '/rules/network-and-security/keycloak/',
'keycloak-1': '/rules/network-and-security/keycloak/#exporter-aerogear-keycloak-metrics-spi',
'cloudflare': '/rules/network-and-security/cloudflare/',
'cloudflare-1': '/rules/network-and-security/cloudflare/#exporter-lablabs-cloudflare-exporter',
'snmp': '/rules/network-and-security/snmp/',
'snmp-1': '/rules/network-and-security/snmp/#exporter-snmp-exporter',
'cilium': '/rules/network-and-security/cilium/',
'cilium-1': '/rules/network-and-security/cilium/#exporter-embedded-exporter',
'wireguard': '/rules/network-and-security/wireguard/',
'wireguard-1': '/rules/network-and-security/wireguard/#exporter-mindflavor-prometheus-wireguard-exporter',
// Storage
'ceph': '/rules/storage/ceph/',
'ceph-1': '/rules/storage/ceph/#exporter-embedded-exporter',
'zfs': '/rules/storage/zfs/',
'zfs-1': '/rules/storage/zfs/#exporter-node-exporter',
'zfs-2': '/rules/storage/zfs/#exporter-zfs_exporter',
'openebs': '/rules/storage/openebs/',
'openebs-1': '/rules/storage/openebs/#exporter-embedded-exporter',
'minio': '/rules/storage/minio/',
'minio-1': '/rules/storage/minio/#exporter-embedded-exporter',
// Cloud providers
'aws-cloudwatch': '/rules/cloud-providers/aws-cloudwatch/',
'aws-cloudwatch-1': '/rules/cloud-providers/aws-cloudwatch/#exporter-prometheus-cloudwatch-exporter',
'google-cloud-stackdriver': '/rules/cloud-providers/google-cloud-stackdriver/',
'google-cloud-stackdriver-1': '/rules/cloud-providers/google-cloud-stackdriver/#exporter-stackdriver-exporter',
'digitalocean': '/rules/cloud-providers/digitalocean/',
'digitalocean-1': '/rules/cloud-providers/digitalocean/#exporter-digitalocean-exporter',
'azure': '/rules/cloud-providers/azure/',
'azure-1': '/rules/cloud-providers/azure/#exporter-azure-metrics-exporter',
// Observability
'thanos': '/rules/observability/thanos/',
'thanos-1': '/rules/observability/thanos/#exporter-thanos-compactor',
'thanos-2': '/rules/observability/thanos/#exporter-thanos-query',
'thanos-3': '/rules/observability/thanos/#exporter-thanos-receiver',
'thanos-4': '/rules/observability/thanos/#exporter-thanos-sidecar',
'thanos-5': '/rules/observability/thanos/#exporter-thanos-store',
'thanos-6': '/rules/observability/thanos/#exporter-thanos-ruler',
'thanos-7': '/rules/observability/thanos/#exporter-thanos-bucket-replicate',
'thanos-8': '/rules/observability/thanos/#exporter-thanos-component-absent',
'loki': '/rules/observability/loki/',
'loki-1': '/rules/observability/loki/#exporter-embedded-exporter',
'promtail': '/rules/observability/promtail/',
'promtail-1': '/rules/observability/promtail/#exporter-embedded-exporter',
'cortex': '/rules/observability/cortex/',
'cortex-1': '/rules/observability/cortex/#exporter-embedded-exporter',
'grafana-tempo': '/rules/observability/grafana-tempo/',
'grafana-tempo-1': '/rules/observability/grafana-tempo/#exporter-embedded-exporter',
'grafana-mimir': '/rules/observability/grafana-mimir/',
'grafana-mimir-1': '/rules/observability/grafana-mimir/#exporter-embedded-exporter',
'grafana-alloy': '/rules/observability/grafana-alloy/',
'grafana-alloy-1': '/rules/observability/grafana-alloy/#exporter-embedded-exporter',
'opentelemetry-collector': '/rules/observability/opentelemetry-collector/',
'opentelemetry-collector-1': '/rules/observability/opentelemetry-collector/#exporter-embedded-exporter',
'jaeger': '/rules/observability/jaeger/',
'jaeger-1': '/rules/observability/jaeger/#exporter-embedded-exporter',
// Other
'apc-ups': '/rules/other/apc-ups/',
'apc-ups-1': '/rules/other/apc-ups/#exporter-apcupsd_exporter',
'graph-node': '/rules/other/graph-node/',
'graph-node-1': '/rules/other/graph-node/#exporter-embedded-exporter',
};
export function buildRedirectMap(base: string): Record<string, string> {
return Object.fromEntries(
Object.entries(ANCHOR_REDIRECT_MAP).map(([anchor, path]) => [anchor, `${base}${path}`])
);
}
/** Format a rule as copy-pasteable Prometheus alert YAML */
export function formatRuleAsYaml(rule: Rule): string {
const alertName = toCamelCase(rule.name);
const forValue = rule.for ?? '0m';
const commentLines = rule.comments
? rule.comments
.trim()
.split('\n')
.map((line) => ` # ${line.trim()}`)
.join('\n') + '\n'
: '';
// Escape double quotes in description
const description = rule.description.replace(/"/g, '\\"');
return `${commentLines}- alert: ${alertName}
expr: ${rule.query}
for: ${forValue}
labels:
severity: ${rule.severity}
annotations:
summary: ${rule.name} (instance {{ $labels.instance }})
description: "${description}\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}"`;
}
/** Format all rules for an exporter as a complete groups YAML block */
export function formatExporterAsYaml(exporter: Exporter): string {
const groupName = toCamelCase(exporter.slug.replace(/-/g, ' '));
const rulesYaml = (exporter.rules ?? [])
.map((rule) => formatRuleAsYaml(rule))
.join('\n\n');
return `groups:
- name: ${groupName}
rules:
${rulesYaml
.split('\n')
.map((line) => ` ${line}`)
.join('\n')}`;
}
/** Ordered list of popular service name fragments (case-insensitive substring match) */
export const popularServiceNames: string[] = [
'prometheus self-monitoring',
'host and hardware',
'kubernetes',
'mysql',
'postgresql',
'redis',
'mongodb',
'elasticsearch',
'rabbitmq',
'nginx',
'kafka',
'docker',
];
/** Returns the ordered list of popular services resolved from the data */
export function getPopularServices() {
const all = getAllServices();
return popularServiceNames
.map((target) => all.find(({ service }) => service.name.toLowerCase().includes(target)))
.filter((s): s is NonNullable<typeof s> => s !== undefined);
}
/** Flat list of all exporters with their parent group/service context */
export function getAllExporters(): Array<{
group: Group;
service: Service;
exporter: Exporter;
groupSlug: string;
serviceSlug: string;
}> {
return data.groups.flatMap((group) =>
group.services.flatMap((service) =>
service.exporters.map((exporter) => ({
group,
service,
exporter,
groupSlug: getGroupSlug(group),
serviceSlug: getServiceSlug(service),
}))
)
);
}
/** Build the raw GitHub URL for a dist file */
export function getDistUrl(serviceName: string, exporterSlug: string): string {
const serviceSlug = serviceName.replace(/ /g, '-').toLowerCase();
return `https://raw.githubusercontent.com/samber/awesome-prometheus-alerts/refs/heads/master/dist/rules/${serviceSlug}/${exporterSlug}.yml`;
}

48
site/src/data/site.ts Normal file
View file

@ -0,0 +1,48 @@
export const SITE_URL = import.meta.env.SITE + import.meta.env.BASE_URL;
export const SITE_ORIGIN = import.meta.env.SITE as string;
export const SITE_NAME = 'Awesome Prometheus Alerts';
/** ISO date the project was first published — used as datePublished across all schemas */
export const SITE_DATE_PUBLISHED = '2018-10-21';
// Author
export const AUTHOR_NAME = 'Samuel Berthe';
export const AUTHOR_GITHUB_URL = 'https://github.com/samber';
export const TWITTER_HANDLE = '@samuelberthe';
// GitHub
export const GITHUB_URL = 'https://github.com/samber/awesome-prometheus-alerts';
export const GITHUB_API_REPO_URL = 'https://api.github.com/repos/samber/awesome-prometheus-alerts';
export const GITHUB_CONTRIBUTING_URL = `${GITHUB_URL}/blob/master/CONTRIBUTING.md`;
export const GITHUB_LICENSE_URL = `${GITHUB_URL}/blob/master/LICENSE`;
// Licenses
export const LICENSE_CC_BY_URL = 'https://creativecommons.org/licenses/by/4.0/';
export const LICENSE_CC_BY_NAME = 'Creative Commons CC BY 4.0';
export const LICENSE_MIT_URL = 'https://opensource.org/licenses/MIT';
export const schemaAuthor = {
'@type': 'Person',
name: AUTHOR_NAME,
url: AUTHOR_GITHUB_URL,
sameAs: [
AUTHOR_GITHUB_URL,
`https://twitter.com/${TWITTER_HANDLE.slice(1)}`,
],
};
export const schemaPublisher = {
'@type': 'Organization',
name: 'Prometheus Alerts authors',
url: GITHUB_URL,
sameAs: [GITHUB_URL],
};
export const schemaWebSite = {
'@type': 'WebSite',
name: SITE_NAME,
url: SITE_URL,
};
export const SCHEMA_IN_LANGUAGE = 'en' as const;

21
site/src/data/sponsors.ts Normal file
View file

@ -0,0 +1,21 @@
export interface Sponsor {
name: string;
url: string;
logo: string;
description: string;
}
export const sponsors: Sponsor[] = [
{
name: 'CAST AI',
url: 'https://cast.ai/samuel',
logo: '/images/sponsor-cast-ai.png',
description: 'Kubernetes cost optimization',
},
{
name: 'Better Stack',
url: 'https://betterstack.com/',
logo: '/images/sponsor-betterstack.png',
description: 'Uptime monitoring and log management',
},
];

View file

@ -0,0 +1,100 @@
---
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import SEO from '../components/SEO.astro';
import { SITE_ORIGIN, AUTHOR_NAME } from '../data/site';
interface Props {
title: string;
description?: string;
canonicalUrl?: string;
ogImage?: string;
ogType?: string;
keywords?: string;
jsonLd?: object | object[];
noIndex?: boolean;
datePublished?: string;
dateModified?: string;
}
const {
title,
description = 'Collection of alerting rules for Prometheus. Copy-pasteable Prometheus alert configurations for 90+ services.',
canonicalUrl,
ogImage,
ogType,
keywords,
jsonLd,
noIndex = false,
datePublished,
dateModified,
} = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.replace(base, '')}`;
---
<!DOCTYPE html>
<html lang="en" class="">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#E6522C" />
<meta name="author" content={AUTHOR_NAME} />
{noIndex && <meta name="robots" content="noindex" />}
<SEO
title={title}
description={description}
canonicalUrl={canonical}
ogImage={ogImage}
ogType={ogType}
keywords={keywords}
jsonLd={jsonLd}
base={base}
siteUrl={SITE_ORIGIN}
datePublished={datePublished}
dateModified={dateModified}
/>
<link rel="preconnect" href="https://www.googletagmanager.com" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
<link rel="icon" type="image/svg+xml" href={`${base}/favicon.svg`} />
<link rel="icon" type="image/x-icon" href={`${base}/favicon.ico`} />
<link rel="manifest" href={`${base}/manifest.json`} />
<!-- Dark mode: set class before paint to avoid flash -->
<script is:inline>
(function() {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GDF25KKVNL"></script>
<script is:inline>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-GDF25KKVNL');
</script>
</head>
<body class="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100 transition-colors duration-150">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-brand text-white px-3 py-1 rounded z-50">
Skip to main content
</a>
<Header base={base} />
<main id="main-content" class="flex-1">
<slot />
</main>
<Footer base={base} />
</body>
</html>

View file

@ -0,0 +1,420 @@
---
import BaseLayout from './BaseLayout.astro';
import Breadcrumbs from '../components/Breadcrumbs.astro';
import { SITE_ORIGIN, SITE_URL, AUTHOR_NAME, AUTHOR_GITHUB_URL, schemaAuthor, schemaPublisher, schemaWebSite, SITE_DATE_PUBLISHED, SCHEMA_IN_LANGUAGE } from '../data/site';
type IconName = 'bell' | 'globe' | 'moon' | 'book';
interface BreadcrumbItem {
label: string;
href?: string;
}
interface Props {
title: string;
description?: string;
breadcrumbs?: BreadcrumbItem[];
icon?: IconName;
badge?: string;
/** Additional JSON-LD schemas to include alongside the auto-generated TechArticle */
extraJsonLd?: object | object[];
/** ISO date string (YYYY-MM-DD) for when this guide was last meaningfully updated */
dateUpdated?: string;
/** Comma-separated keywords for SEO meta tag */
keywords?: string;
/** Approximate reading time in minutes (shown in hero) */
readingTime?: number;
}
const { title, description, breadcrumbs = [], icon = 'book', badge = 'Guide', extraJsonLd, dateUpdated, keywords, readingTime } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const canonicalUrl = `${SITE_ORIGIN}${base}${Astro.url.pathname.replace(base, '')}`;
const dateModified = dateUpdated ?? new Date().toISOString().slice(0, 10);
const displayDate = new Date(dateModified + 'T00:00:00Z').toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });
const guideJsonLd = {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: title,
description: description ?? `${title} — Prometheus monitoring guide`,
url: canonicalUrl,
inLanguage: SCHEMA_IN_LANGUAGE,
image: `${SITE_URL}favicon.svg`,
datePublished: SITE_DATE_PUBLISHED,
dateModified,
author: schemaAuthor,
publisher: schemaPublisher,
isPartOf: schemaWebSite,
};
const allGuideJsonLd: object[] = [
guideJsonLd,
...(extraJsonLd ? (Array.isArray(extraJsonLd) ? extraJsonLd : [extraJsonLd]) : []),
];
const iconPaths: Record<IconName, string> = {
bell: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9',
globe: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
moon: 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z',
book: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253',
};
const allGuides = [
{
title: 'AlertManager Configuration',
description: 'Configure Prometheus and AlertManager with routing, receivers, and notification timing.',
href: `${base}/alertmanager/`,
icon: 'bell' as IconName,
badge: 'Configuration',
},
{
title: 'Blackbox Exporter',
description: 'Monitor HTTP, DNS, TCP and ICMP endpoints from multiple worldwide PoPs.',
href: `${base}/blackbox-exporter/`,
icon: 'globe' as IconName,
badge: 'Monitoring',
},
{
title: 'Sleep Peacefully',
description: 'Suppress noisy alerts during nights and weekends with time-aware PromQL.',
href: `${base}/sleep-peacefully/`,
icon: 'moon' as IconName,
badge: 'PromQL Tips',
},
];
const relatedGuides = allGuides.filter(g => g.title !== title);
---
<BaseLayout title={`${title} | Awesome Prometheus Alerts`} description={description} jsonLd={allGuideJsonLd} ogType="article" keywords={keywords} datePublished={SITE_DATE_PUBLISHED} dateModified={dateModified}>
<!-- Hero banner -->
<div class="guide-hero border-b border-slate-200 dark:border-slate-800 py-10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 xl:pl-[calc(theme(spacing.8)+14rem+3rem)]">
{breadcrumbs.length > 0 && (
<div class="mb-6">
<Breadcrumbs items={breadcrumbs} base={base} />
</div>
)}
<div class="flex items-start gap-5">
<div class="flex-shrink-0 w-14 h-14 rounded-2xl guide-icon-bg flex items-center justify-center">
<svg class="w-7 h-7 text-brand dark:text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[icon]} />
</svg>
</div>
<div>
<div class="flex items-center gap-2 mb-3 flex-wrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold tracking-wide uppercase bg-brand/10 dark:bg-brand/20 text-brand dark:text-orange-400">
{badge}
</span>
<span class="text-xs text-slate-300 dark:text-slate-600">·</span>
<span class="text-xs text-slate-400 dark:text-slate-500">By <a href={AUTHOR_GITHUB_URL} target="_blank" rel="noopener noreferrer" class="hover:text-brand dark:hover:text-orange-400 transition-colors">{AUTHOR_NAME}</a></span>
<span class="text-xs text-slate-300 dark:text-slate-600">·</span>
<time datetime={dateModified} class="text-xs text-slate-400 dark:text-slate-500">Updated {displayDate}</time>
{readingTime && (
<>
<span class="text-xs text-slate-300 dark:text-slate-600">·</span>
<span class="text-xs text-slate-400 dark:text-slate-500">{readingTime} min read</span>
</>
)}
</div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-2 leading-tight">{title}</h1>
{description && (
<p class="text-slate-500 dark:text-slate-400 text-base leading-relaxed max-w-2xl">{description}</p>
)}
</div>
</div>
</div>
</div>
<!-- Main content area -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="flex gap-12">
<!-- TOC sidebar -->
<aside class="hidden xl:block w-56 flex-shrink-0" aria-label="Table of contents">
<div class="sticky top-24">
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3">On this page</p>
<nav id="guide-toc" aria-label="Page sections">
<!-- Populated by client-side JS -->
</nav>
</div>
</aside>
<!-- Article -->
<article id="guide-content" class="flex-1 min-w-0 max-w-3xl">
<slot />
<!-- Related guides -->
<div class="mt-16 pt-10 border-t border-slate-200 dark:border-slate-800">
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-4">Continue reading</p>
<div class="flex flex-col">
{relatedGuides.map((g) => (
<a
href={g.href}
class="group flex items-center gap-4 py-3 border-b border-slate-100 dark:border-slate-800 last:border-0 transition-colors duration-150"
>
<!-- Icon -->
<div class="flex-shrink-0 w-8 h-8 rounded-lg guide-card-icon flex items-center justify-center">
<svg class="w-4 h-4 text-brand dark:text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[g.icon]} />
</svg>
</div>
<!-- Text -->
<div class="flex-1 min-w-0">
<span class="text-xs text-slate-400 dark:text-slate-500">{g.badge} · </span>
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 group-hover:text-brand dark:group-hover:text-orange-400 transition-colors duration-150">{g.title}</span>
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5 leading-relaxed">{g.description}</p>
</div>
<!-- Arrow -->
<svg
class="flex-shrink-0 w-4 h-4 text-slate-300 dark:text-slate-600 group-hover:text-brand dark:group-hover:text-orange-400 group-hover:translate-x-0.5 transition-all duration-150"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</a>
))}
</div>
</div>
</article>
</div>
</div>
</BaseLayout>
<script>
function buildTOC() {
const content = document.getElementById('guide-content');
const toc = document.getElementById('guide-toc');
if (!content || !toc) return;
const headings = Array.from(content.querySelectorAll('h2, h3')).filter(
h => !h.classList.contains('related-guides-heading')
);
if (headings.length === 0) return;
const items = headings.map((h, i) => {
if (!h.id) h.id = `heading-${i}`;
return { id: h.id, text: h.textContent?.trim() || '', depth: parseInt(h.tagName[1]) };
});
const ul = document.createElement('ul');
ul.className = 'space-y-1';
items.forEach(item => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.dataset.tocId = item.id;
a.className = `toc-link block text-sm py-0.5 transition-colors ${
item.depth === 3 ? 'pl-3 text-xs' : ''
} text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark`;
a.textContent = item.text;
li.appendChild(a);
ul.appendChild(li);
});
toc.appendChild(ul);
// Scroll-spy
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const link = toc.querySelector(`[data-toc-id="${entry.target.id}"]`);
if (!link) return;
if (entry.isIntersecting) {
toc.querySelectorAll('.toc-link').forEach(l => {
l.classList.remove('text-brand', 'dark:text-brand-dark', 'font-medium');
l.classList.add('text-slate-500', 'dark:text-slate-400');
});
link.classList.remove('text-slate-500', 'dark:text-slate-400');
link.classList.add('text-brand', 'font-medium');
}
});
},
{ rootMargin: '-10% 0% -80% 0%' }
);
headings.forEach(h => observer.observe(h));
}
document.addEventListener('DOMContentLoaded', buildTOC);
</script>
<style is:global>
/* Hero gradient */
.guide-hero {
background: linear-gradient(135deg, rgba(230, 82, 44, 0.04) 0%, transparent 60%);
}
.dark .guide-hero {
background: linear-gradient(135deg, rgba(230, 82, 44, 0.08) 0%, transparent 60%);
}
.guide-icon-bg {
background: rgba(230, 82, 44, 0.08);
border: 1px solid rgba(230, 82, 44, 0.2);
}
.dark .guide-icon-bg {
background: rgba(230, 82, 44, 0.12);
border-color: rgba(230, 82, 44, 0.25);
}
/* ── Guide content typography ─────────────────────────────────────── */
#guide-content > * + * {
margin-top: 0;
}
#guide-content p {
font-size: 0.9375rem;
line-height: 1.75;
color: #475569; /* slate-600 */
margin-bottom: 1rem;
}
.dark #guide-content p {
color: #94a3b8; /* slate-400 */
}
#guide-content a:not(.group) {
color: #E6522C;
text-decoration: none;
}
#guide-content a:not(.group):hover {
text-decoration: underline;
}
.dark #guide-content a:not(.group) {
color: #fb923c;
}
/* Section headings */
#guide-content h2 {
font-size: 1.125rem;
font-weight: 700;
color: #0f172a;
margin-top: 2.5rem;
margin-bottom: 0.875rem;
padding-left: 0.875rem;
border-left: 3px solid #E6522C;
line-height: 1.4;
}
.dark #guide-content h2 {
color: #f1f5f9;
}
#guide-content h3 {
font-size: 0.9375rem;
font-weight: 600;
color: #1e293b;
margin-top: 1.75rem;
margin-bottom: 0.5rem;
}
.dark #guide-content h3 {
color: #e2e8f0;
}
/* Lists */
#guide-content ul,
#guide-content ol {
padding-left: 1.5rem;
margin-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
#guide-content ul {
list-style-type: disc;
}
#guide-content ol {
list-style-type: decimal;
}
#guide-content li {
font-size: 0.9375rem;
line-height: 1.7;
color: #475569;
}
.dark #guide-content li {
color: #94a3b8;
}
/* Inline code */
#guide-content :not(pre) > code {
background: #f1f5f9;
color: #0f172a;
padding: 0;
border-radius: 0.25rem;
font-size: 0.8125rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
border: 1px solid #e2e8f0;
}
.dark #guide-content :not(pre) > code {
background: #1e293b;
color: #e2e8f0;
border-color: #334155;
}
/* Code blocks */
#guide-content pre {
background: #0f172a;
border-radius: 0.75rem;
border: 1px solid #1e293b;
margin: 1.5rem 0;
overflow: hidden;
}
#guide-content pre code {
display: block;
padding: 0;
overflow-x: auto;
font-size: 0.8125rem;
line-height: 1.65;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
white-space: pre;
background: transparent;
border: none;
}
/* Images */
#guide-content img {
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
margin: 1.5rem 0;
max-width: 100%;
}
.dark #guide-content img {
border-color: #334155;
}
/* Note / callout paragraphs */
#guide-content p.text-sm {
font-size: 0.8125rem;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-left: 3px solid #94a3b8;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
color: #64748b;
}
.dark #guide-content p.text-sm {
background: #1e293b;
border-color: #334155;
border-left-color: #475569;
color: #94a3b8;
}
/* Related guides cards */
.guide-card-icon {
background: rgba(230, 82, 44, 0.08);
border: 1px solid rgba(230, 82, 44, 0.15);
}
.dark .guide-card-icon {
background: rgba(230, 82, 44, 0.12);
border-color: rgba(230, 82, 44, 0.2);
}
.guide-card-glow {
background: radial-gradient(circle, rgba(230, 82, 44, 0.12), transparent 70%);
}
.dark .guide-card-glow {
background: radial-gradient(circle, rgba(230, 82, 44, 0.15), transparent 70%);
}
</style>

View file

@ -0,0 +1,237 @@
---
import GuideLayout from '../layouts/GuideLayout.astro';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const howToJsonLd = {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: 'How to configure Prometheus and AlertManager for production alerting',
description:
'Set up Prometheus alert rules, configure AlertManager routing and receivers, use recording rules to reduce load, and troubleshoot alert delivery delays.',
step: [
{
'@type': 'HowToStep',
name: 'Configure Prometheus scrape and evaluation intervals',
text: 'In prometheus.yml, set scrape_interval and evaluation_interval (e.g. 20s). Point rule_files at your alerts/*.yml directory.',
},
{
'@type': 'HowToStep',
name: 'Write alert rules',
text: 'Create YAML rule files with alert name, expr (PromQL), for duration, severity label, and summary/description annotations.',
},
{
'@type': 'HowToStep',
name: 'Configure AlertManager routing',
text: 'In alertmanager.yml, define a route tree with group_wait, group_interval, repeat_interval, and child routes that match severity labels to specific receivers.',
},
{
'@type': 'HowToStep',
name: 'Set up receivers (Slack, PagerDuty, webhook)',
text: 'Add receiver blocks for each notification channel. For Slack, provide api_url, channel, and a message template. Use continue: true if multiple receivers should handle the same alert.',
},
{
'@type': 'HowToStep',
name: 'Add recording rules for expensive queries',
text: 'Wrap high-cardinality or frequently evaluated expressions in recording rules. Reference the recorded metric in your alert expressions to reduce Prometheus CPU usage.',
},
],
};
---
<GuideLayout
title="AlertManager Configuration"
description="Prometheus and AlertManager configuration examples, recorded rules, inhibition, and troubleshooting guide for alert timing and notification routing."
breadcrumbs={[{ label: 'Guides' }, { label: 'AlertManager Config' }]}
icon="bell"
badge="Configuration Guide"
extraJsonLd={howToJsonLd}
dateUpdated="2025-01-15"
readingTime={5}
keywords="Prometheus, AlertManager, alerting, notification routing, alert timing, Slack alerts, recorded rules, inhibition, PromQL"
>
<p>
If you notice a delay between an event and the first notification, read this post:
{' '}<a href="https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html" target="_blank" rel="noopener noreferrer">
Understanding the delays on alerting
</a>.
</p>
<h2 id="prometheus-config">Prometheus configuration</h2>
<p>
Prometheus reads alert rules from YAML files and evaluates them on every <code>evaluation_interval</code> cycle.
Keep both <code>scrape_interval</code> and <code>evaluation_interval</code> consistent — a mismatch causes stale data in range queries.
</p>
<pre class="rule-code"><code>{`# prometheus.yml
global:
scrape_interval: 20s
# A short evaluation_interval will check alerting rules very often.
# It can be costly if you run Prometheus with 100+ alerts.
evaluation_interval: 20s
rule_files:
- 'alerts/*.yml'
scrape_configs:
# ...`}</code></pre>
<pre class="rule-code"><code>{`# alerts/example-redis.yml
groups:
- name: ExampleRedisGroup
rules:
- alert: ExampleRedisDown
expr: redis_up == 0
for: 2m
labels:
severity: critical
annotations:
summary: Redis instance down (instance {{ $labels.instance }})
description: "Redis is unreachable\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}"
- alert: ExampleRedisHighMemory
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: Redis memory usage above 90% (instance {{ $labels.instance }})
description: "Redis memory usage is {{ $value | humanizePercentage }}\\n LABELS = {{ $labels }}"`}</code></pre>
<h2 id="alertmanager-config">AlertManager configuration</h2>
<p>
AlertManager receives alerts from Prometheus, deduplicates and groups them, then routes them to the right receiver.
The three key timing parameters control when notifications are sent:
</p>
<ul>
<li><code>group_wait</code> — how long to wait for more alerts to batch into the first notification</li>
<li><code>group_interval</code> — how long to wait before sending a follow-up for an ongoing group</li>
<li><code>repeat_interval</code> — how often to re-notify if an alert hasn't resolved</li>
</ul>
<pre class="rule-code"><code>{`# alertmanager.yml
route:
group_wait: 10s
group_interval: 30s
repeat_interval: 4h
receiver: "slack"
routes:
# warnings and criticals → Slack
- receiver: "slack"
matchers:
- severity =~ "critical|warning"
continue: true
# criticals also → PagerDuty
- receiver: "pagerduty"
matchers:
- severity = "critical"
receivers:
- name: "slack"
slack_configs:
- api_url: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxxxxx'
send_resolved: true
channel: '#monitoring'
title: '{{ if eq .Status "firing" }}:fire:{{ else }}:white_check_mark:{{ end }} {{ .CommonLabels.alertname }}'
text: |
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Severity:* {{ .Labels.severity }}
{{ end }}
- name: "pagerduty"
pagerduty_configs:
- routing_key: '<your-pagerduty-integration-key>'
send_resolved: true`}</code></pre>
<h2 id="inhibition">Inhibition rules</h2>
<p>
Inhibition suppresses lower-priority alerts when a higher-priority alert is already firing for the same target.
A common pattern: silence <code>warning</code> alerts when a <code>critical</code> alert is active on the same instance.
</p>
<pre class="rule-code"><code>{`# alertmanager.yml
inhibit_rules:
# Suppress warnings when a critical is firing for the same instance
- source_matchers:
- severity = "critical"
target_matchers:
- severity = "warning"
equal:
- alertname
- instance
# Suppress all alerts for a node when NodeDown is firing
- source_matchers:
- alertname = "NodeDown"
target_matchers:
- job = "node"
equal:
- instance`}</code></pre>
<h2 id="recorded-rules">Reduce Prometheus server load</h2>
<p>
For expensive or frequently evaluated PromQL queries, use recording rules to precompute results.
AlertManager and dashboards then reference the lightweight recorded metric instead of re-evaluating the full expression.
</p>
<pre class="rule-code"><code>{`groups:
# 1. Define the recording rule
- name: recordings
rules:
- record: job:rabbitmq_queue_messages_delivered_total:rate5m
expr: rate(rabbitmq_queue_messages_delivered_total[5m])
# 2. Reference it in alert rules
- name: alerts
rules:
- alert: RabbitmqLowMessageDelivery
expr: sum(job:rabbitmq_queue_messages_delivered_total:rate5m) < 10
for: 2m
labels:
severity: critical
annotations:
summary: Low message delivery rate in RabbitMQ
description: "Delivery rate is {{ $value | humanize }} msg/s\\n LABELS = {{ $labels }}"`}</code></pre>
<h2 id="troubleshooting">Troubleshooting alert delays</h2>
<p>
The total time from an event occurring to a notification being sent is the sum of several independent delays.
Work through them in order:
</p>
<ul>
<li><strong>Scrape delay</strong>: up to <code>scrape_interval</code> (20s) before the metric is collected</li>
<li><strong>Evaluation delay</strong>: up to <code>evaluation_interval</code> (20s) before the rule fires</li>
<li><strong>Pending duration</strong>: the <code>for: 5m</code> window must be satisfied before the alert state changes to <em>firing</em></li>
<li><strong>GroupWait</strong>: AlertManager waits <code>group_wait</code> (10s) for other alerts to batch</li>
</ul>
<p>
In the worst case with <code>for: 5m</code>: 20s + 20s + 5m + 10s ≈ <strong>6 minutes</strong> from event to notification.
Reduce <code>evaluation_interval</code> and <code>for:</code> for time-sensitive alerts, but be careful of false positives from transient spikes.
</p>
<h2 id="resources">Further reading</h2>
<ul>
<li><a href="https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html" target="_blank" rel="noopener noreferrer">Understanding the delays on alerting</a></li>
<li><a href="https://hodovi.cc/blog/creating-awesome-alertmanager-templates-for-slack/" target="_blank" rel="noopener noreferrer">Creating awesome AlertManager templates for Slack</a></li>
<li><a href="https://prometheus.io/docs/alerting/latest/configuration/" target="_blank" rel="noopener noreferrer">AlertManager configuration reference</a></li>
<li><a href="https://grafana.com/blog/2024/10/03/how-to-use-prometheus-to-efficiently-detect-anomalies-at-scale/" target="_blank" rel="noopener noreferrer">How to use Prometheus to efficiently detect anomalies at scale</a></li>
</ul>
</GuideLayout>

View file

@ -0,0 +1,191 @@
---
import GuideLayout from '../layouts/GuideLayout.astro';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const howToJsonLd = {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: 'How to deploy Blackbox Exporter for worldwide endpoint probing',
description: 'Deploy blackbox exporters in multiple Points of Presence to monitor HTTP, HTTPS, DNS, TCP, and ICMP endpoints with geolocation support in Grafana.',
step: [
{
'@type': 'HowToStep',
name: 'Deploy blackbox exporters worldwide',
text: 'Deploy blackbox exporters in multiple PoPs (e.g., Montreal, Paris, Singapore, Sydney). Use community-hosted endpoints at probe-<city>.cleverapps.io or self-host using the samber/blackbox_exporter configuration.',
},
{
'@type': 'HowToStep',
name: 'Define targets with encoded labels',
text: 'Create a service discovery file (sd/blackbox.yml) encoding the exporter host, probe module, city name, geohash, and target URL in the target address using the :_: separator format.',
},
{
'@type': 'HowToStep',
name: 'Configure Prometheus relabeling',
text: 'Add a scrape job to prometheus.yml with relabel_configs to extract module, pop, geohash, and instance labels from the compound target address. Use regex patterns to parse each field.',
},
{
'@type': 'HowToStep',
name: 'Set up geohash labels for Grafana',
text: 'Convert probe PoP coordinates to geohash format using geohash.co. Add the geohash value to your target definitions so Grafana\'s geomap panel can display probe locations on a world map.',
},
],
};
---
<GuideLayout
title="Blackbox Exporter"
description="Deploy blackbox exporters worldwide for endpoint probing over HTTP, HTTPS, DNS, TCP and ICMP. Prometheus configuration and Grafana geohash map setup."
breadcrumbs={[{ label: 'Guides' }, { label: 'Blackbox Exporter' }]}
extraJsonLd={howToJsonLd}
dateUpdated="2025-01-15"
readingTime={6}
icon="globe"
badge="Monitoring Guide"
keywords="Prometheus, blackbox exporter, endpoint probing, HTTP monitoring, ICMP, DNS monitoring, Grafana geomap, worldwide probes, PromQL"
>
<h2 id="worldwide-probes">Worldwide probes</h2>
<p>
<a href="https://github.com/prometheus/blackbox_exporter" target="_blank" rel="noopener noreferrer">Blackbox Exporter</a>
gives you the ability to probe endpoints over HTTP, HTTPS, DNS, TCP and ICMP.
</p>
<p>
You should deploy blackbox exporters in multiple Points of Presence around the globe to monitor latency.
Feel free to use the following endpoints for your own projects:
</p>
<ul>
<li><code>https://probe-<strong>montreal</strong>.cleverapps.io</code></li>
<li><code>https://probe-<strong>paris</strong>.cleverapps.io</code></li>
<li><code>https://probe-<strong>jeddah</strong>.cleverapps.io</code></li>
<li><code>https://probe-<strong>singapore</strong>.cleverapps.io</code></li>
<li><code>https://probe-<strong>sydney</strong>.cleverapps.io</code></li>
<li><code>https://probe-<strong>warsaw</strong>.cleverapps.io</code></li>
</ul>
<p class="text-sm text-slate-500 dark:text-slate-400">
☝️ Server logs have been disabled. More probes from the community would be appreciated —
<a href="https://github.com/samber/awesome-prometheus-alerts/" target="_blank" rel="noopener noreferrer">contribute here</a>!
These blackbox exporters use the following
<a href="https://github.com/samber/blackbox_exporter/blob/master/samber.yml" target="_blank" rel="noopener noreferrer">configuration</a>.
</p>
<h2 id="prometheus-config">Prometheus Configuration</h2>
<p>
Blackbox exporters and endpoints must be declared in Prometheus. Here is a simple configuration,
inspired by <a href="https://medium.com/geekculture/single-prometheus-job-for-dozens-of-blackbox-exporters-2a7ba492d6c8" target="_blank" rel="noopener noreferrer">Hayk Davtyan's medium post</a>:
</p>
<pre class="rule-code"><code>{`# sd/blackbox.yml
- targets:
#
# Montreal
#
# http
- probe-montreal.cleverapps.io:_:http_2xx:_:Montreal:_:f229cy:_:https://api.screeb.app
- probe-montreal.cleverapps.io:_:http_2xx:_:Montreal:_:f229cy:_:https://t.screeb.app/tag.js
# icmp
- probe-montreal.cleverapps.io:_:icmp_ipv4:_:Montreal:_:f229cy:_:api.screeb.app
- probe-montreal.cleverapps.io:_:icmp_ipv4:_:Montreal:_:f229cy:_:t.screeb.app
#
# Paris
#
# http
- probe-paris.cleverapps.io:_:http_2xx:_:Paris:_:u09tgy:_:https://api.screeb.app
- probe-paris.cleverapps.io:_:http_2xx:_:Paris:_:u09tgy:_:https://t.screeb.app/tag.js
# icmp
- probe-paris.cleverapps.io:_:icmp_ipv4:_:Paris:_:u09tgy:_:api.screeb.app
- probe-paris.cleverapps.io:_:icmp_ipv4:_:Paris:_:u09tgy:_:t.screeb.app
#
# Sydney
#
# http
- probe-sydney.cleverapps.io:_:http_2xx:_:Sydney:_:r3gpkn:_:https://api.screeb.app
- probe-sydney.cleverapps.io:_:http_2xx:_:Sydney:_:r3gpkn:_:https://t.screeb.app/tag.js
# icmp
- probe-sydney.cleverapps.io:_:icmp_ipv4:_:Sydney:_:r3gpkn:_:api.screeb.app
- probe-sydney.cleverapps.io:_:icmp_ipv4:_:Sydney:_:r3gpkn:_:t.screeb.app
# ...`}</code></pre>
<pre class="rule-code"><code>{`# prometheus.yml
global:
# ...
scrape_configs:
- job_name: 'blackbox'
metrics_path: /probe
scrape_interval: 30s
scheme: https
file_sd_configs:
- files:
- /etc/prometheus/sd/blackbox.yml
relabel_configs:
# adds "module" label in the final labelset
- source_labels: [__address__]
regex: '.*:_:(.*):_:.*:_:.*:_:.*'
target_label: module
# adds "geohash" label in the final labelset
- source_labels: [__address__]
regex: '.*:_:.*:_:.*:_:(.*):_:.*'
target_label: geohash
# rewrites "instance" label with corresponding URL
- source_labels: [__address__]
regex: '.*:_:.*:_:.*:_:.*:_:(.*)'
target_label: instance
# rewrites "pop" label with corresponding location name
- source_labels: [__address__]
regex: '.*:_:.*:_:(.*):_:.*:_:.*'
target_label: pop
# passes "module" parameter to Blackbox exporter
- source_labels: [module]
target_label: __param_module
# passes "target" parameter to Blackbox exporter
- source_labels: [instance]
target_label: __param_target
# the Blackbox exporter's real hostname:port
- source_labels: [__address__]
regex: '(.*):_:.*:_:.*:_:.*:_:.*'
target_label: __address__
# ...`}</code></pre>
<h2 id="geohash">Geohash</h2>
<img
src={`${base}/images/grafana-map-panel.png`}
alt="Grafana geomap panel showing worldwide Prometheus blackbox exporter probe locations"
class="rounded-lg border border-slate-200 dark:border-slate-700 my-4"
loading="lazy"
/>
<p>To display nice maps in Grafana, you need to instruct blackbox exporters about the location. Grafana map panel speaks the "geohash" format:</p>
<ol>
<li>Go to Google Maps</li>
<li>Extract the lat/long from the URL</li>
<li>Convert lat/long to geohash at <a href="http://geohash.co" target="_blank" rel="noopener noreferrer">geohash.co</a></li>
</ol>
<h2 id="grafana">Grafana</h2>
<p>
Some great dashboards have been created by the community:
<a href="https://grafana.com/grafana/dashboards/?search=blackbox" target="_blank" rel="noopener noreferrer">grafana.com/grafana/dashboards/?search=blackbox</a>
</p>
<p>
Since Grafana v5.0.0, a map panel is available:
<a href="https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/" target="_blank" rel="noopener noreferrer">Geomap panel documentation</a>
</p>
</GuideLayout>

345
site/src/pages/index.astro Normal file
View file

@ -0,0 +1,345 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import StatsBar from '../components/StatsBar.astro';
import ServiceCard from '../components/ServiceCard.astro';
import SearchWidget from '../components/SearchWidget.astro';
import { data, getGroupSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount, getPopularServices } from '../data/rules';
import { SITE_URL, GITHUB_URL, schemaAuthor, schemaPublisher, schemaWebSite, SITE_DATE_PUBLISHED, LICENSE_CC_BY_URL, LICENSE_MIT_URL } from '../data/site';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const totalRules = getTotalRuleCount();
const totalServices = getTotalServiceCount();
const popularServices = getPopularServices();
const org = {
'@type': 'Organization',
'@id': `${SITE_URL}#organization`,
name: schemaPublisher.name,
url: GITHUB_URL,
logo: {
'@type': 'ImageObject',
url: `${SITE_URL}favicon.svg`,
},
sameAs: [GITHUB_URL, SITE_URL],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'technical support',
url: `${GITHUB_URL}/issues`,
},
};
const buildDate = new Date().toISOString().slice(0, 10);
const faqItems = [
{
'@type': 'Question',
name: 'What are Prometheus alerting rules?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Prometheus alerting rules are PromQL-based conditions evaluated by the Prometheus server. When a condition is true for a specified duration, an alert fires and is routed by AlertManager to receivers like Slack, PagerDuty, or email. Rules are defined as YAML files and cover metrics thresholds, absence of expected data, and rate-of-change conditions.',
},
},
{
'@type': 'Question',
name: 'How do I use these Prometheus alert rules?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Find the service you want to monitor, copy the YAML snippet for any rule, and paste it into your Prometheus rules file (e.g., alerts/my-service.yml). Reload Prometheus to apply the rules. Adjust thresholds to match your workload — the values provided are sensible defaults but may need tuning.',
},
},
{
'@type': 'Question',
name: 'What exporters and services are covered?',
acceptedAnswer: {
'@type': 'Answer',
text: `Awesome Prometheus Alerts covers ${totalServices} services across ${data.groups.length} categories: ${data.groups.map((g) => `${g.name} (${g.services.map((s) => s.name).join(', ')})`).join('; ')}.`,
},
},
{
'@type': 'Question',
name: 'What is the difference between warning and critical severity?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Critical alerts require immediate human attention — the system is down or severely degraded and revenue or reliability is directly impacted. Warning alerts need attention soon but are not immediately urgent. Info alerts are awareness-only, such as configuration changes or underutilized resources. Set up AlertManager routes to page on-call engineers only for critical alerts.',
},
},
{
'@type': 'Question',
name: 'What is PromQL?',
acceptedAnswer: {
'@type': 'Answer',
text: 'PromQL (Prometheus Query Language) is the functional query language used to select, filter, and aggregate time-series data in Prometheus. Alert rules use PromQL expressions — for example, rate(http_requests_total[5m]) > 100 fires when request rate exceeds 100/s over a 5-minute window.',
},
},
{
'@type': 'Question',
name: 'Can I contribute new alert rules?',
acceptedAnswer: {
'@type': 'Answer',
text: `Yes! Contributions are welcome. Open a pull request on GitHub at ${GITHUB_URL} with your new rules added to the _data/rules.yml file. Follow the existing format: provide a clear rule name, a description explaining what the alert means and why it matters, a tested PromQL expression, an appropriate severity, and a sensible "for" duration to avoid false positives.`,
},
},
{
'@type': 'Question',
name: 'What is AlertManager and how does it relate to these rules?',
acceptedAnswer: {
'@type': 'Answer',
text: 'AlertManager is the component that receives firing alerts from Prometheus and handles deduplication, grouping, silencing, and routing to receivers (Slack, PagerDuty, email, webhooks). The alert rules in this collection fire alerts from Prometheus — AlertManager then decides who to notify and when. See the AlertManager Configuration guide on this site for setup examples.',
},
},
{
'@type': 'Question',
name: 'How do I silence or suppress an alert?',
acceptedAnswer: {
'@type': 'Answer',
text: 'AlertManager supports silences — time-bounded mutes applied via its UI or API that suppress notifications without disabling the rule. For recurring suppression (nights, weekends, deployments), use inhibition rules or time-based PromQL patterns. See the Sleep Peacefully guide on this site for timezone-aware suppression examples using day_of_week() and hour() functions.',
},
},
{
'@type': 'Question',
name: 'What is the license for these alert rules?',
acceptedAnswer: {
'@type': 'Answer',
text: `The alert rules are licensed under Creative Commons CC BY 4.0. You are free to use, adapt, and redistribute them — including in commercial environments — as long as you provide attribution. See the LICENSE file in the GitHub repository for details.`,
},
},
];
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
org,
{
'@type': 'SoftwareSourceCode',
name: 'awesome-prometheus-alerts',
description: 'Collection of Prometheus alerting rules — YAML configurations for 90+ services',
url: GITHUB_URL,
codeRepository: GITHUB_URL,
programmingLanguage: 'YAML',
author: schemaAuthor,
license: LICENSE_MIT_URL,
},
{
'@type': 'Dataset',
name: 'Awesome Prometheus Alerts',
description: `Collection of ${totalRules} production-ready Prometheus alerting rules covering ${totalServices} services and 13 categories including databases, Kubernetes, cloud providers, and more.`,
url: SITE_URL,
creator: schemaAuthor,
datePublished: SITE_DATE_PUBLISHED,
dateModified: buildDate,
keywords: ['Prometheus', 'alerting rules', 'monitoring', 'PromQL', 'SRE', 'DevOps', 'observability'],
license: LICENSE_CC_BY_URL,
isAccessibleForFree: true,
},
{
'@type': 'FAQPage',
'@id': `${SITE_URL}#faq`,
mainEntity: faqItems,
},
{
'@type': 'WebSite',
'@id': `${SITE_URL}#website`,
name: schemaWebSite.name,
url: SITE_URL,
description: `Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services.`,
publisher: { '@id': `${SITE_URL}#organization` },
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${SITE_URL}rules/?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
},
{
'@type': 'SoftwareApplication',
name: schemaWebSite.name,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'All',
url: SITE_URL,
image: `${SITE_URL}favicon.svg`,
description: `Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services — covering databases, Kubernetes, cloud providers, message brokers, and more.`,
author: schemaAuthor,
publisher: { '@id': `${SITE_URL}#organization` },
offers: {
'@type': 'Offer',
price: 0,
priceCurrency: 'USD',
},
},
{
'@type': 'ItemList',
name: 'Site Navigation',
itemListElement: [
{ '@type': 'SiteNavigationElement', position: 1, name: 'Alert Rules', url: `${SITE_URL}rules/` },
{ '@type': 'SiteNavigationElement', position: 2, name: 'AlertManager Config', url: `${SITE_URL}alertmanager/` },
{ '@type': 'SiteNavigationElement', position: 3, name: 'Blackbox Exporter', url: `${SITE_URL}blackbox-exporter/` },
{ '@type': 'SiteNavigationElement', position: 4, name: 'Sleep Peacefully', url: `${SITE_URL}sleep-peacefully/` },
],
},
],
};
---
<BaseLayout
title="Awesome Prometheus Alerts | Copy-pasteable Prometheus alerting rules"
description={`Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services — covering databases, Kubernetes, cloud providers, message brokers, and more.`}
jsonLd={jsonLd}
>
<!-- Hero -->
<section class="bg-gradient-to-b from-slate-50 dark:from-slate-900/50 to-white dark:to-slate-950 border-b border-slate-200 dark:border-slate-800">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<div class="flex justify-center mb-6">
<img
src={`${base}/images/prometheus-logo.png`}
alt="Prometheus"
class="h-16 w-auto"
width="64"
height="64"
/>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-white mb-4">
Awesome Prometheus Alert Rules
</h1>
<p class="text-lg text-slate-500 dark:text-slate-400 mb-8 max-w-2xl mx-auto">
{totalRules} copy-pasteable Prometheus alerting rules.
Find, copy, and deploy alerts in seconds.
</p>
<!-- Search -->
<div class="max-w-xl mx-auto mb-8">
<SearchWidget />
</div>
<!-- CTA buttons -->
<div class="flex flex-wrap justify-center gap-3">
<a
href={`${base}/rules/`}
class="inline-flex items-center gap-2 px-5 py-2.5 bg-brand hover:bg-brand/90 text-white font-medium rounded-lg transition-colors"
>
Browse all rules
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
<a
href="https://github.com/samber/awesome-prometheus-alerts"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-5 py-2.5 border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-200 font-medium rounded-lg hover:border-slate-300 dark:hover:border-slate-600 transition-colors"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/></svg>
GitHub
</a>
</div>
</div>
</section>
<!-- Stats -->
<section class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<StatsBar />
</div>
</section>
<!-- Popular services -->
<section class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-5">Popular services</h2>
<div class="flex flex-wrap gap-2">
{popularServices.map(({ service, groupSlug, serviceSlug }) => (
<a
href={`${base}/rules/${groupSlug}/${serviceSlug}/`}
class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-brand/10 dark:hover:bg-brand-dark/10 hover:text-brand dark:hover:text-brand-dark transition-colors border border-slate-200 dark:border-slate-700"
>
{service.name}
</a>
))}
</div>
</div>
</section>
<!-- Categories grid -->
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-8">Browse by category</h2>
<div class="space-y-10">
{data.groups.map((group) => {
const groupSlug = getGroupSlug(group);
const groupRuleCount = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0);
return (
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-slate-700 dark:text-slate-300">
<a href={`${base}/rules/${groupSlug}/`} class="hover:text-brand dark:hover:text-brand-dark transition-colors">
{group.name}
</a>
</h3>
<span class="text-xs text-slate-400 dark:text-slate-500">
{group.services.length} services · {groupRuleCount} rules
</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
{group.services.map((service) => (
<ServiceCard service={service} group={group} base={base} />
))}
</div>
</div>
);
})}
</div>
</section>
<!-- Guides section -->
<section class="bg-slate-50 dark:bg-slate-900/50 border-t border-slate-200 dark:border-slate-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-6">Guides</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<a href={`${base}/alertmanager/`} class="group block p-5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700/60 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all">
<div class="text-brand dark:text-brand-dark mb-2">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</div>
<h3 class="font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors mb-1">AlertManager Config</h3>
<p class="text-sm text-slate-500 dark:text-slate-400">Prometheus and AlertManager configuration examples and troubleshooting.</p>
</a>
<a href={`${base}/blackbox-exporter/`} class="group block p-5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700/60 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all">
<div class="text-brand dark:text-brand-dark mb-2">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</div>
<h3 class="font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors mb-1">Blackbox Exporter</h3>
<p class="text-sm text-slate-500 dark:text-slate-400">Worldwide probes, Prometheus config, geohash/Grafana map setup.</p>
</a>
<a href={`${base}/sleep-peacefully/`} class="group block p-5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700/60 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all">
<div class="text-brand dark:text-brand-dark mb-2">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
</div>
<h3 class="font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors mb-1">Sleep Peacefully</h3>
<p class="text-sm text-slate-500 dark:text-slate-400">Time-based alert suppression and timezone-aware PromQL patterns.</p>
</a>
</div>
</div>
</section>
<!-- FAQ section -->
<section class="border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-8">Frequently asked questions</h2>
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-2">
{faqItems.map((item) => (
<details class="group border-b border-slate-100 dark:border-slate-800">
<summary class="flex cursor-pointer items-center justify-between gap-3 py-3 text-sm font-semibold text-slate-800 dark:text-slate-100 list-none [&::-webkit-details-marker]:hidden">
{item.name}
<svg class="w-4 h-4 flex-shrink-0 text-slate-400 transition-transform group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<p class="pb-4 text-sm text-slate-500 dark:text-slate-400 leading-relaxed">{item.acceptedAnswer.text}</p>
</details>
))}
</dl>
</div>
</section>
</BaseLayout>

View file

@ -0,0 +1,65 @@
import type { APIRoute } from 'astro';
import { data, getGroupSlug, getServiceSlug, getTotalRuleCount, getTotalServiceCount } from '../data/rules';
import { SITE_NAME, SITE_URL, GITHUB_URL, AUTHOR_NAME, LICENSE_CC_BY_NAME } from '../data/site';
export const GET: APIRoute = () => {
const totalRules = getTotalRuleCount();
const totalServices = getTotalServiceCount();
const siteBase = SITE_URL.replace(/\/$/, '');
const sections = data.groups
.map((group) => {
const groupSlug = getGroupSlug(group);
const serviceBlocks = group.services
.map((service) => {
const serviceSlug = getServiceSlug(service);
const ruleLines = service.exporters
.flatMap((exporter) =>
(exporter.rules ?? []).map((rule) => {
const forPart = rule.for ? `, for: ${rule.for}` : '';
return ` - **${rule.name}** (severity: ${rule.severity}${forPart})\n ${rule.description}`;
})
)
.join('\n');
return `### [${service.name}](${siteBase}/rules/${groupSlug}/${serviceSlug}/)\n\n${ruleLines}`;
})
.join('\n\n');
return `## ${group.name}\n\n${serviceBlocks}`;
})
.join('\n\n');
const content = `# ${SITE_NAME} — Full Content
> ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} monitored services.
## Overview
Awesome Prometheus Alerts is the most comprehensive collection of Prometheus alerting rules. Rules are organized by category and service, with each rule containing:
- Alert name and description explaining what is happening and why it matters
- PromQL expression tested against the latest exporter version
- Severity level: critical (requires immediate attention), warning (needs attention soon), or info (awareness only)
- Duration (for field) to avoid false positives from transient spikes
All rules are copy-paste ready YAML for direct use in Prometheus configuration files.
## Guides
- [AlertManager Configuration](${siteBase}/alertmanager/): Prometheus configuration (scrape_interval, evaluation_interval, rule_files), AlertManager routing with group_wait/group_interval/repeat_interval, Slack and webhook receivers, recorded rules for expensive queries, and troubleshooting notification delays.
- [Blackbox Exporter](${siteBase}/blackbox-exporter/): Deploy worldwide probes for HTTP, HTTPS, DNS, TCP, ICMP monitoring from multiple Points of Presence. Prometheus relabeling config, geohash setup for Grafana geomap panel, community dashboard links.
- [Sleep Peacefully](${siteBase}/sleep-peacefully/): Suppress noisy alerts during nights and weekends using day_of_week(), hour(), month() PromQL functions. Timezone-aware recording rules for Europe/London and Europe/Paris, public holiday suppression patterns.
## All rules by category and service
${sections}
## Source
- GitHub: ${GITHUB_URL}
- Author: ${AUTHOR_NAME}
- License: ${LICENSE_CC_BY_NAME}
`;
return new Response(content, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
};

View file

@ -0,0 +1,70 @@
import type { APIRoute } from 'astro';
import { data, getGroupSlug, getServiceSlug, getDistUrl, getTotalRuleCount, getTotalServiceCount } from '../data/rules';
import { SITE_NAME, SITE_URL, GITHUB_URL, AUTHOR_NAME, LICENSE_CC_BY_NAME } from '../data/site';
export const GET: APIRoute = () => {
const totalRules = getTotalRuleCount();
const totalServices = getTotalServiceCount();
const siteBase = SITE_URL.replace(/\/$/, '');
const categoryList = data.groups
.map((group) => {
const services = group.services.map((s) => s.name).join(', ');
return `- **${group.name}**: ${services}`;
})
.join('\n');
const distTree = data.groups
.map((group) => {
const groupSlug = getGroupSlug(group);
const serviceLines = group.services
.map((service) => {
const serviceSlug = getServiceSlug(service);
const exporterLines = service.exporters
.map((exporter) => {
const url = getDistUrl(service.name, exporter.slug);
const label = exporter.name ? `${exporter.name} (${exporter.slug})` : exporter.slug;
return ` - [${label}](${url})`;
})
.join('\n');
return ` - [${service.name}](${siteBase}/rules/${groupSlug}/${serviceSlug}/)\n${exporterLines}`;
})
.join('\n');
return `- **${group.name}**\n${serviceLines}`;
})
.join('\n');
const content = `# ${SITE_NAME}
> ${totalRules}+ copy-pasteable Prometheus alerting rules for ${totalServices}+ monitored services. The definitive open-source collection for Prometheus monitoring, covering databases, Kubernetes, cloud providers, message brokers, and more.
## Key pages
- [Alert rules catalog](${siteBase}/rules/): Browse all ${totalRules} alerting rules organized by category and service
- [AlertManager configuration guide](${siteBase}/alertmanager/): Prometheus and AlertManager configuration examples, recorded rules, and troubleshooting
- [Blackbox Exporter guide](${siteBase}/blackbox-exporter/): Worldwide endpoint probing over HTTP, HTTPS, DNS, TCP, and ICMP with Grafana maps
- [Sleep Peacefully guide](${siteBase}/sleep-peacefully/): Time-based alert suppression and timezone-aware PromQL patterns
## Categories and services
${categoryList}
## Downloadable Rule Files
${distTree}
## Full Content
- [llms-full.txt](${siteBase}/llms-full.txt): Complete list of all alert rules with title, description and severity
## About
${SITE_NAME} is a community-driven open-source project maintained by ${AUTHOR_NAME}.
Source: ${GITHUB_URL}
License: ${LICENSE_CC_BY_NAME}
`;
return new Response(content, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
};

View file

@ -0,0 +1,182 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import Breadcrumbs from '../../../components/Breadcrumbs.astro';
import Sidebar from '../../../components/Sidebar.astro';
import ExporterSection from '../../../components/ExporterSection.astro';
import CautionBanner from '../../../components/CautionBanner.astro';
import { data, getGroupSlug, getServiceSlug, getRuleCount, getAllServices } from '../../../data/rules';
import { SITE_URL, schemaAuthor, schemaPublisher, schemaWebSite, SITE_DATE_PUBLISHED, SCHEMA_IN_LANGUAGE } from '../../../data/site';
export function getStaticPaths() {
return getAllServices().map(({ group, service, groupSlug, serviceSlug }) => ({
params: { group: groupSlug, service: serviceSlug },
props: { group, service },
}));
}
const { group, service } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const groupSlug = getGroupSlug(group);
const serviceSlug = getServiceSlug(service);
const ruleCount = getRuleCount(service);
const groupIndex = data.groups.findIndex((g) => getGroupSlug(g) === groupSlug) + 1;
const serviceIndex = group.services.findIndex((s) => getServiceSlug(s) === serviceSlug) + 1;
// Build exporters summary for meta description
const exporterNames = service.exporters.map((e) => e.name).filter(Boolean).join(', ');
const metaDescBase = `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}${exporterNames ? ` (${exporterNames})` : ''}. Copy-paste YAML for critical and warning alerts.`;
const metaDesc = metaDescBase.length > 160
? `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}. Copy-paste YAML for critical and warning alerts.`
: metaDescBase;
// FAQ JSON-LD for GEO (AI search engines)
const faqItems = service.exporters.flatMap((exp) =>
(exp.rules ?? []).map((rule) => ({
'@type': 'Question',
name: `What is the Prometheus alert rule for "${rule.name}"?`,
acceptedAnswer: {
'@type': 'Answer',
text: `${rule.description} PromQL expression: ${rule.query}. Severity: ${rule.severity}${rule.for ? `. Duration: ${rule.for}` : ''}.`,
},
}))
);
const buildDate = new Date().toISOString().slice(0, 10);
const keywords = [
'Prometheus', 'alerting rules', service.name, 'monitoring', 'PromQL',
...service.exporters.map((e) => e.name).filter(Boolean),
].join(', ');
const pageUrl = `${SITE_URL}rules/${groupSlug}/${serviceSlug}/`;
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'TechArticle',
'@id': `${pageUrl}#article`,
headline: `${service.name} Prometheus Alert Rules`,
description: metaDesc,
about: `Prometheus monitoring for ${service.name}`,
url: pageUrl,
inLanguage: SCHEMA_IN_LANGUAGE,
datePublished: SITE_DATE_PUBLISHED,
dateModified: buildDate,
author: schemaAuthor,
publisher: schemaPublisher,
isPartOf: schemaWebSite,
},
...(faqItems.length > 0 ? [{
'@type': 'FAQPage',
'@id': `${pageUrl}#faq`,
mainEntity: faqItems,
}] : []),
],
};
---
<BaseLayout
title={`${service.name} Alert Rules | Awesome Prometheus Alerts`}
description={metaDesc}
ogType="article"
keywords={keywords}
jsonLd={jsonLd}
datePublished={SITE_DATE_PUBLISHED}
dateModified={buildDate}
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Breadcrumbs
items={[
{ label: 'Rules', href: `${base}/rules/` },
{ label: group.name, href: `${base}/rules/${groupSlug}/` },
{ label: service.name },
]}
base={base}
/>
<div class="flex gap-10 mt-6">
<!-- Left sidebar -->
<Sidebar
groups={data.groups}
currentGroupSlug={groupSlug}
currentServiceSlug={serviceSlug}
currentService={service}
base={base}
/>
<!-- Main content -->
<div class="flex-1 min-w-0">
<CautionBanner />
<!-- Page header -->
<div class="mb-6 pb-6 border-b border-slate-200 dark:border-slate-800">
<h1 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">{service.name} Prometheus Alert Rules</h1>
<p class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed mb-2">
{ruleCount} Prometheus alerting rule{ruleCount !== 1 ? 's' : ''} for {service.name}.
{exporterNames && `Exported via ${exporterNames}.`}
These rules cover critical and warning conditions — copy and paste the YAML into your Prometheus configuration.
</p>
</div>
<!-- Exporters -->
{service.exporters.map((exporter, expIdx) => (
<ExporterSection
exporter={exporter}
service={service}
groupIndex={groupIndex}
serviceIndex={serviceIndex}
exporterIndex={expIdx + 1}
showExporterNumber={service.exporters.length > 1}
/>
))}
<!-- Prev/Next navigation -->
<nav class="mt-10 pt-6 border-t border-slate-200 dark:border-slate-800 flex justify-between gap-4" aria-label="Service navigation">
{(() => {
const allSvcs = getAllServices();
const idx = allSvcs.findIndex(s => s.groupSlug === groupSlug && s.serviceSlug === serviceSlug);
const prev = idx > 0 ? allSvcs[idx - 1] : null;
const next = idx < allSvcs.length - 1 ? allSvcs[idx + 1] : null;
return (
<>
{prev ? (
<a href={`${base}/rules/${prev.groupSlug}/${prev.serviceSlug}/`} class="group flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
<span>{prev.service.name}</span>
</a>
) : <div />}
{next ? (
<a href={`${base}/rules/${next.groupSlug}/${next.serviceSlug}/`} class="group flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">
<span>{next.service.name}</span>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</a>
) : <div />}
</>
);
})()}
</nav>
<!-- More in group -->
{group.services.length > 1 && (
<div class="mt-8 pt-6 border-t border-slate-200 dark:border-slate-800">
<h2 class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500 mb-3">
More in <a href={`${base}/rules/${groupSlug}/`} class="hover:text-brand dark:hover:text-brand-dark transition-colors">{group.name}</a>
</h2>
<div class="flex flex-wrap gap-2">
{group.services
.filter((s) => getServiceSlug(s) !== serviceSlug)
.map((s) => (
<a
href={`${base}/rules/${groupSlug}/${getServiceSlug(s)}/`}
class="inline-flex items-center px-3 py-1 text-xs rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-brand/10 dark:hover:bg-brand-dark/10 hover:text-brand dark:hover:text-brand-dark border border-slate-200 dark:border-slate-700 transition-colors"
>
{s.name}
</a>
))}
</div>
</div>
)}
</div>
</div>
</div>
</BaseLayout>

View file

@ -0,0 +1,88 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import Breadcrumbs from '../../../components/Breadcrumbs.astro';
import ServiceCard from '../../../components/ServiceCard.astro';
import CautionBanner from '../../../components/CautionBanner.astro';
import { data, getGroupSlug, getServiceSlug, getRuleCount } from '../../../data/rules';
import { SITE_URL, schemaWebSite, SITE_DATE_PUBLISHED } from '../../../data/site';
export function getStaticPaths() {
return data.groups.map((group) => ({
params: { group: getGroupSlug(group) },
props: { group },
}));
}
const { group } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const groupSlug = getGroupSlug(group);
const totalRules = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0);
// Build a service list for the meta description (truncated to keep under 160 chars)
const serviceNames = group.services.map((s) => s.name);
const serviceList = serviceNames.slice(0, 5).join(', ') + (serviceNames.length > 5 ? `, and ${serviceNames.length - 5} more` : '');
const groupDesc = `Browse ${totalRules} Prometheus alerting rules for ${group.name} — ${serviceList}. Copy-paste ready YAML for ${group.name} monitoring.`;
const pageUrl = `${SITE_URL}rules/${groupSlug}/`;
const buildDate = new Date().toISOString().slice(0, 10);
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'CollectionPage',
name: `${group.name} — Prometheus Alert Rules`,
description: groupDesc,
url: pageUrl,
isPartOf: schemaWebSite,
},
{
'@type': 'ItemList',
name: `${group.name} Prometheus Alert Services`,
url: pageUrl,
numberOfItems: group.services.length,
itemListElement: group.services.map((service, idx) => ({
'@type': 'ListItem',
position: idx + 1,
name: service.name,
url: `${SITE_URL}rules/${groupSlug}/${getServiceSlug(service)}/`,
description: `${getRuleCount(service)} Prometheus alerting rules`,
})),
},
],
};
---
<BaseLayout
title={`${group.name} Prometheus Alerts | Awesome Prometheus Alerts`}
description={groupDesc}
jsonLd={jsonLd}
ogType="article"
datePublished={SITE_DATE_PUBLISHED}
dateModified={buildDate}
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Breadcrumbs
items={[
{ label: 'Rules', href: `${base}/rules/` },
{ label: group.name },
]}
base={base}
/>
<div class="mt-6 mb-8">
<h1 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">{group.name}</h1>
<p class="text-slate-500 dark:text-slate-400">
{group.services.length} service{group.services.length !== 1 ? 's' : ''} · {totalRules} rules
</p>
</div>
<CautionBanner />
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{group.services.map((service) => (
<ServiceCard service={service} group={group} base={base} />
))}
</div>
</div>
</BaseLayout>

View file

@ -0,0 +1,94 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import ServiceCard from '../../components/ServiceCard.astro';
import SearchWidget from '../../components/SearchWidget.astro';
import CautionBanner from '../../components/CautionBanner.astro';
import { data, getGroupSlug, getServiceSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount, buildRedirectMap } from '../../data/rules';
import { SITE_URL, schemaWebSite } from '../../data/site';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const totalRules = getTotalRuleCount();
const totalServices = getTotalServiceCount();
const redirectMap = buildRedirectMap(base);
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Prometheus Alerting Rules',
description: `Browse ${totalRules} Prometheus alerting rules across ${totalServices} services. Organized by category: databases, Kubernetes, cloud providers, message brokers, and more.`,
url: `${SITE_URL}rules/`,
isPartOf: schemaWebSite,
mainEntity: {
'@type': 'ItemList',
numberOfItems: data.groups.length,
itemListElement: data.groups.map((group, i) => ({
'@type': 'ListItem',
position: i + 1,
name: group.name,
url: `${SITE_URL}rules/${getGroupSlug(group)}/`,
})),
},
};
---
<BaseLayout
title="Prometheus Alerting Rules | Awesome Prometheus Alerts"
description={`Browse ${totalRules} Prometheus alerting rules across ${totalServices} services. Organized by category: databases, Kubernetes, cloud providers, message brokers, and more.`}
jsonLd={jsonLd}
>
<!-- Old-anchor redirect handler -->
<script define:vars={{ redirectMap }}>
if (window.location.hash) {
const anchor = window.location.hash.slice(1);
// Try exact match, then strip trailing numeric suffix (e.g. -1, -2) added by the old site
const target = redirectMap[anchor] ?? redirectMap[anchor.replace(/-\d+$/, '')];
if (target) {
window.location.replace(target);
}
}
</script>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">Prometheus Alert Rules</h1>
<p class="text-slate-500 dark:text-slate-400">
{totalRules} alerting rules across {totalServices} services and {data.groups.length} categories.
</p>
</div>
<!-- Search -->
<div class="mb-8 max-w-xl">
<SearchWidget />
</div>
<CautionBanner />
<!-- Groups -->
<div class="space-y-10">
{data.groups.map((group) => {
const groupSlug = getGroupSlug(group);
const groupRuleCount = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0);
return (
<section>
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-slate-800 dark:text-slate-100">
<a href={`${base}/rules/${groupSlug}/`} class="hover:text-brand dark:hover:text-brand-dark transition-colors">
{group.name}
</a>
</h2>
<span class="text-sm text-slate-400 dark:text-slate-500">
{group.services.length} services · {groupRuleCount} rules
</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
{group.services.map((service) => (
<ServiceCard service={service} group={group} base={base} />
))}
</div>
</section>
);
})}
</div>
</div>
</BaseLayout>

View file

@ -0,0 +1,148 @@
---
import GuideLayout from '../layouts/GuideLayout.astro';
const howToJsonLd = {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: 'How to suppress Prometheus alerts during nights and weekends',
description: 'Use PromQL time functions and timezone-aware recording rules to prevent alert fatigue by silencing alerts during off-hours.',
step: [
{
'@type': 'HowToStep',
name: 'Add inline time conditions to alert expressions',
text: 'Append time conditions directly to your alert PromQL: e.g., node_load5 > 10 and ON() (0 < day_of_week() < 6) to suppress weekend alerts, or and ON() (8 < hour() < 18) for business hours only.',
},
{
'@type': 'HowToStep',
name: 'Create timezone offset recording rules',
text: 'Define a european_summer_time_offset recording rule in a "timezones" rule group that returns 1 during DST and 0 otherwise, accounting for exact DST transition weekends.',
},
{
'@type': 'HowToStep',
name: 'Derive local time recording rules',
text: 'Use the timezone offset to compute local time metrics such as europe_london_time (time() + 3600 * european_summer_time_offset) and derive hour and day-of-week variants from them.',
},
{
'@type': 'HowToStep',
name: 'Reference time rules in alerts',
text: 'Use the recording rules as gate conditions in alerts: e.g., node_load5 > 10 and ON() (europe_london_weekday and europe_paris_weekday). Use absent() for the inverse (off-hours suppression).',
},
],
};
---
<GuideLayout
title="Sleep Peacefully"
description="Time-based alert suppression with PromQL. Prevent alert fatigue using day-of-week, hour-of-day filters, and timezone-aware recording rules."
breadcrumbs={[{ label: 'Guides' }, { label: 'Sleep Peacefully' }]}
icon="moon"
badge="PromQL Tips"
extraJsonLd={howToJsonLd}
dateUpdated="2025-01-15"
readingTime={4}
keywords="Prometheus, alert suppression, PromQL, time-based alerting, timezone, day_of_week, alert fatigue, recording rules"
>
<h2 id="alerting-time-window">Alerting time window</h2>
<p>
In some applications, load and activity can vary over the day, week, or year.
To prevent alarm fatigue and busy pager, alerts can be disabled during certain periods
(such as nights or weekends).
</p>
<p>Examples:</p>
<ul>
<li>Weekday: <code>node_load5 &gt; 10 and ON() (0 &lt; day_of_week() &lt; 6)</code></li>
<li>Day time: <code>node_load5 &gt; 10 and ON() (8 &lt; hour() &lt; 18)</code></li>
<li>Exclude December: <code>node_load5 &gt; 10 and ON() (month() != 12)</code></li>
</ul>
<h2 id="advanced-timezones">Advanced time windows and timezones</h2>
<pre class="rule-code"><code>{`# rules.yml
groups:
- name: timezones
rules:
- record: european_summer_time_offset
expr: |
(vector(1) and (month() > 3 and month() < 10))
or
(vector(1) and (month() == 3 and (day_of_month() - day_of_week()) >= 25) and absent((day_of_month() >= 25) and (day_of_week() == 0)))
or
(vector(1) and (month() == 10 and (day_of_month() - day_of_week()) < 25) and absent((day_of_month() >= 25) and (day_of_week() == 0)))
or
(vector(1) and ((month() == 10 and hour() < 1) or (month() == 3 and hour() > 0)) and ((day_of_month() >= 25) and (day_of_week() == 0)))
or
vector(0)
- record: europe_london_time
expr: time() + 3600 * european_summer_time_offset
- record: europe_paris_time
expr: time() + 3600 * (1 + european_summer_time_offset)
- record: europe_london_hour
expr: hour(europe_london_time)
- record: europe_paris_hour
expr: hour(europe_paris_time)
- record: europe_london_weekday
expr: 0 < day_of_week(europe_london_time) < 6
- record: europe_paris_weekday
expr: 0 < day_of_week(europe_paris_time) < 6
# opposite
- record: not_europe_london_weekday
expr: absent(europe_london_weekday)
- record: not_europe_paris_weekday
expr: absent(europe_paris_weekday)
- record: europe_london_business_hours
expr: 9 <= europe_london_hour < 18
- record: europe_paris_business_hours
expr: 9 <= europe_paris_hour < 18
# opposite
- record: not_europe_london_business_hours
expr: absent(europe_london_business_hours)
- record: not_europe_paris_business_hours
expr: absent(europe_paris_business_hours)
# new year's day / xmas / labor day / all saints' day / ...
- record: europe_french_public_holidays
expr: |
(vector(1) and month(europe_paris_time) == 1 and day_of_month(europe_paris_time) == 1)
or
(vector(1) and month(europe_paris_time) == 12 and day_of_month(europe_paris_time) == 25)
or
(vector(1) and month(europe_paris_time) == 5 and day_of_month(europe_paris_time) == 1)
or
(vector(1) and month(europe_paris_time) == 11 and day_of_month(europe_paris_time) == 1)
or
vector(0)
# opposite
- record: not_europe_french_public_holidays
expr: absent(europe_french_public_holidays)`}</code></pre>
<pre class="rule-code"><code>{`# alerts.yml
groups:
- name: CPU Load
rules:
- alert: HighLoadQuietDuringWeekendAndNight
expr: node_load5 > 10 and ON() (europe_london_weekday and europe_paris_weekday)
- alert: HighLoadQuietDuringBackup
expr: node_load5 > 10 and ON() absent(hour() == 2)
- alert: HighLoad
expr: |
node_load5 > 20 and ON() (europe_london_weekday and europe_paris_weekday)
or
node_load5 > 10`}</code></pre>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://medium.com/@tom.fawcett/time-of-day-based-notifications-with-prometheus-and-alertmanager-1bf7a23b7695" target="_blank" rel="noopener noreferrer">Time of day based notifications with Prometheus and AlertManager</a></li>
<li><a href="https://promcon.io/2019-munich/slides/improved-alerting-with-prometheus-and-alertmanager.pdf" target="_blank" rel="noopener noreferrer">Improved alerting with Prometheus and AlertManager (PromCon 2019)</a></li>
</ul>
</GuideLayout>

View file

@ -0,0 +1,85 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--color-brand: #E6522C;
--color-brand-dark: #f06840;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-white text-slate-900 antialiased;
}
.dark body {
@apply bg-slate-950 text-slate-100;
}
/* Code block base styles */
pre {
@apply overflow-x-auto rounded-lg text-sm leading-relaxed;
}
code {
@apply font-mono text-sm;
}
/* Inline code */
:not(pre) > code {
@apply bg-slate-100 dark:bg-slate-800 text-slate-800 dark:text-slate-200 px-1.5 py-0.5 rounded text-xs;
}
}
@layer components {
/* Rule YAML code block — always dark */
.rule-code {
@apply bg-slate-900 text-slate-100 rounded-lg p-4 text-xs font-mono leading-relaxed overflow-x-auto;
}
/* Severity badge */
.badge-critical {
@apply bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400 border border-red-200 dark:border-red-800;
}
.badge-warning {
@apply bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400 border border-amber-200 dark:border-amber-800;
}
.badge-info {
@apply bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400 border border-blue-200 dark:border-blue-800;
}
/* Navigation links */
.nav-link {
@apply text-slate-600 dark:text-slate-300 hover:text-brand dark:hover:text-brand-dark transition-colors duration-150 text-sm font-medium;
}
.nav-link-active {
@apply text-brand dark:text-brand-dark;
}
}
/* Pagefind UI overrides */
#search {
--pagefind-ui-scale: 0.9;
--pagefind-ui-primary: #E6522C;
--pagefind-ui-text: #1e293b;
--pagefind-ui-background: #ffffff;
--pagefind-ui-border: #e2e8f0;
--pagefind-ui-tag: #f1f5f9;
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 8px;
--pagefind-ui-font: inherit;
}
.dark #search {
--pagefind-ui-text: #e2e8f0;
--pagefind-ui-background: #0f172a;
--pagefind-ui-border: #334155;
--pagefind-ui-tag: #1e293b;
}

19
site/tailwind.config.mjs Normal file
View file

@ -0,0 +1,19 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
brand: {
DEFAULT: '#E6522C',
dark: '#f06840',
},
},
fontFamily: {
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'],
},
},
},
plugins: [],
};

10
site/tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@data/*": ["src/data/*"]
}
}
}

View file

@ -1,106 +0,0 @@
<h1 style="text-align: center;">
Sleep Peacefully
</h1>
## Alerting time window
In some applications, load and activity can vary over the day/week/year.
In order to prevent alarm fatigue and busy pager, alerts can be disabled during a period of time (such as night or weekend).
Example:
- Weekday: `node_load5 > 10 and ON() (0 < day_of_week() < 6)`
- Day time: `node_load5 > 10 and ON() (8 < hour() < 18)`
- Exclude December: `node_load5 > 10 and ON() (month() != 12)`
## Advanced time windows and timezones
```yml
# rules.yml
groups:
- name: timezones
rules:
- record: european_summer_time_offset
expr: |
(vector(1) and (month() > 3 and month() < 10))
or
(vector(1) and (month() == 3 and (day_of_month() - day_of_week()) >= 25) and absent((day_of_month() >= 25) and (day_of_week() == 0)))
or
(vector(1) and (month() == 10 and (day_of_month() - day_of_week()) < 25) and absent((day_of_month() >= 25) and (day_of_week() == 0)))
or
(vector(1) and ((month() == 10 and hour() < 1) or (month() == 3 and hour() > 0)) and ((day_of_month() >= 25) and (day_of_week() == 0)))
or
vector(0)
- record: europe_london_time
expr: time() + 3600 * european_summer_time_offset
- record: europe_paris_time
expr: time() + 3600 * (1 + european_summer_time_offset)
- record: europe_london_hour
expr: hour(europe_london_time)
- record: europe_paris_hour
expr: hour(europe_paris_time)
- record: europe_london_weekday
expr: 0 < day_of_week(europe_london_time) < 6
- record: europe_paris_weekday
expr: 0 < day_of_week(europe_paris_time) < 6
# opposite
- record: not_europe_london_weekday
expr: absent(europe_london_weekday)
- record: not_europe_paris_weekday
expr: absent(europe_paris_weekday)
- record: europe_london_business_hours
expr: 9 <= europe_london_hour < 18
- record: europe_paris_business_hours
expr: 9 <= europe_paris_hour < 18
# opposite
- record: not_europe_london_business_hours
expr: absent(europe_london_business_hours)
- record: not_europe_paris_business_hours
expr: absent(europe_paris_business_hours)
# new year's day / xmas / labor day / all saints' day / ...
- record: europe_french_public_holidays
expr: |
(vector(1) and month(europe_paris_time) == 1 and day_of_month(europe_paris_time) == 1)
or
(vector(1) and month(europe_paris_time) == 12 and day_of_month(europe_paris_time) == 25)
or
(vector(1) and month(europe_paris_time) == 5 and day_of_month(europe_paris_time) == 1)
or
(vector(1) and month(europe_paris_time) == 11 and day_of_month(europe_paris_time) == 1)
or
vector(0)
# opposite
- record: not_europe_french_public_holidays
expr: absent(europe_french_public_holidays)
```
```yml
# alerts.yml
groups:
- name: CPU Load
rules:
- alert: HighLoadQuietDuringWeekendAndNight
expr: node_load5 > 10 and ON() (europe_london_weekday and europe_paris_weekday)
- alert: HighLoadQuietDuringBackup
expr: node_load5 > 10 and ON() absent(hour() == 2)
- alert: HighLoad
expr: |
node_load5 > 20 and ON() (europe_london_weekday and europe_paris_weekday)
or
node_load5 > 10
```
## Sources
- [https://medium.com/@tom.fawcett/time-of-day-based-notifications-with-prometheus-and-alertmanager-1bf7a23b7695](https://medium.com/@tom.fawcett/time-of-day-based-notifications-with-prometheus-and-alertmanager-1bf7a23b7695)
- [https://promcon.io/2019-munich/slides/improved-alerting-with-prometheus-and-alertmanager.pdf](https://promcon.io/2019-munich/slides/improved-alerting-with-prometheus-and-alertmanager.pdf)