{"id":1163,"date":"2025-08-25T01:52:14","date_gmt":"2025-08-25T08:52:14","guid":{"rendered":"http:\/\/184.72.63.26\/?p=1163"},"modified":"2026-03-16T20:26:26","modified_gmt":"2026-03-17T03:26:26","slug":"building-a-real-world-dashboard-on-grafana-cloud-logs-metrics-uptime-security","status":"publish","type":"post","link":"https:\/\/www.wallacel.com\/index.php\/2025\/08\/25\/building-a-real-world-dashboard-on-grafana-cloud-logs-metrics-uptime-security\/","title":{"rendered":"Building a Real-World  Dashboard on Grafana Cloud (Logs, Metrics, Uptime &amp; Security)"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Introduction<\/h2>\n\n\n\n<p>I wrote this article to turn my own notes into something useful, for me and for anyone who is trying to stand up a real, no-nonsense dashboard. I wanted one place to see if my blog is up, how fast it feels, where visitors come from, and whether anyone\u2019s rattling the doors. So I choose <strong>Grafana<\/strong> as the tool, wired <strong>Alloy<\/strong> to ship logs into <strong>Loki<\/strong> and built something that I can actually use day to day.<br><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"824\" src=\"http:\/\/184.72.63.26\/wp-content\/uploads\/2025\/08\/dashboard-1024x824.jpg\" alt=\"\" class=\"wp-image-1198\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-1024x824.jpg 1024w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-300x241.jpg 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-768x618.jpg 768w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-1536x1236.jpg 1536w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard.jpg 1558w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">I create this Grafana dashboard to monitor this blog<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Why Grafana Cloud (and how it compares)<\/h2>\n\n\n\n<p>There\u2019s no shortage of options:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Dynatrace<\/strong> \u2013 full-platform observability and automation with powerful topology, causation AI, and managed SLOs; great for enterprise scale and \u201cit just works\u201d workflows.<\/li>\n\n\n\n<li><strong>Datadog \/ New Relic<\/strong> \u2013 polished, hosted APM + logs + synthetics; excellent UX; cost typically grows with event volume.<\/li>\n\n\n\n<li><strong>Elastic Cloud (ELK)<\/strong> \u2013 strong log analytics; dashboards are flexible, PromQL-style workflows are less native.<\/li>\n\n\n\n<li><strong>AWS CloudWatch<\/strong> \u2013 native and cost-friendly in AWS, fewer visualization niceties.<\/li>\n\n\n\n<li><strong>Self-hosted OSS<\/strong> \u2013 Prometheus + Grafana + Loki\/Tempo = control &amp; low cost, but you own the plumbing.<\/li>\n<\/ul>\n\n\n\n<p><strong>Why I chose Grafana Cloud here:<\/strong> I wanted a lightweight, OSS-friendly pipeline (PromQL\/LogQL), easy hosted endpoints, built-in synthetics, and fast iteration for a personal site. I still reach for Dynatrace at work for end-to-end service topology, automation, and AI-assisted triage\u2014but for this blog, Grafana Cloud + Loki + Alloy is the sweet spot.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What makes a good dashboard?<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Purpose<\/strong>: one screen answers \u201cIs it up?\u201d, \u201cIs it healthy?\u201d, \u201cWhere are users from?\u201d, \u201cAny attacks?\u201d<\/li>\n\n\n\n<li><strong>Actionable<\/strong>: panels link to Explore pre-filtered for quick deep dives.<\/li>\n\n\n\n<li><strong>Clarity<\/strong>: consistent <strong>units<\/strong> and <strong>thresholds<\/strong>; don\u2019t make readers translate.<\/li>\n\n\n\n<li><strong>Few, great panels<\/strong> (not too much scrolling). Add short descriptions so on-call knows how to react.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>WordPress\/Apache:<\/strong> my blog runs on a Linux box that writes standard access\/error logs.<\/li>\n\n\n\n<li><strong>Alloy on the host:<\/strong> a tiny agent tails those logs, adds useful fields (status, path, IP \u2192 country), and ships them out.<\/li>\n\n\n\n<li><strong>Grafana Cloud Loki:<\/strong> receives the logs over HTTPS (scoped token), so I can search and chart them with LogQL.<\/li>\n\n\n\n<li><strong>Dashboards in Grafana:<\/strong> panels for status codes, p95\/max latency, top pages\/countries, bytes, and \u201csuspicious activity.\u201d<\/li>\n\n\n\n<li><strong>Synthetic Monitoring:<\/strong> browser checks from distinct geo-locations at regular intervals to verify real-world reachability.<\/li>\n<\/ul>\n\n\n\n<p><strong>Flow:<\/strong> Apache on Linux \u2192 Alloy (parse + GeoIP) \u2192 Grafana Cloud Logs \u2192 Grafana dashboard<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"726\" height=\"252\" src=\"http:\/\/184.72.63.26\/wp-content\/uploads\/2025\/08\/alloy.png\" alt=\"\" class=\"wp-image-1182\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy.png 726w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy-300x104.png 300w\" sizes=\"auto, (max-width: 726px) 100vw, 726px\" \/><figcaption class=\"wp-element-caption\">Image: <a href=\"https:\/\/grafana.com\/docs\/loki\/latest\/send-data\/alloy\/\">https:\/\/grafana.com\/docs\/loki\/latest\/send-data\/alloy\/<\/a><\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">1) Set up Grafana Cloud Logs (Managed Loki)<\/h2>\n\n\n\n<p>Grafana Cloud Logs is a fully managed <strong>Loki <\/strong>service, a log aggregation system powered by <strong>Grafana<\/strong>\u2014an open-source, Prometheus-inspired tool optimized for efficiency and scalability in log management. Unlike traditional log systems that index full-text content, Loki indexes only metadata (labels) for each log stream &#8211; such as service names, environments, or Kubernetes metadata. This minimalist approach keeps indexing lean and cost-effective.<\/p>\n\n\n\n<p>To setup Loki in Grafana Cloud:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In your Grafana Cloud stack, go to <strong>Administration \u2192 Users and access \u2192 Cloud access policies<\/strong>.<br>Create a cloud access policy with scope <strong><code>logs:write<\/code><\/strong>.<\/li>\n\n\n\n<li>Create an access-policy token and add it to the newly created policy<\/li>\n\n\n\n<li>To get the endpoint for pushing logs to Loki, go to <strong>Loki \u2192 Details \/ Send logs<\/strong>:\n<ul class=\"wp-block-list\">\n<li><strong>URL<\/strong>: <code>https:\/\/&lt;cluster&gt;\/loki\/api\/v1\/push<\/code><\/li>\n\n\n\n<li><strong>User<\/strong> (the <strong>basic_auth username<\/strong>)<\/li>\n\n\n\n<li><strong>Password <\/strong>(your <strong>access-policy token<\/strong>)<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">2) Install Grafana Alloy<\/h2>\n\n\n\n<p><strong>Grafana Alloy<\/strong> is Grafana&#8217;s open-source distribution of the <strong>OpenTelemetry Collector<\/strong>, enhanced with built-in support for Prometheus pipelines. It unifies collection, processing, and export of logs, metrics, traces, and profiles\u2014all through a single agent.<\/p>\n\n\n\n<p>If you\u2019ve worked with observability tools like <strong>Dynatrace OneAgent<\/strong>, you\u2019ll notice the contrast: OneAgent is almost zero-touch, automatically discovering workloads and wiring telemetry with little to no configuration. Alloy, by comparison, gives you flexibility and openness, but you do need to roll up your sleeves to manage its pipelines and config.<\/p>\n\n\n\n<p>To install Alloy, from <strong>Grafana Cloud \u2192 Connections <strong>\u2192<\/strong><\/strong> <strong>Collector<\/strong> <strong>\u2192 Install Grafana Alloy<\/strong>, generate the install command for your distro\/arch. Disable <strong>Remote Configuration<\/strong> so you can manage a local <code>config.alloy<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"996\" height=\"760\" src=\"http:\/\/184.72.63.26\/wp-content\/uploads\/2025\/08\/install-alloy.png\" alt=\"\" class=\"wp-image-1183\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/install-alloy.png 996w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/install-alloy-300x229.png 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/install-alloy-768x586.png 768w\" sizes=\"auto, (max-width: 996px) 100vw, 996px\" \/><\/figure>\n\n\n\n<p>Copy and Run the generated alloy install command on your host, then start Alloy (example):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">nohup sudo .\/alloy-linux-amd64 run .\/config.alloy &gt; alloy.log 2&gt;&amp;1 &amp;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">3) Configure Alloy: ship and enrich Apache logs to Loki<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"403\" src=\"http:\/\/184.72.63.26\/wp-content\/uploads\/2025\/08\/alloy-components-1024x403.jpg\" alt=\"\" class=\"wp-image-1212\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy-components-1024x403.jpg 1024w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy-components-300x118.jpg 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy-components-768x302.jpg 768w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy-components-1536x604.jpg 1536w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/alloy-components.jpg 1573w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>There are 4 major components to configure Alloy to ship logs to Loki:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><code><strong>local.file_match<\/strong><\/code>&nbsp;discovers files on the local filesystem using glob patterns<br><\/li>\n\n\n\n<li><code><strong>loki.source.file<\/strong><\/code>&nbsp;reads log entries from files and forwards them to other&nbsp;<code>loki.process<\/code><br><\/li>\n\n\n\n<li><code><strong>loki.process<\/strong><\/code>&nbsp;enrich the log lines (e.g. parsing, adding context, mapping ip address to their originated countries, etc.) and forwards the results to <code>loki.write<\/code><br><\/li>\n\n\n\n<li><code><strong>loki.write<\/strong><\/code> sends the log lines to the Loki endpoint over the network using the Loki&nbsp;<code>logproto<\/code>&nbsp;format<\/li>\n<\/ol>\n\n\n\n<p><strong><code>config.alloy<\/code> (redacted, minimal example)<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">\/\/ 1) Which files to watch\nlocal.file_match \"local_files\" {\n  path_targets = [\n    { \"__path__\" = \"\/opt\/xxx\/apache\/logs\/access_log\", job = \"apache_access\" },\n    { \"__path__\" = \"\/opt\/xxx\/apache\/logs\/error_log\",  job = \"apache_error\" },\n  ]\n  sync_period = \"5s\"\n}\n\n\/\/ 2) where to read logs from\nloki.source.file \"log_scrape\" {\n    targets    = local.file_match.local_files.targets\n    forward_to = [loki.process.enrich.receiver]\n    tail_from_end = true\n}\n\n\/\/ 3) Parse and enrich access logs\nloki.process \"enrich\" {\n  stage.match {\n    selector = \"{job=\\\"apache_access\\\"}\"\n\n    stage.regex {\n      expression = \"^(?P&lt;ip&gt;\\\\S+) \\\\S+ \\\\S+ \\\\[(?P&lt;time&gt;[^\\\\]]+)\\\\] \\\\\\\"(?P&lt;method&gt;\\\\S+) (?P&lt;path&gt;[^ ]+) (?P&lt;proto&gt;[^\\\\\\\"]+)\\\\\\\" (?P&lt;status&gt;\\\\d{3}) (?P&lt;bytes_sent&gt;\\\\d+|-)\"\n    }\n\n    stage.timestamp {\n      source = \"time\"\n      format = \"02\/Jan\/2006:15:04:05 -0700\"\n    }\n\n    \/\/ Enable Geoip using GeoLite Country Database\n    stage.geoip {\n      source  = \"ip\"\n      db      = \"\/usr\/share\/GeoIP\/GeoLite2-Country.mmdb\"\n      db_type = \"country\"\n    }\n\n    stage.labels {\n      values = {\n        ip                 = \"\",\n        method             = \"\",\n        status             = \"\",\n        path               = \"\",\n        bytes_sent         = \"\",\n        geoip_country_code = \"\",\n      }\n    }\n  }\n\n  forward_to = [loki.write.grafana_cloud_loki.receiver]\n}\n\n\/\/ 4) Push to Grafana Cloud Loki\nloki.write \"grafana_cloud_loki\" {\n  endpoint {\n    url = \"https:\/\/&lt;your-loki-cluster&gt;\/loki\/api\/v1\/push\"\n    basic_auth {\n      username = \"&lt;LOKI_INSTANCE_ID&gt;\"\n      password = \"&lt;ACCESS_POLICY_TOKEN&gt;\"\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">4) Enable GeoIP for the geomap<\/h2>\n\n\n\n<p>Apache logs only know visitor <strong>IPs<\/strong>. To draw a world map and a \u201cTop countries\u201d table, I need a <strong>country code<\/strong> on each log line. Country-level GeoIP is a sweet spot: it\u2019s useful for trends and anomaly spotting (e.g., sudden traffic from a new region) without getting too granular or privacy-heavy.<\/p>\n\n\n\n<p>I use the free <strong>MaxMind GeoLite2-Country<\/strong> database. Country (not city) keeps label cardinality low and avoids PII while still powering the Geomap and country panels.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Create a free <strong>MaxMind<\/strong> account and download <strong>GeoLite2-Country<\/strong> (<code>.mmdb<\/code>).<\/li>\n\n\n\n<li>Place the file on the server, e.g. <code>\/usr\/share\/GeoIP\/GeoLite2-Country.mmdb<\/code>.<\/li>\n\n\n\n<li>(Optional) sanity check: <code>mmdblookup --file \/usr\/share\/GeoIP\/GeoLite2-Country.mmdb --ip &lt;some_ip&gt; country iso_code<\/code><\/li>\n\n\n\n<li>In the above Alloy config, enable the <code>stage.geoip<\/code> step so each access log line gets a <strong><code>geoip_country_code<\/code><\/strong> label.<\/li>\n<\/ul>\n\n\n\n<h1 class=\"wp-block-heading\">5) Dashboard Panels<\/h1>\n\n\n\n<p>Below are the panels I have added to my dashboard, what each is for, how to read it during an incident, and the query I used.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">a) Website Uptime (Stat) + Synthetic Monitoring<\/h2>\n\n\n\n<p>To watch real user reachability\u2014not just server CPU\u2014I stood up <strong>three Synthetic Monitoring browser checks<\/strong> in Grafana across <strong>AMER, EMAC, and APAC<\/strong> regions. Each check probes my website every <strong>15 minutes<\/strong>. <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1001\" height=\"357\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe.png\" alt=\"\" class=\"wp-image-1166\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe.png 1001w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe-300x107.png 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe-768x274.png 768w\" sizes=\"auto, (max-width: 1001px) 100vw, 1001px\" \/><\/figure>\n\n\n\n<p><strong>Why this matters:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If <strong>all three<\/strong> fail together, I likely broke the site or origin.<\/li>\n\n\n\n<li>If <strong>one region<\/strong> fails while the others pass, it\u2019s routing, DNS, CDN, or a regional outage.<\/li>\n\n\n\n<li>I add a table showing <em>how many checks succeeded<\/em> per probe in the last window.<\/li>\n<\/ul>\n\n\n\n<p><strong>How I read it:<\/strong><\/p>\n\n\n\n<p>The <strong>table<\/strong> shows each probe\u2019s recent success (green light) or fail (red light).<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"464\" height=\"303\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe_results.png\" alt=\"\" class=\"wp-image-1167\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe_results.png 464w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/probe_results-300x196.png 300w\" sizes=\"auto, (max-width: 464px) 100vw, 464px\" \/><\/figure>\n\n\n\n<p>An Uptime panel showing the current status in the last 15 minutes base on all three probes&#8217; results:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"258\" height=\"151\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/uptime.png\" alt=\"\" class=\"wp-image-1168\"\/><\/figure>\n\n\n\n<p>Query for this panel:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">max by () (max_over_time(probe_success{job=\"Blog Browser Check\", instance=\"test\", probe=~\"Calgary|London|Singapore\"}[900s]))<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">b) Unique Visitors &amp; Realtime Visitors (Stat)<\/h2>\n\n\n\n<p>These two tiles tell two stories:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Range uniques:<\/strong> \u201cHow many distinct visitors over the current dashboard window?\u201d<\/li>\n\n\n\n<li><strong>Real-time uniques:<\/strong> \u201cWho\u2019s on the site in the <strong>last 5 minutes<\/strong>?\u201d<\/li>\n<\/ul>\n\n\n\n<p><strong>Why it\u2019s here:<\/strong> perfect for campaign days and sanity checks during incidents.<br><strong>How I read it:<\/strong> if range uniques look normal but real-time drops to near zero, it\u2019s likely an <strong>availability<\/strong> or <strong>routing<\/strong> issue right now.<br><strong>Panel tips:<\/strong> The 5-minute tile uses a <strong>Relative time override<\/strong> so it always reflects \u201cright now,\u201d no matter what the main range is.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"518\" height=\"149\" src=\"http:\/\/184.72.63.26\/wp-content\/uploads\/2025\/08\/unique-visitors.png\" alt=\"\" class=\"wp-image-1171\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/unique-visitors.png 518w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/unique-visitors-300x86.png 300w\" sizes=\"auto, (max-width: 518px) 100vw, 518px\" \/><\/figure>\n\n\n\n<p>The queries for the above tiles are:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">\/\/Unique Visitors\ncount(sum by (ip) (count_over_time({job=\"apache_access\"}[$__range])))\n\n\/\/Realtime Visitors\ncount(sum by (ip) (count_over_time({job=\"apache_access\"}[5m])))<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">c) Top Requested Pages (Table)<\/h2>\n\n\n\n<p>What do people actually read on my blog? This table shows the <strong>top 10 requested pages<\/strong>, with a small gauge to show relative weight.<\/p>\n\n\n\n<p><strong>Why it\u2019s here:<\/strong> helps prioritize performance work and content decisions.<br><strong>How I read it:<\/strong> While I&#8217;m curious how popular my pages are, I\u2019m also looking for unusual new paths jumping into the top 10. And I exclude <strong>static assets<\/strong> and noisy bot paths so this stays actionable. I also keep the <strong>path<\/strong> column as plain text so gauges only color the count, not the URL.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"838\" height=\"434\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-paths.png\" alt=\"\" class=\"wp-image-1186\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-paths.png 838w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-paths-300x155.png 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-paths-768x398.png 768w\" sizes=\"auto, (max-width: 838px) 100vw, 838px\" \/><\/figure>\n\n\n\n<p>The query for the above table:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">topk(\n  10,\n  sum by (path) (\n    count_over_time(\n      {job=\"apache_access\",\n       status=\"200\",\n       path!=\"\/\",\n       path!~\".*\\\\.(ico|svg|css|png|jpg|jpeg|webp|avif|js|xml|txt|woff2|ttf)(\\\\?.*)?$\"}\n      [$__range]\n    )\n  )\n)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">d) Visitors by Countries (Geomap)<\/h2>\n\n\n\n<p>I use a world map to spot where traffic is coming from at a glance. Bigger bubbles mean more hits; if a new country suddenly lights up, that\u2019s a clue -maybe a campaign landed, or a bot net woke up.<\/p>\n\n\n\n<p><strong>Why it\u2019s here:<\/strong> quick reality check on audience reach and anomalies by region.<br><strong>How I read it:<\/strong> I keep this locked to <strong>last 24 hours<\/strong> so it doesn\u2019t swing wildly. If one country balloons, I click it to drill into just those logs.<br><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"524\" height=\"302\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/visitor-countries.png\" alt=\"\" class=\"wp-image-1177\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/visitor-countries.png 524w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/visitor-countries-300x173.png 300w\" sizes=\"auto, (max-width: 524px) 100vw, 524px\" \/><\/figure>\n\n\n\n<p>Since I already enriched the log lines with geocode, showing the bubble on the map can be easily done by adding a Markers layer to the Geomap with the Lookup mode on <strong>geoip_country_code<\/strong>. <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"320\" height=\"709\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/geomap.png\" alt=\"\" class=\"wp-image-1178\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/geomap.png 320w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/geomap-135x300.png 135w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/figure>\n\n\n\n<p>The query I use is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">sum by (geoip_country_code) (\n  count_over_time({job=\"apache_access\", geoip_country_code!=\"\"} | __error__=`` [24h])\n)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">e) Top 10 Countries (Table)<\/h2>\n\n\n\n<p>This is the map\u2019s sidekick. Same story, but with numbers you can quote.<\/p>\n\n\n\n<p><strong>Why it\u2019s here:<\/strong> to answer \u201cWhich countries topped the list this period?\u201d without eyeballing bubbles.<br><strong>How I read it:<\/strong> sorted descending; if a country jumps from #7 to #2, I open Explore on that country code and look for a pattern (referrer spike, bot user-agents, etc.). I also use Value mappings to show cute flag labels (\ud83c\udde8\ud83c\udde6CA, \ud83c\uddf8\ud83c\uddecSG) so it\u2019s readable at a glance.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"168\" height=\"306\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-countries.png\" alt=\"\" class=\"wp-image-1179\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-countries.png 168w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-countries-165x300.png 165w\" sizes=\"auto, (max-width: 168px) 100vw, 168px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">f) Performance: p95 &amp; max request time (Time series)<\/h2>\n\n\n\n<p>Two lines; two jobs. <strong>p95<\/strong> is what most users feel. <strong>Max<\/strong> catches outliers and ugly spikes.<\/p>\n\n\n\n<p><strong>Why it\u2019s here:<\/strong> when p95 creeps up, I check the top pages; when <strong>max<\/strong> spikes, I look for a single heavy request or backend hiccup.<br><strong>How I read it:<\/strong> I set soft thresholds (e.g., <strong>1 sec warning<\/strong>, <strong>1 sec critical<\/strong>) to color the background. I keep p95 in a solid blue shade and max in a lighter blue light so it\u2019s obvious which is which.<\/p>\n\n\n\n<p>The queries for the performance tile are:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">\/\/p95 percentile performance\nmax by (host) (quantile_over_time(\n  0.95,\n  {job=\"apache_access\"}\n  | regexp \" (?P&lt;request_time&gt;\\\\d+)$\"\n  | unwrap request_time\n  [$__interval]\n) \/ 1000)\n\n\/\/max latency\nmax by (host) (max_over_time(\n  {job=\"apache_access\"}\n  | regexp \" (?P&lt;request_time&gt;\\\\d+)$\"\n  | unwrap request_time\n  [$__interval]\n) \/ 1000)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">g) Security watch: Error paths &amp; Noisy IPs<\/h2>\n\n\n\n<p>A pair of short lists to surface probing and scanning behavior:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Top error paths<\/strong> (4xx\/5xx)<\/li>\n\n\n\n<li><strong>Top IPs causing errors<\/strong><\/li>\n<\/ul>\n\n\n\n<p><strong>Why it\u2019s here:<\/strong> early warning for things like <code>\/xmlrpc.php<\/code>, <code>\/.env<\/code>, or <code>\/.git\/config<\/code> hits\u2014classic probing.<br><strong>How I read it:<\/strong> if a single IP dominates, I open Explore on that IP for status codes and user agents, then decide whether to block or rate-limit upstream.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"895\" height=\"305\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-errors-1.png\" alt=\"\" class=\"wp-image-1187\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-errors-1.png 895w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-errors-1-300x102.png 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/top-errors-1-768x262.png 768w\" sizes=\"auto, (max-width: 895px) 100vw, 895px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Final Thoughts<\/h2>\n\n\n\n<p>In the end, this project was about turning raw Apache logs into a living, trustworthy picture of my site. Grafana Cloud gave me the hosted plumbing; Alloy and Loki kept it OSS-friendly; and a handful of well-chosen panels (status codes, p95\/max, countries, bytes, synthetics, and security) turned noise into signal. The result is a dashboard I actually use: it tells me if I\u2019m up, who I\u2019m serving, how it feels, and whether anyone\u2019s poking the wrong doors. Next steps for me are light alerts on error rate and p95, and maybe a small RUM script to capture real client timing. If this write-up helps you ship your first version faster, even better\u2014steal the ideas, adapt them to your stack, and make the dashboard yours.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"824\" src=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-1024x824.jpg\" alt=\"\" class=\"wp-image-1198\" srcset=\"https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-1024x824.jpg 1024w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-300x241.jpg 300w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-768x618.jpg 768w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard-1536x1236.jpg 1536w, https:\/\/www.wallacel.com\/wp-content\/uploads\/2025\/08\/dashboard.jpg 1558w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Screenshot<\/figcaption><\/figure>\n\n\n\n<p><em>You can find a snapshot of my dashboard&nbsp;<a href=\"https:\/\/wallacelee.grafana.net\/dashboard\/snapshot\/2ezYOI0w098rm2RkmK3gS0YTfrJDQh67\">here<\/a><\/em><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction I wrote this article to turn my own notes into something useful, for me and for anyone who is trying to stand up a real, no-nonsense dashboard. I wanted one place to see if my blog is up, how fast it feels, where visitors come from, and whether anyone\u2019s rattling the doors. So I [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1201,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[77],"tags":[74,76,73,75],"class_list":["post-1163","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-grafana","tag-alloy","tag-dashboard","tag-grafana","tag-loki"],"_links":{"self":[{"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/posts\/1163","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/comments?post=1163"}],"version-history":[{"count":28,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/posts\/1163\/revisions"}],"predecessor-version":[{"id":1331,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/posts\/1163\/revisions\/1331"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/media\/1201"}],"wp:attachment":[{"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/media?parent=1163"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/categories?post=1163"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.wallacel.com\/index.php\/wp-json\/wp\/v2\/tags?post=1163"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}