mirror of
https://github.com/samber/awesome-prometheus-alerts.git
synced 2026-06-24 18:36:59 +08:00
feat: migrate website from Jekyll to Astro
Rebuilds the site using Astro (SSG) with Tailwind CSS v4, replacing the Jekyll/Cayman theme. Key changes: - Splits the monolithic /rules page into 110 statically-generated pages (92 per-service + 13 group index + homepage + guide pages) for SEO - URL structure: /rules/[group-slug]/[service-slug]/ with backward- compatibility redirect map for old anchor-based URLs (/rules#redis) - Modern UI: Prometheus-orange accent, dark mode (system + toggle), sticky sidebar, responsive layout, copy-to-clipboard per rule/section - SEO: per-page <title>, <meta description>, Open Graph, Twitter Card, canonical URLs, sitemap.xml via @astrojs/sitemap - GEO: FAQPage JSON-LD schema on each service page (rules as Q&A pairs for AI search engines), TechArticle schema, BreadcrumbList - Search: Pagefind (build-time index, lazy-loaded, ~200KB) - Zero JS by default; copy buttons and theme toggle use inline scripts - New CI: .github/workflows/deploy.yml builds Astro + Pagefind and deploys to GitHub Pages via actions/deploy-pages - Existing dist.yml and test.yml workflows are untouched - _data/rules.yml remains the single source of truth Note: GitHub Pages source must be changed from "Build from branch" (Jekyll) to "GitHub Actions" in repository settings.
This commit is contained in:
parent
0d148832d3
commit
6ff7e74524
37 changed files with 9513 additions and 0 deletions
62
.github/workflows/deploy.yml
vendored
Normal file
62
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
name: Deploy Astro site to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
# Only allow one concurrent deployment
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: site
|
||||
run: npm ci
|
||||
|
||||
- name: Build Astro site
|
||||
working-directory: site
|
||||
env:
|
||||
ASTRO_TELEMETRY_DISABLED: "1"
|
||||
run: npm run build
|
||||
|
||||
- name: Build Pagefind search index
|
||||
working-directory: site
|
||||
run: npx pagefind --site dist
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: site/dist
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,8 +1,18 @@
|
|||
# Jekyll (legacy)
|
||||
_site/
|
||||
.sass-cache/
|
||||
.jekyll-cache/
|
||||
.jekyll-metadata
|
||||
|
||||
# Generated data
|
||||
_data/rules.json
|
||||
test/rules/
|
||||
|
||||
# Node / Astro
|
||||
/node_modules
|
||||
site/node_modules/
|
||||
site/dist/
|
||||
site/.astro/
|
||||
|
||||
# Misc
|
||||
.worktrees/
|
||||
39
site/astro.config.mjs
Normal file
39
site/astro.config.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import icon from 'astro-icon';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
/** Custom Vite plugin that parses YAML files using the 'yaml' package,
|
||||
* which tolerates duplicate keys (last one wins) unlike js-yaml 4.x. */
|
||||
function yamlPlugin() {
|
||||
return {
|
||||
name: 'vite-plugin-yaml-tolerant',
|
||||
transform(code, id) {
|
||||
if (!id.endsWith('.yml') && !id.endsWith('.yaml')) return null;
|
||||
const content = readFileSync(resolve(id), 'utf-8');
|
||||
const data = parseYaml(content, { merge: true, strict: false, uniqueKeys: false });
|
||||
return {
|
||||
code: `export default ${JSON.stringify(data)};`,
|
||||
map: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://samber.github.io',
|
||||
base: '/awesome-prometheus-alerts',
|
||||
output: 'static',
|
||||
integrations: [
|
||||
tailwind({ applyBaseStyles: false }),
|
||||
sitemap(),
|
||||
icon(),
|
||||
],
|
||||
vite: {
|
||||
plugins: [yamlPlugin()],
|
||||
assetsInclude: ['**/*.yml'],
|
||||
},
|
||||
});
|
||||
7330
site/package-lock.json
generated
Normal file
7330
site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
site/package.json
Normal file
31
site/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "awesome-prometheus-alerts-site",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.0.0",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@iconify-json/lucide": "^1.0.0",
|
||||
"@rollup/plugin-yaml": "^4.0.0",
|
||||
"astro": "^5.0.0",
|
||||
"astro-icon": "^1.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"pagefind": "^1.0.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 0.5%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
BIN
site/public/favicon.ico
Normal file
BIN
site/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
site/public/images/grafana-map-panel.png
Normal file
BIN
site/public/images/grafana-map-panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
BIN
site/public/images/prometheus-logo.png
Normal file
BIN
site/public/images/prometheus-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
site/public/images/sponsor-betterstack.png
Normal file
BIN
site/public/images/sponsor-betterstack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
site/public/images/sponsor-cast-ai.png
Normal file
BIN
site/public/images/sponsor-cast-ai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
4
site/public/robots.txt
Normal file
4
site/public/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://samber.github.io/awesome-prometheus-alerts/sitemap-index.xml
|
||||
52
site/src/components/Breadcrumbs.astro
Normal file
52
site/src/components/Breadcrumbs.astro
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: BreadcrumbItem[];
|
||||
base: string;
|
||||
}
|
||||
|
||||
const { items, base } = Astro.props;
|
||||
const siteUrl = 'https://samber.github.io';
|
||||
|
||||
const allItems = [{ label: 'Home', href: `${base}/` }, ...items];
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: allItems.map((item, i) => ({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: item.label,
|
||||
...(item.href ? { item: `${siteUrl}${item.href}` } : {}),
|
||||
})),
|
||||
};
|
||||
---
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
|
||||
<nav aria-label="Breadcrumb" class="text-sm text-slate-500 dark:text-slate-400">
|
||||
<ol class="flex items-center flex-wrap gap-1">
|
||||
{allItems.map((item, i) => (
|
||||
<li class="flex items-center gap-1">
|
||||
{i > 0 && (
|
||||
<svg class="w-3.5 h-3.5 text-slate-300 dark:text-slate-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
)}
|
||||
{item.href && i < allItems.length - 1 ? (
|
||||
<a href={item.href} class="hover:text-brand dark:hover:text-brand-dark transition-colors">
|
||||
{item.label}
|
||||
</a>
|
||||
) : (
|
||||
<span class={i === allItems.length - 1 ? 'text-slate-700 dark:text-slate-200 font-medium' : ''}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
87
site/src/components/CopyButton.astro
Normal file
87
site/src/components/CopyButton.astro
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
interface Props {
|
||||
targetId: string;
|
||||
label?: string;
|
||||
variant?: 'icon' | 'text';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '' } = Astro.props;
|
||||
const btnId = `copy-btn-${targetId}`;
|
||||
---
|
||||
|
||||
{variant === 'icon' ? (
|
||||
<button
|
||||
id={btnId}
|
||||
data-copy-target={targetId}
|
||||
aria-label="Copy to clipboard"
|
||||
class={`copy-btn inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors ${extraClass}`}
|
||||
>
|
||||
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span class="copy-label sr-only">Copy</span>
|
||||
<span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
id={btnId}
|
||||
data-copy-target={targetId}
|
||||
class={`copy-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:border-brand dark:hover:border-brand-dark hover:text-brand dark:hover:text-brand-dark transition-colors ${extraClass}`}
|
||||
>
|
||||
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span class="copy-label">{label}</span>
|
||||
<span class="copied-label hidden text-green-600 dark:text-green-400">Copied!</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<script>
|
||||
document.querySelectorAll<HTMLButtonElement>('.copy-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const targetId = btn.dataset.copyTarget;
|
||||
if (!targetId) return;
|
||||
const target = document.getElementById(targetId);
|
||||
if (!target) return;
|
||||
|
||||
const text = target.textContent ?? '';
|
||||
try {
|
||||
await navigator.clipboard.writeText(text.trim());
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text.trim();
|
||||
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
|
||||
// Visual feedback
|
||||
const copyIcon = btn.querySelector('.copy-icon');
|
||||
const checkIcon = btn.querySelector('.check-icon');
|
||||
const copyLabel = btn.querySelector('.copy-label');
|
||||
const copiedLabel = btn.querySelector('.copied-label');
|
||||
|
||||
copyIcon?.classList.add('hidden');
|
||||
checkIcon?.classList.remove('hidden');
|
||||
copyLabel?.classList.add('hidden');
|
||||
copiedLabel?.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
copyIcon?.classList.remove('hidden');
|
||||
checkIcon?.classList.add('hidden');
|
||||
copyLabel?.classList.remove('hidden');
|
||||
copiedLabel?.classList.add('hidden');
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
122
site/src/components/ExporterSection.astro
Normal file
122
site/src/components/ExporterSection.astro
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
import type { Exporter, Service } from '../data/rules';
|
||||
import { formatExporterAsYaml, getDistUrl } from '../data/rules';
|
||||
import RuleCard from './RuleCard.astro';
|
||||
import CopyButton from './CopyButton.astro';
|
||||
|
||||
interface Props {
|
||||
exporter: Exporter;
|
||||
service: Service;
|
||||
groupIndex: number;
|
||||
serviceIndex: number;
|
||||
exporterIndex: number;
|
||||
showExporterNumber: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
exporter,
|
||||
service,
|
||||
groupIndex,
|
||||
serviceIndex,
|
||||
exporterIndex,
|
||||
showExporterNumber,
|
||||
} = Astro.props;
|
||||
|
||||
const distUrl = getDistUrl(service.name, exporter.slug);
|
||||
const allRulesId = `exporter-all-${groupIndex}-${serviceIndex}-${exporterIndex}`;
|
||||
const allRulesYaml = formatExporterAsYaml(exporter);
|
||||
const wgetCommand = `wget ${distUrl}`;
|
||||
const wgetId = `wget-${groupIndex}-${serviceIndex}-${exporterIndex}`;
|
||||
|
||||
const exporterPrefix = showExporterNumber
|
||||
? `${groupIndex}.${serviceIndex}.${exporterIndex}.`
|
||||
: `${groupIndex}.${serviceIndex}.`;
|
||||
---
|
||||
|
||||
<section class="mb-10">
|
||||
<!-- Hidden pre for copy-all -->
|
||||
<pre id={allRulesId} class="hidden">{allRulesYaml}</pre>
|
||||
|
||||
<!-- Exporter header -->
|
||||
<div class="flex items-center justify-between flex-wrap gap-3 mb-3 pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="min-w-0">
|
||||
{exporter.name ? (
|
||||
<h2 class="text-base font-semibold text-slate-800 dark:text-slate-100 flex items-center gap-2 flex-wrap">
|
||||
<span class="text-slate-400 dark:text-slate-500 font-normal text-sm">
|
||||
{exporterPrefix}
|
||||
</span>
|
||||
{exporter.doc_url ? (
|
||||
<a href={exporter.doc_url} target="_blank" rel="noopener noreferrer" class="text-brand dark:text-brand-dark hover:underline">
|
||||
{exporter.name}
|
||||
</a>
|
||||
) : (
|
||||
<span>{exporter.name}</span>
|
||||
)}
|
||||
<span class="text-xs font-normal text-slate-400 dark:text-slate-500">
|
||||
({exporter.rules?.length ?? 0} rules)
|
||||
</span>
|
||||
</h2>
|
||||
) : (
|
||||
<h2 class="text-base font-semibold text-slate-800 dark:text-slate-100">
|
||||
<span class="text-slate-400 dark:text-slate-500 font-normal text-sm mr-1">{exporterPrefix}</span>
|
||||
{service.name}
|
||||
<span class="text-xs font-normal text-slate-400 dark:text-slate-500 ml-2">
|
||||
({exporter.rules?.length ?? 0} rules)
|
||||
</span>
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<CopyButton targetId={allRulesId} label="Copy all" variant="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exporter comments -->
|
||||
{exporter.comments && (
|
||||
<div class="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 text-sm text-amber-800 dark:text-amber-200">
|
||||
<pre class="whitespace-pre-wrap font-sans text-sm">{exporter.comments.trim()}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- wget download command -->
|
||||
{(exporter.rules?.length ?? 0) > 0 && (
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<div class="flex-1 flex items-center gap-2 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-2 min-w-0">
|
||||
<svg class="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre>
|
||||
</div>
|
||||
<CopyButton targetId={wgetId} variant="icon" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Rules list -->
|
||||
{(exporter.rules?.length ?? 0) === 0 ? (
|
||||
<div class="rounded-lg border border-dashed border-slate-200 dark:border-slate-700 p-6 text-center">
|
||||
<p class="text-sm text-slate-400 dark:text-slate-500">
|
||||
No rules yet —{' '}
|
||||
<a href="https://github.com/samber/awesome-prometheus-alerts" target="_blank" rel="noopener noreferrer" class="text-brand dark:text-brand-dark hover:underline">
|
||||
contribute on GitHub 👋
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{exporter.rules.map((rule, ruleIdx) => {
|
||||
const anchorId = `rule-${groupIndex}-${serviceIndex}-${exporterIndex}-${ruleIdx + 1}`;
|
||||
const ruleNumber = showExporterNumber
|
||||
? `${groupIndex}.${serviceIndex}.${exporterIndex}.${ruleIdx + 1}.`
|
||||
: `${groupIndex}.${serviceIndex}.${ruleIdx + 1}.`;
|
||||
return (
|
||||
<RuleCard
|
||||
rule={rule}
|
||||
anchorId={anchorId}
|
||||
ruleNumber={ruleNumber}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
71
site/src/components/Footer.astro
Normal file
71
site/src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
interface Props {
|
||||
base: string;
|
||||
}
|
||||
const { base } = Astro.props;
|
||||
---
|
||||
|
||||
<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">
|
||||
|
||||
<!-- 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>
|
||||
</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">
|
||||
<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">
|
||||
<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">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Quick links -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Quick links</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href={`${base}/rules/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Browse all rules</a></li>
|
||||
<li><a href={`${base}/alertmanager/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">AlertManager Config</a></li>
|
||||
<li><a href={`${base}/blackbox-exporter/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Blackbox Exporter</a></li>
|
||||
<li><a href={`${base}/sleep-peacefully/`} class="text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">Sleep Peacefully</a></li>
|
||||
<li><a href="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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Column 3: 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>
|
||||
</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>
|
||||
{' '}is maintained by{' '}
|
||||
<a href="https://github.com/samber" class="hover:text-brand dark:hover:text-brand-dark transition-colors">@samber</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>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
144
site/src/components/Header.astro
Normal file
144
site/src/components/Header.astro
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
|
||||
interface Props {
|
||||
base: string;
|
||||
}
|
||||
|
||||
const { base } = Astro.props;
|
||||
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
|
||||
<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
|
||||
<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>
|
||||
|
||||
<!-- Main header -->
|
||||
<header class="sticky top-0 z-40 bg-white/95 dark:bg-slate-950/95 backdrop-blur border-b border-slate-200 dark:border-slate-800">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-14">
|
||||
|
||||
<!-- Logo -->
|
||||
<a href={`${base}/`} class="flex items-center gap-2 font-semibold text-slate-900 dark:text-white hover:text-brand dark:hover:text-brand-dark transition-colors">
|
||||
<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>
|
||||
<span class="sm:hidden text-sm">APA</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<nav class="hidden md:flex items-center gap-6" aria-label="Main navigation">
|
||||
<a
|
||||
href={`${base}/rules/`}
|
||||
class={`text-sm font-medium transition-colors ${isActive('/rules') ? 'text-brand dark:text-brand-dark' : 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white'}`}
|
||||
>
|
||||
Rules
|
||||
</a>
|
||||
|
||||
<!-- Guides dropdown -->
|
||||
<div class="relative group">
|
||||
<button class={`text-sm font-medium transition-colors flex items-center gap-1 ${isActive('/alertmanager') || isActive('/blackbox') || isActive('/sleep') ? 'text-brand dark:text-brand-dark' : 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white'}`}>
|
||||
Guides
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute top-full right-0 mt-1 w-52 bg-white dark:bg-slate-900 rounded-lg shadow-lg border border-slate-200 dark:border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-150 py-1">
|
||||
<a href={`${base}/alertmanager/`} class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-brand dark:hover:text-brand-dark">
|
||||
AlertManager Config
|
||||
</a>
|
||||
<a href={`${base}/blackbox-exporter/`} class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-brand dark:hover:text-brand-dark">
|
||||
Blackbox Exporter
|
||||
</a>
|
||||
<a href={`${base}/sleep-peacefully/`} class="block px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-brand dark:hover:text-brand-dark">
|
||||
Sleep Peacefully
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://github.com/samber/awesome-prometheus-alerts/blob/master/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm font-medium text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
Contribute
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/samber/awesome-prometheus-alerts"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<ThemeToggle />
|
||||
|
||||
<!-- Mobile hamburger -->
|
||||
<button
|
||||
id="mobile-menu-btn"
|
||||
class="md:hidden p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<svg id="hamburger-icon" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<svg id="close-icon" class="w-5 h-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div
|
||||
id="mobile-menu"
|
||||
class="hidden md:hidden border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950"
|
||||
>
|
||||
<nav class="px-4 py-3 space-y-1" aria-label="Mobile navigation">
|
||||
<a href={`${base}/rules/`} class="block px-3 py-2 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800">Rules</a>
|
||||
<a href={`${base}/alertmanager/`} class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">AlertManager Config</a>
|
||||
<a href={`${base}/blackbox-exporter/`} class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">Blackbox Exporter</a>
|
||||
<a href={`${base}/sleep-peacefully/`} class="block px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">Sleep Peacefully</a>
|
||||
<a href="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>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
const btn = document.getElementById('mobile-menu-btn');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const hamburger = document.getElementById('hamburger-icon');
|
||||
const closeIcon = document.getElementById('close-icon');
|
||||
|
||||
btn?.addEventListener('click', () => {
|
||||
const isOpen = menu?.classList.toggle('hidden') === false;
|
||||
btn.setAttribute('aria-expanded', String(isOpen));
|
||||
hamburger?.classList.toggle('hidden', isOpen);
|
||||
closeIcon?.classList.toggle('hidden', !isOpen);
|
||||
});
|
||||
</script>
|
||||
56
site/src/components/RuleCard.astro
Normal file
56
site/src/components/RuleCard.astro
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
import type { Rule } from '../data/rules';
|
||||
import { formatRuleAsYaml } from '../data/rules';
|
||||
import SeverityBadge from './SeverityBadge.astro';
|
||||
import CopyButton from './CopyButton.astro';
|
||||
|
||||
interface Props {
|
||||
rule: Rule;
|
||||
anchorId: string;
|
||||
ruleNumber: string;
|
||||
}
|
||||
|
||||
const { rule, anchorId, ruleNumber } = Astro.props;
|
||||
const yamlContent = formatRuleAsYaml(rule);
|
||||
const codeId = `code-${anchorId}`;
|
||||
---
|
||||
|
||||
<article class="group mb-4 rounded-xl border border-slate-200 dark:border-slate-700/60 bg-white dark:bg-slate-900 hover:border-slate-300 dark:hover:border-slate-600 transition-colors" id={anchorId}>
|
||||
<!-- Rule header -->
|
||||
<div class="flex items-start justify-between gap-3 px-4 pt-3 pb-2">
|
||||
<div class="flex items-center gap-2 flex-wrap min-w-0">
|
||||
<SeverityBadge severity={rule.severity} />
|
||||
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-100">
|
||||
<span class="text-slate-400 dark:text-slate-500 font-normal text-xs mr-1">{ruleNumber}</span>
|
||||
{rule.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<!-- Permalink -->
|
||||
<a
|
||||
href={`#${anchorId}`}
|
||||
class="p-1.5 rounded text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label="Permalink to this rule"
|
||||
title="Permalink"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
</a>
|
||||
<CopyButton targetId={codeId} variant="icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="px-4 pb-2 text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{rule.description}
|
||||
</p>
|
||||
|
||||
<!-- YAML code block -->
|
||||
<div class="relative mx-4 mb-4">
|
||||
<pre
|
||||
id={codeId}
|
||||
class="rule-code text-xs leading-relaxed whitespace-pre-wrap"
|
||||
>{yamlContent}</pre>
|
||||
</div>
|
||||
</article>
|
||||
42
site/src/components/SEO.astro
Normal file
42
site/src/components/SEO.astro
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
canonicalUrl: string;
|
||||
ogImage?: string;
|
||||
jsonLd?: object | object[];
|
||||
base: string;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
const { title, description, canonicalUrl, jsonLd, base, siteUrl } = Astro.props;
|
||||
const ogImage = Astro.props.ogImage ?? `${base}/images/prometheus-logo.png`;
|
||||
const fullOgImage = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`;
|
||||
|
||||
const jsonLdArray = jsonLd
|
||||
? Array.isArray(jsonLd) ? jsonLd : [jsonLd]
|
||||
: [];
|
||||
---
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<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" />
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
{jsonLdArray.map((schema) => (
|
||||
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
|
||||
))}
|
||||
45
site/src/components/SearchWidget.astro
Normal file
45
site/src/components/SearchWidget.astro
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
// Pagefind search widget
|
||||
// The pagefind bundle is generated after `astro build` with: npx pagefind --site dist
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: extraClass = '' } = Astro.props;
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<div id="search" class={extraClass}></div>
|
||||
|
||||
<link rel="stylesheet" href={`${base}/pagefind/pagefind-ui.css`} />
|
||||
<script>
|
||||
// Load Pagefind UI lazily (only when the search div is visible/focused)
|
||||
function initPagefind() {
|
||||
const base = import.meta.env.BASE_URL;
|
||||
// @ts-ignore
|
||||
import(`${base}/pagefind/pagefind-ui.js`).then((module) => {
|
||||
const PagefindUI = module.PagefindUI;
|
||||
new PagefindUI({
|
||||
element: '#search',
|
||||
showSubResults: true,
|
||||
highlightParam: 'highlight',
|
||||
baseUrl: base,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const searchEl = document.getElementById('search');
|
||||
if (searchEl) {
|
||||
// Load when visible via Intersection Observer
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
initPagefind();
|
||||
observer.disconnect();
|
||||
}
|
||||
}, { rootMargin: '200px' });
|
||||
observer.observe(searchEl);
|
||||
} else {
|
||||
initPagefind();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
41
site/src/components/ServiceCard.astro
Normal file
41
site/src/components/ServiceCard.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import type { Service, Group } from '../data/rules';
|
||||
import { getRuleCount, getGroupSlug, getServiceSlug } from '../data/rules';
|
||||
|
||||
interface Props {
|
||||
service: Service;
|
||||
group: Group;
|
||||
base: string;
|
||||
}
|
||||
|
||||
const { service, group, base } = Astro.props;
|
||||
const ruleCount = getRuleCount(service);
|
||||
const groupSlug = getGroupSlug(group);
|
||||
const serviceSlug = getServiceSlug(service);
|
||||
const href = `${base}/rules/${groupSlug}/${serviceSlug}/`;
|
||||
|
||||
const exporterNames = service.exporters
|
||||
.map((e) => e.name)
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
---
|
||||
|
||||
<a
|
||||
href={href}
|
||||
class="group block p-4 rounded-xl border border-slate-200 dark:border-slate-700/60 bg-white dark:bg-slate-900 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all duration-150"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2 mb-2">
|
||||
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors leading-snug">
|
||||
{service.name}
|
||||
</h3>
|
||||
<span class="flex-shrink-0 text-xs font-medium px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400">
|
||||
{ruleCount} rule{ruleCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{exporterNames.length > 0 && (
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500 truncate">
|
||||
{exporterNames.join(' · ')}{service.exporters.length > 3 ? ` +${service.exporters.length - 3}` : ''}
|
||||
</p>
|
||||
)}
|
||||
</a>
|
||||
16
site/src/components/SeverityBadge.astro
Normal file
16
site/src/components/SeverityBadge.astro
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
interface Props {
|
||||
severity: 'critical' | 'warning' | 'info';
|
||||
}
|
||||
const { severity } = Astro.props;
|
||||
|
||||
const classes = {
|
||||
critical: 'badge-critical',
|
||||
warning: 'badge-warning',
|
||||
info: 'badge-info',
|
||||
}[severity];
|
||||
---
|
||||
|
||||
<span class={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${classes}`}>
|
||||
{severity}
|
||||
</span>
|
||||
52
site/src/components/Sidebar.astro
Normal file
52
site/src/components/Sidebar.astro
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
import type { Group } from '../data/rules';
|
||||
import { getGroupSlug, getServiceSlug } from '../data/rules';
|
||||
|
||||
interface Props {
|
||||
groups: Group[];
|
||||
currentGroupSlug?: string;
|
||||
currentServiceSlug?: string;
|
||||
base: string;
|
||||
}
|
||||
|
||||
const { groups, currentGroupSlug, currentServiceSlug, 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) => {
|
||||
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'}`}>
|
||||
{group.name}
|
||||
</p>
|
||||
<ul class="space-y-0.5">
|
||||
{group.services.map((service) => {
|
||||
const serviceSlug = getServiceSlug(service);
|
||||
const isActive = isGroupActive && serviceSlug === currentServiceSlug;
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={`${base}/rules/${groupSlug}/${serviceSlug}/`}
|
||||
class={`block text-sm py-0.5 px-2 rounded transition-colors truncate ${
|
||||
isActive
|
||||
? 'text-brand dark:text-brand-dark bg-brand/5 dark:bg-brand-dark/10 font-medium'
|
||||
: 'text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
{service.name}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
27
site/src/components/StatsBar.astro
Normal file
27
site/src/components/StatsBar.astro
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import { getTotalRuleCount, getTotalServiceCount, getTotalExporterCount, data } from '../data/rules';
|
||||
|
||||
const totalRules = getTotalRuleCount();
|
||||
const totalServices = getTotalServiceCount();
|
||||
const totalExporters = getTotalExporterCount();
|
||||
const totalGroups = data.groups.length;
|
||||
---
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-6 sm:gap-10 py-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalRules}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">alert rules</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{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>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-brand dark:text-brand-dark">{totalGroups}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">categories</div>
|
||||
</div>
|
||||
</div>
|
||||
25
site/src/components/ThemeToggle.astro
Normal file
25
site/src/components/ThemeToggle.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
---
|
||||
|
||||
<button
|
||||
id="theme-toggle"
|
||||
aria-label="Toggle dark mode"
|
||||
class="p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<!-- Sun icon (shown in dark mode) -->
|
||||
<svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" class="hidden dark:block w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 7a5 5 0 100 10 5 5 0 000-10z" />
|
||||
</svg>
|
||||
<!-- Moon icon (shown in light mode) -->
|
||||
<svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" class="block dark:hidden w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
btn?.addEventListener('click', () => {
|
||||
const isDark = document.documentElement.classList.toggle('dark');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
});
|
||||
</script>
|
||||
164
site/src/data/rules.ts
Normal file
164
site/src/data/rules.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// @ts-ignore — Vite YAML plugin provides this at build time
|
||||
import rulesData from '../../../_data/rules.yml';
|
||||
|
||||
export interface Rule {
|
||||
name: string;
|
||||
description: string;
|
||||
query: string;
|
||||
severity: 'critical' | 'warning' | 'info';
|
||||
for?: string;
|
||||
comments?: string;
|
||||
}
|
||||
|
||||
export interface Exporter {
|
||||
slug: string;
|
||||
name?: string;
|
||||
doc_url?: string;
|
||||
comments?: string;
|
||||
rules: Rule[];
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
name: string;
|
||||
exporters: Exporter[];
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
name: string;
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
export interface RulesData {
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export const data: RulesData = rulesData as RulesData;
|
||||
|
||||
/** Slugify a name for use in URLs — mirrors the dist/ workflow naming */
|
||||
export function toSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
export function getGroupSlug(group: Group): string {
|
||||
return toSlug(group.name);
|
||||
}
|
||||
|
||||
export function getServiceSlug(service: Service): string {
|
||||
return toSlug(service.name);
|
||||
}
|
||||
|
||||
/** CamelCase a rule name for the Prometheus alert name field */
|
||||
export function toCamelCase(name: string): string {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/** Count all rules across a service's exporters */
|
||||
export function getRuleCount(service: Service): number {
|
||||
return service.exporters.reduce((sum, exp) => sum + (exp.rules?.length ?? 0), 0);
|
||||
}
|
||||
|
||||
/** Count all rules in the entire dataset */
|
||||
export function getTotalRuleCount(): number {
|
||||
return data.groups.reduce(
|
||||
(sum, group) => sum + group.services.reduce((s, svc) => s + getRuleCount(svc), 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/** Count all services */
|
||||
export function getTotalServiceCount(): number {
|
||||
return data.groups.reduce((sum, group) => sum + group.services.length, 0);
|
||||
}
|
||||
|
||||
/** Count all exporters */
|
||||
export function getTotalExporterCount(): number {
|
||||
return data.groups.reduce(
|
||||
(sum, group) =>
|
||||
sum + group.services.reduce((s, svc) => s + svc.exporters.length, 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/** Flat list of all services with routing context */
|
||||
export function getAllServices(): Array<{
|
||||
group: Group;
|
||||
service: Service;
|
||||
groupSlug: string;
|
||||
serviceSlug: string;
|
||||
ruleCount: number;
|
||||
}> {
|
||||
return data.groups.flatMap((group) =>
|
||||
group.services.map((service) => ({
|
||||
group,
|
||||
service,
|
||||
groupSlug: getGroupSlug(group),
|
||||
serviceSlug: getServiceSlug(service),
|
||||
ruleCount: getRuleCount(service),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/** Build a redirect map: old anchor slug -> new path (for /rules/ page redirect) */
|
||||
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;
|
||||
}
|
||||
|
||||
/** Format a rule as copy-pasteable Prometheus alert YAML */
|
||||
export function formatRuleAsYaml(rule: Rule): string {
|
||||
const alertName = toCamelCase(rule.name);
|
||||
const forValue = rule.for ?? '0m';
|
||||
|
||||
const commentLines = rule.comments
|
||||
? rule.comments
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => ` # ${line.trim()}`)
|
||||
.join('\n') + '\n'
|
||||
: '';
|
||||
|
||||
// Escape double quotes in description
|
||||
const description = rule.description.replace(/"/g, '\\"');
|
||||
|
||||
return `${commentLines}- alert: ${alertName}
|
||||
expr: ${rule.query}
|
||||
for: ${forValue}
|
||||
labels:
|
||||
severity: ${rule.severity}
|
||||
annotations:
|
||||
summary: ${rule.name} (instance {{ $labels.instance }})
|
||||
description: "${description}\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}"`;
|
||||
}
|
||||
|
||||
/** Format all rules for an exporter as a complete groups YAML block */
|
||||
export function formatExporterAsYaml(exporter: Exporter): string {
|
||||
const groupName = toCamelCase(exporter.slug.replace(/-/g, ' '));
|
||||
const rulesYaml = (exporter.rules ?? [])
|
||||
.map((rule) => formatRuleAsYaml(rule))
|
||||
.join('\n\n');
|
||||
|
||||
return `groups:
|
||||
- name: ${groupName}
|
||||
rules:
|
||||
${rulesYaml
|
||||
.split('\n')
|
||||
.map((line) => ` ${line}`)
|
||||
.join('\n')}`;
|
||||
}
|
||||
|
||||
/** Build the raw GitHub URL for a dist file */
|
||||
export function getDistUrl(serviceName: string, exporterSlug: string): string {
|
||||
const serviceSlug = serviceName.replace(/ /g, '-').toLowerCase();
|
||||
return `https://raw.githubusercontent.com/samber/awesome-prometheus-alerts/refs/heads/master/dist/rules/${serviceSlug}/${exporterSlug}.yml`;
|
||||
}
|
||||
82
site/src/layouts/BaseLayout.astro
Normal file
82
site/src/layouts/BaseLayout.astro
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
import '../styles/global.css';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import SEO from '../components/SEO.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
canonicalUrl?: string;
|
||||
ogImage?: string;
|
||||
jsonLd?: object | object[];
|
||||
noIndex?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = 'Collection of alerting rules for Prometheus. Copy-pasteable Prometheus alert configurations for 90+ services.',
|
||||
canonicalUrl,
|
||||
ogImage,
|
||||
jsonLd,
|
||||
noIndex = false,
|
||||
} = 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, '')}`;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#E6522C" />
|
||||
{noIndex && <meta name="robots" content="noindex" />}
|
||||
|
||||
<SEO
|
||||
title={title}
|
||||
description={description}
|
||||
canonicalUrl={canonical}
|
||||
ogImage={ogImage}
|
||||
jsonLd={jsonLd}
|
||||
base={base}
|
||||
siteUrl={siteUrl}
|
||||
/>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href={`${base}/favicon.ico`} />
|
||||
|
||||
<!-- Dark mode: set class before paint to avoid flash -->
|
||||
<script is:inline>
|
||||
(function() {
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Google Analytics 4 -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
|
||||
<script is:inline>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){ dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-XXXXXXXXXX');
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100 transition-colors duration-150">
|
||||
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-brand text-white px-3 py-1 rounded z-50">
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
<Header base={base} />
|
||||
|
||||
<main id="main-content" class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<Footer base={base} />
|
||||
</body>
|
||||
</html>
|
||||
65
site/src/layouts/GuideLayout.astro
Normal file
65
site/src/layouts/GuideLayout.astro
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Breadcrumbs from '../components/Breadcrumbs.astro';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
headings?: Array<{ depth: number; text: string; slug: string }>;
|
||||
}
|
||||
|
||||
const { title, description, breadcrumbs = [], headings = [] } = Astro.props;
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
<!-- 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>
|
||||
<slot />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style is:global>
|
||||
.prose pre {
|
||||
@apply bg-slate-900 text-slate-100;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
144
site/src/pages/alertmanager.astro
Normal file
144
site/src/pages/alertmanager.astro
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
import GuideLayout from '../layouts/GuideLayout.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<GuideLayout
|
||||
title="AlertManager Configuration"
|
||||
description="Prometheus and AlertManager configuration examples, recorded rules, and troubleshooting guide for alert timing and notification routing."
|
||||
breadcrumbs={[{ label: 'Guides' }, { label: 'AlertManager Config' }]}
|
||||
>
|
||||
<p>
|
||||
If you notice a delay between an event and the first notification, read this post:
|
||||
{' '}<a href="https://pracucci.com/prometheus-understanding-the-delays-on-alerting.html" target="_blank" rel="noopener noreferrer">
|
||||
Understanding the delays on alerting
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="prometheus-config">Prometheus configuration</h2>
|
||||
|
||||
<pre class="rule-code"><code>{`# prometheus.yml
|
||||
|
||||
global:
|
||||
scrape_interval: 20s
|
||||
|
||||
# A short evaluation_interval will check alerting rules very often.
|
||||
# It can be costly if you run Prometheus with 100+ alerts.
|
||||
evaluation_interval: 20s
|
||||
...
|
||||
|
||||
rule_files:
|
||||
- 'alerts/*.yml'
|
||||
|
||||
scrape_configs:
|
||||
...`}</code></pre>
|
||||
|
||||
<pre class="rule-code"><code>{`# alerts/example-redis.yml
|
||||
|
||||
groups:
|
||||
|
||||
- name: ExampleRedisGroup
|
||||
rules:
|
||||
- alert: ExampleRedisDown
|
||||
expr: redis_up{} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Redis instance down"
|
||||
description: "Whatever"`}</code></pre>
|
||||
|
||||
<h2 id="alertmanager-config">AlertManager configuration</h2>
|
||||
|
||||
<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
|
||||
receiver: "slack"
|
||||
|
||||
# All the above attributes are inherited by all child routes and can
|
||||
# overwritten on each.
|
||||
routes:
|
||||
- receiver: "slack"
|
||||
group_wait: 10s
|
||||
match_re:
|
||||
severity: critical|warning
|
||||
continue: true
|
||||
|
||||
- receiver: "pager"
|
||||
group_wait: 10s
|
||||
match_re:
|
||||
severity: critical
|
||||
continue: true
|
||||
|
||||
receivers:
|
||||
- name: "slack"
|
||||
slack_configs:
|
||||
- api_url: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
send_resolved: true
|
||||
channel: 'monitoring'
|
||||
text: "{{ range .Alerts }}<!channel> {{ .Annotations.summary }}\\n{{ .Annotations.description }}\\n{{ end }}"
|
||||
|
||||
- name: "pager"
|
||||
webhook_configs:
|
||||
- url: http://a.b.c.d:8080/send/sms
|
||||
send_resolved: true`}</code></pre>
|
||||
|
||||
<h2 id="recorded-rules">Reduce Prometheus server load</h2>
|
||||
|
||||
<p>
|
||||
For expensive or frequent PromQL queries, Prometheus allows you to precompute rules.
|
||||
</p>
|
||||
|
||||
<pre class="rule-code"><code>{`groups:
|
||||
|
||||
# first define the recorded rule
|
||||
- name: ExampleRecordedGroup
|
||||
rules:
|
||||
- record: job:rabbitmq_queue_messages_delivered_total:rate:5m
|
||||
expr: rate(rabbitmq_queue_messages_delivered_total[5m])
|
||||
|
||||
# then use it in alerts
|
||||
- name: ExampleAlertingGroup
|
||||
rules:
|
||||
- alert: ExampleRabbitmqLowMessageDelivery
|
||||
expr: sum(job:rabbitmq_queue_messages_delivered_total:rate:5m) < 10
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Low delivery rate in Rabbitmq queues"`}</code></pre>
|
||||
|
||||
<h2 id="troubleshooting">Troubleshooting</h2>
|
||||
|
||||
<p>If the notification takes too much time to be triggered, check the following delays:</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]) > 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>
|
||||
</ul>
|
||||
|
||||
<p>Also read:</p>
|
||||
<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://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>
|
||||
156
site/src/pages/blackbox-exporter.astro
Normal file
156
site/src/pages/blackbox-exporter.astro
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
import GuideLayout from '../layouts/GuideLayout.astro';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
|
||||
<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' }]}
|
||||
>
|
||||
<h2 id="worldwide-probes">Worldwide probes</h2>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/prometheus/blackbox_exporter" target="_blank" rel="noopener noreferrer">Blackbox Exporter</a>
|
||||
gives you the ability to probe endpoints over HTTP, HTTPS, DNS, TCP and ICMP.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You should deploy blackbox exporters in multiple Points of Presence around the globe to monitor latency.
|
||||
Feel free to use the following endpoints for your own projects:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><code>https://probe-<strong>montreal</strong>.cleverapps.io</code></li>
|
||||
<li><code>https://probe-<strong>paris</strong>.cleverapps.io</code></li>
|
||||
<li><code>https://probe-<strong>jeddah</strong>.cleverapps.io</code></li>
|
||||
<li><code>https://probe-<strong>singapore</strong>.cleverapps.io</code></li>
|
||||
<li><code>https://probe-<strong>sydney</strong>.cleverapps.io</code></li>
|
||||
<li><code>https://probe-<strong>warsaw</strong>.cleverapps.io</code></li>
|
||||
</ul>
|
||||
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
☝️ Logs have been disabled. More probes from the community would be appreciated —
|
||||
<a href="https://github.com/samber/awesome-prometheus-alerts/" target="_blank" rel="noopener noreferrer">contribute here</a>!
|
||||
These blackbox exporters use the following
|
||||
<a href="https://github.com/samber/blackbox_exporter/blob/master/samber.yml" target="_blank" rel="noopener noreferrer">configuration</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="prometheus-config">Prometheus Configuration</h2>
|
||||
|
||||
<p>
|
||||
Blackbox exporters and endpoints must be declared in Prometheus. Here is a simple configuration,
|
||||
inspired by <a href="https://medium.com/geekculture/single-prometheus-job-for-dozens-of-blackbox-exporters-2a7ba492d6c8" target="_blank" rel="noopener noreferrer">Hayk Davtyan's medium post</a>:
|
||||
</p>
|
||||
|
||||
<pre class="rule-code"><code>{`# sd/blackbox.yml
|
||||
|
||||
- targets:
|
||||
#
|
||||
# Montreal
|
||||
#
|
||||
# http
|
||||
- probe-montreal.cleverapps.io:_:http_2xx:_:Montreal:_:f229cy:_:https://api.screeb.app
|
||||
- probe-montreal.cleverapps.io:_:http_2xx:_:Montreal:_:f229cy:_:https://t.screeb.app/tag.js
|
||||
# icmp
|
||||
- probe-montreal.cleverapps.io:_:icmp_ipv4:_:Montreal:_:f229cy:_:api.screeb.app
|
||||
- probe-montreal.cleverapps.io:_:icmp_ipv4:_:Montreal:_:f229cy:_:t.screeb.app
|
||||
|
||||
|
||||
#
|
||||
# Paris
|
||||
#
|
||||
# http
|
||||
- probe-paris.cleverapps.io:_:http_2xx:_:Paris:_:u09tgy:_:https://api.screeb.app
|
||||
- probe-paris.cleverapps.io:_:http_2xx:_:Paris:_:u09tgy:_:https://t.screeb.app/tag.js
|
||||
# icmp
|
||||
- probe-paris.cleverapps.io:_:icmp_ipv4:_:Paris:_:u09tgy:_:api.screeb.app
|
||||
- probe-paris.cleverapps.io:_:icmp_ipv4:_:Paris:_:u09tgy:_:t.screeb.app
|
||||
|
||||
|
||||
#
|
||||
# Sydney
|
||||
#
|
||||
# http
|
||||
- probe-sydney.cleverapps.io:_:http_2xx:_:Sydney:_:r3gpkn:_:https://api.screeb.app
|
||||
- probe-sydney.cleverapps.io:_:http_2xx:_:Sydney:_:r3gpkn:_:https://t.screeb.app/tag.js
|
||||
# icmp
|
||||
- probe-sydney.cleverapps.io:_:icmp_ipv4:_:Sydney:_:r3gpkn:_:api.screeb.app
|
||||
- probe-sydney.cleverapps.io:_:icmp_ipv4:_:Sydney:_:r3gpkn:_:t.screeb.app
|
||||
|
||||
# ...`}</code></pre>
|
||||
|
||||
<pre class="rule-code"><code>{`# prometheus.yml
|
||||
|
||||
global:
|
||||
# ...
|
||||
|
||||
scrape_configs:
|
||||
|
||||
- job_name: 'blackbox'
|
||||
metrics_path: /probe
|
||||
scrape_interval: 30s
|
||||
scheme: https
|
||||
file_sd_configs:
|
||||
- files:
|
||||
- /etc/prometheus/sd/blackbox.yml
|
||||
relabel_configs:
|
||||
# adds "module" label in the final labelset
|
||||
- source_labels: [__address__]
|
||||
regex: '.*:_:(.*):_:.*:_:.*:_:.*'
|
||||
target_label: module
|
||||
# adds "geohash" label in the final labelset
|
||||
- source_labels: [__address__]
|
||||
regex: '.*:_:.*:_:.*:_:(.*):_:.*'
|
||||
target_label: geohash
|
||||
# rewrites "instance" label with corresponding URL
|
||||
- source_labels: [__address__]
|
||||
regex: '.*:_:.*:_:.*:_:.*:_:(.*)'
|
||||
target_label: instance
|
||||
# rewrites "pop" label with corresponding location name
|
||||
- source_labels: [__address__]
|
||||
regex: '.*:_:.*:_:(.*):_:.*:_:.*'
|
||||
target_label: pop
|
||||
# passes "module" parameter to Blackbox exporter
|
||||
- source_labels: [module]
|
||||
target_label: __param_module
|
||||
# passes "target" parameter to Blackbox exporter
|
||||
- source_labels: [instance]
|
||||
target_label: __param_target
|
||||
# the Blackbox exporter's real hostname:port
|
||||
- source_labels: [__address__]
|
||||
regex: '(.*):_:.*:_:.*:_:.*:_:.*'
|
||||
target_label: __address__
|
||||
|
||||
# ...`}</code></pre>
|
||||
|
||||
<h2 id="geohash">Geohash</h2>
|
||||
|
||||
<img
|
||||
src={`${base}/images/grafana-map-panel.png`}
|
||||
alt="Grafana map panel showing worldwide probe locations"
|
||||
class="rounded-lg border border-slate-200 dark:border-slate-700 my-4"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<p>To display nice maps in Grafana, you need to instruct blackbox exporters about the location. Grafana map panel speaks the "geohash" format:</p>
|
||||
|
||||
<ol>
|
||||
<li>Go to Google Maps</li>
|
||||
<li>Extract the lat/long from the URL</li>
|
||||
<li>Convert lat/long to geohash at <a href="http://geohash.co" target="_blank" rel="noopener noreferrer">geohash.co</a></li>
|
||||
</ol>
|
||||
|
||||
<h2 id="grafana">Grafana</h2>
|
||||
|
||||
<p>
|
||||
Some great dashboards have been created by the community:
|
||||
<a href="https://grafana.com/grafana/dashboards/?search=blackbox" target="_blank" rel="noopener noreferrer">grafana.com/grafana/dashboards/?search=blackbox</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Since Grafana v5.0.0, a map panel is available:
|
||||
<a href="https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/" target="_blank" rel="noopener noreferrer">Geomap panel documentation</a>
|
||||
</p>
|
||||
</GuideLayout>
|
||||
152
site/src/pages/index.astro
Normal file
152
site/src/pages/index.astro
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
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';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
const totalRules = getTotalRuleCount();
|
||||
const totalServices = getTotalServiceCount();
|
||||
|
||||
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}',
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Awesome Prometheus Alerts | Copy-pasteable Prometheus alerting rules"
|
||||
description={`Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services — covering databases, Kubernetes, cloud providers, message brokers, and more.`}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<!-- Hero -->
|
||||
<section class="bg-gradient-to-b from-slate-50 dark:from-slate-900/50 to-white dark:to-slate-950 border-b border-slate-200 dark:border-slate-800">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img
|
||||
src={`${base}/images/prometheus-logo.png`}
|
||||
alt="Prometheus"
|
||||
class="h-16 w-auto"
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-white mb-4">
|
||||
Awesome Prometheus Alerts
|
||||
</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.
|
||||
Find, copy, and deploy alerts in seconds.
|
||||
</p>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="max-w-xl mx-auto mb-8">
|
||||
<SearchWidget />
|
||||
</div>
|
||||
|
||||
<!-- CTA buttons -->
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
<a
|
||||
href={`${base}/rules/`}
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 bg-brand hover:bg-brand/90 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Browse all rules
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/samber/awesome-prometheus-alerts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-200 font-medium rounded-lg hover:border-slate-300 dark:hover:border-slate-600 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats -->
|
||||
<section class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<StatsBar />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Categories grid -->
|
||||
<section class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-8">Browse by category</h2>
|
||||
|
||||
<div class="space-y-10">
|
||||
{data.groups.map((group) => {
|
||||
const groupSlug = getGroupSlug(group);
|
||||
const groupRuleCount = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0);
|
||||
return (
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-semibold text-slate-700 dark:text-slate-300">
|
||||
<a href={`${base}/rules/${groupSlug}/`} class="hover:text-brand dark:hover:text-brand-dark transition-colors">
|
||||
{group.name}
|
||||
</a>
|
||||
</h3>
|
||||
<span class="text-xs text-slate-400 dark:text-slate-500">
|
||||
{group.services.length} services · {groupRuleCount} rules
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||
{group.services.map((service) => (
|
||||
<ServiceCard service={service} group={group} base={base} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Guides section -->
|
||||
<section class="bg-slate-50 dark:bg-slate-900/50 border-t border-slate-200 dark:border-slate-800">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white mb-6">Guides</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<a href={`${base}/alertmanager/`} class="group block p-5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700/60 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all">
|
||||
<div class="text-brand dark:text-brand-dark mb-2">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors mb-1">AlertManager Config</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Prometheus and AlertManager configuration examples and troubleshooting.</p>
|
||||
</a>
|
||||
|
||||
<a href={`${base}/blackbox-exporter/`} class="group block p-5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700/60 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all">
|
||||
<div class="text-brand dark:text-brand-dark mb-2">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors mb-1">Blackbox Exporter</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Worldwide probes, Prometheus config, geohash/Grafana map setup.</p>
|
||||
</a>
|
||||
|
||||
<a href={`${base}/sleep-peacefully/`} class="group block p-5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700/60 hover:border-brand/40 dark:hover:border-brand-dark/40 hover:shadow-sm transition-all">
|
||||
<div class="text-brand dark:text-brand-dark mb-2">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
|
||||
</div>
|
||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors mb-1">Sleep Peacefully</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Time-based alert suppression and timezone-aware PromQL patterns.</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
137
site/src/pages/rules/[group]/[service].astro
Normal file
137
site/src/pages/rules/[group]/[service].astro
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
---
|
||||
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 { data, getGroupSlug, getServiceSlug, getRuleCount, getAllServices } from '../../../data/rules';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return getAllServices().map(({ group, service, groupSlug, serviceSlug }) => ({
|
||||
params: { group: groupSlug, service: serviceSlug },
|
||||
props: { group, service },
|
||||
}));
|
||||
}
|
||||
|
||||
const { group, service } = Astro.props;
|
||||
const base = import.meta.env.BASE_URL;
|
||||
const groupSlug = getGroupSlug(group);
|
||||
const serviceSlug = getServiceSlug(service);
|
||||
const ruleCount = getRuleCount(service);
|
||||
const groupIndex = data.groups.findIndex((g) => getGroupSlug(g) === groupSlug) + 1;
|
||||
const serviceIndex = group.services.findIndex((s) => getServiceSlug(s) === serviceSlug) + 1;
|
||||
|
||||
// Build exporters summary for meta description
|
||||
const exporterNames = service.exporters.map((e) => e.name).filter(Boolean).join(', ');
|
||||
const metaDesc = `${ruleCount} Prometheus alerting rules for ${service.name}${exporterNames ? ` via ${exporterNames}` : ''}. Copy-paste ready YAML configurations for critical and warning alerts.`;
|
||||
|
||||
// FAQ JSON-LD for GEO (AI search engines)
|
||||
const faqItems = service.exporters.flatMap((exp) =>
|
||||
(exp.rules ?? []).map((rule) => ({
|
||||
'@type': 'Question',
|
||||
name: `What is the Prometheus alert rule for "${rule.name}"?`,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: `${rule.description} PromQL expression: ${rule.query}. Severity: ${rule.severity}${rule.for ? `. Duration: ${rule.for}` : ''}.`,
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
const 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/',
|
||||
},
|
||||
},
|
||||
...(faqItems.length > 0 ? [{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqItems,
|
||||
}] : []),
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${service.name} Prometheus Alert Rules | Awesome Prometheus Alerts`}
|
||||
description={metaDesc}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Rules', href: `${base}/rules/` },
|
||||
{ label: group.name, href: `${base}/rules/${groupSlug}/` },
|
||||
{ label: service.name },
|
||||
]}
|
||||
base={base}
|
||||
/>
|
||||
|
||||
<div class="flex gap-10 mt-6">
|
||||
<!-- Left sidebar -->
|
||||
<Sidebar
|
||||
groups={data.groups}
|
||||
currentGroupSlug={groupSlug}
|
||||
currentServiceSlug={serviceSlug}
|
||||
base={base}
|
||||
/>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 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">
|
||||
{ruleCount} Prometheus alerting rule{ruleCount !== 1 ? 's' : ''} for {service.name}.
|
||||
{exporterNames && `Exported via ${exporterNames}.`}
|
||||
These rules cover critical and warning conditions — copy and paste the YAML into your Prometheus configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Exporters -->
|
||||
{service.exporters.map((exporter, expIdx) => (
|
||||
<ExporterSection
|
||||
exporter={exporter}
|
||||
service={service}
|
||||
groupIndex={groupIndex}
|
||||
serviceIndex={serviceIndex}
|
||||
exporterIndex={expIdx + 1}
|
||||
showExporterNumber={service.exporters.length > 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
<!-- Prev/Next navigation -->
|
||||
<nav class="mt-10 pt-6 border-t border-slate-200 dark:border-slate-800 flex justify-between gap-4" aria-label="Service navigation">
|
||||
{(() => {
|
||||
const allSvcs = getAllServices();
|
||||
const idx = allSvcs.findIndex(s => s.groupSlug === groupSlug && s.serviceSlug === serviceSlug);
|
||||
const prev = idx > 0 ? allSvcs[idx - 1] : null;
|
||||
const next = idx < allSvcs.length - 1 ? allSvcs[idx + 1] : null;
|
||||
return (
|
||||
<>
|
||||
{prev ? (
|
||||
<a href={`${base}/rules/${prev.groupSlug}/${prev.serviceSlug}/`} class="group flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
|
||||
<span>{prev.service.name}</span>
|
||||
</a>
|
||||
) : <div />}
|
||||
{next ? (
|
||||
<a href={`${base}/rules/${next.groupSlug}/${next.serviceSlug}/`} class="group flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-brand dark:hover:text-brand-dark transition-colors">
|
||||
<span>{next.service.name}</span>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
|
||||
</a>
|
||||
) : <div />}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
46
site/src/pages/rules/[group]/index.astro
Normal file
46
site/src/pages/rules/[group]/index.astro
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import Breadcrumbs from '../../../components/Breadcrumbs.astro';
|
||||
import ServiceCard from '../../../components/ServiceCard.astro';
|
||||
import { data, getGroupSlug, getServiceSlug, getRuleCount } from '../../../data/rules';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return data.groups.map((group) => ({
|
||||
params: { group: getGroupSlug(group) },
|
||||
props: { group },
|
||||
}));
|
||||
}
|
||||
|
||||
const { group } = Astro.props;
|
||||
const base = import.meta.env.BASE_URL;
|
||||
const groupSlug = getGroupSlug(group);
|
||||
const totalRules = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0);
|
||||
---
|
||||
|
||||
<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.`}
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Rules', href: `${base}/rules/` },
|
||||
{ label: group.name },
|
||||
]}
|
||||
base={base}
|
||||
/>
|
||||
|
||||
<div class="mt-6 mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">{group.name}</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400">
|
||||
{group.services.length} service{group.services.length !== 1 ? 's' : ''} · {totalRules} rules
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{group.services.map((service) => (
|
||||
<ServiceCard service={service} group={group} base={base} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
84
site/src/pages/rules/index.astro
Normal file
84
site/src/pages/rules/index.astro
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
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';
|
||||
|
||||
const base = import.meta.env.BASE_URL;
|
||||
const totalRules = getTotalRuleCount();
|
||||
const totalServices = getTotalServiceCount();
|
||||
const redirectMap = buildRedirectMap(base);
|
||||
---
|
||||
|
||||
<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.`}
|
||||
>
|
||||
<!-- Old-anchor redirect handler -->
|
||||
<script define:vars={{ redirectMap }}>
|
||||
if (window.location.hash) {
|
||||
const anchor = window.location.hash.slice(1);
|
||||
const target = redirectMap[anchor];
|
||||
if (target) {
|
||||
window.location.replace(target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">Alert Rules</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400">
|
||||
{totalRules} alerting rules across {totalServices} services and {data.groups.length} categories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-8 max-w-xl">
|
||||
<SearchWidget />
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Groups -->
|
||||
<div class="space-y-10">
|
||||
{data.groups.map((group) => {
|
||||
const groupSlug = getGroupSlug(group);
|
||||
const groupRuleCount = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0);
|
||||
return (
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-slate-800 dark:text-slate-100">
|
||||
<a href={`${base}/rules/${groupSlug}/`} class="hover:text-brand dark:hover:text-brand-dark transition-colors">
|
||||
{group.name}
|
||||
</a>
|
||||
</h2>
|
||||
<span class="text-sm text-slate-400 dark:text-slate-500">
|
||||
{group.services.length} services · {groupRuleCount} rules
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
||||
{group.services.map((service) => (
|
||||
<ServiceCard service={service} group={group} base={base} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
113
site/src/pages/sleep-peacefully.astro
Normal file
113
site/src/pages/sleep-peacefully.astro
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
import GuideLayout from '../layouts/GuideLayout.astro';
|
||||
---
|
||||
|
||||
<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' }]}
|
||||
>
|
||||
<h2 id="alerting-time-window">Alerting time window</h2>
|
||||
|
||||
<p>
|
||||
In some applications, load and activity can vary over the day, week, or year.
|
||||
To prevent alarm fatigue and busy pager, alerts can be disabled during certain periods
|
||||
(such as nights or weekends).
|
||||
</p>
|
||||
|
||||
<p>Examples:</p>
|
||||
<ul>
|
||||
<li>Weekday: <code>node_load5 > 10 and ON() (0 < day_of_week() < 6)</code></li>
|
||||
<li>Day time: <code>node_load5 > 10 and ON() (8 < hour() < 18)</code></li>
|
||||
<li>Exclude December: <code>node_load5 > 10 and ON() (month() != 12)</code></li>
|
||||
</ul>
|
||||
|
||||
<h2 id="advanced-timezones">Advanced time windows and timezones</h2>
|
||||
|
||||
<pre class="rule-code"><code>{`# rules.yml
|
||||
|
||||
groups:
|
||||
- name: timezones
|
||||
rules:
|
||||
- record: european_summer_time_offset
|
||||
expr: |
|
||||
(vector(1) and (month() > 3 and month() < 10))
|
||||
or
|
||||
(vector(1) and (month() == 3 and (day_of_month() - day_of_week()) >= 25) and absent((day_of_month() >= 25) and (day_of_week() == 0)))
|
||||
or
|
||||
(vector(1) and (month() == 10 and (day_of_month() - day_of_week()) < 25) and absent((day_of_month() >= 25) and (day_of_week() == 0)))
|
||||
or
|
||||
(vector(1) and ((month() == 10 and hour() < 1) or (month() == 3 and hour() > 0)) and ((day_of_month() >= 25) and (day_of_week() == 0)))
|
||||
or
|
||||
vector(0)
|
||||
|
||||
- record: europe_london_time
|
||||
expr: time() + 3600 * european_summer_time_offset
|
||||
- record: europe_paris_time
|
||||
expr: time() + 3600 * (1 + european_summer_time_offset)
|
||||
|
||||
- record: europe_london_hour
|
||||
expr: hour(europe_london_time)
|
||||
- record: europe_paris_hour
|
||||
expr: hour(europe_paris_time)
|
||||
|
||||
- record: europe_london_weekday
|
||||
expr: 0 < day_of_week(europe_london_time) < 6
|
||||
- record: europe_paris_weekday
|
||||
expr: 0 < day_of_week(europe_paris_time) < 6
|
||||
# opposite
|
||||
- record: not_europe_london_weekday
|
||||
expr: absent(europe_london_weekday)
|
||||
- record: not_europe_paris_weekday
|
||||
expr: absent(europe_paris_weekday)
|
||||
|
||||
- record: europe_london_business_hours
|
||||
expr: 9 <= europe_london_hour < 18
|
||||
- record: europe_paris_business_hours
|
||||
expr: 9 <= europe_paris_hour < 18
|
||||
# opposite
|
||||
- record: not_europe_london_business_hours
|
||||
expr: absent(europe_london_business_hours)
|
||||
- record: not_europe_paris_business_hours
|
||||
expr: absent(europe_paris_business_hours)
|
||||
|
||||
# new year's day / xmas / labor day / all saints' day / ...
|
||||
- record: europe_french_public_holidays
|
||||
expr: |
|
||||
(vector(1) and month(europe_paris_time) == 1 and day_of_month(europe_paris_time) == 1)
|
||||
or
|
||||
(vector(1) and month(europe_paris_time) == 12 and day_of_month(europe_paris_time) == 25)
|
||||
or
|
||||
(vector(1) and month(europe_paris_time) == 5 and day_of_month(europe_paris_time) == 1)
|
||||
or
|
||||
(vector(1) and month(europe_paris_time) == 11 and day_of_month(europe_paris_time) == 1)
|
||||
or
|
||||
vector(0)
|
||||
# opposite
|
||||
- record: not_europe_french_public_holidays
|
||||
expr: absent(europe_french_public_holidays)`}</code></pre>
|
||||
|
||||
<pre class="rule-code"><code>{`# alerts.yml
|
||||
|
||||
groups:
|
||||
- name: CPU Load
|
||||
rules:
|
||||
- alert: HighLoadQuietDuringWeekendAndNight
|
||||
expr: node_load5 > 10 and ON() (europe_london_weekday and europe_paris_weekday)
|
||||
|
||||
- alert: HighLoadQuietDuringBackup
|
||||
expr: node_load5 > 10 and ON() absent(hour() == 2)
|
||||
|
||||
- alert: HighLoad
|
||||
expr: |
|
||||
node_load5 > 20 and ON() (europe_london_weekday and europe_paris_weekday)
|
||||
or
|
||||
node_load5 > 10`}</code></pre>
|
||||
|
||||
<h2 id="sources">Sources</h2>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://medium.com/@tom.fawcett/time-of-day-based-notifications-with-prometheus-and-alertmanager-1bf7a23b7695" target="_blank" rel="noopener noreferrer">Time of day based notifications with Prometheus and AlertManager</a></li>
|
||||
<li><a href="https://promcon.io/2019-munich/slides/improved-alerting-with-prometheus-and-alertmanager.pdf" target="_blank" rel="noopener noreferrer">Improved alerting with Prometheus and AlertManager (PromCon 2019)</a></li>
|
||||
</ul>
|
||||
</GuideLayout>
|
||||
85
site/src/styles/global.css
Normal file
85
site/src/styles/global.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--color-brand: #E6522C;
|
||||
--color-brand-dark: #f06840;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-white text-slate-900 antialiased;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-slate-950 text-slate-100;
|
||||
}
|
||||
|
||||
/* Code block base styles */
|
||||
pre {
|
||||
@apply overflow-x-auto rounded-lg text-sm leading-relaxed;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply font-mono text-sm;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code {
|
||||
@apply bg-slate-100 dark:bg-slate-800 text-slate-800 dark:text-slate-200 px-1.5 py-0.5 rounded text-xs;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Rule YAML code block — always dark */
|
||||
.rule-code {
|
||||
@apply bg-slate-900 text-slate-100 rounded-lg p-4 text-xs font-mono leading-relaxed overflow-x-auto;
|
||||
}
|
||||
|
||||
/* Severity badge */
|
||||
.badge-critical {
|
||||
@apply bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400 border border-red-200 dark:border-red-800;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400 border border-amber-200 dark:border-amber-800;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400 border border-blue-200 dark:border-blue-800;
|
||||
}
|
||||
|
||||
/* Navigation links */
|
||||
.nav-link {
|
||||
@apply text-slate-600 dark:text-slate-300 hover:text-brand dark:hover:text-brand-dark transition-colors duration-150 text-sm font-medium;
|
||||
}
|
||||
|
||||
.nav-link-active {
|
||||
@apply text-brand dark:text-brand-dark;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pagefind UI overrides */
|
||||
#search {
|
||||
--pagefind-ui-scale: 0.9;
|
||||
--pagefind-ui-primary: #E6522C;
|
||||
--pagefind-ui-text: #1e293b;
|
||||
--pagefind-ui-background: #ffffff;
|
||||
--pagefind-ui-border: #e2e8f0;
|
||||
--pagefind-ui-tag: #f1f5f9;
|
||||
--pagefind-ui-border-width: 1px;
|
||||
--pagefind-ui-border-radius: 8px;
|
||||
--pagefind-ui-font: inherit;
|
||||
}
|
||||
|
||||
.dark #search {
|
||||
--pagefind-ui-text: #e2e8f0;
|
||||
--pagefind-ui-background: #0f172a;
|
||||
--pagefind-ui-border: #334155;
|
||||
--pagefind-ui-tag: #1e293b;
|
||||
}
|
||||
19
site/tailwind.config.mjs
Normal file
19
site/tailwind.config.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
DEFAULT: '#E6522C',
|
||||
dark: '#f06840',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
10
site/tsconfig.json
Normal file
10
site/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@data/*": ["src/data/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue