mirror of
https://github.com/samber/awesome-prometheus-alerts.git
synced 2026-06-22 17:37:19 +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 1259 additions and 1405 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
|
- 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: |
|
||||||
3
.github/workflows/site.yml
vendored
3
.github/workflows/site.yml
vendored
|
|
@ -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
1
.gitignore
vendored
|
|
@ -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/
|
||||||
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)
|
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
|
- 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
|
|
||||||
|
|
|
||||||
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
|
- 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 }}"
|
||||||
|
|
|
||||||
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({
|
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
1671
site/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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}` } : {}),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 { 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>
|
|
||||||
|
|
|
||||||
|
|
@ -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 '../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>
|
||||||
|
|
|
||||||
|
|
@ -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 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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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