diff --git a/site/astro.config.mjs b/site/astro.config.mjs index beefe60..691e905 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -4,7 +4,10 @@ import sitemap from '@astrojs/sitemap'; import icon from 'astro-icon'; import { parse as parseYaml } from 'yaml'; import { readFileSync } from 'fs'; -import { resolve } from 'path'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); /** Custom Vite plugin that parses YAML files using the 'yaml' package, * which tolerates duplicate keys (last one wins) unlike js-yaml 4.x. */ @@ -23,13 +26,72 @@ function yamlPlugin() { }; } +const toSlug = (name) => + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + +/** Build redirect map: old flat /rules/{service} paths → new /rules/{group}/{service}/ paths */ +function buildRedirects(base) { + try { + const rulesPath = resolve(__dirname, '../_data/rules.yml'); + const raw = readFileSync(rulesPath, 'utf-8'); + const { groups } = parseYaml(raw, { merge: true, strict: false, uniqueKeys: false }); + const redirects = {}; + for (const group of groups) { + const groupSlug = toSlug(group.name); + for (const service of group.services) { + const serviceSlug = toSlug(service.name); + // Old anchor slug (spaces → hyphens only, no other substitutions) + const oldSlug = service.name.replace(/ /g, '-').toLowerCase(); + const newPath = `${base}/rules/${groupSlug}/${serviceSlug}/`; + // Redirect from flat old path (with and without trailing slash) + for (const oldPath of [`${base}/rules/${oldSlug}`, `${base}/rules/${oldSlug}/`]) { + if (oldPath !== newPath && oldPath !== newPath.slice(0, -1)) { + redirects[oldPath] = { destination: newPath, status: 301 }; + } + } + } + } + return redirects; + } catch { + return {}; + } +} + +const base = '/awesome-prometheus-alerts'; + export default defineConfig({ site: 'https://samber.github.io', - base: '/awesome-prometheus-alerts', + base, + redirects: buildRedirects(base), output: 'static', integrations: [ tailwind({ applyBaseStyles: false }), - sitemap(), + sitemap({ + serialize(item) { + const path = new URL(item.url).pathname; + const segments = path.replace(/^\/|\/$/g, '').split('/').filter(Boolean); + // segments[0] = 'awesome-prometheus-alerts', [1] = 'rules'|guide, [2] = group, [3] = service + + if (segments.length <= 1) { + // Homepage + return { ...item, changefreq: 'weekly', priority: 1.0, lastmod: new Date() }; + } + if (segments.length === 2 && segments[1] === 'rules') { + // /rules/ index + return { ...item, changefreq: 'weekly', priority: 0.9, lastmod: new Date() }; + } + if (segments.length === 3 && segments[1] === 'rules') { + // /rules/[group]/ index + return { ...item, changefreq: 'monthly', priority: 0.7, lastmod: new Date() }; + } + if (segments.length === 4 && segments[1] === 'rules') { + // /rules/[group]/[service]/ — main content pages + return { ...item, changefreq: 'monthly', priority: 0.8, lastmod: new Date() }; + } + // Guide pages and others + return { ...item, changefreq: 'yearly', priority: 0.6, lastmod: new Date() }; + }, + }), icon(), ], vite: { diff --git a/site/public/favicon.ico b/site/public/favicon.ico deleted file mode 100644 index bf23ccf..0000000 Binary files a/site/public/favicon.ico and /dev/null differ diff --git a/site/public/favicon.svg b/site/public/favicon.svg new file mode 100644 index 0000000..0470226 --- /dev/null +++ b/site/public/favicon.svg @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/site/public/manifest.json b/site/public/manifest.json new file mode 100644 index 0000000..aa4da0d --- /dev/null +++ b/site/public/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Awesome Prometheus Alerts", + "short_name": "Prom Alerts", + "description": "Collection of copy-pasteable Prometheus alerting rules for 90+ services.", + "start_url": "/awesome-prometheus-alerts/", + "scope": "/awesome-prometheus-alerts/", + "display": "browser", + "background_color": "#0f172a", + "theme_color": "#E6522C", + "lang": "en", + "icons": [ + { + "src": "/awesome-prometheus-alerts/favicon.svg", + "type": "image/svg+xml", + "sizes": "any", + "purpose": "any maskable" + } + ] +} diff --git a/site/public/robots.txt b/site/public/robots.txt index f3776b3..eff0940 100644 --- a/site/public/robots.txt +++ b/site/public/robots.txt @@ -1,4 +1,28 @@ User-agent: * Allow: / +# AI search bots — explicitly allowed for citation +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: Bingbot +Allow: / + Sitemap: https://samber.github.io/awesome-prometheus-alerts/sitemap-index.xml +LLMs: https://samber.github.io/awesome-prometheus-alerts/llms.txt +LLMs-full: https://samber.github.io/awesome-prometheus-alerts/llms-full.txt diff --git a/site/src/components/Breadcrumbs.astro b/site/src/components/Breadcrumbs.astro index 0ddf1ad..e1f57f5 100644 --- a/site/src/components/Breadcrumbs.astro +++ b/site/src/components/Breadcrumbs.astro @@ -9,8 +9,9 @@ interface Props { base: string; } +import { SITE_ORIGIN } from '../data/site'; + const { items, base } = Astro.props; -const siteUrl = 'https://samber.github.io'; const allItems = [{ label: 'Home', href: `${base}/` }, ...items]; @@ -21,7 +22,7 @@ const jsonLd = { '@type': 'ListItem', position: i + 1, name: item.label, - ...(item.href ? { item: `${siteUrl}${item.href}` } : {}), + ...(item.href ? { item: `${SITE_ORIGIN}${item.href}` } : {}), })), }; --- diff --git a/site/src/components/CautionBanner.astro b/site/src/components/CautionBanner.astro new file mode 100644 index 0000000..29ff95d --- /dev/null +++ b/site/src/components/CautionBanner.astro @@ -0,0 +1,12 @@ +--- +--- +
+
+ ⚠️ +

+ Alert thresholds depend on the nature of your applications. + Some queries may have arbitrary tolerance thresholds. + Building an efficient monitoring platform takes time. 😉 +

+
+
diff --git a/site/src/components/ExporterSection.astro b/site/src/components/ExporterSection.astro index bb96b7f..8a11c4c 100644 --- a/site/src/components/ExporterSection.astro +++ b/site/src/components/ExporterSection.astro @@ -33,7 +33,7 @@ const exporterPrefix = showExporterNumber : `${groupIndex}.${serviceIndex}.`; --- -
+
diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro index e49c553..80bf8bd 100644 --- a/site/src/components/Footer.astro +++ b/site/src/components/Footer.astro @@ -1,33 +1,45 @@ --- +import { sponsors } from '../data/sponsors'; +import { getPopularServices, data, getGroupSlug } from '../data/rules'; +import { SITE_NAME, SITE_URL, GITHUB_URL, GITHUB_CONTRIBUTING_URL, GITHUB_LICENSE_URL, AUTHOR_NAME, AUTHOR_GITHUB_URL, TWITTER_HANDLE, LICENSE_CC_BY_NAME } from '../data/site'; + interface Props { base: string; } const { base } = Astro.props; + +const popularServices = getPopularServices(); + +const featuredGroupSlugs = [ + 'basic-resource-monitoring', + 'databases', + 'orchestrators', + 'network-and-security', +]; +const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(getGroupSlug(g))); --- diff --git a/site/src/components/Header.astro b/site/src/components/Header.astro index 016c722..d1b28e8 100644 --- a/site/src/components/Header.astro +++ b/site/src/components/Header.astro @@ -1,5 +1,7 @@ --- import ThemeToggle from './ThemeToggle.astro'; +import { sponsors } from '../data/sponsors'; +import { SITE_NAME, GITHUB_URL, GITHUB_API_REPO_URL, GITHUB_CONTRIBUTING_URL } from '../data/site'; interface Props { base: string; @@ -11,19 +13,20 @@ const currentPath = Astro.url.pathname; function isActive(path: string) { return currentPath.startsWith(`${base}${path}`); } ---- - -
- Kindly supported by  - - CAST AI - - and  - - Better Stack - -
+let stars = 0; +try { + const res = await fetch(GITHUB_API_REPO_URL, { + headers: { 'Accept': 'application/vnd.github+json' } + }); + if (res.ok) { + const data = await res.json(); + stars = data.stargazers_count ?? 0; + } +} catch {} + +const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars); +---
@@ -31,11 +34,9 @@ function isActive(path: string) {
- - - + + + APA @@ -70,7 +71,7 @@ function isActive(path: string) {
+ {stars > 0 && ( + {starsLabel} + )} +
@@ -113,6 +118,18 @@ function isActive(path: string) {
+ +
+
+ Sponsored by + {sponsors.map((s) => ( + + {`${s.name} + + ))} +
+
+
diff --git a/site/src/components/RuleCard.astro b/site/src/components/RuleCard.astro index 2f02432..9bb85ea 100644 --- a/site/src/components/RuleCard.astro +++ b/site/src/components/RuleCard.astro @@ -15,7 +15,7 @@ const yamlContent = formatRuleAsYaml(rule); const codeId = `code-${anchorId}`; --- -
+
@@ -50,7 +50,7 @@ const codeId = `code-${anchorId}`;
{yamlContent}
diff --git a/site/src/components/SEO.astro b/site/src/components/SEO.astro index f465cb5..767283f 100644 --- a/site/src/components/SEO.astro +++ b/site/src/components/SEO.astro @@ -1,15 +1,22 @@ --- +import { SITE_NAME, AUTHOR_NAME, TWITTER_HANDLE } from '../data/site'; + interface Props { title: string; description: string; canonicalUrl: string; ogImage?: string; + ogType?: string; + keywords?: string; jsonLd?: object | object[]; base: string; siteUrl: string; + datePublished?: string; + dateModified?: string; } -const { title, description, canonicalUrl, jsonLd, base, siteUrl } = Astro.props; +const { title, description, canonicalUrl, jsonLd, keywords, datePublished, dateModified, base, siteUrl } = Astro.props; +const ogType = Astro.props.ogType ?? 'website'; const ogImage = Astro.props.ogImage ?? `${base}/images/prometheus-logo.png`; const fullOgImage = ogImage.startsWith('http') ? ogImage : `${siteUrl}${ogImage}`; @@ -20,22 +27,29 @@ const jsonLdArray = jsonLd {title} +{keywords && } - + - + + + +{ogType === 'article' && datePublished && } +{ogType === 'article' && dateModified && } +{ogType === 'article' && } - + + {jsonLdArray.map((schema) => ( - + diff --git a/site/src/layouts/GuideLayout.astro b/site/src/layouts/GuideLayout.astro index 7564794..0b34d07 100644 --- a/site/src/layouts/GuideLayout.astro +++ b/site/src/layouts/GuideLayout.astro @@ -1,6 +1,9 @@ --- import BaseLayout from './BaseLayout.astro'; import Breadcrumbs from '../components/Breadcrumbs.astro'; +import { SITE_ORIGIN, SITE_URL, AUTHOR_NAME, AUTHOR_GITHUB_URL, schemaAuthor, schemaPublisher, schemaWebSite, SITE_DATE_PUBLISHED, SCHEMA_IN_LANGUAGE } from '../data/site'; + +type IconName = 'bell' | 'globe' | 'moon' | 'book'; interface BreadcrumbItem { label: string; @@ -11,55 +14,407 @@ interface Props { title: string; description?: string; breadcrumbs?: BreadcrumbItem[]; - headings?: Array<{ depth: number; text: string; slug: string }>; + icon?: IconName; + badge?: string; + /** Additional JSON-LD schemas to include alongside the auto-generated TechArticle */ + extraJsonLd?: object | object[]; + /** ISO date string (YYYY-MM-DD) for when this guide was last meaningfully updated */ + dateUpdated?: string; + /** Comma-separated keywords for SEO meta tag */ + keywords?: string; + /** Approximate reading time in minutes (shown in hero) */ + readingTime?: number; } -const { title, description, breadcrumbs = [], headings = [] } = Astro.props; +const { title, description, breadcrumbs = [], icon = 'book', badge = 'Guide', extraJsonLd, dateUpdated, keywords, readingTime } = Astro.props; const base = import.meta.env.BASE_URL; +const canonicalUrl = `${SITE_ORIGIN}${base}${Astro.url.pathname.replace(base, '')}`; + +const dateModified = dateUpdated ?? new Date().toISOString().slice(0, 10); +const displayDate = new Date(dateModified + 'T00:00:00Z').toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' }); + +const guideJsonLd = { + '@context': 'https://schema.org', + '@type': 'TechArticle', + headline: title, + description: description ?? `${title} — Prometheus monitoring guide`, + url: canonicalUrl, + inLanguage: SCHEMA_IN_LANGUAGE, + image: `${SITE_URL}favicon.svg`, + datePublished: SITE_DATE_PUBLISHED, + dateModified, + author: schemaAuthor, + publisher: schemaPublisher, + isPartOf: schemaWebSite, +}; + +const allGuideJsonLd: object[] = [ + guideJsonLd, + ...(extraJsonLd ? (Array.isArray(extraJsonLd) ? extraJsonLd : [extraJsonLd]) : []), +]; + +const iconPaths: Record = { + bell: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9', + globe: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9', + moon: 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z', + book: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253', +}; + +const allGuides = [ + { + title: 'AlertManager Configuration', + description: 'Configure Prometheus and AlertManager with routing, receivers, and notification timing.', + href: `${base}/alertmanager/`, + icon: 'bell' as IconName, + badge: 'Configuration', + }, + { + title: 'Blackbox Exporter', + description: 'Monitor HTTP, DNS, TCP and ICMP endpoints from multiple worldwide PoPs.', + href: `${base}/blackbox-exporter/`, + icon: 'globe' as IconName, + badge: 'Monitoring', + }, + { + title: 'Sleep Peacefully', + description: 'Suppress noisy alerts during nights and weekends with time-aware PromQL.', + href: `${base}/sleep-peacefully/`, + icon: 'moon' as IconName, + badge: 'PromQL Tips', + }, +]; + +const relatedGuides = allGuides.filter(g => g.title !== title); --- - -
- {breadcrumbs.length > 0 && } - -
- - {headings.length > 0 && ( - + + +
+
+ {breadcrumbs.length > 0 && ( +
+ +
)} +
+
+ + + +
+
+
+ + {badge} + + · + By {AUTHOR_NAME} + · + + {readingTime && ( + <> + · + {readingTime} min read + + )} +
+

{title}

+ {description && ( +

{description}

+ )} +
+
+
+
- -
-

{title}

+ +
+
+ + + + + + +
- diff --git a/site/src/pages/alertmanager.astro b/site/src/pages/alertmanager.astro index 22a6b34..5e2b5e5 100644 --- a/site/src/pages/alertmanager.astro +++ b/site/src/pages/alertmanager.astro @@ -2,12 +2,53 @@ import GuideLayout from '../layouts/GuideLayout.astro'; const base = import.meta.env.BASE_URL; + +const howToJsonLd = { + '@context': 'https://schema.org', + '@type': 'HowTo', + name: 'How to configure Prometheus and AlertManager for production alerting', + description: + 'Set up Prometheus alert rules, configure AlertManager routing and receivers, use recording rules to reduce load, and troubleshoot alert delivery delays.', + step: [ + { + '@type': 'HowToStep', + name: 'Configure Prometheus scrape and evaluation intervals', + text: 'In prometheus.yml, set scrape_interval and evaluation_interval (e.g. 20s). Point rule_files at your alerts/*.yml directory.', + }, + { + '@type': 'HowToStep', + name: 'Write alert rules', + text: 'Create YAML rule files with alert name, expr (PromQL), for duration, severity label, and summary/description annotations.', + }, + { + '@type': 'HowToStep', + name: 'Configure AlertManager routing', + text: 'In alertmanager.yml, define a route tree with group_wait, group_interval, repeat_interval, and child routes that match severity labels to specific receivers.', + }, + { + '@type': 'HowToStep', + name: 'Set up receivers (Slack, PagerDuty, webhook)', + text: 'Add receiver blocks for each notification channel. For Slack, provide api_url, channel, and a message template. Use continue: true if multiple receivers should handle the same alert.', + }, + { + '@type': 'HowToStep', + name: 'Add recording rules for expensive queries', + text: 'Wrap high-cardinality or frequently evaluated expressions in recording rules. Reference the recorded metric in your alert expressions to reduce Prometheus CPU usage.', + }, + ], +}; ---

If you notice a delay between an event and the first notification, read this post: @@ -18,6 +59,11 @@ const base = import.meta.env.BASE_URL;

Prometheus configuration

+

+ Prometheus reads alert rules from YAML files and evaluates them on every evaluation_interval cycle. + Keep both scrape_interval and evaluation_interval consistent — a mismatch causes stale data in range queries. +

+
{`# prometheus.yml
 
 global:
@@ -26,13 +72,12 @@ global:
   # A short evaluation_interval will check alerting rules very often.
   # It can be costly if you run Prometheus with 100+ alerts.
   evaluation_interval: 20s
-  ...
 
 rule_files:
   - 'alerts/*.yml'
 
 scrape_configs:
-  ...`}
+ # ...`}
{`# alerts/example-redis.yml
 
@@ -41,104 +86,152 @@ groups:
 - name: ExampleRedisGroup
   rules:
   - alert: ExampleRedisDown
-    expr: redis_up{} == 0
+    expr: redis_up == 0
     for: 2m
     labels:
       severity: critical
     annotations:
-      summary: "Redis instance down"
-      description: "Whatever"`}
+ summary: Redis instance down (instance {{ $labels.instance }}) + description: "Redis is unreachable\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}" + + - alert: ExampleRedisHighMemory + expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9 + for: 5m + labels: + severity: warning + annotations: + summary: Redis memory usage above 90% (instance {{ $labels.instance }}) + description: "Redis memory usage is {{ $value | humanizePercentage }}\\n LABELS = {{ $labels }}"`}

AlertManager configuration

+

+ AlertManager receives alerts from Prometheus, deduplicates and groups them, then routes them to the right receiver. + The three key timing parameters control when notifications are sent: +

+
    +
  • group_wait — how long to wait for more alerts to batch into the first notification
  • +
  • group_interval — how long to wait before sending a follow-up for an ongoing group
  • +
  • repeat_interval — how often to re-notify if an alert hasn't resolved
  • +
+
{`# alertmanager.yml
 
 route:
-  # When a new group of alerts is created by an incoming alert, wait at
-  # least 'group_wait' to send the initial notification.
-  # This way ensures that you get multiple alerts for the same group that start
-  # firing shortly after another are batched together on the first notification.
   group_wait: 10s
-
-  # When the first notification was sent, wait 'group_interval' to send a batch
-  # of new alerts that started firing for that group.
   group_interval: 30s
-
-  # If an alert has successfully been sent, wait 'repeat_interval' to
-  # resend them.
-  repeat_interval: 30m
-
-  # A default receiver
+  repeat_interval: 4h
   receiver: "slack"
 
-  # All the above attributes are inherited by all child routes and can
-  # overwritten on each.
   routes:
+    # warnings and criticals → Slack
     - receiver: "slack"
-      group_wait: 10s
-      match_re:
-        severity: critical|warning
+      matchers:
+        - severity =~ "critical|warning"
       continue: true
 
-    - receiver: "pager"
-      group_wait: 10s
-      match_re:
-        severity: critical
-      continue: true
+    # criticals also → PagerDuty
+    - receiver: "pagerduty"
+      matchers:
+        - severity = "critical"
 
 receivers:
   - name: "slack"
     slack_configs:
       - api_url: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxxxxx'
         send_resolved: true
-        channel: 'monitoring'
-        text: "{{ range .Alerts }} {{ .Annotations.summary }}\\n{{ .Annotations.description }}\\n{{ end }}"
+        channel: '#monitoring'
+        title: '{{ if eq .Status "firing" }}:fire:{{ else }}:white_check_mark:{{ end }} {{ .CommonLabels.alertname }}'
+        text: |
+          {{ range .Alerts }}
+          *Alert:* {{ .Annotations.summary }}
+          *Description:* {{ .Annotations.description }}
+          *Severity:* {{ .Labels.severity }}
+          {{ end }}
 
-  - name: "pager"
-    webhook_configs:
-      - url: http://a.b.c.d:8080/send/sms
+  - name: "pagerduty"
+    pagerduty_configs:
+      - routing_key: ''
         send_resolved: true`}
+

Inhibition rules

+ +

+ Inhibition suppresses lower-priority alerts when a higher-priority alert is already firing for the same target. + A common pattern: silence warning alerts when a critical alert is active on the same instance. +

+ +
{`# alertmanager.yml
+
+inhibit_rules:
+  # Suppress warnings when a critical is firing for the same instance
+  - source_matchers:
+      - severity = "critical"
+    target_matchers:
+      - severity = "warning"
+    equal:
+      - alertname
+      - instance
+
+  # Suppress all alerts for a node when NodeDown is firing
+  - source_matchers:
+      - alertname = "NodeDown"
+    target_matchers:
+      - job = "node"
+    equal:
+      - instance`}
+

Reduce Prometheus server load

- For expensive or frequent PromQL queries, Prometheus allows you to precompute rules. + For expensive or frequently evaluated PromQL queries, use recording rules to precompute results. + AlertManager and dashboards then reference the lightweight recorded metric instead of re-evaluating the full expression.

{`groups:
 
-  # first define the recorded rule
-  - name: ExampleRecordedGroup
+  # 1. Define the recording rule
+  - name: recordings
     rules:
-    - record: job:rabbitmq_queue_messages_delivered_total:rate:5m
+    - record: job:rabbitmq_queue_messages_delivered_total:rate5m
       expr: rate(rabbitmq_queue_messages_delivered_total[5m])
 
-  # then use it in alerts
-  - name: ExampleAlertingGroup
+  # 2. Reference it in alert rules
+  - name: alerts
     rules:
-    - alert: ExampleRabbitmqLowMessageDelivery
-      expr: sum(job:rabbitmq_queue_messages_delivered_total:rate:5m) < 10
+    - alert: RabbitmqLowMessageDelivery
+      expr: sum(job:rabbitmq_queue_messages_delivered_total:rate5m) < 10
       for: 2m
       labels:
         severity: critical
       annotations:
-        summary: "Low delivery rate in Rabbitmq queues"`}
+ summary: Low message delivery rate in RabbitMQ + description: "Delivery rate is {{ $value | humanize }} msg/s\\n LABELS = {{ $labels }}"`} -

Troubleshooting

+

Troubleshooting alert delays

-

If the notification takes too much time to be triggered, check the following delays:

+

+ The total time from an event occurring to a notification being sent is the sum of several independent delays. + Work through them in order: +

    -
  • scrape_interval = 20s (prometheus.yml)
  • -
  • evaluation_interval = 20s (prometheus.yml)
  • -
  • increase(mysql_global_status_slow_queries[1m]) > 0 (alerts/example-mysql.yml)
  • -
  • for: 5m (alerts/example-mysql.yml)
  • -
  • group_wait = 10s (alertmanager.yml)
  • +
  • Scrape delay: up to scrape_interval (20s) before the metric is collected
  • +
  • Evaluation delay: up to evaluation_interval (20s) before the rule fires
  • +
  • Pending duration: the for: 5m window must be satisfied before the alert state changes to firing
  • +
  • GroupWait: AlertManager waits group_wait (10s) for other alerts to batch
-

Also read:

+

+ In the worst case with for: 5m: 20s + 20s + 5m + 10s ≈ 6 minutes from event to notification. + Reduce evaluation_interval and for: for time-sensitive alerts, but be careful of false positives from transient spikes. +

+ +

Further reading

+
diff --git a/site/src/pages/blackbox-exporter.astro b/site/src/pages/blackbox-exporter.astro index c7a4c0f..7edbcaa 100644 --- a/site/src/pages/blackbox-exporter.astro +++ b/site/src/pages/blackbox-exporter.astro @@ -2,12 +2,47 @@ import GuideLayout from '../layouts/GuideLayout.astro'; const base = import.meta.env.BASE_URL; + +const howToJsonLd = { + '@context': 'https://schema.org', + '@type': 'HowTo', + name: 'How to deploy Blackbox Exporter for worldwide endpoint probing', + description: 'Deploy blackbox exporters in multiple Points of Presence to monitor HTTP, HTTPS, DNS, TCP, and ICMP endpoints with geolocation support in Grafana.', + step: [ + { + '@type': 'HowToStep', + name: 'Deploy blackbox exporters worldwide', + text: 'Deploy blackbox exporters in multiple PoPs (e.g., Montreal, Paris, Singapore, Sydney). Use community-hosted endpoints at probe-.cleverapps.io or self-host using the samber/blackbox_exporter configuration.', + }, + { + '@type': 'HowToStep', + name: 'Define targets with encoded labels', + text: 'Create a service discovery file (sd/blackbox.yml) encoding the exporter host, probe module, city name, geohash, and target URL in the target address using the :_: separator format.', + }, + { + '@type': 'HowToStep', + name: 'Configure Prometheus relabeling', + text: 'Add a scrape job to prometheus.yml with relabel_configs to extract module, pop, geohash, and instance labels from the compound target address. Use regex patterns to parse each field.', + }, + { + '@type': 'HowToStep', + name: 'Set up geohash labels for Grafana', + text: 'Convert probe PoP coordinates to geohash format using geohash.co. Add the geohash value to your target definitions so Grafana\'s geomap panel can display probe locations on a world map.', + }, + ], +}; ---

Worldwide probes

@@ -31,7 +66,7 @@ const base = import.meta.env.BASE_URL;

- ☝️ Logs have been disabled. More probes from the community would be appreciated — + ☝️ Server logs have been disabled. More probes from the community would be appreciated — contribute here! These blackbox exporters use the following configuration. @@ -129,7 +164,7 @@ scrape_configs: Grafana map panel showing worldwide probe locations diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 09bbdd8..e23aff5 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -3,26 +3,182 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import StatsBar from '../components/StatsBar.astro'; import ServiceCard from '../components/ServiceCard.astro'; import SearchWidget from '../components/SearchWidget.astro'; -import { data, getGroupSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount } from '../data/rules'; +import { data, getGroupSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount, getPopularServices } from '../data/rules'; +import { SITE_URL, GITHUB_URL, schemaAuthor, schemaPublisher, schemaWebSite, SITE_DATE_PUBLISHED, LICENSE_CC_BY_URL, LICENSE_MIT_URL } from '../data/site'; const base = import.meta.env.BASE_URL; const totalRules = getTotalRuleCount(); const totalServices = getTotalServiceCount(); +const popularServices = getPopularServices(); + +const org = { + '@type': 'Organization', + '@id': `${SITE_URL}#organization`, + name: schemaPublisher.name, + url: GITHUB_URL, + logo: { + '@type': 'ImageObject', + url: `${SITE_URL}favicon.svg`, + }, + sameAs: [GITHUB_URL, SITE_URL], + contactPoint: { + '@type': 'ContactPoint', + contactType: 'technical support', + url: `${GITHUB_URL}/issues`, + }, +}; + +const buildDate = new Date().toISOString().slice(0, 10); + +const faqItems = [ + { + '@type': 'Question', + name: 'What are Prometheus alerting rules?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Prometheus alerting rules are PromQL-based conditions evaluated by the Prometheus server. When a condition is true for a specified duration, an alert fires and is routed by AlertManager to receivers like Slack, PagerDuty, or email. Rules are defined as YAML files and cover metrics thresholds, absence of expected data, and rate-of-change conditions.', + }, + }, + { + '@type': 'Question', + name: 'How do I use these Prometheus alert rules?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Find the service you want to monitor, copy the YAML snippet for any rule, and paste it into your Prometheus rules file (e.g., alerts/my-service.yml). Reload Prometheus to apply the rules. Adjust thresholds to match your workload — the values provided are sensible defaults but may need tuning.', + }, + }, + { + '@type': 'Question', + name: 'What exporters and services are covered?', + acceptedAnswer: { + '@type': 'Answer', + text: `Awesome Prometheus Alerts covers ${totalServices} services across ${data.groups.length} categories: ${data.groups.map((g) => `${g.name} (${g.services.map((s) => s.name).join(', ')})`).join('; ')}.`, + }, + }, + { + '@type': 'Question', + name: 'What is the difference between warning and critical severity?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Critical alerts require immediate human attention — the system is down or severely degraded and revenue or reliability is directly impacted. Warning alerts need attention soon but are not immediately urgent. Info alerts are awareness-only, such as configuration changes or underutilized resources. Set up AlertManager routes to page on-call engineers only for critical alerts.', + }, + }, + { + '@type': 'Question', + name: 'What is PromQL?', + acceptedAnswer: { + '@type': 'Answer', + text: 'PromQL (Prometheus Query Language) is the functional query language used to select, filter, and aggregate time-series data in Prometheus. Alert rules use PromQL expressions — for example, rate(http_requests_total[5m]) > 100 fires when request rate exceeds 100/s over a 5-minute window.', + }, + }, + { + '@type': 'Question', + name: 'Can I contribute new alert rules?', + acceptedAnswer: { + '@type': 'Answer', + text: `Yes! Contributions are welcome. Open a pull request on GitHub at ${GITHUB_URL} with your new rules added to the _data/rules.yml file. Follow the existing format: provide a clear rule name, a description explaining what the alert means and why it matters, a tested PromQL expression, an appropriate severity, and a sensible "for" duration to avoid false positives.`, + }, + }, + { + '@type': 'Question', + name: 'What is AlertManager and how does it relate to these rules?', + acceptedAnswer: { + '@type': 'Answer', + text: 'AlertManager is the component that receives firing alerts from Prometheus and handles deduplication, grouping, silencing, and routing to receivers (Slack, PagerDuty, email, webhooks). The alert rules in this collection fire alerts from Prometheus — AlertManager then decides who to notify and when. See the AlertManager Configuration guide on this site for setup examples.', + }, + }, + { + '@type': 'Question', + name: 'How do I silence or suppress an alert?', + acceptedAnswer: { + '@type': 'Answer', + text: 'AlertManager supports silences — time-bounded mutes applied via its UI or API that suppress notifications without disabling the rule. For recurring suppression (nights, weekends, deployments), use inhibition rules or time-based PromQL patterns. See the Sleep Peacefully guide on this site for timezone-aware suppression examples using day_of_week() and hour() functions.', + }, + }, + { + '@type': 'Question', + name: 'What is the license for these alert rules?', + acceptedAnswer: { + '@type': 'Answer', + text: `The alert rules are licensed under Creative Commons CC BY 4.0. You are free to use, adapt, and redistribute them — including in commercial environments — as long as you provide attribution. See the LICENSE file in the GitHub repository for details.`, + }, + }, +]; const jsonLd = { '@context': 'https://schema.org', - '@type': 'WebSite', - name: 'Awesome Prometheus Alerts', - url: 'https://samber.github.io/awesome-prometheus-alerts/', - description: `Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services.`, - potentialAction: { - '@type': 'SearchAction', - target: { - '@type': 'EntryPoint', - urlTemplate: 'https://samber.github.io/awesome-prometheus-alerts/rules/?q={search_term_string}', + '@graph': [ + org, + { + '@type': 'SoftwareSourceCode', + name: 'awesome-prometheus-alerts', + description: 'Collection of Prometheus alerting rules — YAML configurations for 90+ services', + url: GITHUB_URL, + codeRepository: GITHUB_URL, + programmingLanguage: 'YAML', + author: schemaAuthor, + license: LICENSE_MIT_URL, }, - 'query-input': 'required name=search_term_string', - }, + { + '@type': 'Dataset', + name: 'Awesome Prometheus Alerts', + description: `Collection of ${totalRules} production-ready Prometheus alerting rules covering ${totalServices} services and 13 categories including databases, Kubernetes, cloud providers, and more.`, + url: SITE_URL, + creator: schemaAuthor, + datePublished: SITE_DATE_PUBLISHED, + dateModified: buildDate, + keywords: ['Prometheus', 'alerting rules', 'monitoring', 'PromQL', 'SRE', 'DevOps', 'observability'], + license: LICENSE_CC_BY_URL, + isAccessibleForFree: true, + }, + { + '@type': 'FAQPage', + '@id': `${SITE_URL}#faq`, + mainEntity: faqItems, + }, + { + '@type': 'WebSite', + '@id': `${SITE_URL}#website`, + name: schemaWebSite.name, + url: SITE_URL, + description: `Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services.`, + publisher: { '@id': `${SITE_URL}#organization` }, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${SITE_URL}rules/?q={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, + }, + { + '@type': 'SoftwareApplication', + name: schemaWebSite.name, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'All', + url: SITE_URL, + image: `${SITE_URL}favicon.svg`, + description: `Collection of ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} services — covering databases, Kubernetes, cloud providers, message brokers, and more.`, + author: schemaAuthor, + publisher: { '@id': `${SITE_URL}#organization` }, + offers: { + '@type': 'Offer', + price: 0, + priceCurrency: 'USD', + }, + }, + { + '@type': 'ItemList', + name: 'Site Navigation', + itemListElement: [ + { '@type': 'SiteNavigationElement', position: 1, name: 'Alert Rules', url: `${SITE_URL}rules/` }, + { '@type': 'SiteNavigationElement', position: 2, name: 'AlertManager Config', url: `${SITE_URL}alertmanager/` }, + { '@type': 'SiteNavigationElement', position: 3, name: 'Blackbox Exporter', url: `${SITE_URL}blackbox-exporter/` }, + { '@type': 'SiteNavigationElement', position: 4, name: 'Sleep Peacefully', url: `${SITE_URL}sleep-peacefully/` }, + ], + }, + ], }; --- @@ -44,10 +200,10 @@ const jsonLd = { />

- Awesome Prometheus Alerts + Awesome Prometheus Alert Rules

- {totalRules} copy-pasteable Prometheus alerting rules for {totalServices} services. + {totalRules} copy-pasteable Prometheus alerting rules. Find, copy, and deploy alerts in seconds.

@@ -87,6 +243,23 @@ const jsonLd = {
+ +
+
+

Popular services

+
+ {popularServices.map(({ service, groupSlug, serviceSlug }) => ( + + {service.name} + + ))} +
+
+
+

Browse by category

@@ -149,4 +322,24 @@ const jsonLd = {
+ + +
+
+

Frequently asked questions

+
+ {faqItems.map((item) => ( +
+ + {item.name} + + + + +

{item.acceptedAnswer.text}

+
+ ))} +
+
+
diff --git a/site/src/pages/llms-full.txt.ts b/site/src/pages/llms-full.txt.ts new file mode 100644 index 0000000..da00ba9 --- /dev/null +++ b/site/src/pages/llms-full.txt.ts @@ -0,0 +1,65 @@ +import type { APIRoute } from 'astro'; +import { data, getGroupSlug, getServiceSlug, getTotalRuleCount, getTotalServiceCount } from '../data/rules'; +import { SITE_NAME, SITE_URL, GITHUB_URL, AUTHOR_NAME, LICENSE_CC_BY_NAME } from '../data/site'; + +export const GET: APIRoute = () => { + const totalRules = getTotalRuleCount(); + const totalServices = getTotalServiceCount(); + const siteBase = SITE_URL.replace(/\/$/, ''); + + const sections = data.groups + .map((group) => { + const groupSlug = getGroupSlug(group); + const serviceBlocks = group.services + .map((service) => { + const serviceSlug = getServiceSlug(service); + const ruleLines = service.exporters + .flatMap((exporter) => + (exporter.rules ?? []).map((rule) => { + const forPart = rule.for ? `, for: ${rule.for}` : ''; + return ` - **${rule.name}** (severity: ${rule.severity}${forPart})\n ${rule.description}`; + }) + ) + .join('\n'); + return `### [${service.name}](${siteBase}/rules/${groupSlug}/${serviceSlug}/)\n\n${ruleLines}`; + }) + .join('\n\n'); + return `## ${group.name}\n\n${serviceBlocks}`; + }) + .join('\n\n'); + + const content = `# ${SITE_NAME} — Full Content + +> ${totalRules} copy-pasteable Prometheus alerting rules for ${totalServices} monitored services. + +## Overview + +Awesome Prometheus Alerts is the most comprehensive collection of Prometheus alerting rules. Rules are organized by category and service, with each rule containing: +- Alert name and description explaining what is happening and why it matters +- PromQL expression tested against the latest exporter version +- Severity level: critical (requires immediate attention), warning (needs attention soon), or info (awareness only) +- Duration (for field) to avoid false positives from transient spikes + +All rules are copy-paste ready YAML for direct use in Prometheus configuration files. + +## Guides + +- [AlertManager Configuration](${siteBase}/alertmanager/): Prometheus configuration (scrape_interval, evaluation_interval, rule_files), AlertManager routing with group_wait/group_interval/repeat_interval, Slack and webhook receivers, recorded rules for expensive queries, and troubleshooting notification delays. +- [Blackbox Exporter](${siteBase}/blackbox-exporter/): Deploy worldwide probes for HTTP, HTTPS, DNS, TCP, ICMP monitoring from multiple Points of Presence. Prometheus relabeling config, geohash setup for Grafana geomap panel, community dashboard links. +- [Sleep Peacefully](${siteBase}/sleep-peacefully/): Suppress noisy alerts during nights and weekends using day_of_week(), hour(), month() PromQL functions. Timezone-aware recording rules for Europe/London and Europe/Paris, public holiday suppression patterns. + +## All rules by category and service + +${sections} + +## Source + +- GitHub: ${GITHUB_URL} +- Author: ${AUTHOR_NAME} +- License: ${LICENSE_CC_BY_NAME} +`; + + return new Response(content, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); +}; diff --git a/site/src/pages/llms.txt.ts b/site/src/pages/llms.txt.ts new file mode 100644 index 0000000..078e187 --- /dev/null +++ b/site/src/pages/llms.txt.ts @@ -0,0 +1,70 @@ +import type { APIRoute } from 'astro'; +import { data, getGroupSlug, getServiceSlug, getDistUrl, getTotalRuleCount, getTotalServiceCount } from '../data/rules'; +import { SITE_NAME, SITE_URL, GITHUB_URL, AUTHOR_NAME, LICENSE_CC_BY_NAME } from '../data/site'; + +export const GET: APIRoute = () => { + const totalRules = getTotalRuleCount(); + const totalServices = getTotalServiceCount(); + const siteBase = SITE_URL.replace(/\/$/, ''); + + const categoryList = data.groups + .map((group) => { + const services = group.services.map((s) => s.name).join(', '); + return `- **${group.name}**: ${services}`; + }) + .join('\n'); + + const distTree = data.groups + .map((group) => { + const groupSlug = getGroupSlug(group); + const serviceLines = group.services + .map((service) => { + const serviceSlug = getServiceSlug(service); + const exporterLines = service.exporters + .map((exporter) => { + const url = getDistUrl(service.name, exporter.slug); + const label = exporter.name ? `${exporter.name} (${exporter.slug})` : exporter.slug; + return ` - [${label}](${url})`; + }) + .join('\n'); + return ` - [${service.name}](${siteBase}/rules/${groupSlug}/${serviceSlug}/)\n${exporterLines}`; + }) + .join('\n'); + return `- **${group.name}**\n${serviceLines}`; + }) + .join('\n'); + + const content = `# ${SITE_NAME} + +> ${totalRules}+ copy-pasteable Prometheus alerting rules for ${totalServices}+ monitored services. The definitive open-source collection for Prometheus monitoring, covering databases, Kubernetes, cloud providers, message brokers, and more. + +## Key pages + +- [Alert rules catalog](${siteBase}/rules/): Browse all ${totalRules} alerting rules organized by category and service +- [AlertManager configuration guide](${siteBase}/alertmanager/): Prometheus and AlertManager configuration examples, recorded rules, and troubleshooting +- [Blackbox Exporter guide](${siteBase}/blackbox-exporter/): Worldwide endpoint probing over HTTP, HTTPS, DNS, TCP, and ICMP with Grafana maps +- [Sleep Peacefully guide](${siteBase}/sleep-peacefully/): Time-based alert suppression and timezone-aware PromQL patterns + +## Categories and services + +${categoryList} + +## Downloadable Rule Files + +${distTree} + +## Full Content + +- [llms-full.txt](${siteBase}/llms-full.txt): Complete list of all alert rules with title, description and severity + +## About + +${SITE_NAME} is a community-driven open-source project maintained by ${AUTHOR_NAME}. +Source: ${GITHUB_URL} +License: ${LICENSE_CC_BY_NAME} +`; + + return new Response(content, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); +}; diff --git a/site/src/pages/rules/[group]/[service].astro b/site/src/pages/rules/[group]/[service].astro index 22e6a48..6888f7d 100644 --- a/site/src/pages/rules/[group]/[service].astro +++ b/site/src/pages/rules/[group]/[service].astro @@ -3,7 +3,9 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'; import Breadcrumbs from '../../../components/Breadcrumbs.astro'; import Sidebar from '../../../components/Sidebar.astro'; import ExporterSection from '../../../components/ExporterSection.astro'; +import CautionBanner from '../../../components/CautionBanner.astro'; import { data, getGroupSlug, getServiceSlug, getRuleCount, getAllServices } from '../../../data/rules'; +import { SITE_URL, schemaAuthor, schemaPublisher, schemaWebSite, SITE_DATE_PUBLISHED, SCHEMA_IN_LANGUAGE } from '../../../data/site'; export function getStaticPaths() { return getAllServices().map(({ group, service, groupSlug, serviceSlug }) => ({ @@ -22,7 +24,10 @@ const serviceIndex = group.services.findIndex((s) => getServiceSlug(s) === servi // Build exporters summary for meta description const exporterNames = service.exporters.map((e) => e.name).filter(Boolean).join(', '); -const metaDesc = `${ruleCount} Prometheus alerting rules for ${service.name}${exporterNames ? ` via ${exporterNames}` : ''}. Copy-paste ready YAML configurations for critical and warning alerts.`; +const metaDescBase = `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}${exporterNames ? ` (${exporterNames})` : ''}. Copy-paste YAML for critical and warning alerts.`; +const metaDesc = metaDescBase.length > 160 + ? `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}. Copy-paste YAML for critical and warning alerts.` + : metaDescBase; // FAQ JSON-LD for GEO (AI search engines) const faqItems = service.exporters.flatMap((exp) => @@ -36,32 +41,48 @@ const faqItems = service.exporters.flatMap((exp) => })) ); -const jsonLd = [ - { - '@context': 'https://schema.org', - '@type': 'TechArticle', - headline: `${service.name} Prometheus Alert Rules`, - description: metaDesc, - about: `Prometheus monitoring for ${service.name}`, - url: `https://samber.github.io${base}/rules/${groupSlug}/${serviceSlug}/`, - isPartOf: { - '@type': 'WebSite', - name: 'Awesome Prometheus Alerts', - url: 'https://samber.github.io/awesome-prometheus-alerts/', +const buildDate = new Date().toISOString().slice(0, 10); +const keywords = [ + 'Prometheus', 'alerting rules', service.name, 'monitoring', 'PromQL', + ...service.exporters.map((e) => e.name).filter(Boolean), +].join(', '); + +const pageUrl = `${SITE_URL}rules/${groupSlug}/${serviceSlug}/`; + +const jsonLd = { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'TechArticle', + '@id': `${pageUrl}#article`, + headline: `${service.name} Prometheus Alert Rules`, + description: metaDesc, + about: `Prometheus monitoring for ${service.name}`, + url: pageUrl, + inLanguage: SCHEMA_IN_LANGUAGE, + datePublished: SITE_DATE_PUBLISHED, + dateModified: buildDate, + author: schemaAuthor, + publisher: schemaPublisher, + isPartOf: schemaWebSite, }, - }, - ...(faqItems.length > 0 ? [{ - '@context': 'https://schema.org', - '@type': 'FAQPage', - mainEntity: faqItems, - }] : []), -]; + ...(faqItems.length > 0 ? [{ + '@type': 'FAQPage', + '@id': `${pageUrl}#faq`, + mainEntity: faqItems, + }] : []), + ], +}; ---
+ +
-

{service.name}

-

+

{service.name} Prometheus Alert Rules

+

{ruleCount} Prometheus alerting rule{ruleCount !== 1 ? 's' : ''} for {service.name}. {exporterNames && `Exported via ${exporterNames}.`} These rules cover critical and warning conditions — copy and paste the YAML into your Prometheus configuration. @@ -131,6 +155,27 @@ const jsonLd = [ ); })()} + + + {group.services.length > 1 && ( +

+

+ More in {group.name} +

+
+ {group.services + .filter((s) => getServiceSlug(s) !== serviceSlug) + .map((s) => ( + + {s.name} + + ))} +
+
+ )}
diff --git a/site/src/pages/rules/[group]/index.astro b/site/src/pages/rules/[group]/index.astro index e20d109..8c1f6ef 100644 --- a/site/src/pages/rules/[group]/index.astro +++ b/site/src/pages/rules/[group]/index.astro @@ -2,7 +2,9 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'; import Breadcrumbs from '../../../components/Breadcrumbs.astro'; import ServiceCard from '../../../components/ServiceCard.astro'; +import CautionBanner from '../../../components/CautionBanner.astro'; import { data, getGroupSlug, getServiceSlug, getRuleCount } from '../../../data/rules'; +import { SITE_URL, schemaWebSite, SITE_DATE_PUBLISHED } from '../../../data/site'; export function getStaticPaths() { return data.groups.map((group) => ({ @@ -15,11 +17,49 @@ const { group } = Astro.props; const base = import.meta.env.BASE_URL; const groupSlug = getGroupSlug(group); const totalRules = group.services.reduce((sum, svc) => sum + getRuleCount(svc), 0); + +// Build a service list for the meta description (truncated to keep under 160 chars) +const serviceNames = group.services.map((s) => s.name); +const serviceList = serviceNames.slice(0, 5).join(', ') + (serviceNames.length > 5 ? `, and ${serviceNames.length - 5} more` : ''); +const groupDesc = `Browse ${totalRules} Prometheus alerting rules for ${group.name} — ${serviceList}. Copy-paste ready YAML for ${group.name} monitoring.`; + +const pageUrl = `${SITE_URL}rules/${groupSlug}/`; +const buildDate = new Date().toISOString().slice(0, 10); + +const jsonLd = { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'CollectionPage', + name: `${group.name} — Prometheus Alert Rules`, + description: groupDesc, + url: pageUrl, + isPartOf: schemaWebSite, + }, + { + '@type': 'ItemList', + name: `${group.name} Prometheus Alert Services`, + url: pageUrl, + numberOfItems: group.services.length, + itemListElement: group.services.map((service, idx) => ({ + '@type': 'ListItem', + position: idx + 1, + name: service.name, + url: `${SITE_URL}rules/${groupSlug}/${getServiceSlug(service)}/`, + description: `${getRuleCount(service)} Prometheus alerting rules`, + })), + }, + ], +}; ---
sum + getRuleCount(svc),

+ +
{group.services.map((service) => ( diff --git a/site/src/pages/rules/index.astro b/site/src/pages/rules/index.astro index 635cc1c..bc705a3 100644 --- a/site/src/pages/rules/index.astro +++ b/site/src/pages/rules/index.astro @@ -2,23 +2,46 @@ import BaseLayout from '../../layouts/BaseLayout.astro'; import ServiceCard from '../../components/ServiceCard.astro'; import SearchWidget from '../../components/SearchWidget.astro'; -import { data, getGroupSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount, buildRedirectMap } from '../../data/rules'; +import CautionBanner from '../../components/CautionBanner.astro'; +import { data, getGroupSlug, getServiceSlug, getRuleCount, getTotalRuleCount, getTotalServiceCount, buildRedirectMap } from '../../data/rules'; +import { SITE_URL, schemaWebSite } from '../../data/site'; const base = import.meta.env.BASE_URL; const totalRules = getTotalRuleCount(); const totalServices = getTotalServiceCount(); const redirectMap = buildRedirectMap(base); + +const jsonLd = { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: 'Prometheus Alerting Rules', + description: `Browse ${totalRules} Prometheus alerting rules across ${totalServices} services. Organized by category: databases, Kubernetes, cloud providers, message brokers, and more.`, + url: `${SITE_URL}rules/`, + isPartOf: schemaWebSite, + mainEntity: { + '@type': 'ItemList', + numberOfItems: data.groups.length, + itemListElement: data.groups.map((group, i) => ({ + '@type': 'ListItem', + position: i + 1, + name: group.name, + url: `${SITE_URL}rules/${getGroupSlug(group)}/`, + })), + }, +}; ---