Compare commits

..

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

34 changed files with 1259 additions and 1405 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 - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 'latest' node-version: 20
cache: npm cache: npm
cache-dependency-path: site/package-lock.json cache-dependency-path: site/package-lock.json
@ -45,7 +45,7 @@ jobs:
run: npx pagefind --site dist run: npx pagefind --site dist
- name: Upload Pages artifact - name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5 uses: actions/upload-pages-artifact@v4
with: with:
path: site/dist path: site/dist

View file

@ -12,6 +12,7 @@ permissions:
jobs: jobs:
publish: publish:
name: Publish name: Publish
# Check if the PR is not from a fork
if: github.repository_owner == 'samber' if: github.repository_owner == 'samber'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -21,15 +22,15 @@ jobs:
- name: Set up Ruby - name: Set up Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.4' ruby-version: 3.4
- name: Set up yq - name: Set up yq
uses: mikefarah/yq@v4 uses: mikefarah/yq@v4
- name: Install liquid - name: Install liquid
run: | run: |
gem install liquid -v 5.5.1 gem install liquid -v 5.5.1
gem install liquid-cli gem install liquid-cli
- name: Build rule configuration - name: Build rule configuration
run: | run: |

View file

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

1
.gitignore vendored
View file

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

View file

@ -12,7 +12,7 @@ This repository uses a dual license:
Creative Commons Attribution 4.0 International License (CC BY 4.0) 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 - name: Jaeger
exporters: exporters:
- name: Embedded exporter (v2+) - name: Embedded exporter
slug: embedded-exporter slug: embedded-exporter
doc_url: https://www.jaegertracing.io/docs/2.dev/operations/monitoring/ doc_url: https://www.jaegertracing.io/docs/latest/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: rules:
- name: Jaeger agent HTTP server errors - name: Jaeger agent HTTP server errors
description: "Jaeger agent on {{ $labels.instance }} is experiencing {{ $value | humanize }}% HTTP server errors." description: "Jaeger agent on {{ $labels.instance }} is experiencing {{ $value | humanize }}% HTTP server errors."
@ -5918,28 +5845,3 @@ groups:
severity: critical severity: critical
comments: | comments: |
Threshold of 20ms. Adjust based on your expected database latency. 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 - 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: rules:
- alert: JaegerHighStorageErrorRate - alert: JaegerAgentHttpServerErrors
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' 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: 5m for: 15m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger high storage error rate (instance {{ $labels.instance }}) summary: Jaeger agent HTTP server errors (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} is experiencing {{ $value | humanize }}% storage errors on {{ $labels.operation }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 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: JaegerClientRpcRequestErrors
- alert: JaegerSlowStorageOperations 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'
expr: 'histogram_quantile(0.99, sum(rate(jaeger_storage_latency_seconds_bucket[5m])) by (le, instance, job, namespace, operation)) > 1' for: 15m
for: 5m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger slow storage operations (instance {{ $labels.instance }}) summary: Jaeger client RPC request errors (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} storage p99 latency for {{ $labels.operation }} is {{ $value | humanizeDuration }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 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 - alert: JaegerClientSpansDropped
# metric is emitted by the otelhttp middleware used by the Jaeger query service. 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'
- alert: JaegerQueryServiceHighErrorRate for: 15m
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: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger query service high error rate (instance {{ $labels.instance }}) summary: Jaeger client spans dropped (instance {{ $labels.instance }})
description: "Jaeger query service on {{ $labels.instance }} is returning {{ $value | humanize }}% HTTP 5xx errors.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 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: JaegerAgentSpansDropped
- alert: JaegerQueryServiceSlowResponses 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'
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: 15m
for: 5m
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger query service slow responses (instance {{ $labels.instance }}) summary: Jaeger agent spans dropped (instance {{ $labels.instance }})
description: "Jaeger query service on {{ $labels.instance }} p99 response latency is {{ $value | humanizeDuration }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 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. - alert: JaegerCollectorDroppingSpans
# Indicates the storage backend (Cassandra, Elasticsearch, etc.) is likely unreachable or misconfigured. 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'
- alert: JaegerStorageCompletelyUnavailable for: 15m
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
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger slow single trace retrieval (instance {{ $labels.instance }}) summary: Jaeger collector dropping spans (instance {{ $labels.instance }})
description: "Jaeger on {{ $labels.instance }} p99 latency for single trace retrieval is {{ $value | humanizeDuration }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 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, - alert: JaegerSamplingUpdateFailing
# which breaks the Jaeger UI service selector. 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'
- alert: JaegerServiceDiscoveryErrors for: 15m
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: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger service discovery errors (instance {{ $labels.instance }}) summary: Jaeger sampling update failing (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 }}" 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. - alert: JaegerThrottlingUpdateFailing
# May indicate a persistent storage error or a backend that is slow to recover. 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'
- alert: JaegerNoStorageReadsSucceeding for: 15m
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: labels:
severity: warning severity: warning
annotations: annotations:
summary: Jaeger no storage reads succeeding (instance {{ $labels.instance }}) summary: Jaeger throttling update failing (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 }}" 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({ export default defineConfig({
site: 'https://samber.github.io', site: 'https://samber.github.io',
base, base,
redirects: { ...buildRedirects(base) }, redirects: buildRedirects(base),
output: 'static', output: 'static',
integrations: [ integrations: [
sitemap({ 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) { serialize(item) {
const path = new URL(item.url).pathname; const path = new URL(item.url).pathname;
const segments = path.replace(/^\/|\/$/g, '').split('/').filter(Boolean); const segments = path.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
@ -83,22 +78,22 @@ export default defineConfig({
if (segments.length <= 1) { if (segments.length <= 1) {
// Homepage // 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') { if (segments.length === 2 && segments[1] === 'rules') {
// /rules/ index // /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') { if (segments.length === 3 && segments[1] === 'rules') {
// /rules/[group]/ index // /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') { if (segments.length === 4 && segments[1] === 'rules') {
// /rules/[group]/[service]/ — main content pages // /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 // Guide pages and others
return { ...item, changefreq: 'yearly', priority: 0.6 }; return { ...item, changefreq: 'yearly', priority: 0.6, lastmod: new Date() };
}, },
}), }),
icon(), icon(),

1671
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", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build && pagefind --site dist --output-path public/pagefind", "build": "astro build",
"pagefind": "pagefind --site dist --output-path public/pagefind",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/sitemap": "^3.7.3", "@astrojs/sitemap": "^3.0.0",
"@iconify-json/lucide": "^1.2.111", "@tailwindcss/vite": "^4.2.2",
"@iconify-json/lucide": "^1.2.102",
"@rollup/plugin-yaml": "^4.0.0", "@rollup/plugin-yaml": "^4.0.0",
"@tailwindcss/vite": "^4.2.4", "astro": "^5.0.0",
"astro": "^6.4.6",
"astro-icon": "^1.0.0", "astro-icon": "^1.0.0",
"js-yaml": "^4.2.0", "js-yaml": "^4.1.0",
"pagefind": "^1.5.2", "pagefind": "^1.0.0",
"posthog-js": "^1.391.2", "tailwindcss": "^4.2.2",
"tailwindcss": "^4.2.4", "yaml": "^2.8.3"
"yaml": "^2.9.0"
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.0" "@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 allItems = [{ label: 'Home', href: `${base}/` }, ...items];
const currentUrl = Astro.url.href;
const jsonLd = { const jsonLd = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'BreadcrumbList', '@type': 'BreadcrumbList',
@ -24,7 +22,7 @@ const jsonLd = {
'@type': 'ListItem', '@type': 'ListItem',
position: i + 1, position: i + 1,
name: item.label, 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 { interface Props {
targetId: string; targetId: string;
label?: string; label?: string;
variant?: 'icon' | 'text'; variant?: 'icon' | 'text';
class?: string; 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 btnId = `copy-btn-${targetId}`;
const nudgeId = `star-nudge-${btnId}`;
--- ---
{variant === 'icon' ? ( {variant === 'icon' ? (
<span class={`inline-flex items-center gap-1 ${extraClass}`}> <button
<button id={btnId}
id={btnId} data-copy-target={targetId}
data-copy-target={targetId} aria-label="Copy to clipboard"
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}`}
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">
<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" />
<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> <svg class="check-icon w-3.5 h-3.5 hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> </svg>
</svg> <span class="copy-label sr-only">Copy</span>
<span class="copy-label sr-only">Copy</span> <span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span>
<span class="copied-label hidden text-green-500 not-sr-only text-xs">Copied!</span> </button>
</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 <button
id={btnId} id={btnId}
@ -66,7 +43,7 @@ const nudgeId = `star-nudge-${btnId}`;
</button> </button>
)} )}
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL, copyData }}> <script define:vars={{ btnId }}>
const btn = document.getElementById(btnId); const btn = document.getElementById(btnId);
if (!(btn instanceof HTMLButtonElement)) return; if (!(btn instanceof HTMLButtonElement)) return;
if (btn.dataset.copyBound === 'true') return; if (btn.dataset.copyBound === 'true') return;
@ -78,17 +55,13 @@ const nudgeId = `star-nudge-${btnId}`;
const target = document.getElementById(targetId); const target = document.getElementById(targetId);
if (!target) return; if (!target) return;
let text = target.textContent ?? ''; const text = target.textContent ?? '';
text = withAttribution
? `# Source: ${GITHUB_URL}\n${text.trim()}`
: text.trim();
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text.trim());
} catch { } catch {
// Fallback for older browsers // Fallback for older browsers
const ta = document.createElement('textarea'); const ta = document.createElement('textarea');
ta.value = text; ta.value = text.trim();
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;'; ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
document.body.appendChild(ta); document.body.appendChild(ta);
ta.select(); ta.select();
@ -113,23 +86,5 @@ const nudgeId = `star-nudge-${btnId}`;
copyLabel?.classList.remove('hidden'); copyLabel?.classList.remove('hidden');
copiedLabel?.classList.add('hidden'); copiedLabel?.classList.add('hidden');
}, 2000); }, 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> </script>

View file

@ -8,10 +8,7 @@ interface Props {
exporter: Exporter; exporter: Exporter;
service: Service; service: Service;
groupIndex: number; groupIndex: number;
groupName: string;
groupSlug: string;
serviceIndex: number; serviceIndex: number;
serviceSlug: string;
exporterIndex: number; exporterIndex: number;
showExporterNumber: boolean; showExporterNumber: boolean;
} }
@ -20,10 +17,7 @@ const {
exporter, exporter,
service, service,
groupIndex, groupIndex,
groupName,
groupSlug,
serviceIndex, serviceIndex,
serviceSlug,
exporterIndex, exporterIndex,
showExporterNumber, showExporterNumber,
} = Astro.props; } = Astro.props;
@ -74,26 +68,7 @@ const exporterPrefix = showExporterNumber
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CopyButton <CopyButton targetId={allRulesId} label="Copy all" variant="text" />
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>
</div> </div>
@ -113,23 +88,7 @@ const exporterPrefix = showExporterNumber
</svg> </svg>
<pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre> <pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre>
</div> </div>
<CopyButton <CopyButton targetId={wgetId} variant="icon" />
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> </div>
)} )}
@ -155,26 +114,6 @@ const exporterPrefix = showExporterNumber
rule={rule} rule={rule}
anchorId={anchorId} anchorId={anchorId}
ruleNumber={ruleNumber} 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> <h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Sponsors</h3>
<div class="space-y-4"> <div class="space-y-4">
{sponsors.map((s) => ( {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" /> <img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-6" />
</a> </a>
))} ))}
@ -98,8 +98,3 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get
</div> </div>
</div> </div>
</footer> </footer>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>

