├── public ├── favicon.png ├── plus.svg ├── blue-tick.svg ├── right-arrow.svg ├── copy.svg ├── close.svg ├── search.svg ├── github.svg ├── zap.svg ├── component.svg ├── fire.svg ├── file.svg ├── green-tick.svg ├── last9.svg └── logo.svg ├── postcss.config.mjs ├── .eslintrc.json ├── prometheus-server ├── nginx │ └── nginx.conf ├── clickhouse │ └── config.xml ├── haproxy │ └── haproxy.cfg ├── prometheus │ ├── prometheus.yml │ └── rules.yml └── docker-compose.yml ├── next.config.mjs ├── components ├── Divider.tsx ├── Container.tsx ├── Page.tsx ├── RulesCount.tsx ├── Provider.tsx ├── AptLogo.tsx ├── LoadingDots.tsx ├── AlertGroup.tsx ├── ExternalLink.tsx ├── AllRulesApplied.tsx ├── Tab.tsx ├── NoAppliedRules.tsx ├── Loader.tsx ├── SideBarComponent.tsx ├── Button.tsx ├── ServiceIcon.tsx ├── Footer.tsx ├── ui │ ├── checkbox.tsx │ ├── tooltip.tsx │ └── dialog.tsx ├── Input.tsx ├── Sidebar.tsx ├── RuleCard.tsx ├── Header.tsx ├── YamlView.tsx ├── SidebarContent.tsx └── AlertComponent.tsx ├── lib ├── discovery.ts ├── utils.ts └── types.ts ├── components.json ├── styles └── globals.css ├── .prettierrc ├── .gitignore ├── hooks ├── queries │ ├── useGithub.ts │ ├── useAlerts.ts │ ├── useRules.ts │ ├── useDiscovery.ts │ └── usePrometheusQuery.ts ├── useAuth.ts └── useLocalStorage.ts ├── tsconfig.json ├── app ├── layout.tsx ├── api │ ├── rules │ │ └── route.ts │ ├── prometheus │ │ └── route.ts │ ├── discovery │ │ └── route.ts │ └── alerts │ │ └── route.ts ├── home │ └── page.tsx ├── library │ └── page.tsx └── page.tsx ├── package.json ├── tailwind.config.ts └── README.md /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/last9/awesome-prometheus-toolkit/HEAD/public/favicon.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@tanstack/query" 4 | ], 5 | "extends": [ 6 | "next/core-web-vitals", 7 | "plugin:@tanstack/eslint-plugin-query/recommended" 8 | ] 9 | } -------------------------------------------------------------------------------- /prometheus-server/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | server { 7 | listen 80; 8 | 9 | location / { 10 | stub_status; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | dangerouslyAllowSVG: true, 5 | remotePatterns: [{ hostname: "cdn.simpleicons.org" }], 6 | }, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface DividerProps { 4 | className?: string; 5 | } 6 | 7 | export const Divider = ({ className }: DividerProps) => { 8 | return
; 9 | }; 10 | -------------------------------------------------------------------------------- /public/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /prometheus-server/clickhouse/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | /metrics 4 | 9363 5 | true 6 | true 7 | true 8 | true 9 | 10 | -------------------------------------------------------------------------------- /components/Container.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface ContainerProps { 5 | children?: ReactNode; 6 | className?: string; 7 | } 8 | 9 | export const Container = ({ children, className }: ContainerProps) => { 10 | return
{children}
; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/discovery.ts: -------------------------------------------------------------------------------- 1 | // The key should match the component name in the library 2 | 3 | export const ComponentExpressionList = { 4 | Clickhouse: "ClickHouseMetrics_Read", 5 | Elasticsearch: "elasticsearch_node_stats_up", 6 | "HaProxy (Embedded exporter (HAProxy >= v2))": "haproxy_process_uptime_seconds", 7 | Kubernetes: "kube_node_status_condition", 8 | Nginx: "nginx_up", 9 | PostgreSQL: "pg_up", 10 | }; 11 | -------------------------------------------------------------------------------- /public/blue-tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useRouter } from "next/navigation"; 4 | import { Tab } from "@/components/Tab"; 5 | 6 | interface PageProps { 7 | path: string; 8 | title: string; 9 | } 10 | 11 | export const Page = ({ path, title }: PageProps) => { 12 | const router = useRouter(); 13 | const pathname = usePathname(); 14 | 15 | return router.replace(path)} title={title} />; 16 | }; 17 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url("https://rsms.me/inter/inter.css"); 6 | @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap"); 7 | 8 | /* 9 | To make sure the font weight looks the same on web as it does on Figma. 10 | See: https://forum.figma.com/t/font-in-browser-seem-bolder-than-in-the-figma/24656/6 11 | */ 12 | 13 | * { 14 | @apply antialiased; 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "importOrder": [ 4 | "^(react|next(?:/.*))?$", 5 | "", 6 | "^@/components/(.*)$", 7 | "^@/lib/(.*)$", 8 | "^@/styles/(.*)$", 9 | "^@/utils/(.*)$", 10 | "^[./]" 11 | ], 12 | "importOrderSeparation": false, 13 | "importOrderSortSpecifiers": true, 14 | "importOrderCaseInsensitive": true, 15 | "plugins": [ 16 | "@trivago/prettier-plugin-sort-imports", 17 | "prettier-plugin-tailwindcss" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /prometheus-server/haproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | daemon 3 | 4 | defaults 5 | mode http 6 | timeout client 50000ms 7 | timeout connect 5000ms 8 | timeout server 50000ms 9 | 10 | frontend stats 11 | bind *:8404 12 | http-request use-service prometheus-exporter if { path /metrics } 13 | stats enable 14 | stats uri /stats 15 | stats refresh 10s 16 | 17 | frontend http_front 18 | bind *:8208 19 | default_backend http_back 20 | 21 | backend http_back 22 | server server1 localhost:8080 23 | -------------------------------------------------------------------------------- /components/RulesCount.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface RulesCountProps { 4 | className?: string; 5 | count: number; 6 | } 7 | 8 | export const RulesCount = ({ className, count }: RulesCountProps) => { 9 | return ( 10 | 16 | {count} {count === 0 || count > 1 ? "RULES" : "RULE"} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /components/Provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC, PropsWithChildren } from "react"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 6 | 7 | const queryClient = new QueryClient(); 8 | 9 | export const Provider: FC = ({ children }) => { 10 | return ( 11 | 12 | {children} 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/AptLogo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useRouter } from "next/navigation"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface AptLogoProps { 6 | className?: string; 7 | } 8 | 9 | export default function AptLogo({ className }: AptLogoProps) { 10 | const router = useRouter(); 11 | 12 | return ( 13 | APT logo router.push("/")} 17 | priority 18 | src="/logo.svg" 19 | height={0} 20 | width={0} 21 | /> 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface LoadingDotsProps { 4 | className?: string; 5 | } 6 | 7 | export const LoadingDots = ({ className }: LoadingDotsProps) => { 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /public/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /hooks/queries/useGithub.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { ApiError, GithubResponse } from "@/lib/types"; 3 | 4 | export default function useGithub() { 5 | return useQuery({ 6 | queryKey: ["github"], 7 | queryFn: () => { 8 | return fetch("https://api.github.com/repos/last9/awesome-prometheus-toolkit") 9 | .then((res) => res.json()) 10 | .then((response: GithubResponse) => response) 11 | .catch((error: ApiError) => { 12 | throw new Error(error.reason, { cause: error.code }); 13 | }); 14 | }, 15 | refetchOnWindowFocus: false, 16 | staleTime: Infinity, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /components/AlertGroup.tsx: -------------------------------------------------------------------------------- 1 | import { AlertComponent } from "@/components/AlertComponent"; 2 | import { Alert } from "@/lib/types"; 3 | 4 | export const AlertGroup = ({ group, components }: Alert) => { 5 | return ( 6 |
7 |

{group.toUpperCase()}

8 | 9 |
10 | {components.map((component, index) => ( 11 | 16 | ))} 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | 5 | interface ExternalLinkProps { 6 | image: string; 7 | link?: string; 8 | title: string; 9 | } 10 | 11 | export const ExternalLink = ({ image, link, title }: ExternalLinkProps) => { 12 | const handleClick = () => { 13 | if (link) { 14 | window.open(link, "_blank"); 15 | } 16 | }; 17 | 18 | return ( 19 |
20 | {title} 21 |

{title}

22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /components/AllRulesApplied.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export const AllRulesApplied = () => { 4 | return ( 5 |
6 | green tick 14 | 15 |
16 |

17 | {"Looks like all our recommendations have\nbeen applied! No new ones as of now."} 18 |

19 |
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /public/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /hooks/queries/useAlerts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { ApiError, UseAlertsResponse } from "@/lib/types"; 3 | 4 | export default function useAlerts() { 5 | return useQuery({ 6 | queryKey: ["alerts"], 7 | queryFn: () => { 8 | return fetch("/api/alerts") 9 | .then((res) => res.json()) 10 | .then((response: UseAlertsResponse) => { 11 | if (response.status === "error") { 12 | throw response; 13 | } 14 | return response.data.alerts; 15 | }) 16 | .catch((error: ApiError) => { 17 | throw new Error(error.reason, { cause: error.code }); 18 | }); 19 | }, 20 | refetchOnWindowFocus: false, 21 | staleTime: Infinity, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface TabProps { 4 | active: boolean; 5 | onClick: () => void; 6 | title: string; 7 | textClassName?: string; 8 | } 9 | 10 | export const Tab = ({ active, title, onClick, textClassName }: TabProps) => { 11 | return ( 12 |
13 |

21 | {title} 22 |

23 | 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /prometheus-server/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | 4 | rule_files: 5 | - "/etc/prometheus/rules.yml" 6 | 7 | scrape_configs: 8 | - job_name: "clickhouse" 9 | static_configs: 10 | - targets: ["clickhouse:9363"] 11 | 12 | - job_name: "elasticsearch" 13 | static_configs: 14 | - targets: ["elasticsearch-exporter:9114"] 15 | 16 | - job_name: "haproxy" 17 | static_configs: 18 | - targets: ["haproxy:8404"] 19 | 20 | - job_name: "nginx" 21 | static_configs: 22 | - targets: ["nginx-exporter:9113"] 23 | 24 | - job_name: "postgres" 25 | static_configs: 26 | - targets: ["postgres_exporter:9187"] 27 | 28 | - job_name: "prometheus" 29 | static_configs: 30 | - targets: ["prometheus:9090"] 31 | -------------------------------------------------------------------------------- /components/NoAppliedRules.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export const NoAppliedRules = () => { 4 | return ( 5 |
6 | plus 7 | 8 |
9 |

10 | {"No rules for this component\nwere found in the config."} 11 |

12 | 13 |

14 | {"Apply rules from the Recommended\nsection and they’ll start showing up here."} 15 |

16 |
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./*" 27 | ] 28 | } 29 | }, 30 | "include": [ 31 | "next-env.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | ".next/types/**/*.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /public/github.svg: -------------------------------------------------------------------------------- 1 | GitHub -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface LoaderProps { 4 | className?: string; 5 | mode?: "dark" | "light"; 6 | size?: "xs" | "sm" | "md" | "lg" | "xl"; 7 | } 8 | 9 | export const Loader = ({ className, mode = "dark", size = "md" }: LoaderProps) => { 10 | const getSize = () => { 11 | if (size === "xs") return "h-2 w-2"; 12 | else if (size === "sm") return "h-4 w-4"; 13 | else if (size === "md") return "h-6 w-6"; 14 | else if (size === "lg") return "h-8 w-8"; 15 | else if (size === "xl") return "h-10 w-10"; 16 | }; 17 | 18 | return ( 19 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useLocalStorage } from "@/hooks/useLocalStorage"; 3 | 4 | export default function useAuth() { 5 | const [ready, setReady] = useState(false); 6 | const [loggedIn, setLoggedIn] = useState(); 7 | const [promUrl, setPromUrl] = useLocalStorage("prometheus_server_url", ""); 8 | const [promUsername, setPromUsername] = useLocalStorage("prometheus_server_username", ""); 9 | const [promPassword, setPromPassword] = useLocalStorage("prometheus_server_password", ""); 10 | 11 | useEffect(() => { 12 | setReady(true); 13 | }, [loggedIn]); 14 | 15 | useEffect(() => { 16 | setLoggedIn(!!promUrl); 17 | }, [promUrl]); 18 | 19 | return { 20 | loggedIn, 21 | ready, 22 | promUrl, 23 | setPromUrl, 24 | setPromUsername, 25 | setPromPassword, 26 | promUsername, 27 | promPassword, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import { Rule } from "@/lib/types"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | export const withProtocol = (url: string) => { 10 | if (/^localhost/i.test(url)) { 11 | return `http://${url}`; 12 | } 13 | return !/^https?:\/\//i.test(url) ? `https://${url}` : url; 14 | }; 15 | 16 | export function sanitiseDescription(description: string) { 17 | return `"${description.replaceAll(`\n`, `\\n`).replaceAll(`"{{`, `\\"{{`).replaceAll(`}}"`, `}}\\"`)}"`; 18 | } 19 | 20 | export function getYaml(rule: Rule) { 21 | return ` 22 | - alert: ${rule.alert} 23 | expr: ${rule.expr} 24 | for: ${rule.for} 25 | labels: 26 | severity: ${rule.labels.severity} 27 | annotations: 28 | summary: ${rule.annotations.summary} 29 | description: ${sanitiseDescription(rule.annotations.description)} 30 | `.trim(); 31 | } 32 | -------------------------------------------------------------------------------- /components/SideBarComponent.tsx: -------------------------------------------------------------------------------- 1 | import { ServiceIcon } from "@/components/ServiceIcon"; 2 | import { Component } from "@/lib/types"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface SideBarComponentProps extends Component { 6 | active?: boolean; 7 | onClick?: () => void; 8 | } 9 | 10 | export const SideBarComponent = ({ active, name, onClick }: SideBarComponentProps) => { 11 | return ( 12 |
19 | 20 | 21 |

27 | {name} 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /public/zap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { LoadingDots } from "@/components/LoadingDots"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface ButtonProps { 6 | className?: string; 7 | children?: ReactNode; 8 | disabled?: boolean; 9 | icon?: ReactNode; 10 | loading?: boolean; 11 | onClick: () => void; 12 | } 13 | 14 | export const Button = ({ 15 | className, 16 | children, 17 | disabled = false, 18 | icon, 19 | loading = false, 20 | onClick, 21 | }: ButtonProps) => { 22 | return ( 23 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /components/ServiceIcon.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useMemo, useState } from "react"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | interface ServiceIcon { 8 | color?: string | null; 9 | className?: string; 10 | name: string; 11 | } 12 | 13 | const errored: string[] = []; 14 | 15 | export const ServiceIcon = ({ color, className, name }: ServiceIcon) => { 16 | const [error, setError] = useState(false); 17 | 18 | const imageUrl = useMemo(() => { 19 | const serviceName = name.split(" ")[0].toLowerCase(); 20 | 21 | if (color) { 22 | return `https://cdn.simpleicons.org/${serviceName}/${color}`; 23 | } 24 | 25 | return `https://cdn.simpleicons.org/${serviceName}`; 26 | }, [color, name]); 27 | 28 | return ( 29 | icon { 34 | setError(true); 35 | errored.push(name); 36 | }} 37 | src={errored.includes(name) || error ? "/component.svg" : imageUrl} 38 | height={0} 39 | width={0} 40 | /> 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Toaster } from "sonner"; 3 | import { Footer } from "@/components/Footer"; 4 | import { Header } from "@/components/Header"; 5 | import { Provider } from "@/components/Provider"; 6 | import "@/styles/globals.css"; 7 | 8 | export const metadata: Metadata = { 9 | title: "Awesome Prometheus Toolkit", 10 | description: "A collection of alerting rules for Prometheus", 11 | icons: { 12 | icon: [ 13 | { 14 | url: "/favicon.png", 15 | href: "/favicon.png", 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 29 | 30 |
31 |
32 |
{children}
33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /hooks/queries/useRules.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { ApiError, UseRulesResponse } from "@/lib/types"; 3 | 4 | interface UseRulesProps { 5 | enabled: boolean; 6 | url: string; 7 | username: string | null; 8 | password: string | null; 9 | } 10 | 11 | export default function useRules(params: UseRulesProps) { 12 | const { enabled, url, username, password } = params || {}; 13 | 14 | return useQuery({ 15 | queryKey: ["rules", url, username, password], 16 | queryFn: () => { 17 | return fetch("/api/rules", { 18 | method: "POST", 19 | body: JSON.stringify({ url, username, password }), 20 | }) 21 | .then((res) => res.json()) 22 | .then((response: UseRulesResponse) => { 23 | if (response.status === "error") { 24 | throw response; 25 | } 26 | return response.data.groups.map((group) => group.rules).flat(); 27 | }) 28 | .catch((error: ApiError) => { 29 | throw new Error(error.reason, { cause: error.code }); 30 | }); 31 | }, 32 | enabled: enabled && url !== null && url !== undefined && url !== "", 33 | refetchOnWindowFocus: false, 34 | staleTime: Infinity, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /public/component.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /hooks/queries/useDiscovery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { ApiError, UseDiscoveryResponse } from "@/lib/types"; 3 | 4 | interface UseDiscoveryProps { 5 | enabled: boolean; 6 | url: string; 7 | username: string | null; 8 | password: string | null; 9 | } 10 | 11 | export default function useDiscovery(params: UseDiscoveryProps) { 12 | const { enabled, url, username, password } = params || {}; 13 | 14 | return useQuery({ 15 | queryKey: ["discovery", url, username, password], 16 | queryFn: () => { 17 | return fetch("/api/discovery", { 18 | method: "POST", 19 | body: JSON.stringify({ url, username, password }), 20 | }) 21 | .then((res) => res.json()) 22 | .then((response: UseDiscoveryResponse) => { 23 | if (response.status === "error") { 24 | throw response; 25 | } 26 | return response.data.discovered; 27 | }) 28 | .catch((error: ApiError) => { 29 | throw new Error(error.reason, { cause: error.code }); 30 | }); 31 | }, 32 | retry: false, 33 | enabled: enabled && url !== null && url !== undefined && url !== "", 34 | refetchOnWindowFocus: false, 35 | staleTime: Infinity, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | 6 | export const Footer = () => { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 |

16 | window.open("https://github.com/last9/awesome-prometheus-toolkit", "_blank") 17 | } 18 | > 19 | Contribute on GitHub 20 |

21 |
22 | 23 | 24 |

Maintained by Last9

25 | 26 | icon 34 | 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /hooks/queries/usePrometheusQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { ApiError, PrometheusQueryProps, PrometheusQueryResponse } from "@/lib/types"; 3 | 4 | interface UsePrometheusQueryProps extends PrometheusQueryProps { 5 | enabled: boolean; 6 | } 7 | 8 | export default function usePrometheusQuery(params: UsePrometheusQueryProps) { 9 | const { enabled, url, query, username, password } = params || {}; 10 | 11 | return useQuery({ 12 | queryKey: ["prometheus", url, query, username, password], 13 | queryFn: () => { 14 | return fetch("/api/prometheus", { 15 | method: "POST", 16 | body: JSON.stringify({ url, query, username, password }), 17 | }) 18 | .then((res) => res.json()) 19 | .then((result: PrometheusQueryResponse) => { 20 | if (result.status === "error") { 21 | throw result; 22 | } 23 | return result.data.result; 24 | }) 25 | .catch((error: ApiError) => { 26 | throw new Error(error.reason, { cause: error.code }); 27 | }); 28 | }, 29 | retry: false, 30 | enabled: enabled && url !== null && url !== undefined && url !== "", 31 | refetchOnWindowFocus: false, 32 | staleTime: Infinity, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { Check } from "lucide-react"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Checkbox = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 21 | 22 | 23 | 24 | )); 25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 26 | 27 | export { Checkbox }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awesome-prometheus-toolkit", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-checkbox": "^1.0.4", 13 | "@radix-ui/react-dialog": "^1.0.5", 14 | "@radix-ui/react-tooltip": "^1.0.7", 15 | "@tanstack/react-query": "^5.40.1", 16 | "@tanstack/react-query-devtools": "^5.40.1", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "fuse.js": "^7.0.0", 20 | "js-yaml": "^4.1.0", 21 | "lucide-react": "^0.379.0", 22 | "next": "14.2.3", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "sonner": "^1.4.41", 26 | "tailwind-merge": "^2.3.0", 27 | "tailwindcss-animate": "^1.0.7", 28 | "usehooks-ts": "^3.1.0", 29 | "vaul": "^0.9.1" 30 | }, 31 | "devDependencies": { 32 | "@tanstack/eslint-plugin-query": "^5.35.6", 33 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 34 | "@types/js-yaml": "^4.0.9", 35 | "@types/node": "^20", 36 | "@types/react": "^18", 37 | "@types/react-dom": "^18", 38 | "eslint": "^8", 39 | "eslint-config-next": "14.2.3", 40 | "postcss": "^8", 41 | "prettier": "^3.2.5", 42 | "prettier-plugin-tailwindcss": "^0.5.14", 43 | "tailwindcss": "^3.4.1", 44 | "typescript": "^5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | const TooltipProvider = TooltipPrimitive.Provider; 8 | 9 | const Tooltip = TooltipPrimitive.Root; 10 | 11 | const TooltipTrigger = TooltipPrimitive.Trigger; 12 | 13 | const TooltipArrow = TooltipPrimitive.Arrow; 14 | 15 | const TooltipContent = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, sideOffset = 4, ...props }, ref) => ( 19 | 28 | )); 29 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 30 | 31 | export { Tooltip, TooltipArrow, TooltipTrigger, TooltipContent, TooltipProvider }; 32 | -------------------------------------------------------------------------------- /components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useState } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface InputProps extends React.InputHTMLAttributes { 5 | containerClassName?: string; 6 | disabled?: boolean; 7 | left?: React.ReactNode; 8 | right?: React.ReactNode; 9 | } 10 | 11 | export const Input = forwardRef( 12 | ({ disabled, left, right, className, containerClassName, ...props }, ref) => { 13 | const [focused, setFocused] = useState(false); 14 | 15 | return ( 16 |
24 | {left ? left : null} 25 | 26 | setFocused(true)} 34 | onBlur={() => setFocused(false)} 35 | ref={ref} 36 | {...props} 37 | /> 38 | 39 | {right ? right : null} 40 |
41 | ); 42 | }, 43 | ); 44 | Input.displayName = "Input"; 45 | -------------------------------------------------------------------------------- /public/fire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useRouter } from "next/navigation"; 3 | import { Button } from "@/components/Button"; 4 | import { SideBarComponent } from "@/components/SideBarComponent"; 5 | import { Component } from "@/lib/types"; 6 | 7 | interface SidebarProps { 8 | components: Component[]; 9 | onSelect: (index: number) => void; 10 | selected: number; 11 | } 12 | 13 | export const Sidebar = ({ components, onSelect, selected }: SidebarProps) => { 14 | const router = useRouter(); 15 | 16 | return ( 17 |
18 |
19 |

DISCOVERED

20 |
21 | 22 |
23 | {components.map((component, index) => ( 24 | onSelect(index)} 29 | /> 30 | ))} 31 |
32 | 33 | 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import defaultTheme from "tailwindcss/defaultTheme"; 3 | 4 | const config = { 5 | darkMode: "class", 6 | content: ["./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}"], 7 | theme: { 8 | fontFamily: { 9 | sans: [ 10 | ["Inter", ...defaultTheme.fontFamily.sans], 11 | { fontFeatureSettings: '"ss04", "ss01", "cpsp"' }, 12 | ], 13 | mono: ["JetBrains Mono", ...defaultTheme.fontFamily.mono], 14 | }, 15 | extend: { 16 | colors: { 17 | "yaml-key": "rgba(34, 134, 58, 1)", 18 | "yaml-value": "rgba(3, 47, 98, 1)", 19 | overlay: "rgba(39, 55, 71, 0.5)", 20 | }, 21 | keyframes: { 22 | "accordion-down": { 23 | from: { height: "0" }, 24 | to: { height: "var(--radix-accordion-content-height)" }, 25 | }, 26 | "accordion-up": { 27 | from: { height: "var(--radix-accordion-content-height)" }, 28 | to: { height: "0" }, 29 | }, 30 | "loading-dots": { 31 | "0%": { 32 | transform: "translate(0, 0)", 33 | }, 34 | "50%": { 35 | transform: "translate(0, 6px)", 36 | }, 37 | "100%": { 38 | transform: "translate(0, 0)", 39 | }, 40 | }, 41 | }, 42 | animation: { 43 | "accordion-down": "accordion-down 0.2s ease-out", 44 | "accordion-up": "accordion-up 0.2s ease-out", 45 | "loading-dots": "loading-dots 1s ease-in-out infinite", 46 | }, 47 | }, 48 | }, 49 | plugins: [require("tailwindcss-animate")], 50 | } satisfies Config; 51 | 52 | export default config; 53 | -------------------------------------------------------------------------------- /app/api/rules/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { PrometheusQueryProps, PrometheusRulesResponse } from "@/lib/types"; 3 | 4 | export async function POST(req: NextRequest) { 5 | try { 6 | const headers = new Headers(); 7 | const { url, username, password }: PrometheusQueryProps = await req.json(); 8 | const queryUrl = `${url}/api/v1/rules`; 9 | 10 | if (username && password) { 11 | headers.set( 12 | "Authorization", 13 | "Basic " + Buffer.from(username + ":" + password).toString("base64"), 14 | ); 15 | } 16 | 17 | const response = await fetch(queryUrl, { headers }); 18 | 19 | if (!response.ok) { 20 | throw response.status; 21 | } 22 | 23 | const { data, status }: PrometheusRulesResponse = await response.json(); 24 | 25 | if (status !== "success") { 26 | throw 500; 27 | } 28 | 29 | return NextResponse.json({ 30 | status: "success", 31 | data: { groups: data.groups }, 32 | }); 33 | } catch (error) { 34 | let code; 35 | let reason; 36 | 37 | if (typeof error !== "number") { 38 | code = 500; 39 | } else { 40 | code = error; 41 | } 42 | 43 | if (code === 404) { 44 | reason = "APT was not able to connect to your Prometheus. Please try again."; 45 | } else if (code === 401) { 46 | reason = "Unauthorized: Prometheus server requires username and password"; 47 | } else if (code === 500) { 48 | reason = "Something went wrong. Please try again later."; 49 | } 50 | 51 | return NextResponse.json( 52 | { 53 | code, 54 | reason, 55 | status: "error", 56 | }, 57 | { status: code }, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/api/prometheus/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { PrometheusQueryProps, PrometheusQueryResponse } from "@/lib/types"; 3 | 4 | export async function POST(req: NextRequest) { 5 | try { 6 | const headers = new Headers(); 7 | const { url, query, username, password }: PrometheusQueryProps = await req.json(); 8 | const queryUrl = `${url}/api/v1/query?query=${encodeURIComponent(query)}`; 9 | 10 | if (username && password) { 11 | headers.set( 12 | "Authorization", 13 | "Basic " + Buffer.from(username + ":" + password).toString("base64"), 14 | ); 15 | } 16 | 17 | const response = await fetch(queryUrl, { headers }); 18 | 19 | if (!response.ok) { 20 | throw response.status; 21 | } 22 | 23 | const { data, status }: PrometheusQueryResponse = await response.json(); 24 | 25 | if (status !== "success") { 26 | throw 500; 27 | } 28 | 29 | return NextResponse.json({ 30 | status: "success", 31 | data: { result: data.result }, 32 | }); 33 | } catch (error) { 34 | let code; 35 | let reason; 36 | 37 | if (typeof error !== "number") { 38 | code = 500; 39 | } else { 40 | code = error; 41 | } 42 | 43 | if (code === 404) { 44 | reason = "APT was not able to connect to your Prometheus. Please try again."; 45 | } else if (code === 401) { 46 | reason = "Unauthorized: Invalid username/password"; 47 | } else if (code === 500) { 48 | reason = "APT was not able to connect to your Prometheus. Please try again."; 49 | } 50 | 51 | return NextResponse.json( 52 | { 53 | code, 54 | reason, 55 | status: "error", 56 | }, 57 | { status: code }, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /components/RuleCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { YamlView } from "@/components/YamlView"; 3 | import { RuleDetails } from "@/lib/types"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface RuleCardProps { 7 | applied?: boolean; 8 | number: number; 9 | rule: RuleDetails; 10 | } 11 | 12 | export const RuleCard = ({ applied = false, number, rule }: RuleCardProps) => { 13 | const formattedNumber = number < 10 ? `0${number}` : number; 14 | 15 | return ( 16 |
17 |
18 |
24 | {applied ? ( 25 | tick 33 | ) : ( 34 |

{formattedNumber}

35 | )} 36 |
37 | 38 |
39 |

{rule.summary}

40 |

{rule.description}

41 |
42 |
43 | 44 |
45 |
46 |

{rule.summary}

47 |

{rule.description}

48 |
49 | 50 | 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /public/green-tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /prometheus-server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | clickhouse: 3 | image: clickhouse/clickhouse-server 4 | ports: 5 | - "8123:8123" 6 | - "9000:9000" 7 | - "9363:9363" 8 | volumes: 9 | - clickhouse-data:/var/lib/clickhouse 10 | - ./clickhouse/config.xml:/etc/clickhouse-server/config.d/config.xml 11 | 12 | elasticsearch: 13 | image: docker.elastic.co/elasticsearch/elasticsearch:8.14.1 14 | ports: 15 | - "9200:9200" 16 | - "9300:9300" 17 | environment: 18 | - discovery.type=single-node 19 | volumes: 20 | - es-data:/usr/share/elasticsearch/data 21 | 22 | elasticsearch-exporter: 23 | image: prometheuscommunity/elasticsearch-exporter 24 | ports: 25 | - "9114:9114" 26 | environment: 27 | - ES_URI=http://elasticsearch:9200 28 | - ES_ALL=true 29 | 30 | haproxy: 31 | image: haproxy 32 | ports: 33 | - "8404:8404" 34 | volumes: 35 | - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg 36 | 37 | nginx: 38 | image: nginx 39 | ports: 40 | - "8080:80" 41 | volumes: 42 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf 43 | 44 | nginx-exporter: 45 | image: nginx/nginx-prometheus-exporter 46 | ports: 47 | - "9113:9113" 48 | command: ["-nginx.scrape-uri", "http://nginx:8080/stub_status"] 49 | 50 | postgres: 51 | image: postgres 52 | ports: 53 | - "5432:5432" 54 | environment: 55 | - POSTGRES_USER=myuser 56 | - POSTGRES_PASSWORD=mypassword 57 | - POSTGRES_DB=mydb 58 | volumes: 59 | - pg-data:/var/lib/postgresql/data 60 | 61 | postgres_exporter: 62 | image: prometheuscommunity/postgres-exporter 63 | ports: 64 | - "9187:9187" 65 | environment: 66 | DATA_SOURCE_NAME: "postgresql://myuser:mypassword@postgres:5432/mydb?sslmode=disable" 67 | depends_on: 68 | - postgres 69 | 70 | prometheus: 71 | image: prom/prometheus 72 | ports: 73 | - "9090:9090" 74 | volumes: 75 | - ./prometheus:/etc/prometheus 76 | 77 | volumes: 78 | clickhouse-data: 79 | es-data: 80 | pg-data: 81 | -------------------------------------------------------------------------------- /app/api/discovery/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { ComponentExpressionList } from "@/lib/discovery"; 3 | import { PrometheusQueryProps, PrometheusQueryResponse } from "@/lib/types"; 4 | 5 | export async function POST(req: NextRequest) { 6 | try { 7 | const headers = new Headers(); 8 | const { url, username, password }: PrometheusQueryProps = await req.json(); 9 | 10 | if (username && password) { 11 | headers.set( 12 | "Authorization", 13 | "Basic " + Buffer.from(username + ":" + password).toString("base64"), 14 | ); 15 | } 16 | 17 | const discovery = await Promise.all( 18 | Object.entries(ComponentExpressionList).map(async ([component, expr]) => { 19 | const queryUrl = `${url}/api/v1/query?query=${encodeURIComponent(expr)}`; 20 | const response = await fetch(queryUrl, { headers }); 21 | 22 | if (!response.ok) { 23 | return null; 24 | } 25 | 26 | const { data, status }: PrometheusQueryResponse = await response.json(); 27 | 28 | if (status !== "success") { 29 | return null; 30 | } 31 | 32 | return { 33 | component: component, 34 | exists: data.result.length > 0, 35 | }; 36 | }), 37 | ); 38 | 39 | const discovered = discovery.filter((component) => component && component.exists); 40 | 41 | if (discovered.length === 0) { 42 | throw 404; 43 | } 44 | 45 | return NextResponse.json({ 46 | status: "success", 47 | data: { discovered }, 48 | }); 49 | } catch (error) { 50 | let code; 51 | let reason; 52 | 53 | if (typeof error !== "number") { 54 | code = 500; 55 | } else { 56 | code = error; 57 | } 58 | 59 | if (code === 404) { 60 | reason = "No components were discovered on this Prometheus server."; 61 | } else if (code === 401) { 62 | reason = "Unauthorized: Prometheus server requires username and password"; 63 | } else if (code === 500) { 64 | reason = "Something went wrong. Please try again later."; 65 | } 66 | 67 | return NextResponse.json( 68 | { 69 | code, 70 | reason, 71 | status: "error", 72 | }, 73 | { status: code }, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Alert { 2 | group: string; 3 | components: Component[]; 4 | } 5 | 6 | export interface Component { 7 | name: string; 8 | rules: RuleDetails[]; 9 | } 10 | 11 | export interface RuleDetails { 12 | summary: string; 13 | description: string; 14 | yml: Rule; 15 | } 16 | 17 | export interface Rule { 18 | alert: string; 19 | expr: string; 20 | for: string; 21 | labels: { 22 | severity: string; 23 | }; 24 | annotations: { 25 | summary: string; 26 | description: string; 27 | }; 28 | } 29 | 30 | export interface MappingRule { 31 | name: string; 32 | description: string; 33 | query: string; 34 | severity: string; 35 | for: string; 36 | } 37 | 38 | export interface MappingFile { 39 | groups: Group[]; 40 | } 41 | 42 | export interface RuleFile { 43 | groups: { 44 | name: string; 45 | rules: Rule[]; 46 | }[]; 47 | } 48 | 49 | export interface Group { 50 | name: string; 51 | services: Service[]; 52 | } 53 | 54 | export interface Service { 55 | name: string; 56 | exporters: Exporter[]; 57 | } 58 | 59 | export interface Exporter { 60 | name: string; 61 | slug: string; 62 | rules: MappingRule[]; 63 | } 64 | export interface ApiError { 65 | code: number; 66 | reason: string; 67 | status: string; 68 | } 69 | 70 | export interface UseAlertsResponse { 71 | data: { alerts: Alert[] }; 72 | status: string; 73 | } 74 | 75 | export interface Discovery { 76 | component: string; 77 | exists: boolean; 78 | } 79 | 80 | export interface RuleGroup { 81 | name: string; 82 | file: string; 83 | rules: PrometheusRule[]; 84 | } 85 | 86 | export interface PrometheusRule { 87 | name: string; 88 | query: string; 89 | labels: { 90 | severity: string; 91 | }; 92 | annotations: { 93 | summary: string; 94 | description: string; 95 | }; 96 | } 97 | 98 | export interface UseDiscoveryResponse { 99 | data: { discovered: Discovery[] }; 100 | status: string; 101 | } 102 | 103 | export interface UseRulesResponse { 104 | data: { groups: RuleGroup[] }; 105 | status: string; 106 | } 107 | 108 | export interface PrometheusQueryResponse { 109 | data: { 110 | result: object[]; 111 | resultType: string; 112 | }; 113 | isPartial?: boolean; 114 | status: string; 115 | } 116 | 117 | export interface PrometheusQueryProps { 118 | url: string; 119 | query: string; 120 | username?: string | null; 121 | password?: string | null; 122 | } 123 | 124 | export interface PrometheusRulesResponse { 125 | data: { 126 | groups: RuleGroup[]; 127 | }; 128 | status: string; 129 | } 130 | 131 | export interface GithubResponse { 132 | stargazers_count: number; 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Awesome Prometheus Toolkit 4 | 5 | _The most apt toolkit for your Prometheus setup._ 6 | 7 | Kickstarting your monitoring journey with Prometheus is a broken experience and one struggles with a standardized set of components, alerting rules, and dashboards to use. APT aims to build a standardized resource across the instrumentation, query, and alerting pipelines. 8 | 9 | With the v0.1 of Awesome Prometheus Toolkit, we are setting a foundation for an amazing Prometheus developer experience we want: 10 | 11 | 1. You point APT to your running Prometheus server 12 | 2. APT identifies which components are sending metrics to Prometheus 13 | 3. APT gives recommendations on what alert rules (sourced from [awesome-prometheus-alerts](https://github.com/samber/awesome-prometheus-alerts)) should be applied 14 | 15 | Read our launch blog post [here](https://last9.io/blog/announcing-awesome-prometheus-toolkit). 16 | 17 | [![Demo of Awesome Prometheus Toolkit](https://github.com/last9/awesome-prometheus-toolkit/assets/1834234/b0ed8f22-f2f2-4a3f-a8bb-76bd00753681)](https://www.youtube.com/watch?v=yFqCdkc23Gc) 18 | 19 | Currently, APT gives you recommendations and tracks which rules are already applied for the following components: 20 | 21 | 1. Clickhouse 22 | 2. Elasticsearch 23 | 3. HaProxy 24 | 4. Kubernetes 25 | 5. Nginx 26 | 6. PostgreSQL 27 | 28 | ## 💻 Getting Started 29 | 30 | 1. Run `npm install` to install the dependencies 31 | 2. Run `npm run dev` to run the dev server 32 | 3. Open `localhost:3000` in your browser 33 | 4. Enter the URL of your local/test/production Prometheus server, and click Connect 34 | - You can also set the auth, if your server requires it 35 | 5. Once APT identifies the supported components in your emitted metrics, you can view the recommendations. You can simply copy the recommended rules and apply them in your Prometheus’ `rules.yml` 36 | 6. If you have any additional components, you can also use the Browse Library section to find and copy those rules 37 | 38 | ## 🔧 Setup Demo Prometheus Server (Optional) 39 | 40 | If you don’t have a Prometheus server handy but still want to play around with APT, you can also use the demo setup provided in the repo to generate metrics for the supported components (except Kubernetes). 41 | 42 | 1. Run `cd prometheus-server` 43 | 2. Run `docker compose up` to start the local server 44 | 3. Use `localhost:9090` as the source URL on the APT home screen, without any required auth 45 | 46 | ## About Last9 47 | 48 | [Last9](https://last9.io) builds reliability tools for SRE and DevOps. 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/api/alerts/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import yaml from "js-yaml"; 3 | import { Alert, MappingFile, RuleFile } from "@/lib/types"; 4 | 5 | const FILE_BASE = "https://raw.githubusercontent.com/samber/awesome-prometheus-alerts/master"; 6 | 7 | // To fetch yml file from a given url 8 | async function fetchYml(url: string) { 9 | const response = await fetch(url); 10 | 11 | if (!response.ok) { 12 | throw 404; 13 | } 14 | 15 | return await response.text(); 16 | } 17 | 18 | // Refactoring data to serve it on the frontend 19 | export async function GET() { 20 | try { 21 | const mappingYaml: string = await fetchYml(`${FILE_BASE}/_data/rules.yml`); 22 | const mappings = yaml.load(mappingYaml) as MappingFile; 23 | 24 | const alerts: Alert[] = await Promise.all( 25 | mappings.groups.map(async (group) => { 26 | const components = await Promise.all( 27 | group.services.map(async (service) => { 28 | const name = service.name.replaceAll(" ", "-").toLowerCase().trim(); 29 | 30 | return await Promise.all( 31 | service.exporters.flatMap(async (exporter) => { 32 | const ruleYml = await fetchYml( 33 | `${FILE_BASE}/dist/rules/${name}/${exporter.slug}.yml`, 34 | ); 35 | 36 | const ruleJson = yaml.load(ruleYml) as RuleFile; 37 | 38 | const rules = ruleJson.groups 39 | .flatMap((group) => group.rules) 40 | .filter((rule) => rule !== null) 41 | .map((rule, index) => ({ 42 | summary: exporter.rules[index].name, 43 | description: exporter.rules[index].description, 44 | yml: rule, 45 | })); 46 | 47 | return { 48 | name: 49 | service.exporters.length > 1 50 | ? `${service.name} (${exporter.name})` 51 | : service.name, 52 | rules, 53 | }; 54 | }), 55 | ); 56 | }), 57 | ); 58 | 59 | return { 60 | group: group.name, 61 | components: components.flat(), 62 | }; 63 | }), 64 | ); 65 | 66 | return NextResponse.json({ 67 | status: "success", 68 | data: { alerts }, 69 | }); 70 | } catch (error) { 71 | let code; 72 | let reason; 73 | 74 | if (typeof error !== "number") { 75 | code = 500; 76 | } else { 77 | code = error; 78 | } 79 | 80 | if (code === 404) { 81 | reason = "Failed to fetch repository contents"; 82 | } else if (code === 500) { 83 | reason = "Something went wrong. Please try again later."; 84 | } 85 | 86 | return NextResponse.json( 87 | { 88 | code, 89 | reason, 90 | status: "error", 91 | }, 92 | { status: code }, 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useRouter } from "next/navigation"; 4 | import useGithub from "@/hooks/queries/useGithub"; 5 | import useAuth from "@/hooks/useAuth"; 6 | import { useQueryClient } from "@tanstack/react-query"; 7 | import AptLogo from "@/components/AptLogo"; 8 | import { Divider } from "@/components/Divider"; 9 | import { ExternalLink } from "@/components/ExternalLink"; 10 | import { Page } from "@/components/Page"; 11 | import { 12 | Tooltip, 13 | TooltipArrow, 14 | TooltipContent, 15 | TooltipProvider, 16 | TooltipTrigger, 17 | } from "@/components/ui/tooltip"; 18 | import { cn } from "@/lib/utils"; 19 | 20 | export const Header = () => { 21 | const router = useRouter(); 22 | const { data } = useGithub(); 23 | const { promUrl } = useAuth(); 24 | const pathname = usePathname(); 25 | const queryClient = useQueryClient(); 26 | 27 | const changeSourcePath = () => { 28 | queryClient.clear(); 29 | localStorage.clear(); 30 | router.replace("/"); 31 | }; 32 | 33 | return ( 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |

{promUrl}

56 | 57 |
58 | 59 |

63 | Edit 64 |

65 |
66 | 67 | 68 | 69 | 70 | {data?.stargazers_count ? ( 71 | 76 | ) : null} 77 |
78 |
79 |
80 | 81 | 82 |
83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /components/YamlView.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { toast } from "sonner"; 3 | import { Rule } from "@/lib/types"; 4 | import { getYaml, sanitiseDescription } from "@/lib/utils"; 5 | 6 | interface YamlViewProps { 7 | rule: Rule; 8 | } 9 | 10 | export const YamlView = ({ rule }: YamlViewProps) => { 11 | return ( 12 |
13 |
14 | 15 | -  16 | alert: 17 | {rule.alert} 18 | 19 | 20 | 21 |    22 | expr: 23 | {rule.expr} 24 | 25 | 26 | 27 |    28 | for: 29 | {rule.for} 30 | 31 | 32 | 33 |    34 | labels: 35 | 36 | 37 | 38 |      39 | severity: 40 | {rule.labels.severity} 41 | 42 | 43 | 44 |    45 | annotations: 46 | 47 | 48 | 49 |      50 | summary: 51 | {rule.annotations.summary} 52 | 53 | 54 | 55 |      56 | description: 57 | 58 | {sanitiseDescription(rule.annotations.description)} 59 | 60 | 61 |
62 | 63 |
{ 66 | toast.success("Copied to clipboard"); 67 | navigator.clipboard.writeText(getYaml(rule)); 68 | }} 69 | > 70 | copy 71 | COPY 72 |
73 |
74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /public/last9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { AllRulesApplied } from "@/components/AllRulesApplied"; 3 | import { Divider } from "@/components/Divider"; 4 | import { NoAppliedRules } from "@/components/NoAppliedRules"; 5 | import { RuleCard } from "@/components/RuleCard"; 6 | import { RulesCount } from "@/components/RulesCount"; 7 | import { Tab } from "@/components/Tab"; 8 | import { Component, PrometheusRule } from "@/lib/types"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | interface SidebarContentProps { 12 | component: Component; 13 | rules: PrometheusRule[]; 14 | } 15 | 16 | export const SidebarContent = ({ component, rules }: SidebarContentProps) => { 17 | const [tab, setTab] = useState<"recommended" | "applied">("recommended"); 18 | 19 | const applied = useMemo(() => { 20 | return component.rules.filter((rule1) => rules.find((rule2) => rule1.yml.alert === rule2.name)); 21 | }, [component.rules, rules]); 22 | 23 | const recommended = useMemo(() => { 24 | return component.rules.filter( 25 | (rule1) => !applied.find((rule2) => rule1.yml.alert === rule2.yml.alert), 26 | ); 27 | }, [applied, component.rules]); 28 | 29 | return ( 30 |
31 |
32 |

{component.name}

33 | 34 |
35 | 36 | 37 |

38 | {recommended.length} recommended, 39 | {applied.length} applied 40 |

41 |
42 |
43 | 44 |
45 | setTab("recommended")} 48 | textClassName={cn( 49 | "text-xs font-bold", 50 | tab === "recommended" ? "text-slate-600" : "text-slate-500", 51 | )} 52 | title="Recommended" 53 | /> 54 | 55 | 56 | 57 | setTab("applied")} 60 | textClassName={cn( 61 | "text-xs font-bold", 62 | tab === "applied" ? "text-slate-600" : "text-slate-500", 63 | )} 64 | title="Applied" 65 | /> 66 | 67 | 68 |
69 | 70 | {tab === "recommended" ? ( 71 | recommended.length > 0 ? ( 72 |
73 | {recommended.map((rule, index) => ( 74 | 75 | ))} 76 |
77 | ) : ( 78 | 79 | ) 80 | ) : applied.length > 0 ? ( 81 |
82 | {applied.map((rule, index) => ( 83 | 84 | ))} 85 |
86 | ) : ( 87 | 88 | )} 89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /app/home/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useRouter } from "next/navigation"; 5 | import { useEffect, useMemo, useState } from "react"; 6 | import useAlerts from "@/hooks/queries/useAlerts"; 7 | import useDiscovery from "@/hooks/queries/useDiscovery"; 8 | import useRules from "@/hooks/queries/useRules"; 9 | import useAuth from "@/hooks/useAuth"; 10 | import { Button } from "@/components/Button"; 11 | import { Container } from "@/components/Container"; 12 | import { Loader } from "@/components/Loader"; 13 | import { Sidebar } from "@/components/Sidebar"; 14 | import { SidebarContent } from "@/components/SidebarContent"; 15 | import { Component } from "@/lib/types"; 16 | 17 | export default function Home() { 18 | const router = useRouter(); 19 | const [selected, setSelected] = useState(0); 20 | const { data: alerts, isLoading: gettingAlerts } = useAlerts(); 21 | const { loggedIn, ready, promUrl, promUsername, promPassword } = useAuth(); 22 | 23 | const { data: discovery, isLoading: discovering } = useDiscovery({ 24 | enabled: true, 25 | url: promUrl, 26 | username: promUsername, 27 | password: promPassword, 28 | }); 29 | 30 | const { data: rules, isLoading: gettingRules } = useRules({ 31 | enabled: true, 32 | url: promUrl, 33 | username: promUsername, 34 | password: promPassword, 35 | }); 36 | 37 | const discovered = useMemo(() => { 38 | const store: Component[] = []; 39 | const components = alerts?.map((alert) => alert.components).flat() || []; 40 | 41 | discovery?.map((discoveredComponent) => { 42 | const foundComponent = components.find( 43 | (component) => component.name === discoveredComponent.component, 44 | ); 45 | 46 | if (foundComponent) { 47 | store.push(foundComponent); 48 | } 49 | }); 50 | 51 | return store; 52 | }, [alerts, discovery]); 53 | 54 | useEffect(() => { 55 | if (ready && !loggedIn) { 56 | router.replace("/"); 57 | } 58 | }, [loggedIn, ready, router]); 59 | 60 | if (gettingAlerts || discovering || gettingRules || !ready) { 61 | return ( 62 | 63 | 64 | 65 | ); 66 | } 67 | 68 | return discovered.length > 0 ? ( 69 | 70 | {discovered ? ( 71 | 72 | ) : null} 73 | 74 | {discovered[selected] && rules !== undefined ? ( 75 | 76 | ) : null} 77 | 78 | ) : ( 79 | 80 |

81 | No services were discovered on this Prometheus server. 82 |

83 | 84 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /app/library/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useEffect, useMemo, useRef, useState } from "react"; 5 | import useAlerts from "@/hooks/queries/useAlerts"; 6 | import Fuse, { IFuseOptions } from "fuse.js"; 7 | import { AlertGroup } from "@/components/AlertGroup"; 8 | import { Container } from "@/components/Container"; 9 | import { Input } from "@/components/Input"; 10 | import { Loader } from "@/components/Loader"; 11 | import { Alert, Component } from "@/lib/types"; 12 | 13 | const fuseOptions = { 14 | threshold: 0.3, 15 | includeMatches: true, 16 | keys: ["group", "components.name"], 17 | } satisfies IFuseOptions; 18 | 19 | export default function Browse() { 20 | const { data: alerts, isLoading } = useAlerts(); 21 | const [searchPattern, setSearchPattern] = useState(""); 22 | const searchRef = useRef(null); 23 | const [alertsList, setAlertsList] = useState(null); 24 | const fuse = useMemo(() => new Fuse(alerts || [], fuseOptions), [alerts]); 25 | 26 | // To focus on search when '/' key is pressed 27 | useEffect(() => { 28 | const down = (e: KeyboardEvent) => { 29 | if (e.key === "/") { 30 | e.preventDefault(); 31 | searchRef.current?.focus(); 32 | } 33 | }; 34 | 35 | document.addEventListener("keydown", down); 36 | return () => document.removeEventListener("keydown", down); 37 | }, []); 38 | 39 | // To fuzzy search through alert groups and components 40 | useEffect(() => { 41 | if (searchPattern) { 42 | const results = fuse.search(searchPattern); 43 | 44 | const alertList = results.map((result) => { 45 | const components: Component[] = []; 46 | 47 | result.matches?.map((match) => { 48 | if (match.key === "components.name") { 49 | const component = result.item.components.at(match.refIndex || 0); 50 | 51 | if (component) { 52 | components.push(component); 53 | } 54 | } 55 | }); 56 | 57 | return { 58 | group: result.item.group, 59 | components, 60 | }; 61 | }); 62 | 63 | setAlertsList(alertList); 64 | } else { 65 | setAlertsList(alerts || []); 66 | } 67 | }, [alerts, fuse, searchPattern]); 68 | 69 | if (isLoading) { 70 | return ( 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | return ( 78 | 79 |

Browse Library

80 | 81 | } 85 | right={ 86 |
87 |

/

88 |
89 | } 90 | onChange={(e) => setSearchPattern(e.target.value)} 91 | placeholder="Search for a component" 92 | value={searchPattern} 93 | /> 94 | 95 |
96 | {alertsList 97 | ? alertsList.map((alert, index) => ( 98 | 103 | )) 104 | : null} 105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /components/AlertComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul"; 2 | import { RuleCard } from "@/components/RuleCard"; 3 | import { RulesCount } from "@/components/RulesCount"; 4 | import { ServiceIcon } from "@/components/ServiceIcon"; 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "@/components/ui/dialog"; 12 | import { Component } from "@/lib/types"; 13 | 14 | export const AlertComponent = ({ name, rules }: Component) => { 15 | return ( 16 |
17 |
18 | 19 |

{name}

20 |
21 | 22 |

23 | 24 | {rules.map((rule) => rule.summary).join(", ")} 25 |

26 | 27 | 28 |
29 | 30 | 36 | 37 |
38 | 39 | 40 | 41 | 42 | {name} 43 | 44 | 45 | 46 |
47 | {rules.map((rule, index) => ( 48 | 49 | ))} 50 |
51 |
52 |
53 | 54 | 55 |
56 | 57 |
58 |

View Alert Rules

59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 |
67 | 68 |
69 |
70 | 71 | {name} 72 |
73 | 74 | 75 |
76 | 77 |
78 | 79 |
80 | {rules.map((rule, index) => ( 81 | 82 | ))} 83 |
84 | 85 | 86 | 87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import * as React from "react"; 5 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 6 | import { Divider } from "@/components/Divider"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | close 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ children, className, ...props }: React.HTMLAttributes) => ( 57 |
58 |
59 | {children} 60 |
61 | 62 | 63 |
64 | ); 65 | DialogHeader.displayName = "DialogHeader"; 66 | 67 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 68 |
72 | ); 73 | DialogFooter.displayName = "DialogFooter"; 74 | 75 | const DialogTitle = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, ...props }, ref) => ( 79 | 84 | )); 85 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 86 | 87 | const DialogDescription = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => ( 91 | 96 | )); 97 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 98 | 99 | export { 100 | Dialog, 101 | DialogPortal, 102 | DialogOverlay, 103 | DialogClose, 104 | DialogTrigger, 105 | DialogContent, 106 | DialogHeader, 107 | DialogFooter, 108 | DialogTitle, 109 | DialogDescription, 110 | }; 111 | -------------------------------------------------------------------------------- /hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import type { Dispatch, SetStateAction } from "react"; 3 | import { useEventCallback, useEventListener } from "usehooks-ts"; 4 | 5 | declare global { 6 | interface WindowEventMap { 7 | "local-storage": CustomEvent; 8 | } 9 | } 10 | 11 | type UseLocalStorageOptions = { 12 | serializer?: (value: T) => string; 13 | deserializer?: (value: string) => T; 14 | initializeWithValue?: boolean; 15 | }; 16 | 17 | const IS_SERVER = typeof window === "undefined"; 18 | 19 | export function useLocalStorage( 20 | key: string, 21 | initialValue: T | (() => T), 22 | options: UseLocalStorageOptions = {}, 23 | ): [T, Dispatch>, () => void] { 24 | const { initializeWithValue = true } = options; 25 | 26 | const serializer = useCallback<(value: T) => string>( 27 | (value) => { 28 | if (options.serializer) { 29 | return options.serializer(value); 30 | } 31 | 32 | return JSON.stringify(value); 33 | }, 34 | [options], 35 | ); 36 | 37 | const deserializer = useCallback<(value: string) => T>( 38 | (value) => { 39 | if (options.deserializer) { 40 | return options.deserializer(value); 41 | } 42 | // Support 'undefined' as a value 43 | if (value === "undefined") { 44 | return undefined as unknown as T; 45 | } 46 | 47 | const defaultValue = initialValue instanceof Function ? initialValue() : initialValue; 48 | 49 | let parsed: unknown; 50 | try { 51 | parsed = JSON.parse(value); 52 | } catch (error) { 53 | console.error("Error parsing JSON:", error); 54 | return defaultValue; // Return initialValue if parsing fails 55 | } 56 | 57 | return parsed as T; 58 | }, 59 | [options, initialValue], 60 | ); 61 | 62 | // Get from local storage then 63 | // parse stored json or return initialValue 64 | const readValue = useCallback((): T => { 65 | const initialValueToUse = initialValue instanceof Function ? initialValue() : initialValue; 66 | 67 | // Prevent build error "window is undefined" but keep working 68 | if (IS_SERVER) { 69 | return initialValueToUse; 70 | } 71 | 72 | try { 73 | const raw = window.localStorage.getItem(key); 74 | return raw ? deserializer(raw) : initialValueToUse; 75 | } catch (error) { 76 | console.warn(`Error reading localStorage key “${key}”:`, error); 77 | return initialValueToUse; 78 | } 79 | }, [initialValue, key, deserializer]); 80 | 81 | const [storedValue, setStoredValue] = useState(() => { 82 | if (initializeWithValue) { 83 | return readValue(); 84 | } 85 | 86 | return initialValue instanceof Function ? initialValue() : initialValue; 87 | }); 88 | 89 | // Return a wrapped version of useState's setter function that ... 90 | // ... persists the new value to localStorage. 91 | const setValue: Dispatch> = useEventCallback((value) => { 92 | // Prevent build error "window is undefined" but keeps working 93 | if (IS_SERVER) { 94 | console.warn( 95 | `Tried setting localStorage key “${key}” even though environment is not a client`, 96 | ); 97 | } 98 | 99 | try { 100 | // Allow value to be a function so we have the same API as useState 101 | const newValue = value instanceof Function ? value(readValue()) : value; 102 | 103 | // Save to local storage 104 | window.localStorage.setItem(key, serializer(newValue)); 105 | 106 | // Save state 107 | setStoredValue(newValue); 108 | 109 | // We dispatch a custom event so every similar useLocalStorage hook is notified 110 | window.dispatchEvent(new StorageEvent("local-storage", { key })); 111 | } catch (error) { 112 | console.warn(`Error setting localStorage key “${key}”:`, error); 113 | } 114 | }); 115 | 116 | const removeValue = useEventCallback(() => { 117 | // Prevent build error "window is undefined" but keeps working 118 | if (IS_SERVER) { 119 | console.warn( 120 | `Tried removing localStorage key “${key}” even though environment is not a client`, 121 | ); 122 | } 123 | 124 | const defaultValue = initialValue instanceof Function ? initialValue() : initialValue; 125 | 126 | // Remove the key from local storage 127 | window.localStorage.removeItem(key); 128 | 129 | // Save state with default value 130 | setStoredValue(defaultValue); 131 | 132 | // We dispatch a custom event so every similar useLocalStorage hook is notified 133 | window.dispatchEvent(new StorageEvent("local-storage", { key })); 134 | }); 135 | 136 | useEffect(() => { 137 | setStoredValue(readValue()); 138 | // eslint-disable-next-line react-hooks/exhaustive-deps 139 | }, [key]); 140 | 141 | const handleStorageChange = useCallback( 142 | (event: StorageEvent | CustomEvent) => { 143 | if ((event as StorageEvent).key && (event as StorageEvent).key !== key) { 144 | return; 145 | } 146 | setStoredValue(readValue()); 147 | }, 148 | [key, readValue], 149 | ); 150 | 151 | // this only works for other documents, not the current one 152 | useEventListener("storage", handleStorageChange); 153 | 154 | // this is a custom event, triggered in writeValueToLocalStorage 155 | // See: useLocalStorage() 156 | useEventListener("local-storage", handleStorageChange); 157 | 158 | return [storedValue, setValue, removeValue]; 159 | } 160 | -------------------------------------------------------------------------------- /prometheus-server/prometheus/rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: example 3 | rules: 4 | - alert: NginxLatencyHigh 5 | expr: histogram_quantile(0.99, sum(rate(nginx_http_request_duration_seconds_bucket[2m])) by (host, node, le)) > 3 6 | for: 2m 7 | labels: 8 | severity: warning 9 | annotations: 10 | summary: "Nginx latency high (instance {{ $labels.instance }})" 11 | description: "Nginx p99 latency is higher than 3 seconds\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 12 | 13 | - alert: NginxDown 14 | expr: up{job="nginx"} == 0 15 | for: 1m 16 | labels: 17 | severity: critical 18 | annotations: 19 | summary: "Nginx is down" 20 | description: "Nginx service is not available." 21 | 22 | - alert: ClickhouseAccessDeniedErrors 23 | expr: increase(ClickHouseErrorMetric_RESOURCE_ACCESS_DENIED[5m]) > 0 24 | for: 0m 25 | labels: 26 | severity: info 27 | annotations: 28 | summary: ClickHouse Access Denied Errors (instance {{ $labels.instance }}) 29 | description: "Access denied errors have been logged, which could indicate permission issues or unauthorized access attempts.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 30 | 31 | - alert: ClickhouseHighNetworkTraffic 32 | expr: ClickHouseMetrics_NetworkSend > 250 or ClickHouseMetrics_NetworkReceive > 250 33 | for: 5m 34 | labels: 35 | severity: warning 36 | annotations: 37 | summary: ClickHouse High Network Traffic (instance {{ $labels.instance }}) 38 | description: "Network traffic is unusually high, may affect cluster performance.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 39 | 40 | - alert: ClickhouseDiskSpaceLowOnBackups 41 | expr: ClickHouseAsyncMetrics_DiskAvailable_backups / (ClickHouseAsyncMetrics_DiskAvailable_backups + ClickHouseAsyncMetrics_DiskUsed_backups) * 100 < 20 42 | for: 2m 43 | labels: 44 | severity: warning 45 | annotations: 46 | summary: ClickHouse Disk Space Low on Backups (instance {{ $labels.instance }}) 47 | description: "Disk space on backups is below 20%.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 48 | 49 | - alert: ElasticsearchHighQueryRate 50 | expr: sum(rate(elasticsearch_indices_search_query_total[1m])) > 100 51 | for: 5m 52 | labels: 53 | severity: warning 54 | annotations: 55 | summary: Elasticsearch High Query Rate (instance {{ $labels.instance }}) 56 | description: "The query rate on Elasticsearch cluster is higher than the threshold.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 57 | 58 | - alert: ElasticsearchClusterYellow 59 | expr: elasticsearch_cluster_health_status{color="yellow"} == 1 60 | for: 0m 61 | labels: 62 | severity: warning 63 | annotations: 64 | summary: Elasticsearch Cluster Yellow (instance {{ $labels.instance }}) 65 | description: "Elastic Cluster Yellow status\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 66 | 67 | - alert: ElasticsearchHeapUsageTooHigh 68 | expr: (elasticsearch_jvm_memory_used_bytes{area="heap"} / elasticsearch_jvm_memory_max_bytes{area="heap"}) * 100 > 90 69 | for: 2m 70 | labels: 71 | severity: critical 72 | annotations: 73 | summary: Elasticsearch Heap Usage Too High (instance {{ $labels.instance }}) 74 | description: "The heap usage is over 90%\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 75 | 76 | - alert: ElasticsearchHeapUsageWarning 77 | expr: (elasticsearch_jvm_memory_used_bytes{area="heap"} / elasticsearch_jvm_memory_max_bytes{area="heap"}) * 100 > 80 78 | for: 2m 79 | labels: 80 | severity: warning 81 | annotations: 82 | summary: Elasticsearch Heap Usage warning (instance {{ $labels.instance }}) 83 | description: "The heap usage is over 80%\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 84 | 85 | - alert: ElasticsearchHighIndexingRate 86 | expr: sum(rate(elasticsearch_indices_indexing_index_total[1m]))> 10000 87 | for: 5m 88 | labels: 89 | severity: warning 90 | annotations: 91 | summary: Elasticsearch High Indexing Rate (instance {{ $labels.instance }}) 92 | description: "The indexing rate on Elasticsearch cluster is higher than the threshold.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 93 | 94 | - alert: ElasticsearchNoNewDocuments 95 | expr: increase(elasticsearch_indices_indexing_index_total{es_data_node="true"}[10m]) < 1 96 | for: 0m 97 | labels: 98 | severity: warning 99 | annotations: 100 | summary: Elasticsearch no new documents (instance {{ $labels.instance }}) 101 | description: "No new documents for 10 min!\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 102 | 103 | - alert: HaproxyHasNoAliveBackends 104 | expr: haproxy_backend_active_servers + haproxy_backend_backup_servers == 0 105 | for: 0m 106 | labels: 107 | severity: critical 108 | annotations: 109 | summary: HAproxy has no alive backends (instance {{ $labels.instance }}) 110 | description: "HAProxy has no alive active or backup backends for {{ $labels.proxy }}\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 111 | 112 | - alert: HaproxyServerConnectionErrors 113 | expr: (sum by (proxy) (rate(haproxy_server_connection_errors_total[1m]))) > 100 114 | for: 0m 115 | labels: 116 | severity: critical 117 | annotations: 118 | summary: HAProxy server connection errors (instance {{ $labels.instance }}) 119 | description: "Too many connection errors to {{ $labels.server }} server (> 100 req/s). Request throughput may be too high.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 120 | 121 | - alert: PostgresqlLowXidConsumption 122 | expr: rate(pg_txid_current[1m]) < 5 123 | for: 2m 124 | labels: 125 | severity: warning 126 | annotations: 127 | summary: Postgresql low XID consumption (instance {{ $labels.instance }}) 128 | description: "Postgresql seems to be consuming transaction IDs very slowly\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 129 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { useRouter } from "next/navigation"; 5 | import { KeyboardEvent, useEffect, useState } from "react"; 6 | import useAlerts from "@/hooks/queries/useAlerts"; 7 | import useDiscovery from "@/hooks/queries/useDiscovery"; 8 | import usePrometheusQuery from "@/hooks/queries/usePrometheusQuery"; 9 | import useRules from "@/hooks/queries/useRules"; 10 | import useAuth from "@/hooks/useAuth"; 11 | import AptLogo from "@/components/AptLogo"; 12 | import { Button } from "@/components/Button"; 13 | import { Container } from "@/components/Container"; 14 | import { Input } from "@/components/Input"; 15 | import { Loader } from "@/components/Loader"; 16 | import { Checkbox } from "@/components/ui/checkbox"; 17 | import { cn, withProtocol } from "@/lib/utils"; 18 | 19 | const sourceUrl = 20 | "https://github.com/samber/awesome-prometheus-alerts?tab=License-1-ov-file#readme"; 21 | 22 | const urlRegex = 23 | /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?localhost|([a-zA-Z0-9]+[a-zA-Z0-9-]*\.)+[a-zA-Z]{2,6}(:[0-9]{1,5})?(\/.*)?$/; 24 | 25 | export default function Home() { 26 | useAlerts(); 27 | const router = useRouter(); 28 | const [url, setUrl] = useState(""); 29 | const [username, setUsername] = useState(""); 30 | const [password, setPassword] = useState(""); 31 | const [authFields, setAuthFields] = useState(false); 32 | const { loggedIn, ready, setPromUrl, setPromUsername, setPromPassword } = useAuth(); 33 | 34 | const { 35 | data: server, 36 | error: serverError, 37 | isLoading: connecting, 38 | refetch: connectWithPrometheus, 39 | } = usePrometheusQuery({ 40 | enabled: false, 41 | url: withProtocol(url), 42 | query: "up", 43 | username: authFields ? username : null, 44 | password: authFields ? password : null, 45 | }); 46 | 47 | const { 48 | data: discovery, 49 | error: discoveryError, 50 | isLoading: discovering, 51 | } = useDiscovery({ 52 | enabled: !!server, 53 | url: withProtocol(url), 54 | username: authFields ? username : null, 55 | password: authFields ? password : null, 56 | }); 57 | 58 | const { data: rules, isLoading: gettingRules } = useRules({ 59 | enabled: !!discovery && discovery.length !== 0, 60 | url: withProtocol(url), 61 | username: authFields ? username : null, 62 | password: authFields ? password : null, 63 | }); 64 | 65 | const onEnter = (e: KeyboardEvent) => { 66 | if (e.key === "Enter" && url.match(urlRegex) && !(connecting || discovering || gettingRules)) { 67 | connect(); 68 | } 69 | }; 70 | 71 | const connect = () => { 72 | connectWithPrometheus(); 73 | }; 74 | 75 | useEffect(() => { 76 | if (ready && loggedIn) { 77 | router.replace("/home"); 78 | } 79 | }, [loggedIn, ready, router]); 80 | 81 | useEffect(() => { 82 | if (server && discovery?.length !== 0 && rules) { 83 | setPromUrl(withProtocol(url)); 84 | setPromUsername(username); 85 | setPromPassword(password); 86 | router.push("/home"); 87 | } 88 | }, [ 89 | server, 90 | password, 91 | router, 92 | url, 93 | username, 94 | discovery, 95 | rules, 96 | setPromUrl, 97 | setPromUsername, 98 | setPromPassword, 99 | ]); 100 | 101 | if (!ready) { 102 | return ( 103 | 104 | 105 | 106 | ); 107 | } 108 | 109 | return ( 110 | 111 | 112 | 113 |
114 |

115 | {"The most apt alert rules toolkit for\nyour Prometheus setup."} 116 |

117 | 118 |
119 |
120 |

121 | Get alert rule recommendations. 122 |

123 |
124 | 125 |
126 |

127 | Browse the alert rule library. 128 |

129 |
130 | 131 |
132 |

133 | Simplify setting up Prometheus. 134 |

135 |
136 |
137 | 138 |

139 | a companion project to{" "} 140 | window.open(sourceUrl, "_blank")}> 141 | awesome-prometheus-alerts 142 | 143 |

144 |
145 | 146 |
147 | file 155 | 156 |

157 | {`To get started, provide your local or\nproduction Prometheus read URL.`} 158 |

159 | 160 |
161 | setUrl(e.target.value.replaceAll(" ", ""))} 168 | onKeyDown={onEnter} 169 | /> 170 | 171 | {authFields ? ( 172 |
173 | setUsername(e.target.value)} 180 | onKeyDown={onEnter} 181 | /> 182 | 183 | setPassword(e.target.value)} 191 | onKeyDown={onEnter} 192 | /> 193 |
194 | ) : null} 195 | 196 |
202 | setAuthFields(value)} 207 | /> 208 | 209 | 212 |
213 | 214 | 230 |
231 | 232 | {serverError?.cause || discoveryError?.cause ? ( 233 |

234 | {serverError?.message || discoveryError?.message} 235 |

236 | ) : null} 237 |
238 |
239 | ); 240 | } 241 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------