mirror of
https://github.com/samber/awesome-prometheus-alerts.git
synced 2026-06-21 00:47:18 +08:00
Compare commits
No commits in common. "master" and "2026-04-10.1" have entirely different histories.
master
...
2026-04-10
34 changed files with 1251 additions and 1732 deletions
25
.github/workflows/dependabot-automerge.yaml
vendored
25
.github/workflows/dependabot-automerge.yaml
vendored
|
|
@ -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 }}
|
||||
|
||||
4
.github/workflows/deploy.yml
vendored
4
.github/workflows/deploy.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
3
.github/workflows/site.yml
vendored
3
.github/workflows/site.yml
vendored
|
|
@ -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
1
.gitignore
vendored
|
|
@ -7,7 +7,6 @@ test/rules/
|
|||
site/node_modules/
|
||||
site/dist/
|
||||
site/.astro/
|
||||
site/public/pagefind/
|
||||
|
||||
# Misc
|
||||
.worktrees/
|
||||
2
LICENSE
2
LICENSE
|
|
@ -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/
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
102
_data/rules.yml
102
_data/rules.yml
|
|
@ -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
|
||||
|
|
|
|||
82
dist/rules/jaeger/embedded-exporter-legacy.yml
vendored
82
dist/rules/jaeger/embedded-exporter-legacy.yml
vendored
|
|
@ -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 }}"
|
||||
104
dist/rules/jaeger/embedded-exporter.yml
vendored
104
dist/rules/jaeger/embedded-exporter.yml
vendored
|
|
@ -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 }}"
|
||||
|
|
|
|||
36
dist/rules/litellm/embedded-exporter.yml
vendored
36
dist/rules/litellm/embedded-exporter.yml
vendored
|
|
@ -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
1
site/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
.env
|
||||
|
|
@ -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
1992
site/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}` } : {}),
|
||||
})),
|
||||
};
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
10
site/src/env.d.ts
vendored
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue