doc: new website based on astro

This commit is contained in:
Samuel Berthe 2026-04-07 16:47:39 +02:00
parent 6ff7e74524
commit cc6835cdf0
No known key found for this signature in database
GPG key ID: 64863511FFBD0E3C
28 changed files with 1745 additions and 225 deletions

View file

@ -4,7 +4,10 @@ import sitemap from '@astrojs/sitemap';
import icon from 'astro-icon';
import { parse as parseYaml } from 'yaml';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/** Custom Vite plugin that parses YAML files using the 'yaml' package,
* which tolerates duplicate keys (last one wins) unlike js-yaml 4.x. */
@ -23,13 +26,72 @@ function yamlPlugin() {
};
}
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 (with and without trailing slash)
for (const oldPath of [`${base}/rules/${oldSlug}`, `${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: '/awesome-prometheus-alerts',
base,
redirects: buildRedirects(base),
output: 'static',
integrations: [
tailwind({ applyBaseStyles: false }),
sitemap(),
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: {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

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

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"
}
]
}

View file

@ -1,4 +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

@ -9,8 +9,9 @@ interface Props {
base: string;
}
import { SITE_ORIGIN } from '../data/site';
const { items, base } = Astro.props;
const siteUrl = 'https://samber.github.io';
const allItems = [{ label: 'Home', href: `${base}/` }, ...items];
@ -21,7 +22,7 @@ const jsonLd = {
'@type': 'ListItem',
position: i + 1,
name: item.label,
...(item.href ? { item: `${siteUrl}${item.href}` } : {}),
...(item.href ? { item: `${SITE_ORIGIN}${item.href}` } : {}),
})),
};
---

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

@ -33,7 +33,7 @@ const exporterPrefix = showExporterNumber
: `${groupIndex}.${serviceIndex}.`;
---
<section class="mb-10">
<section class="mb-10 scroll-mt-36" id={`exporter-${exporter.slug}`}>
<!-- Hidden pre for copy-all -->
<pre id={allRulesId} class="hidden">{allRulesYaml}</pre>

View file

@ -1,33 +1,45 @@
---
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-3 gap-8">
<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">
<svg class="w-5 h-5 text-brand" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/>
</svg>
<span class="font-semibold text-slate-900 dark:text-white text-sm">Awesome Prometheus Alerts</span>
<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=samuelberthe&text=🚨 Awesome Prometheus Alerts&url=https://samber.github.io/awesome-prometheus-alerts" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-blue-400 transition-colors" aria-label="Share on Twitter">
<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=https://samber.github.io/awesome-prometheus-alerts/" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-blue-600 transition-colors" aria-label="Share on LinkedIn">
<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="https://github.com/samber/awesome-prometheus-alerts" target="_blank" rel="noopener noreferrer" class="text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors" aria-label="GitHub repository">
<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>
@ -41,31 +53,44 @@ const { base } = Astro.props;
<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="https://github.com/samber/awesome-prometheus-alerts/blob/master/CONTRIBUTING.md" 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>
<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: Sponsors -->
<!-- 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">
<a href="https://cast.ai/samuel" target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity">
<img src={`${base}/images/sponsor-cast-ai.png`} alt="CAST AI — Kubernetes cost optimization" class="h-6" />
</a>
<a href="https://betterstack.com/" target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity">
<img src={`${base}/images/sponsor-betterstack.png`} alt="Better Stack — Uptime monitoring and log management" class="h-6" />
</a>
{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="https://github.com/samber/awesome-prometheus-alerts" class="hover:text-brand dark:hover:text-brand-dark transition-colors">awesome-prometheus-alerts</a>
<a href={GITHUB_URL} class="hover:text-brand dark:hover:text-brand-dark transition-colors">awesome-prometheus-alerts</a>
{' '}is maintained by{' '}
<a href="https://github.com/samber" class="hover:text-brand dark:hover:text-brand-dark transition-colors">@samber</a>
<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="https://github.com/samber/awesome-prometheus-alerts/blob/master/LICENSE" class="hover:text-brand dark:hover:text-brand-dark transition-colors">Creative Commons CC4</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

@ -1,5 +1,7 @@
---
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;
@ -11,19 +13,20 @@ const currentPath = Astro.url.pathname;
function isActive(path: string) {
return currentPath.startsWith(`${base}${path}`);
}
---
<!-- Sponsor banner -->
<div class="bg-slate-50 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-xs text-slate-500 dark:text-slate-400 py-1.5 px-4 text-center">
Kindly supported by&nbsp;
<a href="https://cast.ai/samuel" target="_blank" rel="noopener noreferrer" class="inline-flex items-center mx-2 hover:opacity-80">
<img src={`${base}/images/sponsor-cast-ai.png`} alt="CAST AI" class="h-4 inline" />
</a>
and&nbsp;
<a href="https://betterstack.com/" target="_blank" rel="noopener noreferrer" class="inline-flex items-center mx-2 hover:opacity-80">
<img src={`${base}/images/sponsor-betterstack.png`} alt="Better Stack" class="h-4 inline" />
</a>
</div>
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">
@ -31,11 +34,9 @@ function isActive(path: string) {
<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">
<svg class="w-6 h-6 text-brand" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/>
</svg>
<span class="hidden sm:block text-sm">Awesome Prometheus Alerts</span>
<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>
@ -70,7 +71,7 @@ function isActive(path: string) {
</div>
<a
href="https://github.com/samber/awesome-prometheus-alerts/blob/master/CONTRIBUTING.md"
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"
@ -79,16 +80,20 @@ function isActive(path: string) {
</a>
<a
href="https://github.com/samber/awesome-prometheus-alerts"
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub repository"
class="text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
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">
@ -113,6 +118,18 @@ function isActive(path: string) {
</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"
@ -123,8 +140,8 @@ function isActive(path: string) {
<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="https://github.com/samber/awesome-prometheus-alerts/blob/master/CONTRIBUTING.md" 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="https://github.com/samber/awesome-prometheus-alerts" 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>
<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>

View file

@ -15,7 +15,7 @@ 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" id={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">
@ -50,7 +50,7 @@ const codeId = `code-${anchorId}`;
<div class="relative mx-4 mb-4">
<pre
id={codeId}
class="rule-code text-xs leading-relaxed whitespace-pre-wrap"
class="rule-code text-xs leading-relaxed whitespace-pre overflow-x-auto"
>{yamlContent}</pre>
</div>
</article>

View file

@ -1,15 +1,22 @@
---
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, base, siteUrl } = Astro.props;
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}`;
@ -20,22 +27,29 @@ const jsonLdArray = 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="website" />
<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:site_name" content="Awesome Prometheus Alerts" />
<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="@samuelberthe" />
<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

@ -1,29 +1,33 @@
---
import type { Group } from '../data/rules';
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, base } = Astro.props;
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-4">
{groups.map((group) => {
<ul class="space-y-3">
{groups.map((group, i) => {
const groupSlug = getGroupSlug(group);
const isGroupActive = groupSlug === currentGroupSlug;
return (
<li>
<p class={`text-xs font-semibold uppercase tracking-wider mb-1.5 ${isGroupActive ? 'text-brand dark:text-brand-dark' : 'text-slate-400 dark:text-slate-500'}`}>
<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}
</p>
</a>
<ul class="space-y-0.5">
{group.services.map((service) => {
const serviceSlug = getServiceSlug(service);
@ -35,11 +39,27 @@ const { groups, currentGroupSlug, currentServiceSlug, base } = Astro.props;
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-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800/50'
: '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>
);
})}

View file

@ -1,8 +1,7 @@
---
import { getTotalRuleCount, getTotalServiceCount, getTotalExporterCount, data } from '../data/rules';
import { getTotalRuleCount, getTotalExporterCount, data } from '../data/rules';
const totalRules = getTotalRuleCount();
const totalServices = getTotalServiceCount();
const totalExporters = getTotalExporterCount();
const totalGroups = data.groups.length;
---
@ -12,10 +11,6 @@ const totalGroups = data.groups.length;
<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">{totalServices}</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">services</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>

View file

@ -104,15 +104,249 @@ export function getAllServices(): Array<{
);
}
/** Build a redirect map: old anchor slug -> new path (for /rules/ page redirect) */
/** 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> {
const map: Record<string, string> = {};
for (const { group, service, groupSlug, serviceSlug } of getAllServices()) {
// Current site uses: service.name | replace: " ", "-" | downcase
const oldAnchor = service.name.replace(/ /g, '-').toLowerCase();
map[oldAnchor] = `${base}/rules/${groupSlug}/${serviceSlug}/`;
}
return map;
return Object.fromEntries(
Object.entries(ANCHOR_REDIRECT_MAP).map(([anchor, path]) => [anchor, `${base}${path}`])
);
}
/** Format a rule as copy-pasteable Prometheus alert YAML */
@ -157,6 +391,51 @@ ${rulesYaml
.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();

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

@ -3,14 +3,19 @@ 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 {
@ -18,13 +23,16 @@ const {
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;
const siteUrl = 'https://samber.github.io';
const canonical = canonicalUrl ?? `${siteUrl}${base}${Astro.url.pathname.replace(base, '')}`;
const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.replace(base, '')}`;
---
<!DOCTYPE html>
@ -33,6 +41,7 @@ const canonical = canonicalUrl ?? `${siteUrl}${base}${Astro.url.pathname.replace
<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
@ -40,12 +49,21 @@ const canonical = canonicalUrl ?? `${siteUrl}${base}${Astro.url.pathname.replace
description={description}
canonicalUrl={canonical}
ogImage={ogImage}
ogType={ogType}
keywords={keywords}
jsonLd={jsonLd}
base={base}
siteUrl={siteUrl}
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>
@ -58,12 +76,12 @@ const canonical = canonicalUrl ?? `${siteUrl}${base}${Astro.url.pathname.replace
</script>
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<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-XXXXXXXXXX');
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">

View file

@ -1,6 +1,9 @@
---
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;
@ -11,55 +14,407 @@ interface Props {
title: string;
description?: string;
breadcrumbs?: BreadcrumbItem[];
headings?: Array<{ depth: number; text: string; slug: string }>;
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 = [], headings = [] } = Astro.props;
const { title, description, breadcrumbs = [], icon = 'book', badge = 'Guide', extraJsonLd, dateUpdated, keywords, readingTime } = Astro.props;
const base = import.meta.env.BASE_URL;
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}>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} base={base} />}
<div class="flex gap-12 mt-6">
<!-- TOC sidebar -->
{headings.length > 0 && (
<aside class="hidden xl:block w-56 flex-shrink-0">
<div class="sticky top-20">
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3">On this page</p>
<nav>
<ul class="space-y-1.5">
{headings.filter(h => h.depth <= 3).map(h => (
<li class={h.depth === 3 ? 'pl-3' : ''}>
<a
href={`#${h.slug}`}
class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors block"
>
{h.text}
</a>
</li>
))}
</ul>
</nav>
</div>
</aside>
<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>
<!-- Content -->
<article class="flex-1 min-w-0 max-w-3xl prose prose-slate dark:prose-invert prose-code:before:content-none prose-code:after:content-none">
<h1>{title}</h1>
<!-- 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>
<style is:global>
.prose pre {
@apply bg-slate-900 text-slate-100;
<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));
}
.prose 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;
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

@ -2,12 +2,53 @@
import GuideLayout from '../layouts/GuideLayout.astro';
const base = import.meta.env.BASE_URL;
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, and troubleshooting guide for alert timing and notification routing."
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:
@ -18,6 +59,11 @@ const base = import.meta.env.BASE_URL;
<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:
@ -26,13 +72,12 @@ global:
# 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>
# ...`}</code></pre>
<pre class="rule-code"><code>{`# alerts/example-redis.yml
@ -41,104 +86,152 @@ groups:
- name: ExampleRedisGroup
rules:
- alert: ExampleRedisDown
expr: redis_up{} == 0
expr: redis_up == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Redis instance down"
description: "Whatever"`}</code></pre>
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:
# 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
repeat_interval: 4h
receiver: "slack"
# All the above attributes are inherited by all child routes and can
# overwritten on each.
routes:
# warnings and criticals → Slack
- receiver: "slack"
group_wait: 10s
match_re:
severity: critical|warning
matchers:
- severity =~ "critical|warning"
continue: true
- receiver: "pager"
group_wait: 10s
match_re:
severity: critical
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'
text: "{{ range .Alerts }}<!channel> {{ .Annotations.summary }}\\n{{ .Annotations.description }}\\n{{ end }}"
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: "pager"
webhook_configs:
- url: http://a.b.c.d:8080/send/sms
- 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 frequent PromQL queries, Prometheus allows you to precompute rules.
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:
# first define the recorded rule
- name: ExampleRecordedGroup
# 1. Define the recording rule
- name: recordings
rules:
- record: job:rabbitmq_queue_messages_delivered_total:rate:5m
- record: job:rabbitmq_queue_messages_delivered_total:rate5m
expr: rate(rabbitmq_queue_messages_delivered_total[5m])
# then use it in alerts
- name: ExampleAlertingGroup
# 2. Reference it in alert rules
- name: alerts
rules:
- alert: ExampleRabbitmqLowMessageDelivery
expr: sum(job:rabbitmq_queue_messages_delivered_total:rate:5m) < 10
- alert: RabbitmqLowMessageDelivery
expr: sum(job:rabbitmq_queue_messages_delivered_total:rate5m) < 10
for: 2m
labels:
severity: critical
annotations:
summary: "Low delivery rate in Rabbitmq queues"`}</code></pre>
summary: Low message delivery rate in RabbitMQ
description: "Delivery rate is {{ $value | humanize }} msg/s\\n LABELS = {{ $labels }}"`}</code></pre>
<h2 id="troubleshooting">Troubleshooting</h2>
<h2 id="troubleshooting">Troubleshooting alert delays</h2>
<p>If the notification takes too much time to be triggered, check the following delays:</p>
<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><code>scrape_interval = 20s</code> (prometheus.yml)</li>
<li><code>evaluation_interval = 20s</code> (prometheus.yml)</li>
<li><code>increase(mysql_global_status_slow_queries[1m]) &gt; 0</code> (alerts/example-mysql.yml)</li>
<li><code>for: 5m</code> (alerts/example-mysql.yml)</li>
<li><code>group_wait = 10s</code> (alertmanager.yml)</li>
<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>Also read:</p>
<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

@ -2,12 +2,47 @@
import GuideLayout from '../layouts/GuideLayout.astro';
const base = import.meta.env.BASE_URL;
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>
@ -31,7 +66,7 @@ const base = import.meta.env.BASE_URL;
</ul>
<p class="text-sm text-slate-500 dark:text-slate-400">
☝️ Logs have been disabled. More probes from the community would be appreciated —
☝️ 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>.
@ -129,7 +164,7 @@ scrape_configs:
<img
src={`${base}/images/grafana-map-panel.png`}
alt="Grafana map panel showing worldwide probe locations"
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"
/>

View file

@ -3,26 +3,182 @@ 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 } from '../data/rules';
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;
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',
'@type': 'WebSite',
name: 'Awesome Prometheus Alerts',
url: 'https://samber.github.io/awesome-prometheus-alerts/',
description: `Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services.`,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: 'https://samber.github.io/awesome-prometheus-alerts/rules/?q={search_term_string}',
'@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,
},
'query-input': 'required name=search_term_string',
},
{
'@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/` },
],
},
],
};
---
@ -44,10 +200,10 @@ const jsonLd = {
/>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-white mb-4">
Awesome Prometheus Alerts
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 for {totalServices} services.
{totalRules} copy-pasteable Prometheus alerting rules.
Find, copy, and deploy alerts in seconds.
</p>
@ -87,6 +243,23 @@ const jsonLd = {
</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>
@ -149,4 +322,24 @@ const jsonLd = {
</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

@ -3,7 +3,9 @@ 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 }) => ({
@ -22,7 +24,10 @@ const serviceIndex = group.services.findIndex((s) => getServiceSlug(s) === servi
// Build exporters summary for meta description
const exporterNames = service.exporters.map((e) => e.name).filter(Boolean).join(', ');
const metaDesc = `${ruleCount} Prometheus alerting rules for ${service.name}${exporterNames ? ` via ${exporterNames}` : ''}. Copy-paste ready YAML configurations for critical and warning alerts.`;
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) =>
@ -36,32 +41,48 @@ const faqItems = service.exporters.flatMap((exp) =>
}))
);
const jsonLd = [
{
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: `${service.name} Prometheus Alert Rules`,
description: metaDesc,
about: `Prometheus monitoring for ${service.name}`,
url: `https://samber.github.io${base}/rules/${groupSlug}/${serviceSlug}/`,
isPartOf: {
'@type': 'WebSite',
name: 'Awesome Prometheus Alerts',
url: 'https://samber.github.io/awesome-prometheus-alerts/',
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 ? [{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqItems,
}] : []),
];
...(faqItems.length > 0 ? [{
'@type': 'FAQPage',
'@id': `${pageUrl}#faq`,
mainEntity: faqItems,
}] : []),
],
};
---
<BaseLayout
title={`${service.name} Prometheus Alert Rules | Awesome Prometheus Alerts`}
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
@ -79,15 +100,18 @@ const jsonLd = [
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}</h1>
<p class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed">
<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.
@ -131,6 +155,27 @@ const jsonLd = [
);
})()}
</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>

