Compare commits

..

46 commits

Author SHA1 Message Date
dependabot[bot]
5cc052fc0a
build(deps): bump @astrojs/sitemap from 3.7.2 to 3.7.3 in /site (#567)
Bumps [@astrojs/sitemap](https://github.com/withastro/astro/tree/HEAD/packages/integrations/sitemap) from 3.7.2 to 3.7.3.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/sitemap/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/sitemap@3.7.3/packages/integrations/sitemap)

---
updated-dependencies:
- dependency-name: "@astrojs/sitemap"
  dependency-version: 3.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 18:39:30 +02:00
dependabot[bot]
63b80c8078
build(deps): bump posthog-js from 1.372.6 to 1.378.1 in /site (#566)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.372.6 to 1.378.1.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.372.6...posthog-js@1.378.1)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-version: 1.378.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 18:38:03 +02:00
dependabot[bot]
3a847e3d02
build(deps): bump astro from 6.2.1 to 6.4.2 in /site (#568)
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 6.2.1 to 6.4.2.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.4.2/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 18:37:56 +02:00
dependabot[bot]
96fc299432
build(deps): bump @iconify-json/lucide from 1.2.102 to 1.2.111 in /site (#569)
Bumps [@iconify-json/lucide](https://github.com/iconify/icon-sets) from 1.2.102 to 1.2.111.
- [Commits](https://github.com/iconify/icon-sets/commits)

---
updated-dependencies:
- dependency-name: "@iconify-json/lucide"
  dependency-version: 1.2.111
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 18:37:48 +02:00
dependabot[bot]
074736db2c
build(deps): bump yaml from 2.8.3 to 2.9.0 in /site (#570)
Bumps [yaml](https://github.com/eemeli/yaml) from 2.8.3 to 2.9.0.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.8.3...v2.9.0)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 2.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-02 18:36:52 +02:00
samber (headless)
832376c598
fix(ci): fix Publish workflow startup_failure (#565)
* fix(ci): fix Publish workflow startup_failure

* fix(ci): fix Publish workflow startup_failure
2026-05-22 20:33:32 +02:00
Samuel Berthe
5c41e54297
chore: add Bing Webmaster Tools verification meta tag 2026-05-16 21:44:37 +02:00
samber (headless)
49dbf0309f
ci: add dependabot automerge workflow (#564)
Co-authored-by: headless-samber <150833725+headless-samber@users.noreply.github.com>
2026-05-15 18:51:23 +02:00
dependabot[bot]
0cb56fdcfc
build(deps): bump devalue from 5.6.4 to 5.8.1 in /site (#563)
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.6.4 to 5.8.1.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.6.4...v5.8.1)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 18:49:32 +02:00
dependabot[bot]
56c10ee930
build(deps): bump protobufjs from 7.5.5 to 7.5.8 in /site (#562)
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.5 to 7.5.8.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.5.8/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.5...protobufjs-v7.5.8)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 12:02:59 +02:00
dependabot[bot]
2bdcdbb54e
build(deps): bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in /site (#561)
Bumps [@protobufjs/utf8](https://github.com/dcodeIO/protobuf.js) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/dcodeIO/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dcodeIO/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1)

---
updated-dependencies:
- dependency-name: "@protobufjs/utf8"
  dependency-version: 1.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 20:16:18 +02:00
dependabot[bot]
1fb78854d4
build(deps): bump postcss from 8.5.8 to 8.5.13 in /site (#560)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.13.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.13)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:52:23 +02:00
dependabot[bot]
07b24067f3
build(deps): bump astro from 6.1.6 to 6.2.1 in /site (#555)
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 6.1.6 to 6.2.1.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.2.1/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:51:51 +02:00
dependabot[bot]
09a0755bee
build(deps): bump posthog-js from 1.369.2 to 1.372.6 in /site (#556)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.369.2 to 1.372.6.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.369.2...posthog-js@1.372.6)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-version: 1.372.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:50:57 +02:00
dependabot[bot]
bbdcbb7956
build(deps): bump actions/upload-pages-artifact from 4 to 5 (#554)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:50:54 +02:00
dependabot[bot]
d1021e7c8b
build(deps): bump @tailwindcss/vite from 4.2.2 to 4.2.4 in /site (#557)
Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:49:36 +02:00
dependabot[bot]
adc5477b1e
build(deps): bump tailwindcss from 4.2.2 to 4.2.4 in /site (#558)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:49:23 +02:00
dependabot[bot]
b14bfb236b
build(deps): bump pagefind from 1.5.0 to 1.5.2 in /site (#559)
Bumps [pagefind](https://github.com/Pagefind/pagefind) from 1.5.0 to 1.5.2.
- [Release notes](https://github.com/Pagefind/pagefind/releases)
- [Changelog](https://github.com/Pagefind/pagefind/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Pagefind/pagefind/compare/v1.5.0...v1.5.2)

---
updated-dependencies:
- dependency-name: pagefind
  dependency-version: 1.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:49:15 +02:00
samber
43427987af Publish 2026-04-29 13:03:37 +00:00
nucocloud
4c9da9ed24
Add LiteLLM section to Other group with 3 alerting rules (#553)
LiteLLM (https://github.com/BerriAI/litellm) is a popular LLM-gateway/proxy
that exposes Prometheus metrics via its built-in callback. There were no
existing alerting rules for LiteLLM in this repo, despite its growing
adoption as an OpenAI/Anthropic-compatible proxy.

Added 3 alerts covering the most common operational concerns:

1. **LiteLLM provider spend over budget** — soft-warning on cumulative
   24h spend per model-name regex. Useful when LiteLLM's native
   `provider_budget_config` hard-cap is unavailable, disabled, or
   buggy (e.g. BerriAI/litellm#26701).

2. **LiteLLM proxy failed requests rate high** — error-rate ratio
   alert for downstream LLM provider availability/auth issues.

3. **LiteLLM request latency p95 high** — histogram-quantile alert
   for downstream provider response-time degradation.

All 3 rules tested via `promtool check rules` (SUCCESS) and validated
on a real LiteLLM v1.83.7 production deployment.

Reference: https://docs.litellm.ai/docs/proxy/prometheus
2026-04-29 15:03:07 +02:00
Samuel Berthe
8ca1fe591f
chore: improve seo 2026-04-26 16:52:07 +02:00
Samuel Berthe
f5f4fdfba4
ci: pin Node.js to 24 for Astro 6 compatibility
Astro 6 requires Node.js >=22.12.0; 'latest' was resolving to v20.
2026-04-22 01:49:13 +02:00
Samuel Berthe
73fff11969
chore: fix Astro deployment 2026-04-22 01:44:01 +02:00
Samuel Berthe
7fd73364a0
ci: pin Node.js to 24 for Astro 6 compatibility
Astro 6 requires Node.js >=22.12.0; 'latest' was resolving to v20.
2026-04-22 01:40:32 +02:00
dependabot[bot]
b2563bb228
build(deps): bump astro from 5.18.1 to 6.1.6 in /site (#551)
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 5.18.1 to 6.1.6.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.1.6/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.1.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 01:00:59 +02:00
samber
90f0a63450 Publish 2026-04-21 22:56:04 +00:00
Samuel Berthe
353133d23f
jaeger v2 otel exporter alerts (#552)
* feat(jaeger): add v2 OTEL-based alerts and keep v1 as legacy

Jaeger v2 is built on OpenTelemetry Collector and no longer exposes
jaeger_agent_* / jaeger_collector_* / jaeger_client_* metrics.

- Add "Embedded exporter (v2+)" with 8 rules targeting:
  - jaeger_storage_requests_total (error rate, unavailability, no reads)
  - jaeger_storage_latency_seconds_bucket (p99 latency)
  - http_server_request_duration_seconds_* via otelhttp (search errors,
    search latency, single-trace retrieval latency, service discovery errors)
- Rename existing exporter to "Embedded exporter (legacy, <v2)" with
  slug embedded-exporter-legacy and a v1 EOL notice (Dec 31 2025)

* chore: adding node version to github action
2026-04-22 00:55:36 +02:00
Samuel Berthe
eccf556bdb
fix: remove dead legacyHtmlRedirects and clean up sitemap/SEO config
- Drop legacyHtmlRedirects from astro.config.mjs (no-op on static GitHub Pages host; superseded by *.html.astro pages from e0311c3)
- Remove lastmod: new Date() from sitemap serializer (generates unstable dates on every build)
- Add sitemap .html filter comment, tighten service page meta description, include rule count in titles
2026-04-21 17:18:36 +02:00
Samuel Berthe
e0311c3c09
fix: replace Astro redirects with static meta-refresh pages for legacy .html URLs
GitHub Pages is a static host and does not support server-side redirects.
Astro redirects config only works for SSR targets, so legacyHtmlRedirects had
no effect. Replace with real .html.astro pages using meta http-equiv=refresh
and link rel=canonical. Also disallow legacy URLs in robots.txt.
2026-04-21 16:39:01 +02:00
Samuel Berthe
6d8b2b3671
doc(seo): improve seo after migration 2026-04-21 16:24:57 +02:00
Samuel Berthe
bb8ac9b0cd
fix: always include item field in BreadcrumbList JSON-LD
Fixes Google Search Console error: missing field "item" in itemListElement.
Also removes unused ahrefs site verification meta tag.
2026-04-21 16:00:30 +02:00
Samuel Berthe
6b2a5af9f9
oops 2026-04-17 20:01:55 +02:00
Samuel Berthe
b58c180dcb
improve seo 2026-04-17 19:59:24 +02:00
Samuel Berthe
6070e81097
improve seo 2026-04-17 19:53:36 +02:00
Samuel Berthe
4481bb3276
oops 2026-04-17 13:40:37 +02:00
Samuel Berthe
b4324742be
feat: replace Tinybird tracking with PostHog
- Remove Tinybird fetch pipeline from pipe.ts, keep only session/lifetime copy counters
- Wire session_copy_count and lifetime_copy_count into posthog.capture calls
- Remove Tinybird calls from sponsor click tracking, use posthog only
- Hardcode PostHog project ID and reverse proxy host (hogpost.samber.dev)
2026-04-17 12:07:50 +02:00
Samuel Berthe
5a5976c9a3
feat: track sponsor clicks with blocking event before navigation
- Add recordCopy() for copy events (bumps session/lifetime counters)
- Add recordAndWait() for blocking events (1500ms timeout, errors swallowed)
- Extract shared sponsor click handler into site/src/scripts/sponsor.ts
- Plain left-click blocks navigation until HTTP response; modifier/middle
  clicks track fire-and-forget and let the browser navigate natively
- Distinguish header vs footer placement via data-sponsor-slot attribute
2026-04-15 16:28:25 +02:00
Samuel Berthe
1c5f626046
feat: add first-party copy event pipe to Tinybird
Sends rule_copy, wget_copy events on clipboard interactions,
bypassing ad blockers. Tracks user_id (localStorage apa_uid),
session_id (sessionStorage apa_sid), session/lifetime copy counts,
full rule coordinates (group/service/exporter/rule slugs + indices),
page context, and browser environment. Event name is the Tinybird
data source name, scoped to "rule" or "exporter" per copy type.
2026-04-15 11:34:12 +02:00
Samuel Berthe
bb055773b4
feat: add GitHub star nudges across the site
- Prepend attribution comment to "Copy all" exporter clipboard
- Show inline  Star nudge on individual rule copy (3s, dismisses automatically)
- Change StatsBar stars label to "engineers starred" for social proof
- Add milestone progress bar toward 10k stars in StatsBar
- Fix header/StatsBar showing "0" when SSR GitHub API fetch fails (use "—" placeholder)
2026-04-14 21:52:27 +02:00
Samuel Berthe
d38511d7cb
chore: generate pagefind index at build time, not committed to git
- Add pagefind run step to build script in site/package.json
- Add site/public/pagefind/ to .gitignore (generated at deploy time)
2026-04-14 20:33:29 +02:00
Samuel Berthe
a56d8cf2a4
feat: refine star toast — brand orange, idle trigger, 15s auto-hide
- Style: brand orange background with white text (visible on any bg)
- Trigger: every 5 copies OR after 10 minutes of inactivity on page
- Auto-hide: 15s (reset if toast re-triggers before expiry)
- Idle timer resets on each copy
2026-04-14 20:30:08 +02:00
Samuel Berthe
25418c5db2
feat: add star nudge toast after every 5 rule copies
Show a dismissible toast (bottom-right, 20s auto-hide) nudging users
to star the GitHub repo. Fires every 5 copies via a sessionStorage
counter. CopyButton dispatches a copy-success custom event; StarToast
listens for it and manages display logic.
2026-04-14 20:09:30 +02:00
Samuel Berthe
5366d4b9ae
fix: replace invalid top-level return with isFresh flag in star scripts
Top-level return is a syntax error in ES modules. Replace the early
return pattern with an isFresh boolean guard. Also revert the hero
"Star on GitHub" button change.
2026-04-14 19:59:36 +02:00
Samuel Berthe
1f8bcca779
feat: add GitHub stars to StatsBar and fix cache early-return
Add a 4th stat ( GitHub stars) to StatsBar with build-time fallback
and live client-side fetch. Both Header and StatsBar share the same
sessionStorage cache key and skip the API call when the cache is fresh
(1h TTL), reducing fetches to at most one per session.
2026-04-14 19:51:12 +02:00
Samuel Berthe
954999dfa9
feat: replace GitHub icon with Star button and live star count
Replace the plain GitHub icon+count in the header with a proper two-zone
star button (★ Star | 8.4k). The count is seeded at build time from the
GitHub API and refreshed client-side on page load with a 1-hour
sessionStorage cache.
2026-04-14 19:47:49 +02:00
Samuel Berthe
297fd9864c
fix: use https in CC BY URL and trigger site build on _data changes 2026-04-14 16:27:01 +02:00
34 changed files with 1731 additions and 1250 deletions

View file

@ -0,0 +1,25 @@
name: Dependabot automerge
on:
pull_request:
types: [opened, synchronize]
jobs:
automerge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
permissions:
contents: write
pull-requests: write
steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v3
- name: Enable auto-merge for github-actions updates
if: steps.metadata.outputs.package-ecosystem == 'github_actions'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -26,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 'latest'
cache: npm
cache-dependency-path: site/package-lock.json
@ -45,7 +45,7 @@ jobs:
run: npx pagefind --site dist
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: site/dist

View file

@ -12,7 +12,6 @@ permissions:
jobs:
publish:
name: Publish
# Check if the PR is not from a fork
if: github.repository_owner == 'samber'
runs-on: ubuntu-latest
steps:
@ -22,15 +21,15 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
ruby-version: '3.4'
- name: Set up yq
uses: mikefarah/yq@v4
- name: Install liquid
run: |
gem install liquid -v 5.5.1
gem install liquid-cli
gem install liquid -v 5.5.1
gem install liquid-cli
- name: Build rule configuration
run: |
@ -43,7 +42,7 @@ jobs:
mkdir -p "${subdir}"
# groupName=$(echo "{% assign groupName = name | split: ' ' %}{% capture groupNameCamelcase %}{% for word in groupName %}{{ word | capitalize }} {% endfor %}{% endcapture %} {{ groupNameCamelcase | remove: ' ' | remove: '-' }}" | liquid $(echo ${service} | base64 --decode | jq -r '.name | ascii_downcase | split(" ") | join("-")'))
for exporter in $(echo ${service} | base64 --decode | jq -r '.exporters[] | @base64'); do
exporterName=$(echo ${exporter} | base64 --decode | jq -r '.slug')
cat dist/template.yml | liquid "$(echo ${exporter} | base64 --decode)" > ${subdir}/${exporterName}.yml

View file

@ -4,11 +4,13 @@ on:
pull_request:
paths:
- site/**
- _data/**
push:
branches:
- master
paths:
- site/**
- _data/**
jobs:
site-build:
@ -21,6 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 'latest'
cache: npm
cache-dependency-path: site/package-lock.json

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ test/rules/
site/node_modules/
site/dist/
site/.astro/
site/public/pagefind/
# Misc
.worktrees/

View file

@ -12,7 +12,7 @@ This repository uses a dual license:
Creative Commons Attribution 4.0 International License (CC BY 4.0)
http://creativecommons.org/licenses/by/4.0/
https://creativecommons.org/licenses/by/4.0/
---

View file

@ -5733,9 +5733,82 @@ groups:
- name: Jaeger
exporters:
- name: Embedded exporter
- name: Embedded exporter (v2+)
slug: embedded-exporter
doc_url: https://www.jaegertracing.io/docs/latest/monitoring/
doc_url: https://www.jaegertracing.io/docs/2.dev/operations/monitoring/
comments: |
Jaeger v2 is built on OpenTelemetry Collector and exposes metrics on port 8888 (/metrics).
It emits standard otelcol_* pipeline metrics alongside Jaeger-specific storage and query metrics.
For span ingestion pipeline alerts (refused spans, export failures, queue saturation),
use the OpenTelemetry Collector rules instead.
rules:
- name: Jaeger high storage error rate
description: "Jaeger on {{ $labels.instance }} is experiencing {{ $value | humanize }}% storage errors on {{ $labels.operation }}."
query: '100 * sum(rate(jaeger_storage_requests_total{result="err"}[1m])) by (instance, job, namespace, operation) / sum(rate(jaeger_storage_requests_total[1m])) by (instance, job, namespace, operation) > 1 and sum(rate(jaeger_storage_requests_total[1m])) by (instance, job, namespace, operation) > 0'
severity: warning
for: 5m
- name: Jaeger slow storage operations
description: "Jaeger on {{ $labels.instance }} storage p99 latency for {{ $labels.operation }} is {{ $value | humanizeDuration }}."
query: 'histogram_quantile(0.99, sum(rate(jaeger_storage_latency_seconds_bucket[5m])) by (le, instance, job, namespace, operation)) > 1'
severity: warning
for: 5m
comments: |
Threshold of 1s is a rough default. Adjust based on your storage backend and data volume.
- name: Jaeger query service high error rate
description: "Jaeger query service on {{ $labels.instance }} is returning {{ $value | humanize }}% HTTP 5xx errors."
query: '100 * sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces",http_response_status_code=~"5.."}[1m])) by (instance, job, namespace) / sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces"}[1m])) by (instance, job, namespace) > 1 and sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces"}[1m])) by (instance, job, namespace) > 0'
severity: warning
for: 5m
comments: |
Filters on http_route="/api/traces" (the trace search endpoint). The http_server_request_duration_seconds
metric is emitted by the otelhttp middleware used by the Jaeger query service.
- name: Jaeger query service slow responses
description: "Jaeger query service on {{ $labels.instance }} p99 response latency is {{ $value | humanizeDuration }}."
query: 'histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{http_route="/api/traces"}[5m])) by (le, instance, job, namespace)) > 2'
severity: warning
for: 5m
comments: |
Threshold of 2s is a rough default. Adjust based on your storage backend and data volume.
- name: Jaeger storage completely unavailable
description: "Jaeger on {{ $labels.instance }} has 100% storage errors for {{ $labels.operation }} — storage backend may be down."
query: 'sum(rate(jaeger_storage_requests_total{result="err"}[1m])) by (instance, job, namespace, operation) > 0 and sum(rate(jaeger_storage_requests_total{result="ok"}[1m])) by (instance, job, namespace, operation) == 0'
severity: critical
for: 2m
comments: |
Fires when all storage operations for a given type are failing and none are succeeding.
Indicates the storage backend (Cassandra, Elasticsearch, etc.) is likely unreachable or misconfigured.
- name: Jaeger slow single trace retrieval
description: "Jaeger on {{ $labels.instance }} p99 latency for single trace retrieval is {{ $value | humanizeDuration }}."
query: 'histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{http_route="/api/traces/{traceID}"}[5m])) by (le, instance, job, namespace)) > 5'
severity: warning
for: 5m
comments: |
Single trace retrieval (/api/traces/{traceID}) can be slower than search, especially for large traces.
Threshold of 5s is a rough default.
- name: Jaeger service discovery errors
description: "Jaeger on {{ $labels.instance }} is returning {{ $value | humanize }}% HTTP 5xx errors on the services endpoint."
query: '100 * sum(rate(http_server_request_duration_seconds_count{http_route="/api/services",http_response_status_code=~"5.."}[1m])) by (instance, job, namespace) / sum(rate(http_server_request_duration_seconds_count{http_route="/api/services"}[1m])) by (instance, job, namespace) > 1 and sum(rate(http_server_request_duration_seconds_count{http_route="/api/services"}[1m])) by (instance, job, namespace) > 0'
severity: warning
for: 5m
comments: |
Errors on /api/services indicate the storage backend cannot return the list of instrumented services,
which breaks the Jaeger UI service selector.
- name: Jaeger no storage reads succeeding
description: "Jaeger on {{ $labels.instance }} has no successful storage reads for {{ $labels.operation }} in the past 15 minutes."
query: 'sum(increase(jaeger_storage_requests_total{result="ok"}[15m])) by (instance, job, namespace, operation) == 0 and sum(increase(jaeger_storage_requests_total[15m])) by (instance, job, namespace, operation) > 0'
severity: warning
for: 5m
comments: |
Fires when an operation (e.g. find_traces, get_services) has received requests but none succeeded.
May indicate a persistent storage error or a backend that is slow to recover.
- name: Embedded exporter (legacy, <v2)
slug: embedded-exporter-legacy
doc_url: https://www.jaegertracing.io/docs/1.x/monitoring/
comments: |
These rules target Jaeger v1.x metrics (jaeger_* prefix).
Jaeger v1 reached end-of-life on December 31, 2025.
For Jaeger v2+, use the "Embedded exporter (v2+)" rules instead.
Note: jaeger-agent was deprecated in v1.35 and removed in v2.0.
rules:
- name: Jaeger agent HTTP server errors
description: "Jaeger agent on {{ $labels.instance }} is experiencing {{ $value | humanize }}% HTTP server errors."
@ -5845,3 +5918,28 @@ groups:
severity: critical
comments: |
Threshold of 20ms. Adjust based on your expected database latency.
- name: LiteLLM
exporters:
- slug: embedded-exporter
doc_url: https://docs.litellm.ai/docs/proxy/prometheus
rules:
- name: LiteLLM provider spend over budget
description: "Cumulative spend for an LLM provider has exceeded the daily budget threshold. Replace the regex `(claude-|anthropic/).*` with your provider's model-name pattern. Useful as a soft-warning when `provider_budget_config` hard-cap is unavailable or disabled."
query: 'sum(increase(litellm_spend_metric_total{model=~"(claude-|anthropic/).*"}[24h])) > 1'
severity: warning
for: 5m
comments: |
The threshold (1) is in USD. The `model` label carries the resolved model-name (post-routing).
PromQL `increase()` requires ≥2 datapoints with growth-difference to extrapolate positive —
for brand-new counter series this needs ≥2 distinct request bursts ≥1 scrape-cycle apart.
- name: LiteLLM proxy failed requests rate high
description: "LiteLLM proxy is returning failed responses to clients (>5% error rate over 5min). Investigate downstream LLM provider availability or auth issues."
query: 'sum(rate(litellm_proxy_failed_requests_metric_total[5m])) / sum(rate(litellm_proxy_total_requests_metric_total[5m])) > 0.05'
severity: warning
for: 10m
- name: LiteLLM request latency p95 high
description: "LiteLLM request total latency p95 exceeds 10 seconds over 5min. Check downstream LLM provider response-times and proxy queue-depth."
query: 'histogram_quantile(0.95, sum(rate(litellm_request_total_latency_metric_bucket[5m])) by (le)) > 10'
severity: warning
for: 10m

View file

@ -0,0 +1,82 @@
groups:
- name: EmbeddedExporterLegacy
# These rules target Jaeger v1.x metrics (jaeger_* prefix).
# Jaeger v1 reached end-of-life on December 31, 2025.
# For Jaeger v2+, use the "Embedded exporter (v2+)" rules instead.
# Note: jaeger-agent was deprecated in v1.35 and removed in v2.0.
rules:
- alert: JaegerAgentHttpServerErrors
expr: '100 * sum(rate(jaeger_agent_http_server_errors_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_http_server_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_agent_http_server_total[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger agent HTTP server errors (instance {{ $labels.instance }})
description: "Jaeger agent on {{ $labels.instance }} is experiencing {{ $value | humanize }}% HTTP server errors.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerClientRpcRequestErrors
expr: '100 * sum(rate(jaeger_client_jaeger_rpc_http_requests{status_code=~"4xx|5xx"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_client_jaeger_rpc_http_requests[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_client_jaeger_rpc_http_requests[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger client RPC request errors (instance {{ $labels.instance }})
description: "Jaeger client on {{ $labels.instance }} is experiencing {{ $value | humanize }}% RPC HTTP errors.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerClientSpansDropped
expr: '100 * sum(rate(jaeger_reporter_spans{result=~"dropped|err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_reporter_spans[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_reporter_spans[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger client spans dropped (instance {{ $labels.instance }})
description: "Jaeger client on {{ $labels.instance }} is dropping {{ $value | humanize }}% of spans.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerAgentSpansDropped
expr: '100 * sum(rate(jaeger_agent_reporter_batches_failures_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_reporter_batches_submitted_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_agent_reporter_batches_submitted_total[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger agent spans dropped (instance {{ $labels.instance }})
description: "Jaeger agent on {{ $labels.instance }} is dropping {{ $value | humanize }}% of span batches.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerCollectorDroppingSpans
expr: '100 * sum(rate(jaeger_collector_spans_dropped_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_collector_spans_received_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_collector_spans_received_total[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger collector dropping spans (instance {{ $labels.instance }})
description: "Jaeger collector on {{ $labels.instance }} is dropping {{ $value | humanize }}% of spans.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerSamplingUpdateFailing
expr: '100 * sum(rate(jaeger_sampler_queries{result="err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_sampler_queries[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_sampler_queries[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger sampling update failing (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is failing {{ $value | humanize }}% of sampling policy updates.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerThrottlingUpdateFailing
expr: '100 * sum(rate(jaeger_throttler_updates{result="err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_throttler_updates[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_throttler_updates[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger throttling update failing (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is failing {{ $value | humanize }}% of throttling policy updates.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerQueryRequestFailures
expr: '100 * sum(rate(jaeger_query_requests_total{result="err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_query_requests_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_query_requests_total[1m])) by (instance, job, namespace) > 0'
for: 15m
labels:
severity: warning
annotations:
summary: Jaeger query request failures (instance {{ $labels.instance }})
description: "Jaeger query on {{ $labels.instance }} is failing {{ $value | humanize }}% of requests.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

View file

@ -2,77 +2,93 @@ groups:
- name: EmbeddedExporter
# Jaeger v2 is built on OpenTelemetry Collector and exposes metrics on port 8888 (/metrics).
# It emits standard otelcol_* pipeline metrics alongside Jaeger-specific storage and query metrics.
# For span ingestion pipeline alerts (refused spans, export failures, queue saturation),
# use the OpenTelemetry Collector rules instead.
rules:
- alert: JaegerAgentHttpServerErrors
expr: '100 * sum(rate(jaeger_agent_http_server_errors_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_http_server_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_agent_http_server_total[1m])) by (instance, job, namespace) > 0'
for: 15m
- alert: JaegerHighStorageErrorRate
expr: '100 * sum(rate(jaeger_storage_requests_total{result="err"}[1m])) by (instance, job, namespace, operation) / sum(rate(jaeger_storage_requests_total[1m])) by (instance, job, namespace, operation) > 1 and sum(rate(jaeger_storage_requests_total[1m])) by (instance, job, namespace, operation) > 0'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger agent HTTP server errors (instance {{ $labels.instance }})
description: "Jaeger agent on {{ $labels.instance }} is experiencing {{ $value | humanize }}% HTTP server errors.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger high storage error rate (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is experiencing {{ $value | humanize }}% storage errors on {{ $labels.operation }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerClientRpcRequestErrors
expr: '100 * sum(rate(jaeger_client_jaeger_rpc_http_requests{status_code=~"4xx|5xx"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_client_jaeger_rpc_http_requests[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_client_jaeger_rpc_http_requests[1m])) by (instance, job, namespace) > 0'
for: 15m
# Threshold of 1s is a rough default. Adjust based on your storage backend and data volume.
- alert: JaegerSlowStorageOperations
expr: 'histogram_quantile(0.99, sum(rate(jaeger_storage_latency_seconds_bucket[5m])) by (le, instance, job, namespace, operation)) > 1'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger client RPC request errors (instance {{ $labels.instance }})
description: "Jaeger client on {{ $labels.instance }} is experiencing {{ $value | humanize }}% RPC HTTP errors.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger slow storage operations (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} storage p99 latency for {{ $labels.operation }} is {{ $value | humanizeDuration }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerClientSpansDropped
expr: '100 * sum(rate(jaeger_reporter_spans{result=~"dropped|err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_reporter_spans[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_reporter_spans[1m])) by (instance, job, namespace) > 0'
for: 15m
# Filters on http_route="/api/traces" (the trace search endpoint). The http_server_request_duration_seconds
# metric is emitted by the otelhttp middleware used by the Jaeger query service.
- alert: JaegerQueryServiceHighErrorRate
expr: '100 * sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces",http_response_status_code=~"5.."}[1m])) by (instance, job, namespace) / sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces"}[1m])) by (instance, job, namespace) > 1 and sum(rate(http_server_request_duration_seconds_count{http_route="/api/traces"}[1m])) by (instance, job, namespace) > 0'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger client spans dropped (instance {{ $labels.instance }})
description: "Jaeger client on {{ $labels.instance }} is dropping {{ $value | humanize }}% of spans.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger query service high error rate (instance {{ $labels.instance }})
description: "Jaeger query service on {{ $labels.instance }} is returning {{ $value | humanize }}% HTTP 5xx errors.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerAgentSpansDropped
expr: '100 * sum(rate(jaeger_agent_reporter_batches_failures_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_reporter_batches_submitted_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_agent_reporter_batches_submitted_total[1m])) by (instance, job, namespace) > 0'
for: 15m
# Threshold of 2s is a rough default. Adjust based on your storage backend and data volume.
- alert: JaegerQueryServiceSlowResponses
expr: 'histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{http_route="/api/traces"}[5m])) by (le, instance, job, namespace)) > 2'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger agent spans dropped (instance {{ $labels.instance }})
description: "Jaeger agent on {{ $labels.instance }} is dropping {{ $value | humanize }}% of span batches.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger query service slow responses (instance {{ $labels.instance }})
description: "Jaeger query service on {{ $labels.instance }} p99 response latency is {{ $value | humanizeDuration }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerCollectorDroppingSpans
expr: '100 * sum(rate(jaeger_collector_spans_dropped_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_collector_spans_received_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_collector_spans_received_total[1m])) by (instance, job, namespace) > 0'
for: 15m
# Fires when all storage operations for a given type are failing and none are succeeding.
# Indicates the storage backend (Cassandra, Elasticsearch, etc.) is likely unreachable or misconfigured.
- alert: JaegerStorageCompletelyUnavailable
expr: 'sum(rate(jaeger_storage_requests_total{result="err"}[1m])) by (instance, job, namespace, operation) > 0 and sum(rate(jaeger_storage_requests_total{result="ok"}[1m])) by (instance, job, namespace, operation) == 0'
for: 2m
labels:
severity: warning
severity: critical
annotations:
summary: Jaeger collector dropping spans (instance {{ $labels.instance }})
description: "Jaeger collector on {{ $labels.instance }} is dropping {{ $value | humanize }}% of spans.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger storage completely unavailable (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} has 100% storage errors for {{ $labels.operation }} — storage backend may be down.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerSamplingUpdateFailing
expr: '100 * sum(rate(jaeger_sampler_queries{result="err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_sampler_queries[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_sampler_queries[1m])) by (instance, job, namespace) > 0'
for: 15m
# Single trace retrieval (/api/traces/{traceID}) can be slower than search, especially for large traces.
# Threshold of 5s is a rough default.
- alert: JaegerSlowSingleTraceRetrieval
expr: 'histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{http_route="/api/traces/{traceID}"}[5m])) by (le, instance, job, namespace)) > 5'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger sampling update failing (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is failing {{ $value | humanize }}% of sampling policy updates.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger slow single trace retrieval (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} p99 latency for single trace retrieval is {{ $value | humanizeDuration }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerThrottlingUpdateFailing
expr: '100 * sum(rate(jaeger_throttler_updates{result="err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_throttler_updates[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_throttler_updates[1m])) by (instance, job, namespace) > 0'
for: 15m
# Errors on /api/services indicate the storage backend cannot return the list of instrumented services,
# which breaks the Jaeger UI service selector.
- alert: JaegerServiceDiscoveryErrors
expr: '100 * sum(rate(http_server_request_duration_seconds_count{http_route="/api/services",http_response_status_code=~"5.."}[1m])) by (instance, job, namespace) / sum(rate(http_server_request_duration_seconds_count{http_route="/api/services"}[1m])) by (instance, job, namespace) > 1 and sum(rate(http_server_request_duration_seconds_count{http_route="/api/services"}[1m])) by (instance, job, namespace) > 0'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger throttling update failing (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is failing {{ $value | humanize }}% of throttling policy updates.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger service discovery errors (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is returning {{ $value | humanize }}% HTTP 5xx errors on the services endpoint.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: JaegerQueryRequestFailures
expr: '100 * sum(rate(jaeger_query_requests_total{result="err"}[1m])) by (instance, job, namespace) / sum(rate(jaeger_query_requests_total[1m])) by (instance, job, namespace) > 1 and sum(rate(jaeger_query_requests_total[1m])) by (instance, job, namespace) > 0'
for: 15m
# Fires when an operation (e.g. find_traces, get_services) has received requests but none succeeded.
# May indicate a persistent storage error or a backend that is slow to recover.
- alert: JaegerNoStorageReadsSucceeding
expr: 'sum(increase(jaeger_storage_requests_total{result="ok"}[15m])) by (instance, job, namespace, operation) == 0 and sum(increase(jaeger_storage_requests_total[15m])) by (instance, job, namespace, operation) > 0'
for: 5m
labels:
severity: warning
annotations:
summary: Jaeger query request failures (instance {{ $labels.instance }})
description: "Jaeger query on {{ $labels.instance }} is failing {{ $value | humanize }}% of requests.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
summary: Jaeger no storage reads succeeding (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} has no successful storage reads for {{ $labels.operation }} in the past 15 minutes.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

View file

@ -0,0 +1,36 @@
groups:
- name: EmbeddedExporter
rules:
# The threshold (1) is in USD. The `model` label carries the resolved model-name (post-routing).
# PromQL `increase()` requires ≥2 datapoints with growth-difference to extrapolate positive —
# for brand-new counter series this needs ≥2 distinct request bursts ≥1 scrape-cycle apart.
- alert: LitellmProviderSpendOverBudget
expr: 'sum(increase(litellm_spend_metric_total{model=~"(claude-|anthropic/).*"}[24h])) > 1'
for: 5m
labels:
severity: warning
annotations:
summary: LiteLLM provider spend over budget (instance {{ $labels.instance }})
description: "Cumulative spend for an LLM provider has exceeded the daily budget threshold. Replace the regex `(claude-|anthropic/).*` with your provider's model-name pattern. Useful as a soft-warning when `provider_budget_config` hard-cap is unavailable or disabled.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: LitellmProxyFailedRequestsRateHigh
expr: 'sum(rate(litellm_proxy_failed_requests_metric_total[5m])) / sum(rate(litellm_proxy_total_requests_metric_total[5m])) > 0.05'
for: 10m
labels:
severity: warning
annotations:
summary: LiteLLM proxy failed requests rate high (instance {{ $labels.instance }})
description: "LiteLLM proxy is returning failed responses to clients (>5% error rate over 5min). Investigate downstream LLM provider availability or auth issues.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: LitellmRequestLatencyP95High
expr: 'histogram_quantile(0.95, sum(rate(litellm_request_total_latency_metric_bucket[5m])) by (le)) > 10'
for: 10m
labels:
severity: warning
annotations:
summary: LiteLLM request latency p95 high (instance {{ $labels.instance }})
description: "LiteLLM request total latency p95 exceeds 10 seconds over 5min. Check downstream LLM provider response-times and proxy queue-depth.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

1
site/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

View file

@ -67,10 +67,15 @@ const base = '/awesome-prometheus-alerts';
export default defineConfig({
site: 'https://samber.github.io',
base,
redirects: buildRedirects(base),
redirects: { ...buildRedirects(base) },
output: 'static',
integrations: [
sitemap({
/** Exclude redirect source URLs from the sitemap.
* Astro generates static HTML redirect files for every entry in `redirects`, and the
* sitemap plugin naively picks them up. We must explicitly filter them out so that Google
* only indexes canonical destinations, not the redirect intermediaries. */
filter: (page) => !page.includes('.html'),
serialize(item) {
const path = new URL(item.url).pathname;
const segments = path.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
@ -78,22 +83,22 @@ export default defineConfig({
if (segments.length <= 1) {
// Homepage
return { ...item, changefreq: 'weekly', priority: 1.0, lastmod: new Date() };
return { ...item, changefreq: 'weekly', priority: 1.0 };
}
if (segments.length === 2 && segments[1] === 'rules') {
// /rules/ index
return { ...item, changefreq: 'weekly', priority: 0.9, lastmod: new Date() };
return { ...item, changefreq: 'weekly', priority: 0.9 };
}
if (segments.length === 3 && segments[1] === 'rules') {
// /rules/[group]/ index
return { ...item, changefreq: 'monthly', priority: 0.7, lastmod: new Date() };
return { ...item, changefreq: 'monthly', priority: 0.7 };
}
if (segments.length === 4 && segments[1] === 'rules') {
// /rules/[group]/[service]/ — main content pages
return { ...item, changefreq: 'monthly', priority: 0.8, lastmod: new Date() };
return { ...item, changefreq: 'monthly', priority: 0.8 };
}
// Guide pages and others
return { ...item, changefreq: 'yearly', priority: 0.6, lastmod: new Date() };
return { ...item, changefreq: 'yearly', priority: 0.6 };
},
}),
icon(),

1996
site/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,21 +4,23 @@
"version": "1.0.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build": "astro build && pagefind --site dist --output-path public/pagefind",
"pagefind": "pagefind --site dist --output-path public/pagefind",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.0.0",
"@tailwindcss/vite": "^4.2.2",
"@iconify-json/lucide": "^1.2.102",
"@astrojs/sitemap": "^3.7.3",
"@iconify-json/lucide": "^1.2.111",
"@rollup/plugin-yaml": "^4.0.0",
"astro": "^5.0.0",
"@tailwindcss/vite": "^4.2.4",
"astro": "^6.4.2",
"astro-icon": "^1.0.0",
"js-yaml": "^4.1.0",
"pagefind": "^1.0.0",
"tailwindcss": "^4.2.2",
"yaml": "^2.8.3"
"pagefind": "^1.5.2",
"posthog-js": "^1.378.1",
"tailwindcss": "^4.2.4",
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.0"

View file

@ -15,6 +15,8 @@ const { items, base } = Astro.props;
const allItems = [{ label: 'Home', href: `${base}/` }, ...items];
const currentUrl = Astro.url.href;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
@ -22,7 +24,7 @@ const jsonLd = {
'@type': 'ListItem',
position: i + 1,
name: item.label,
...(item.href ? { item: `${SITE_ORIGIN}${item.href}` } : {}),
item: item.href ? `${SITE_ORIGIN}${item.href}` : currentUrl,
})),
};
---

View file

@ -1,31 +1,54 @@
---
import { GITHUB_URL } from '../data/site';
interface Props {
targetId: string;
label?: string;
variant?: 'icon' | 'text';
class?: string;
withAttribution?: boolean;
nudge?: boolean;
copyData?: Record<string, unknown> | null;
}
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '' } = Astro.props;
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '', withAttribution = false, nudge = false, copyData = null } = Astro.props;
const btnId = `copy-btn-${targetId}`;
const nudgeId = `star-nudge-${btnId}`;
---
{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>
<span class={`inline-flex items-center gap-1 ${extraClass}`}>
<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"
>
<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>
{nudge && (
<a
id={nudgeId}
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
class="hidden items-center gap-0.5 text-xs text-yellow-500 hover:text-yellow-600 dark:text-yellow-400 dark:hover:text-yellow-300 transition-colors whitespace-nowrap"
aria-label="Star on GitHub"
>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
Star
</a>
)}
</span>
) : (
<button
id={btnId}
@ -43,7 +66,7 @@ const btnId = `copy-btn-${targetId}`;
</button>
)}
<script define:vars={{ btnId }}>
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL, copyData }}>
const btn = document.getElementById(btnId);
if (!(btn instanceof HTMLButtonElement)) return;
if (btn.dataset.copyBound === 'true') return;
@ -55,13 +78,17 @@ const btnId = `copy-btn-${targetId}`;
const target = document.getElementById(targetId);
if (!target) return;
const text = target.textContent ?? '';
let text = target.textContent ?? '';
text = withAttribution
? `# Source: ${GITHUB_URL}\n${text.trim()}`
: text.trim();
try {
await navigator.clipboard.writeText(text.trim());
await navigator.clipboard.writeText(text);
} catch {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = text.trim();
ta.value = text;
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
document.body.appendChild(ta);
ta.select();
@ -86,5 +113,23 @@ const btnId = `copy-btn-${targetId}`;
copyLabel?.classList.remove('hidden');
copiedLabel?.classList.add('hidden');
}, 2000);
// Inline star nudge
if (nudge) {
const nudgeEl = document.getElementById(nudgeId);
if (nudgeEl) {
nudgeEl.classList.remove('hidden');
nudgeEl.classList.add('inline-flex');
setTimeout(() => {
nudgeEl.classList.add('hidden');
nudgeEl.classList.remove('inline-flex');
}, 3000);
}
}
if (copyData) {
window.dispatchEvent(new CustomEvent('apa-copy', { detail: copyData }));
}
window.dispatchEvent(new CustomEvent('copy-success'));
});
</script>

View file

@ -8,7 +8,10 @@ interface Props {
exporter: Exporter;
service: Service;
groupIndex: number;
groupName: string;
groupSlug: string;
serviceIndex: number;
serviceSlug: string;
exporterIndex: number;
showExporterNumber: boolean;
}
@ -17,7 +20,10 @@ const {
exporter,
service,
groupIndex,
groupName,
groupSlug,
serviceIndex,
serviceSlug,
exporterIndex,
showExporterNumber,
} = Astro.props;
@ -68,7 +74,26 @@ const exporterPrefix = showExporterNumber
</div>
<div class="flex items-center gap-2">
<CopyButton targetId={allRulesId} label="Copy all" variant="text" />
<CopyButton
targetId={allRulesId}
label="Copy all"
variant="text"
withAttribution={true}
copyData={{
name: 'rule_copy',
scope: 'exporter',
group_index: groupIndex,
group_name: groupName,
group_slug: groupSlug,
service_index: serviceIndex,
service_name: service.name,
service_slug: serviceSlug,
exporter_index: exporterIndex,
exporter_name: exporter.name ?? null,
exporter_slug: exporter.slug,
with_attribution: true,
}}
/>
</div>
</div>
@ -88,7 +113,23 @@ const exporterPrefix = showExporterNumber
</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" />
<CopyButton
targetId={wgetId}
variant="icon"
copyData={{
name: 'wget_copy',
scope: 'exporter',
group_index: groupIndex,
group_name: groupName,
group_slug: groupSlug,
service_index: serviceIndex,
service_name: service.name,
service_slug: serviceSlug,
exporter_index: exporterIndex,
exporter_name: exporter.name ?? null,
exporter_slug: exporter.slug,
}}
/>
</div>
)}
@ -114,6 +155,26 @@ const exporterPrefix = showExporterNumber
rule={rule}
anchorId={anchorId}
ruleNumber={ruleNumber}
copyData={{
name: 'rule_copy',
scope: 'rule',
group_index: groupIndex,
group_name: groupName,
group_slug: groupSlug,
service_index: serviceIndex,
service_name: service.name,
service_slug: serviceSlug,
exporter_index: exporterIndex,
exporter_name: exporter.name ?? null,
exporter_slug: exporter.slug,
rule_index: ruleIdx + 1,
rule_slug: anchorId,
rule_name: rule.name,
rule_severity: rule.severity,
rule_for: rule.for ?? null,
rule_number: ruleNumber,
with_attribution: false,
}}
/>
);
})}

View file

@ -76,7 +76,7 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Sponsors</h3>
<div class="space-y-4">
{sponsors.map((s) => (
<a href={s.url} target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity">
<a href={s.url} target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity" data-sponsor-name={s.name} data-sponsor-slot="footer">
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-6" />
</a>
))}
@ -98,3 +98,8 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get
</div>
</div>
</footer>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>

View file

@ -25,7 +25,7 @@ try {
}
} catch {}
const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars)) : '—';
---
<!-- Main header -->
@ -83,15 +83,18 @@ const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(star
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub repository"
class="flex items-center gap-1.5 text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
aria-label="Star on GitHub"
class="flex items-center gap-0 rounded-md border border-slate-200 dark:border-slate-700 overflow-hidden text-xs font-medium hover:border-slate-300 dark:hover:border-slate-600 transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
</svg>
{stars > 0 && (
<span class="text-xs font-medium tabular-nums">{starsLabel}</span>
)}
<span class="flex items-center gap-1.5 px-2.5 py-1 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors">
<svg class="w-3.5 h-3.5 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
Star
</span>
<span id="github-stars" class="px-2.5 py-1 bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border-l border-slate-200 dark:border-slate-700 tabular-nums">
{starsLabel}
</span>
</a>
</nav>
@ -123,7 +126,7 @@ const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(star
<div class="max-w-7xl mx-auto flex items-center justify-center gap-3">
<span class="text-xs font-medium tracking-wider uppercase text-slate-400 dark:text-slate-500">Sponsored by</span>
{sponsors.map((s) => (
<a href={s.url} target="_blank" rel="noopener noreferrer" class="hover:opacity-75 transition-opacity" title={s.name}>
<a href={s.url} target="_blank" rel="noopener noreferrer" class="hover:opacity-75 transition-opacity" title={s.name} data-sponsor-name={s.name} data-sponsor-slot="header">
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-10 w-auto" />
</a>
))}
@ -146,6 +149,11 @@ const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(star
</div>
</header>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>
<script>
const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu');
@ -158,4 +166,37 @@ const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(star
hamburger?.classList.toggle('hidden', isOpen);
closeIcon?.classList.toggle('hidden', !isOpen);
});
// Live star count — fetch from GitHub API on page load, cache in sessionStorage
const starsEl = document.getElementById('github-stars');
if (starsEl) {
const CACHE_KEY = 'gh_stars_apa';
const CACHE_TTL = 3600 * 1000; // 1 hour
function formatStars(n: number): string {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
}
const cached = sessionStorage.getItem(CACHE_KEY);
let isFresh = false;
if (cached) {
const { value, ts } = JSON.parse(cached);
if (Date.now() - ts < CACHE_TTL) {
starsEl.textContent = formatStars(value);
isFresh = true;
}
}
if (!isFresh) fetch('https://api.github.com/repos/samber/awesome-prometheus-alerts', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => r.ok ? r.json() : null)
.then((data) => {
if (data?.stargazers_count) {
starsEl.textContent = formatStars(data.stargazers_count);
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ value: data.stargazers_count, ts: Date.now() }));
}
})
.catch(() => {});
}
</script>

View file

@ -8,9 +8,10 @@ interface Props {
rule: Rule;
anchorId: string;
ruleNumber: string;
copyData?: Record<string, unknown> | null;
}
const { rule, anchorId, ruleNumber } = Astro.props;
const { rule, anchorId, ruleNumber, copyData = null } = Astro.props;
const yamlContent = formatRuleAsYaml(rule);
const codeId = `code-${anchorId}`;
---
@ -37,7 +38,7 @@ const codeId = `code-${anchorId}`;
<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" />
<CopyButton targetId={codeId} variant="icon" nudge={true} copyData={copyData} />
</div>
</div>

View file

@ -41,5 +41,13 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
} else {
initPagefind();
}
// Track search queries via PostHog
searchEl.addEventListener('pagefind:search', (e: Event) => {
const query = (e as CustomEvent<{ query: string }>).detail?.query;
if (query) {
window.posthog?.capture('search_performed', { query });
}
});
}
</script>

View file

@ -0,0 +1,78 @@
---
import { GITHUB_URL } from '../data/site';
---
<!-- Star nudge toast — shown every 5 copies -->
<div
id="star-toast"
role="status"
aria-live="polite"
class="fixed bottom-6 right-6 z-50 hidden max-w-sm w-full"
>
<div class="flex items-center gap-3 bg-brand rounded-xl shadow-xl px-4 py-3">
<svg class="w-4 h-4 text-white/80 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<p class="flex-1 text-sm text-white">
Useful? <a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
class="font-semibold underline underline-offset-2 hover:no-underline transition-all"
>Star on GitHub</a> — it helps others discover it.
</p>
<button
id="star-toast-close"
aria-label="Dismiss"
class="flex-shrink-0 text-white/60 hover:text-white 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<script>
const COPY_COUNT_KEY = 'gh_star_copy_count';
const SHOW_EVERY = 5;
const IDLE_MS = 10 * 60 * 1000; // 10 minutes
const toast = document.getElementById('star-toast');
const closeBtn = document.getElementById('star-toast-close');
let autoHideTimer: ReturnType<typeof setTimeout> | null = null;
let idleTimer: ReturnType<typeof setTimeout> | null = null;
function displayToast() {
if (!toast) return;
if (autoHideTimer) clearTimeout(autoHideTimer);
toast.classList.remove('hidden');
autoHideTimer = setTimeout(hideToast, 15000);
}
function hideToast() {
toast?.classList.add('hidden');
if (autoHideTimer) { clearTimeout(autoHideTimer); autoHideTimer = null; }
}
function resetIdleTimer() {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(displayToast, IDLE_MS);
}
function onCopy() {
// Increment counter and show every 5th copy
const count = (parseInt(sessionStorage.getItem(COPY_COUNT_KEY) ?? '0', 10) || 0) + 1;
sessionStorage.setItem(COPY_COUNT_KEY, String(count));
if (count % SHOW_EVERY === 0) displayToast();
// Reset the idle timer on each copy
resetIdleTimer();
}
closeBtn?.addEventListener('click', hideToast);
window.addEventListener('copy-success', onCopy);
// Start idle timer on page load
resetIdleTimer();
</script>

View file

@ -1,9 +1,29 @@
---
import { getTotalRuleCount, getTotalExporterCount, data } from '../data/rules';
import { GITHUB_API_REPO_URL, GITHUB_URL } from '../data/site';
const totalRules = getTotalRuleCount();
const totalExporters = getTotalExporterCount();
const totalGroups = data.groups.length;
const STAR_MILESTONE = 10000;
const MILESTONE_LABEL = '10k';
let stars = 0;
try {
const res = await fetch(GITHUB_API_REPO_URL, {
headers: { 'Accept': 'application/vnd.github+json' }
});
if (res.ok) {
const json = await res.json();
stars = json.stargazers_count ?? 0;
}
} catch {}
const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars)) : '—';
const progressPct = stars > 0 ? Math.min(100, (stars / STAR_MILESTONE) * 100).toFixed(1) : '0';
const starsFormatted = stars > 0 ? stars.toLocaleString('en') : '';
const milestoneFormatted = STAR_MILESTONE.toLocaleString('en');
---
<div class="flex flex-wrap justify-center gap-6 sm:gap-10 py-4 text-center">
@ -19,4 +39,85 @@ const totalGroups = data.groups.length;
<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>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" class="group">
<div class="text-2xl font-bold text-brand dark:text-brand-dark flex items-center justify-center gap-1">
<svg class="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
<span id="statsbar-stars">{starsLabel}</span>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 group-hover:text-brand dark:group-hover:text-brand-dark transition-colors">engineers starred</div>
</a>
</div>
<!-- Milestone progress bar -->
<div id="star-milestone-row" class={`mt-2 px-4 ${stars === 0 ? 'opacity-0' : ''}`}>
<div class="max-w-xs mx-auto">
<div class="flex items-center justify-between text-xs text-slate-400 dark:text-slate-500 mb-1">
<span>
<span id="statsbar-stars-count">{starsFormatted}</span><span id="statsbar-milestone-suffix">{stars > 0 ? ` / ${milestoneFormatted} — help us reach ${MILESTONE_LABEL}!` : ''}</span>
</span>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" class="text-yellow-500 hover:text-yellow-600 dark:text-yellow-400 dark:hover:text-yellow-300 font-medium transition-colors flex items-center gap-0.5 ml-2 flex-shrink-0">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
Star it
</a>
</div>
<div class="bg-slate-100 dark:bg-slate-800 rounded-full h-1.5">
<div
id="statsbar-star-bar"
class="bg-yellow-400 dark:bg-yellow-500 h-1.5 rounded-full transition-all duration-700"
style={`width: ${progressPct}%`}
></div>
</div>
</div>
</div>
<script define:vars={{ STAR_MILESTONE, MILESTONE_LABEL }}>
const starsEl = document.getElementById('statsbar-stars');
const starsCountEl = document.getElementById('statsbar-stars-count');
const milestoneSuffixEl = document.getElementById('statsbar-milestone-suffix');
const barEl = document.getElementById('statsbar-star-bar');
const milestoneRow = document.getElementById('star-milestone-row');
if (starsEl) {
const CACHE_KEY = 'gh_stars_apa';
const CACHE_TTL = 3600 * 1000;
function fmt(n) {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
}
function updateMilestone(n) {
if (starsCountEl) starsCountEl.textContent = n.toLocaleString('en');
if (milestoneSuffixEl) milestoneSuffixEl.textContent = ` / ${STAR_MILESTONE.toLocaleString('en')} — help us reach ${MILESTONE_LABEL}!`;
if (barEl) barEl.style.width = `${Math.min(100, (n / STAR_MILESTONE) * 100).toFixed(1)}%`;
if (milestoneRow) milestoneRow.classList.remove('opacity-0');
}
const cached = sessionStorage.getItem(CACHE_KEY);
let isFresh = false;
if (cached) {
const { value, ts } = JSON.parse(cached);
if (Date.now() - ts < CACHE_TTL) {
starsEl.textContent = fmt(value);
updateMilestone(value);
isFresh = true;
}
}
if (!isFresh) fetch('https://api.github.com/repos/samber/awesome-prometheus-alerts', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => r.ok ? r.json() : null)
.then((data) => {
if (data?.stargazers_count) {
starsEl.textContent = fmt(data.stargazers_count);
updateMilestone(data.stargazers_count);
sessionStorage.setItem(CACHE_KEY, JSON.stringify({ value: data.stargazers_count, ts: Date.now() }));
}
})
.catch(() => {});
}
</script>

View file

@ -0,0 +1,12 @@
---
// PostHog analytics snippet
---
<script is:inline>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_rzLVwu5RRWhbJqQqpgcsNHyIaZOUs3Pw5laOq1sTdtI', {
api_host: 'https://hogpost.samber.dev',
defaults: '2026-01-30',
disable_session_recording: true,
// autocapture: false,
});
</script>

10
site/src/env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/// <reference types="astro/client" />
interface Window {
posthog?: {
capture: (event: string, properties?: Record<string, unknown>) => void;
identify: (distinctId: string, properties?: Record<string, unknown>) => void;
reset: () => void;
captureException: (error: unknown) => void;
};
}

View file

@ -2,7 +2,9 @@
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import StarToast from '../components/StarToast.astro';
import SEO from '../components/SEO.astro';
import PostHog from '../components/posthog.astro';
import { SITE_ORIGIN, AUTHOR_NAME } from '../data/site';
interface Props {
@ -42,6 +44,7 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#E6522C" />
<meta name="author" content={AUTHOR_NAME} />
<meta name="msvalidate.01" content="4576E3F85783A82149A0DB35A150F7EB" />
{noIndex && <meta name="robots" content="noindex" />}
<SEO
@ -83,6 +86,8 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
gtag('js', new Date());
gtag('config', 'G-GDF25KKVNL');
</script>
<PostHog />
</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">
@ -96,5 +101,21 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
</main>
<Footer base={base} />
<StarToast />
<script>
import { bumpSessionCount, bumpLifetimeCount } from '../scripts/pipe';
window.addEventListener('apa-copy', (e: Event) => {
const { name, ...data } = (e as CustomEvent<Record<string, unknown>>).detail;
const counts = { session_copy_count: bumpSessionCount(), lifetime_copy_count: bumpLifetimeCount() };
if (name === 'rule_copy' && data.scope === 'rule') {
window.posthog?.capture('rule_copied', { ...data, ...counts });
} else if (name === 'rule_copy' && data.scope === 'exporter') {
window.posthog?.capture('all_rules_copied', { ...data, ...counts });
} else if (name === 'wget_copy') {
window.posthog?.capture('wget_copied', { ...data, ...counts });
}
});
</script>
<script src="https://analytics.ahrefs.com/analytics.js" data-key="ZTtNkZmI6cXSnXXtm2Jbzg" async></script>
</body>
</html>

View file

@ -0,0 +1,16 @@
---
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const destination = `${base}/alertmanager/`;
---
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<link rel="canonical" href={destination} />
<meta http-equiv="refresh" content={`0; url=${destination}`} />
</head>
<body>
<p>Redirecting to <a href={destination}>{destination}</a>…</p>
</body>
</html>

View file

@ -0,0 +1,16 @@
---
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const destination = `${base}/blackbox-exporter/`;
---
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<link rel="canonical" href={destination} />
<meta http-equiv="refresh" content={`0; url=${destination}`} />
</head>
<body>
<p>Redirecting to <a href={destination}>{destination}</a>…</p>
</body>
</html>

View file

@ -0,0 +1,16 @@
---
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const destination = `${base}/rules/`;
---
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<link rel="canonical" href={destination} />
<meta http-equiv="refresh" content={`0; url=${destination}`} />
</head>
<body>
<p>Redirecting to <a href={destination}>{destination}</a>…</p>
</body>
</html>

View file

@ -22,12 +22,10 @@ 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
// Build exporters summary for keywords (kept for <meta keywords> but removed from description)
const exporterNames = service.exporters.map((e) => e.name).filter(Boolean).join(', ');
const metaDescBase = `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}${exporterNames ? ` (${exporterNames})` : ''}. Copy-paste YAML for critical and warning alerts.`;
const metaDesc = metaDescBase.length > 160
? `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}. Copy-paste YAML for critical and warning alerts.`
: metaDescBase;
// Description: lead with count + service + format signals. Exporter names go in keywords, not here.
const metaDesc = `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}. Critical and warning YAML snippets — copy-paste into your Prometheus config or wget download.`;
// FAQ JSON-LD for GEO (AI search engines)
const faqItems = service.exporters.flatMap((exp) =>
@ -55,7 +53,7 @@ const jsonLd = {
{
'@type': 'TechArticle',
'@id': `${pageUrl}#article`,
headline: `${service.name} Prometheus Alert Rules`,
headline: `${service.name} Prometheus Alert Rules (${ruleCount})`,
description: metaDesc,
about: `Prometheus monitoring for ${service.name}`,
url: pageUrl,
@ -76,7 +74,7 @@ const jsonLd = {
---
<BaseLayout
title={`${service.name} Alert Rules | Awesome Prometheus Alerts`}
title={`${service.name} Prometheus Alert Rules (${ruleCount}) | Awesome Prometheus Alerts`}
description={metaDesc}
ogType="article"
keywords={keywords}
@ -106,8 +104,6 @@ const jsonLd = {
<!-- Main content -->
<div class="flex-1 min-w-0">
<CautionBanner />
<!-- Page header -->
<div class="mb-6 pb-6 border-b border-slate-200 dark:border-slate-800">
<h1 class="text-2xl font-bold text-slate-900 dark:text-white mb-2">{service.name} Prometheus Alert Rules</h1>
@ -118,13 +114,18 @@ const jsonLd = {
</p>
</div>
<CautionBanner />
<!-- Exporters -->
{service.exporters.map((exporter, expIdx) => (
<ExporterSection
exporter={exporter}
service={service}
groupIndex={groupIndex}
groupName={group.name}
groupSlug={groupSlug}
serviceIndex={serviceIndex}
serviceSlug={serviceSlug}
exporterIndex={expIdx + 1}
showExporterNumber={service.exporters.length > 1}
/>

View file

@ -14,7 +14,7 @@ const redirectMap = buildRedirectMap(base);
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Prometheus Alerting Rules',
name: `${totalRules} Prometheus Alerting Rules for ${totalServices} Services`,
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,
@ -32,7 +32,7 @@ const jsonLd = {
---
<BaseLayout
title="Prometheus Alerting Rules | Awesome Prometheus Alerts"
title={`${totalRules} Prometheus Alerting Rules for ${totalServices} Services | Awesome Prometheus Alerts`}
description={`Browse ${totalRules} Prometheus alerting rules across ${totalServices} services. Organized by category: databases, Kubernetes, cloud providers, message brokers, and more.`}
jsonLd={jsonLd}
>

View file

@ -0,0 +1,16 @@
---
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const destination = `${base}/sleep-peacefully/`;
---
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<link rel="canonical" href={destination} />
<meta http-equiv="refresh" content={`0; url=${destination}`} />
</head>
<body>
<p>Redirecting to <a href={destination}>{destination}</a>…</p>
</body>
</html>

15
site/src/scripts/pipe.ts Normal file
View file

@ -0,0 +1,15 @@
export function bumpSessionCount(): number {
try {
const n = parseInt(sessionStorage.getItem('apa_sc') ?? '0', 10);
sessionStorage.setItem('apa_sc', String(n + 1));
return n + 1;
} catch { return 0; }
}
export function bumpLifetimeCount(): number {
try {
const n = parseInt(localStorage.getItem('apa_lc') ?? '0', 10);
localStorage.setItem('apa_lc', String(n + 1));
return n + 1;
} catch { return 0; }
}

View file

@ -0,0 +1,28 @@
export function initSponsorClickTracking(): void {
document.querySelectorAll<HTMLAnchorElement>('a[data-sponsor-name]').forEach((a) => {
a.addEventListener('click', (e) => {
const me = e as MouseEvent;
const href = a.href;
const sponsorName = a.dataset.sponsorName!;
const sponsorSlot = a.dataset.sponsorSlot!;
const eventData = { sponsor_name: sponsorName, sponsor_url: href, sponsor_slot: sponsorSlot };
// Modifier / non-primary clicks: track fire-and-forget, let browser handle navigation
if (me.button !== 0 || me.metaKey || me.ctrlKey || me.shiftKey) {
window.posthog?.capture('sponsor_clicked', eventData);
return;
}
// Plain left-click: block navigation until event is recorded
e.preventDefault();
window.posthog?.capture('sponsor_clicked', eventData);
// Open blank tab now (inside user gesture) to avoid popup-blocker after await
const w = a.target === '_blank' ? window.open('', '_blank') : null;
if (w) {
w.location.href = href;
} else {
window.open(href, '_blank') ?? (window.location.href = href);
}
});
});
}