View file

@ -25,7 +25,7 @@ try {
} }
} catch {} } 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 --> <!-- Main header -->
@ -83,18 +83,15 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
href={GITHUB_URL} href={GITHUB_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Star on GitHub" aria-label="GitHub repository"
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" 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-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<svg class="w-3.5 h-3.5 text-yellow-500" 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" />
<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>
</svg> {stars > 0 && (
Star <span class="text-xs font-medium tabular-nums">{starsLabel}</span>
</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> </a>
</nav> </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"> <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> <span class="text-xs font-medium tracking-wider uppercase text-slate-400 dark:text-slate-500">Sponsored by</span>
{sponsors.map((s) => ( {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" /> <img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-10 w-auto" />
</a> </a>
))} ))}
@ -149,11 +146,6 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
</div> </div>
</header> </header>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>
<script> <script>
const btn = document.getElementById('mobile-menu-btn'); const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu'); 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); hamburger?.classList.toggle('hidden', isOpen);
closeIcon?.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> </script>

View file

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

View file

@ -41,13 +41,5 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
} else { } else {
initPagefind(); 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> </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 { getTotalRuleCount, getTotalExporterCount, data } from '../data/rules';
import { GITHUB_API_REPO_URL, GITHUB_URL } from '../data/site';
const totalRules = getTotalRuleCount(); const totalRules = getTotalRuleCount();
const totalExporters = getTotalExporterCount(); const totalExporters = getTotalExporterCount();
const totalGroups = data.groups.length; 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"> <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-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 class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">categories</div>
</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> </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 '../styles/global.css';
import Header from '../components/Header.astro'; import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import StarToast from '../components/StarToast.astro';
import SEO from '../components/SEO.astro'; import SEO from '../components/SEO.astro';
import PostHog from '../components/posthog.astro';
import { SITE_ORIGIN, AUTHOR_NAME } from '../data/site'; import { SITE_ORIGIN, AUTHOR_NAME } from '../data/site';
interface Props { 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="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#E6522C" /> <meta name="theme-color" content="#E6522C" />
<meta name="author" content={AUTHOR_NAME} /> <meta name="author" content={AUTHOR_NAME} />
<meta name="msvalidate.01" content="4576E3F85783A82149A0DB35A150F7EB" />
{noIndex && <meta name="robots" content="noindex" />} {noIndex && <meta name="robots" content="noindex" />}
<SEO <SEO
@ -86,8 +83,6 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'G-GDF25KKVNL'); gtag('config', 'G-GDF25KKVNL');
</script> </script>
<PostHog />
</head> </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"> <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"> <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> </main>
<Footer base={base} /> <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> </body>
</html> </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 groupIndex = data.groups.findIndex((g) => getGroupSlug(g) === groupSlug) + 1;
const serviceIndex = group.services.findIndex((s) => getServiceSlug(s) === serviceSlug) + 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(', '); 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 metaDescBase = `${ruleCount} ready-to-use Prometheus alert rules for ${service.name}${exporterNames ? ` (${exporterNames})` : ''}. Copy-paste YAML for critical and warning alerts.`;
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 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) // FAQ JSON-LD for GEO (AI search engines)
const faqItems = service.exporters.flatMap((exp) => const faqItems = service.exporters.flatMap((exp) =>
@ -53,7 +55,7 @@ const jsonLd = {
{ {
'@type': 'TechArticle', '@type': 'TechArticle',
'@id': `${pageUrl}#article`, '@id': `${pageUrl}#article`,
headline: `${service.name} Prometheus Alert Rules (${ruleCount})`, headline: `${service.name} Prometheus Alert Rules`,
description: metaDesc, description: metaDesc,
about: `Prometheus monitoring for ${service.name}`, about: `Prometheus monitoring for ${service.name}`,
url: pageUrl, url: pageUrl,
@ -74,7 +76,7 @@ const jsonLd = {
--- ---
<BaseLayout <BaseLayout
title={`${service.name} Prometheus Alert Rules (${ruleCount}) | Awesome Prometheus Alerts`} title={`${service.name} Alert Rules | Awesome Prometheus Alerts`}
description={metaDesc} description={metaDesc}
ogType="article" ogType="article"
keywords={keywords} keywords={keywords}
@ -104,6 +106,8 @@ const jsonLd = {
<!-- Main content --> <!-- Main content -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<CautionBanner />
<!-- Page header --> <!-- Page header -->
<div class="mb-6 pb-6 border-b border-slate-200 dark:border-slate-800"> <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> <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> </p>
</div> </div>
<CautionBanner />
<!-- Exporters --> <!-- Exporters -->
{service.exporters.map((exporter, expIdx) => ( {service.exporters.map((exporter, expIdx) => (
<ExporterSection <ExporterSection
exporter={exporter} exporter={exporter}
service={service} service={service}
groupIndex={groupIndex} groupIndex={groupIndex}
groupName={group.name}
groupSlug={groupSlug}
serviceIndex={serviceIndex} serviceIndex={serviceIndex}
serviceSlug={serviceSlug}
exporterIndex={expIdx + 1} exporterIndex={expIdx + 1}
showExporterNumber={service.exporters.length > 1} showExporterNumber={service.exporters.length > 1}
/> />

View file

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