Compare commits

..

No commits in common. "master" and "2026-04-10.1" have entirely different histories.

34 changed files with 1251 additions and 1732 deletions

View file

@ -1,25 +0,0 @@
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: 'latest'
node-version: 20
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@v5
uses: actions/upload-pages-artifact@v4
with:
path: site/dist

View file

@ -12,6 +12,7 @@ permissions:
jobs:
publish:
name: Publish
# Check if the PR is not from a fork
if: github.repository_owner == 'samber'
runs-on: ubuntu-latest
steps:
@ -21,15 +22,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: |
@ -42,7 +43,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,13 +4,11 @@ on:
pull_request:
paths:
- site/**
- _data/**
push:
branches:
- master
paths:
- site/**
- _data/**
jobs:
site-build:
@ -23,7 +21,6 @@ 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,7 +7,6 @@ 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)
https://creativecommons.org/licenses/by/4.0/
http://creativecommons.org/licenses/by/4.0/
---

View file

@ -5733,82 +5733,9 @@ groups:
- name: Jaeger
exporters:
- name: Embedded exporter (v2+)
- name: Embedded exporter
slug: embedded-exporter
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.
doc_url: https://www.jaegertracing.io/docs/latest/monitoring/
rules:
- name: Jaeger agent HTTP server errors
description: "Jaeger agent on {{ $labels.instance }} is experiencing {{ $value | humanize }}% HTTP server errors."
@ -5918,28 +5845,3 @@ 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

@ -1,82 +0,0 @@
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,93 +2,77 @@ 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: 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
- 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 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 }}"
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 }}"
# 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
- 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 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 }}"
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 }}"
# 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
- 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 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 }}"
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 }}"
# 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
- 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 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 }}"
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 }}"
# 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: critical
annotations:
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 }}"
# 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
- 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 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 }}"
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 }}"
# 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
- 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 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 }}"
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 }}"
# 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
- 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 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 }}"
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

@ -1,36 +0,0 @@
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
View file

@ -1 +0,0 @@
.env

View file

@ -67,15 +67,10 @@ 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);
@ -83,22 +78,22 @@ export default defineConfig({
if (segments.length <= 1) {
// Homepage
return { ...item, changefreq: 'weekly', priority: 1.0 };
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 };
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 };
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 };
return { ...item, changefreq: 'monthly', priority: 0.8, lastmod: new Date() };
}
// Guide pages and others
return { ...item, changefreq: 'yearly', priority: 0.6 };
return { ...item, changefreq: 'yearly', priority: 0.6, lastmod: new Date() };
},
}),
icon(),

1992
site/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -15,8 +15,6 @@ 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',
@ -24,7 +22,7 @@ const jsonLd = {
'@type': 'ListItem',
position: i + 1,
name: item.label,
item: item.href ? `${SITE_ORIGIN}${item.href}` : currentUrl,
...(item.href ? { item: `${SITE_ORIGIN}${item.href}` } : {}),
})),
};
---

View file

@ -1,54 +1,31 @@
---
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 = '', withAttribution = false, nudge = false, copyData = null } = Astro.props;
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '' } = Astro.props;
const btnId = `copy-btn-${targetId}`;
const nudgeId = `star-nudge-${btnId}`;
---
{variant === 'icon' ? (
<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}
data-copy-target={targetId}
aria-label="Copy to clipboard"
class={`copy-btn inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded text-slate-400 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors ${extraClass}`}
>
<svg class="copy-icon w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span class="copy-label sr-only">Copy</span>
<span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span>
</button>
) : (
<button
id={btnId}
@ -66,7 +43,7 @@ const nudgeId = `star-nudge-${btnId}`;
</button>
)}
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL, copyData }}>
<script define:vars={{ btnId }}>
const btn = document.getElementById(btnId);
if (!(btn instanceof HTMLButtonElement)) return;
if (btn.dataset.copyBound === 'true') return;
@ -78,17 +55,13 @@ const nudgeId = `star-nudge-${btnId}`;
const target = document.getElementById(targetId);
if (!target) return;
let text = target.textContent ?? '';
text = withAttribution
? `# Source: ${GITHUB_URL}\n${text.trim()}`
: text.trim();
const text = target.textContent ?? '';
try {
await navigator.clipboard.writeText(text);
await navigator.clipboard.writeText(text.trim());
} catch {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = text;
ta.value = text.trim();
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
document.body.appendChild(ta);
ta.select();
@ -113,23 +86,5 @@ const nudgeId = `star-nudge-${btnId}`;
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,10 +8,7 @@ interface Props {
exporter: Exporter;
service: Service;
groupIndex: number;
groupName: string;
groupSlug: string;
serviceIndex: number;
serviceSlug: string;
exporterIndex: number;
showExporterNumber: boolean;
}
@ -20,10 +17,7 @@ const {
exporter,
service,
groupIndex,
groupName,
groupSlug,
serviceIndex,
serviceSlug,
exporterIndex,
showExporterNumber,
} = Astro.props;
@ -74,26 +68,7 @@ const exporterPrefix = showExporterNumber
</div>
<div class="flex items-center gap-2">
<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,
}}
/>
<CopyButton targetId={allRulesId} label="Copy all" variant="text" />
</div>
</div>
@ -113,23 +88,7 @@ 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"
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,
}}
/>
<CopyButton targetId={wgetId} variant="icon" />
</div>
)}
@ -155,26 +114,6 @@ 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" data-sponsor-name={s.name} data-sponsor-slot="footer">
<a href={s.url} target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity">
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-6" />
</a>
))}
@ -98,8 +98,3 @@ 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 > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars)) : '—';
const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
---
<!-- Main header -->
@ -83,18 +83,15 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
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"
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"
>
<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>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
</svg>
{stars > 0 && (
<span class="text-xs font-medium tabular-nums">{starsLabel}</span>
)}
</a>
</nav>
@ -126,7 +123,7 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
<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} data-sponsor-name={s.name} data-sponsor-slot="header">
<a href={s.url} target="_blank" rel="noopener noreferrer" class="hover:opacity-75 transition-opacity" title={s.name}>
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-10 w-auto" />
</a>
))}
@ -149,11 +146,6 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
</div>
</header>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>
<script>
const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu');
@ -166,37 +158,4 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
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,10 +8,9 @@ interface Props {
rule: Rule;
anchorId: string;
ruleNumber: string;
copyData?: Record<string, unknown> | null;
}
const { rule, anchorId, ruleNumber, copyData = null } = Astro.props;
const { rule, anchorId, ruleNumber } = Astro.props;
const yamlContent = formatRuleAsYaml(rule);
const codeId = `code-${anchorId}`;
---
@ -38,7 +37,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" nudge={true} copyData={copyData} />
<CopyButton targetId={codeId} variant="icon" />
</div>
</div>

View file

@ -41,13 +41,5 @@ 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

@ -1,78 +0,0 @@
---
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,29 +1,9 @@
---
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">
@ -39,85 +19,4 @@ const milestoneFormatted = STAR_MILESTONE.toLocaleString('en');
<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

@ -1,12 +0,0 @@
---
// 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
View file

@ -1,10 +0,0 @@
/// <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,9 +2,7 @@
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 {
@ -44,7 +42,6 @@ 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
@ -86,8 +83,6 @@ 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">
@ -101,21 +96,5 @@ 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

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

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

@ -1,16 +0,0 @@
---
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,10 +22,12 @@ 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 keywords (kept for <meta keywords> but removed from description)
// Build exporters summary for meta description
const exporterNames = service.exporters.map((e) => e.name).filter(Boolean).join(', ');
// 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.`;
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) =>
@ -53,7 +55,7 @@ const jsonLd = {
{
'@type': 'TechArticle',
'@id': `${pageUrl}#article`,
headline: `${service.name} Prometheus Alert Rules (${ruleCount})`,
headline: `${service.name} Prometheus Alert Rules`,
description: metaDesc,
about: `Prometheus monitoring for ${service.name}`,
url: pageUrl,
@ -74,7 +76,7 @@ const jsonLd = {
---
<BaseLayout
title={`${service.name} Prometheus Alert Rules (${ruleCount}) | Awesome Prometheus Alerts`}
title={`${service.name} Alert Rules | Awesome Prometheus Alerts`}
description={metaDesc}
ogType="article"
keywords={keywords}
@ -104,6 +106,8 @@ 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>
@ -114,18 +118,13 @@ 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: `${totalRules} Prometheus Alerting Rules for ${totalServices} Services`,
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,
@ -32,7 +32,7 @@ const jsonLd = {
---
<BaseLayout
title={`${totalRules} Prometheus Alerting Rules for ${totalServices} Services | Awesome Prometheus Alerts`}
title="Prometheus Alerting Rules | Awesome Prometheus Alerts"
description={`Browse ${totalRules} Prometheus alerting rules across ${totalServices} services. Organized by category: databases, Kubernetes, cloud providers, message brokers, and more.`}
jsonLd={jsonLd}
>

View file

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

View file

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

@ -1,28 +0,0 @@
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);
}
});
});
}