View file

@ -2,7 +2,9 @@
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) => ({
@ -15,11 +17,49 @@ const { group } = Astro.props;
const base = import.meta.env.BASE_URL;
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 Alert Rules | Awesome Prometheus Alerts`}
description={`${totalRules} Prometheus alerting rules for ${group.services.length} services in the ${group.name} category.`}
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
@ -37,6 +77,8 @@ const totalRules = group.services.reduce((sum, svc) => sum + getRuleCount(svc),
</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} />

View file

@ -2,23 +2,46 @@
import BaseLayout from '../../layouts/BaseLayout.astro';
import ServiceCard from '../../components/ServiceCard.astro';
import SearchWidget from '../../components/SearchWidget.astro';
import { data, getGroupSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount, buildRedirectMap } from '../../data/rules';
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;
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);
const target = redirectMap[anchor];
// 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);
}
@ -28,7 +51,7 @@ const redirectMap = buildRedirectMap(base);
<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">Alert Rules</h1>
<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>
@ -39,20 +62,7 @@ const redirectMap = buildRedirectMap(base);
<SearchWidget />
</div>
<!-- Caution banner -->
<div class="mb-8 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>
<div>
<h2 class="text-sm font-semibold text-amber-800 dark:text-amber-200 mb-1">Caution</h2>
<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>
</div>
<CautionBanner />
<!-- Groups -->
<div class="space-y-10">

View file

@ -1,11 +1,46 @@
---
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>