<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Phare</title>
    <description>The latest articles on DEV Community by Phare (@phare).</description>
    <link>https://dev.to/phare</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9424%2F5fab0bd2-0baf-4146-b295-e0d22e4a42b0.jpg</url>
      <title>DEV Community: Phare</title>
      <link>https://dev.to/phare</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phare"/>
    <language>en</language>
    <item>
      <title>Recreating Laravel Cloud’s range input with native HTML</title>
      <dc:creator>Nicolas Beauvais</dc:creator>
      <pubDate>Wed, 02 Jul 2025 15:27:54 +0000</pubDate>
      <link>https://dev.to/phare/recreating-laravel-clouds-range-input-with-native-html-1b69</link>
      <guid>https://dev.to/phare/recreating-laravel-clouds-range-input-with-native-html-1b69</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8m3mf7zrbfgwnlr94fo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8m3mf7zrbfgwnlr94fo.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the past few days, I’ve been working on improving the billing experience in Phare, with the addition of &lt;a href="https://docs.phare.io/changelog/platform/2025#credits-payment-for-scale-plan" rel="noopener noreferrer"&gt;prepaid credits&lt;/a&gt;. While tweaking the billing UI, I realized the current input for configuring additional quota wasn’t great, it didn’t clearly show what was already included in the paid plan versus what the user could add.&lt;/p&gt;

&lt;p&gt;The UX of entering large number in a text input could certainly be improved. It wasn't horrible, but I felt it could be better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frff1ne8ri458z5wr14o1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frff1ne8ri458z5wr14o1.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I went hunting for inspiration on Dribbble, and other listing websites that post screenshots of SaaS services interfaces. After some research, I stumbled upon the &lt;a href="https://cloud.laravel.com/pricing" rel="noopener noreferrer"&gt;Laravel Cloud pricing calculator&lt;/a&gt;. Their range input design was spot-on: clear separation between included and additional values, visually appealing, and user-friendly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8q9aglkwpgep7xj045o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8q9aglkwpgep7xj045o.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Naturally, I did what any self-respecting developer would do, open the browser inspector to &lt;del&gt;steal&lt;/del&gt; look at the code. Turns out, they recreated a full range input with a few HTML elements and glued everything with JavaScript using Alpine.js. Here's the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="group/range relative h-8 self-stretch"&amp;gt;
  &amp;lt;!– Full track --&amp;gt;
  &amp;lt;div /&amp;gt;

  &amp;lt;!-- Static track --&amp;gt;
  &amp;lt;div /&amp;gt;

  &amp;lt;!-- Progress bar --&amp;gt;
  &amp;lt;div /&amp;gt;

  &amp;lt;!-- Handle --&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;!-- Tick --&amp;gt;
  &amp;lt;div /&amp;gt;

  &amp;lt;!-- HTML range input --&amp;gt;
  &amp;lt;input /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because I'm the laziest developer, rebuilding all this for the six people (love you guys 🫶) that pay for Phare felt like overkill. Could I recreate a similar input with less work? There's probably a way to get a similar result with some CSS on top of the native range input, and maybe a few lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the range input
&lt;/h2&gt;

&lt;p&gt;To match the Laravel Cloud design, we need the following components:  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Range track&lt;/strong&gt; : the rail where the handle moves.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Handle&lt;/strong&gt; : the draggable thumb.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Progress bar&lt;/strong&gt; : the filled area left of the handle.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Static part&lt;/strong&gt; : a fixed section showing the value already included in the plan.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Tick&lt;/strong&gt; : a visual marker where the included value ends and extra begins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd7aijwkkzsf6rtqo4ave.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd7aijwkkzsf6rtqo4ave.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The static part and tick are mostly cosmetic and can easily be visually faked outside the range input itself. Everything else is already included in the native HTML range input.  &lt;/p&gt;

&lt;p&gt;So why did the Laravel team go full custom?&lt;/p&gt;
&lt;h2&gt;
  
  
  Limitations of the native HTML range input
&lt;/h2&gt;

&lt;p&gt;To look great, the handle needs to land &lt;strong&gt;exactly at the tick’s position&lt;/strong&gt; when at the minimum value. It should also cover the tick to be visually appealing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcngft55u5lj5aw5q4p3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcngft55u5lj5aw5q4p3.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, this isn't possible as the native range input’s handle is confined to the boundaries of the track. So, what if we make the range input track overlap under the static part to allow the handle to sit on the tick? (such a weird sentence).&lt;/p&gt;

&lt;p&gt;Well, the native range inputs don’t let us set a different &lt;code&gt;z-index&lt;/code&gt; for the handle and for the track. If we push the track behind the static part, the handle goes with it. If you bring it forward, the whole thing looks messy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm36fujvcgisxj5fml7ui.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm36fujvcgisxj5fml7ui.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;Enter the &lt;strong&gt;CSS inner shadow&lt;/strong&gt; : using an inner shadow allows us to fake a few extra pixels of the static part &lt;strong&gt;inside&lt;/strong&gt; the track. This lets the handle glide over it without getting hidden.  &lt;/p&gt;

&lt;p&gt;By carefully layering the tick and the static track visually outside the actual input, and using this inner shadow to fake part of the static part, we can get something that works well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvghry1msjk3hjr48cgz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvghry1msjk3hjr48cgz.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Styling the handle
&lt;/h2&gt;

&lt;p&gt;Using the border property on the handle with &lt;code&gt;-moz-range-thumb&lt;/code&gt; work great in Firefox, but Chrome does not seem to support it. Again, inner shadows are here to save the day, and bring us cross browser consistency.&lt;/p&gt;
&lt;h2&gt;
  
  
  Styling the progress bar
&lt;/h2&gt;

&lt;p&gt;To make the progress bar pattern, Laravel's team used a clever trick based on &lt;code&gt;repeating-linear-gradient&lt;/code&gt; to create infinitely repeating stripes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;background-image: repeating-linear-gradient(135deg, black 0px, black 1px, #99a1af 1px, #99a1af 4px);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But applying that to our native range input will cover the entire track. I only wanted it on the left side of the handle to represent progress.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9djd8hakd0vm6w64os27.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9djd8hakd0vm6w64os27.webp" alt="Recreating Laravel Cloud’s range input with native HTML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For fixing this, there isn't any other solution, we will need a few lines of JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.getElementById("range").addEventListener('input', function (event) {
    let input = event.target
    let value = parseInt(input.value)
    let min = parseInt(input.getAttribute('min'))
    let max = parseInt(input.getAttribute('max'))

    let percentage = (value - min) / (max - min) * 100

    input.style.backgroundSize = `${percentage}% 100%, 100% 100%`
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final result
&lt;/h2&gt;

&lt;p&gt;The end result is not quite as flexible as Laravel Cloud’s full custom implementation. Since the track should fake the design of the static part and tick, it does not allow more complex design, but it fits perfectly for my use case:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59e6je76xh0mx6hn3cap.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59e6je76xh0mx6hn3cap.gif" alt="Final input version"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The final native HTML approach is quite simple, with minimal tricks, and still good-looking. I think it shows that it's possible to go quite far with native elements without having to resort to recreating everything with JavaScript.  &lt;/p&gt;

&lt;p&gt;You can see a fully working example and the code to recreate the input on CodePen:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/nicbvs/embed/raVgORg?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;And if you like my attention to details, you should try Phare, it's a great tool for &lt;a href="https://phare.io/products/uptime/website-monitoring" rel="noopener noreferrer"&gt;uptime monitoring&lt;/a&gt;, &lt;a href="https://phare.io/products/uptime/incident-management" rel="noopener noreferrer"&gt;incident management&lt;/a&gt;, and &lt;a href="https://phare.io/products/uptime/status-pages" rel="noopener noreferrer"&gt;status pages&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What to look for in an uptime monitoring tool</title>
      <dc:creator>Nicolas Beauvais</dc:creator>
      <pubDate>Mon, 16 Jun 2025 20:43:51 +0000</pubDate>
      <link>https://dev.to/phare/what-to-look-for-in-an-uptime-monitoring-tool-4chk</link>
      <guid>https://dev.to/phare/what-to-look-for-in-an-uptime-monitoring-tool-4chk</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiuy7gzemehnuo834tif5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiuy7gzemehnuo834tif5.webp" alt="What to look for in an uptime monitoring tool" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your website is how you pay the bills, whether it’s a SaaS, an API, or that side project financing your daily ramen, you need to know when it’s down. Preferably before your customers start angrily spamming that F5 key.&lt;/p&gt;

&lt;p&gt;There are thousands of uptime monitoring tools out there, but after running one myself for a few years, here’s what I think you should actually be paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a tool that won’t sleep on the job
&lt;/h2&gt;

&lt;p&gt;There’s no point in using an uptime monitoring service that’s less reliable than the thing you’re monitoring.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9sxpm1xwleh5r175b1t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy9sxpm1xwleh5r175b1t.jpg" alt="What to look for in an uptime monitoring tool" width="774" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I love indie products (obviously), but this is one of those times where time in the market beats timing the market*.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check how long the tool’s been around&lt;/li&gt;
&lt;li&gt;Carefully review stats on their status page, some are... enlightening&lt;/li&gt;
&lt;li&gt;Check out the documentation, it’s usually a good indicator of quality&lt;/li&gt;
&lt;li&gt;Most reviews online are fake, ask your friends instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;*Not financial advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud or self-hosted? Choose wisely
&lt;/h2&gt;

&lt;p&gt;Your monitoring system should live outside your infrastructure, &lt;a href="https://dev.to/phare/the-3-year-journey-to-an-actually-good-monitoring-stack-2dd2"&gt;spread across multiple data centers&lt;/a&gt;, poking your endpoints from different parts of the world. That’s typically not something you get with a self-hosted setup.&lt;/p&gt;

&lt;p&gt;That said, self-hosted might make sense if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’re monitoring a closed/private network&lt;/li&gt;
&lt;li&gt;You’ve got confidential credentials involved&lt;/li&gt;
&lt;li&gt;You like the smell of YAML in the morning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you go the DIY route, open-source projects like &lt;a href="https://github.com/rajnandan1/kener" rel="noopener noreferrer"&gt;Kener&lt;/a&gt; and &lt;a href="https://github.com/openstatusHQ/openstatus" rel="noopener noreferrer"&gt;OpenStatus&lt;/a&gt; give you slick status pages and great features while being easy to host.&lt;/p&gt;

&lt;p&gt;Otherwise, uptime monitoring being a brutally competitive market, good cloud options are often &lt;a href="https://phare.io/pricing" rel="noopener noreferrer"&gt;cheaper than spinning up a new VPS&lt;/a&gt;, with the benefit of not having to spend time on maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing that won’t eat your runway
&lt;/h2&gt;

&lt;p&gt;Some tools charge by tiers, others charge by usage. Both can be good, but you do need to know how far your plan will take you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvv280dp9m763l9uihva.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvv280dp9m763l9uihva.jpg" alt="What to look for in an uptime monitoring tool" width="390" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep an eye out for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Surprise features locked behind expensive plans, like &lt;a href="https://sso.tax/" rel="noopener noreferrer"&gt;SSO&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pricing jumps that increase your monthly plan by 600% to monitor that one additional endpoint&lt;/li&gt;
&lt;li&gt;Per-seat costs (gets expensive fast if you grow)&lt;/li&gt;
&lt;li&gt;Extra costs for things like API monitoring, fancy assertions, or exotic check types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re planning to grow, plan for growth. Otherwise, a cheap starter plan could turn into a budget black hole real quick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast and silent checks
&lt;/h2&gt;

&lt;p&gt;Short intervals of 1 minute to 30 seconds are great, but the increased false positive alerts are not. Make sure your monitoring service confirms failures before it blows up your phone in the middle of the night. Good providers give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failure confirmation&lt;/li&gt;
&lt;li&gt;Recovery confirmation&lt;/li&gt;
&lt;li&gt;Options to tune how aggressive or chill your alerts are&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote a whole guide about this if you want the nerdy details: &lt;a href="https://phare.io/blog/best-practices-to-configure-an-uptime-monitoring-service" rel="noopener noreferrer"&gt;Best practices to configure an uptime monitoring service&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Check from around the world
&lt;/h2&gt;

&lt;p&gt;Just because your site work in Paris does not mean it works in Singapore. Routing is weird. DNS is weird. Internet infrastructure is insanely complex, and sometimes fragile.&lt;/p&gt;

&lt;p&gt;If your users are global, your monitoring should be too, especially if you’re doing edge deployments or running multi-region setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than just up and down
&lt;/h2&gt;

&lt;p&gt;“Up or down” is just the start. Depending on what you’re building, you’ll want your uptime tool to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API checks with custom payloads&lt;/li&gt;
&lt;li&gt;SSL certificate monitoring (inventory, validity, expiration, AIA, OCSP)&lt;/li&gt;
&lt;li&gt;DNS validation&lt;/li&gt;
&lt;li&gt;Performance &amp;amp; response times&lt;/li&gt;
&lt;li&gt;Tracing &amp;amp; diagnostic info&lt;/li&gt;
&lt;li&gt;Custom assertions (e.g., make sure that PHP version header is not present in production)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://ohdear.app/" rel="noopener noreferrer"&gt;OhDear&lt;/a&gt; is a great example that offers an extensive list of extra checks, like SEO monitoring or broken link and mixed content detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solo today, team tomorrow
&lt;/h2&gt;

&lt;p&gt;Right now you might be a team of one (hey friend 👋), but good monitoring tools support teams, shared dashboards, incident timelines, etc.&lt;/p&gt;

&lt;p&gt;Even solo founders need to sleep occasionally. Having a friend or colleague see the same alerts is a life upgrade worth investing in early on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plays nice with your stack
&lt;/h2&gt;

&lt;p&gt;Your monitoring tool should work with whatever communication channels you already use, no matter if you’re a Slack, Email, or Webhook person. Alerts should come to you, and not the other way around.   &lt;/p&gt;

&lt;p&gt;Also keep an eye for generic integration like incoming and outgoing webhooks as well as APIs. They will provide you with ways of integrating a third party or custom-made solution as you grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good logs save bad days
&lt;/h2&gt;

&lt;p&gt;When things break, you need as many details as possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The actual HTTP status code&lt;/li&gt;
&lt;li&gt;The full response body&lt;/li&gt;
&lt;li&gt;Headers&lt;/li&gt;
&lt;li&gt;DNS resolution steps&lt;/li&gt;
&lt;li&gt;Request trace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbr26jne9upa1ww114z99.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbr26jne9upa1ww114z99.jpg" alt="What to look for in an uptime monitoring tool" width="500" height="729"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You could even go further with traceroute logging or screenshot capture. Most cloud solutions provide this. If you’re going self-hosted, you can rig something with webhooks and a great screenshot API like &lt;a href="https://www.capturekit.dev/" rel="noopener noreferrer"&gt;CaptureKit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’ll save you hours writing postmortems, debugging edge cases, or explaining to your users why everything went sideways last Thursday.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pick a tool you can trust&lt;/li&gt;
&lt;li&gt;Make sure it’s got the features you need today and tomorrow&lt;/li&gt;
&lt;li&gt;Choose something that helps you fix problems, not just point at them&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I’m building Phare.io with this mindset, check it out if you’re looking for a great uptime monitoring tool with incident management and status pages. It’s free to start and scales with your needs.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>uptime</category>
      <category>monitoring</category>
      <category>guide</category>
    </item>
    <item>
      <title>The 3-Year Journey to an Actually Good Monitoring Stack</title>
      <dc:creator>Nicolas Beauvais</dc:creator>
      <pubDate>Tue, 15 Apr 2025 19:34:35 +0000</pubDate>
      <link>https://dev.to/phare/the-3-year-journey-to-an-actually-good-monitoring-stack-2dd2</link>
      <guid>https://dev.to/phare/the-3-year-journey-to-an-actually-good-monitoring-stack-2dd2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pyailovqdubw4f8y944.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pyailovqdubw4f8y944.webp" alt="The 3-Year Journey to an Actually Good Monitoring Stack" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I started building &lt;a href="https://phare.io" rel="noopener noreferrer"&gt;Phare&lt;/a&gt; in early 2022, I planned the architecture assuming that fetching websites to perform uptime checks would be the main scaling bottleneck, and oh boy, was I wrong. While scaling that part is challenging, this assumption led to suboptimal architectural choices that I had to carry for the past three years.&lt;/p&gt;

&lt;p&gt;Of course, when you build an uptime monitoring service, the last thing you want is your monitoring infrastructure to be inefficient, or worse, inaccurate. Maintenance and planning take priority over everything else, and your product stops evolving. You're no longer building a fast-paced side project, you're just babysitting a web crawler.&lt;/p&gt;

&lt;p&gt;It took a lot of work to fix things while maintaining the best possible service for the hundreds of users relying on it. But it was worth it, and the future is now brighter than ever for Phare.io.&lt;/p&gt;

&lt;p&gt;Let’s go back to an afternoon in the summer of 2022, when I said to myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fuck it, I’m going to make an uptime monitoring tool and compete with the 2,000 that already exist. It should only take a weekend to build anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(It was probably in French in my head, but you get the idea).&lt;/p&gt;

&lt;h2&gt;
  
  
  The very first version: Python on AWS Lambda
&lt;/h2&gt;

&lt;p&gt;AWS Lambda immediately felt like a perfect fit. I had written a few Lambda functions before, and it seemed like a good choice to easily run code in multiple regions, with the major benefit of no upfront costs and no maintenance. Compared to setting up multiple VPSs, with provisioning and maintenance on top, the choice was clear.&lt;/p&gt;

&lt;p&gt;I wrote the Python code for the Lambda, and all that was left was to invoke it in all required regions from my PHP backend whenever I needed to run an uptime check.&lt;/p&gt;

&lt;p&gt;The AWS SDK supports parallel invocation, which solved the problem of data reconciliation. I had the results of all regions in a single array and could easily decide if a monitor was up or down, sweet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$results = Utils::unwrap([
  $lambdaClient-&amp;gt;invokeAsync('eu-central-1', $payload),
  $lambdaClient-&amp;gt;invokeAsync('us-east-1', $payload),
  $lambdaClient-&amp;gt;invokeAsync('ap-south-2', $payload),
]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of the business logic was built on top of that result set. How many regions are returning errors? How many consecutive errors does this particular monitor have? Is an incident already in progress? Should the user be notified? etc. (As you guessed, this becomes important later.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8yaugv4yaj6x2zlce42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8yaugv4yaj6x2zlce42.png" alt="Phare on AWS Lambda" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This setup worked well, delivering accurate and reliable uptime monitoring to the early adopters of Phare, while I focused on building incident management and status pages.&lt;/p&gt;

&lt;p&gt;Until May of 2024, when I received a ~25 euro invoice from AWS. Okay, that’s not much, but that was for only 4M performed checks. That’s the cost of five entry-level VPSs, all to monitor about 100 websites. Not cost efficient at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxqn5lrltckasfyh22uz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxqn5lrltckasfyh22uz.png" alt="The 3-Year Journey to an Actually Good Monitoring Stack" width="800" height="611"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;I might have created the most expensive uptime monitoring service&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The biggest part of the spending was from Lambda duration (GB-Seconds). As Phare’s user base grew, websites got more complex, no more just monitoring my friends’ single-page portfolios with 100 out of 100 Lighthouse scores. Websites can be slow, and even with a 5-second timeout, the Lambda execution ended up being far too expensive.&lt;/p&gt;

&lt;p&gt;Another issue was request timing accuracy. AWS Lambda lets you select the memory limit from 128MB to 10GB, and with more memory comes more CPU power. To fetch a URL with realistic browser-like timing, the Lambda needed at least 512MB of memory, a significant cost factor for longer checks, and a huge financial attack vector.&lt;/p&gt;

&lt;p&gt;It was time to find an alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Cloudflare Workers
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers seemed unreal, much cheaper than AWS Lambda, and you only pay for actual CPU time. That meant all the idle time waiting for timeouts was now completely free. I could build the &lt;a href="https://phare.io/pricing" rel="noopener noreferrer"&gt;cheapest uptime monitoring service&lt;/a&gt; while keeping a good margin, and offer an unbeatable 180 regions.&lt;/p&gt;

&lt;p&gt;Setting it up wasn’t straightforward. On top of having to rewrite the code in JavaScript, it was not possible to invoke a Worker in a specific region. And that was a major blocker.&lt;/p&gt;

&lt;p&gt;After many failed attempts, I came across a post from another Cloudflare user who had figured out how to do exactly that, using a first Worker to invoke another one in a chosen region. It wasn’t documented, but Cloudflare seemed aware of this loophole for a while, with no public plan to restrict it. The performance and pricing were too good to ignore, so I went with it. YOLO.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febvs3yyles1diwuzs9sf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febvs3yyles1diwuzs9sf.png" alt="Phare on workers" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two-Workers technique changed everything. I could send large payloads of monitors, have the first Worker create smaller regional batches, and return reconciled results. My backend became more and more dependent to the way Cloudflare Workers behaved.&lt;/p&gt;

&lt;p&gt;Of course, there were limitations: non-secure HTTP checks were a no no, it was impossible to get details on SSL certificate errors, and TCP port access was restricted. But I managed to find a few workarounds, and everything was running smoothly.&lt;/p&gt;

&lt;p&gt;The ecosystem was growing fast, edge databases and integrated queues were being released by Cloudflare, my workers averaged sub 3ms execution times. The future looked bright.&lt;/p&gt;

&lt;p&gt;Of course, after just a few months, on November 14th, 2024, regional invocation was patched, and &lt;strong&gt;the entire uptime infrastructure went down&lt;/strong&gt;. That day was a looong day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqal1eewl1uy3icywl23l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqal1eewl1uy3icywl23l.jpg" alt="That's your face while reading this" width="450" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I quickly patched the script, rerouting all requests to the invoking region so uptime checks still ran, even if not in the right region.&lt;/p&gt;

&lt;p&gt;It was time to find an alternative. Fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bunny.net Edge Scripts to the rescue
&lt;/h2&gt;

&lt;p&gt;At that time, Bunny.net had just released their Edge Scripts service in closed beta, a direct competitor to Cloudflare Workers, built on Deno. Pricing was similar, and the migration looked plug-and-play, which was all that mattered, because I couldn’t afford the time to rewrite the backend logic.&lt;/p&gt;

&lt;p&gt;I got into the beta, rewrote the script in Deno using the same two-invocation strategy, and began rerouting traffic from Cloudflare to Bunny.&lt;/p&gt;

&lt;p&gt;The first part of the migration went smoothly, regional monitoring was back up, and I could finally relax a bit.&lt;/p&gt;

&lt;p&gt;Of course it wasn't long until shit hits the fan, and the uptime monitoring performance data started to get funky. Cloudflare was a more mature solution that handled many things in the background, like keeping TCP pools in an healthy state, which is important when you perform thousands of requests to different domains.&lt;/p&gt;

&lt;p&gt;Thankfully, Bunny’s technical team was amazing. They helped me a lot, and I gave them plenty to work on in return.&lt;/p&gt;

&lt;p&gt;Eventually, things got better. Edge Scripts left beta and became generally available, and that’s when a new bottleneck appeared.&lt;/p&gt;

&lt;p&gt;The backend code was still invoking Edge Scripts and waiting for a batched response. As Phare gained new users daily, the number of invocations grew. My backend started hitting 502/503 errors on Bunny’s side. Queue wait times forced me to increase concurrency. And I was still facing the same limitations I previously had with Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3nqdu7odamgvqag8cit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3nqdu7odamgvqag8cit.png" alt="This costed me a Sentry subscription" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Maybe Edge Scripts weren’t the best long-term solution after all.&lt;/p&gt;

&lt;p&gt;I knew what I had to do from the beginning: decouple the backend from the edge scripts and process results asynchronously. But doing so meant reworking the deepest, most fundamental part of my backend logic, now massive after years of accumulated features.&lt;/p&gt;

&lt;p&gt;Again, I had no choice if I wanted to keep improving Phare.&lt;/p&gt;

&lt;p&gt;It was time to find an alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious answer: Bunny Magic Containers
&lt;/h2&gt;

&lt;p&gt;In early 2025, Bunny announced &lt;a href="https://bunny.net/magic-containers/" rel="noopener noreferrer"&gt;Magic Containers&lt;/a&gt;, a new service letting you deploy full Docker containers across Bunny’s global network. I had been desperately trying to find a European hosting provider with such a diverse range of locations. I was already integrated with the Bunny ecosystem, and had full confidence in their amazing support team.&lt;/p&gt;

&lt;p&gt;This time, I did things slowly. I built a few preview regions to test at scale with real users, in parallel with the still-working Edge Script setup. Of course this meant running two versions of the backend logic at the same time, two different ways of triggering monitoring checks, and thousands of new line of code to make it work. Not fun, but necessary to finally fix the past mistakes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjohz89sopgumba5qmxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjohz89sopgumba5qmxm.png" alt="I don't often do pull request on a solo project" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new uptime monitoring agent would run continuously in a Docker container, billed by CPU and memory usage. Cost was a major concern, so I rebuilt it in Go with the following goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Phare backend and the monitoring agent must be fully decoupled.&lt;/li&gt;
&lt;li&gt;The agent should fetch its monitor list from an API, no backend push.&lt;/li&gt;
&lt;li&gt;Results are sent asynchronously to the backend.&lt;/li&gt;
&lt;li&gt;Data exchange should be minimal.&lt;/li&gt;
&lt;li&gt;The agent must be fault-tolerant and self-healing.&lt;/li&gt;
&lt;li&gt;It should match the feature set of the Edge Script version.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frv2zokm2blagxjsbmct1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frv2zokm2blagxjsbmct1.png" alt="Phare on magic containers" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And just like that, six new preview regions were added to Phare at the end of February, and they ran like fine clockwork. I actually went on vacation a few days after the release, for a full month, and didn’t have a single issue. I did have a lot of time to reflect on my past mistakes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmm9v8y6n6lrxu1vvk90.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmm9v8y6n6lrxu1vvk90.webp" alt="The 3-Year Journey to an Actually Good Monitoring Stack" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I won’t go into too much detail about the new infrastructure, this post is painfully long enough already. Today, all checks run on Bunny Magic Containers. And for the first time in years, I can focus on building new features for both, the agent, and the platform.&lt;/p&gt;

&lt;p&gt;And if I ever need to change provider again, I can just spin up a few VPSs with my Docker image and it’ll work. I should’ve done that from the beginning, but I wanted to go fast, and that costed me a few years of real progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;The current infrastructure works well, but it’s not perfect. When a container is restarted there's a brief overlap where two instances might run the same check. If a region goes offline, there’s no re-routing, users need to monitor from at least two regions to stay safe.&lt;/p&gt;

&lt;p&gt;Fetching the monitor list every minute via API works surprisingly well, thanks to ETags and a two-tier cache system. But I’m still exploring how to reduce HTTP calls. Having read replicas closer to the containers might be the best bet.&lt;/p&gt;

&lt;p&gt;From the outside, it didn’t look so bad, Phare grew to nearly a thousand users during all this infra chaos. Users loved the quality of the service far more than I did.&lt;/p&gt;

&lt;p&gt;This post is mostly a rant at my past self. I took too many shortcuts while building what started as a weekend project, which held the company back once it grew beyond that. &lt;strong&gt;But maybe that’s what startups are all about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That said… see you in three years for the blog post about Phare.io Monitoring Stack v8, probably rewritten in Rust, because history repeats itself.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>learning</category>
    </item>
    <item>
      <title>Best practices to configure an uptime monitoring service</title>
      <dc:creator>Nicolas Beauvais</dc:creator>
      <pubDate>Mon, 26 Aug 2024 16:00:22 +0000</pubDate>
      <link>https://dev.to/phare/best-practices-to-configure-an-uptime-monitoring-service-1oep</link>
      <guid>https://dev.to/phare/best-practices-to-configure-an-uptime-monitoring-service-1oep</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszkqy91byewfdrv9peo0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszkqy91byewfdrv9peo0.jpg" alt="Best practices to configure an uptime monitoring service" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting alerted of downtime is an essential part of running a healthy website. It's a problem that got solved a long time ago by uptime monitoring services, but as simple as setting up a monitoring service for your website might seem, there are a few best practices that I learned other the years maintaining dozens of websites from side-projects to Fortune 500, and building Phare.io, my own take on uptime monitoring.&lt;/p&gt;

&lt;p&gt;We will dive into some best practices to get the best possible monitoring without false positives, the configurations explored in the article should work with most monitoring services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right URLs to Monitor
&lt;/h2&gt;

&lt;p&gt;Defining which resources to monitor is the first step to a successful uptime monitoring strategy, and as simple as it might seem, there some thinking to do here.&lt;/p&gt;

&lt;p&gt;The first thing to consider is how your website is hosted. Many modern startups will have landing pages on a static hosting provider like Vercel or Netlify, and a backend API hosted on a cloud provider like AWS or GCP. Then you might have external services hosted on a subdomain like a blog, a status page, a changelog, etc. Each of these resources can go down independently, and you should monitor them separately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Find all resources that can independently go down and monitor them separately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For each of these resources, you need to define the right URL to monitor, and there are again a few things to consider:&lt;/p&gt;

&lt;h3&gt;
  
  
  Static hosting
&lt;/h3&gt;

&lt;p&gt;Most statically hosted websites will use some form of caching through a CDN. If you monitor a URL cached at the CDN level, you might not get alerted when the origin server is down. You then need to check with your monitoring service or your CDN for a way to bypass the cache layer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Make sure you monitor the origin server and not a cached version of your website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dynamic websites
&lt;/h3&gt;

&lt;p&gt;For dynamic websites or API endpoints, it's tempting to monitor a simple health check route that returns a static JSON response, but you might miss issues that are only visible when hitting API endpoints that do some actual work.&lt;/p&gt;

&lt;p&gt;Ideally, the URL that you monitor should at least perform a database query, or execute any critical resources of your application to make sure everything is working as expected. Creating a dedicated URL for monitoring is usually a good idea.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Monitor an endpoint that performs actual work and not just a static health check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  External services
&lt;/h3&gt;

&lt;p&gt;Monitoring external services is usually not as important as you are not responsible for their uptime. However, it's always good to be proactive and get alerted before your users do. This will allow you to communicate about the issues and show that you are on top of things.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Monitor external services to be proactive and communicate about issues before your users do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Redirections
&lt;/h3&gt;

&lt;p&gt;Now you should have a good idea of the urls you need to monitor, you need to check for any redirections. Be careful with the URL format that you use to monitor your resources, some services will end all URLs with a &lt;code&gt;/&lt;/code&gt; and some won't, you will put an unnecessary load on your server if you don't use the right format and will likely get wrong performance metrics on your uptime monitoring service.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Be mindful of unnecessary URL redirection to avoid load on your server and inaccurate performance metrics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Monitoring that a few critical redirections work as expected is also a good idea, things like www to non-www, or http to https redirections are critical for your website SEO and user experience and could be monitored.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Monitor critical redirections to make sure they work as expected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Response monitoring
&lt;/h2&gt;

&lt;p&gt;Now that you have defined the right URLs to monitor, you need to define the excepted result of your monitors. In the case of HTTP checks, that will usually be a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" rel="noopener noreferrer"&gt;status code&lt;/a&gt; or a keyword on the page.&lt;/p&gt;

&lt;p&gt;It is common knowledge among web developers that status codes are not always to be trusted, and that a &lt;code&gt;200 OK&lt;/code&gt; status code doesn't mean that the page is working as expected. This is why it's a good idea to also monitor for the presence of a keyword on the page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoso35hpix91wdzeouiz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoso35hpix91wdzeouiz.jpg" alt="Best practices to configure an uptime monitoring service" width="800" height="791"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A good keyword is something unique to the page that would not be present on any error page. For example, if you choose the name of your website, there's a high chance that it will also be present on a 4xx error page, and you will get false positives monitoring for it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Always check the response status and the presence of a unique keyword on the page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Request timeout
&lt;/h2&gt;

&lt;p&gt;Finding the right timeout for your monitors is a true balancing act. You want to make sure that the timeout is not too wide to avoid any false positives, but you also want to make sure that it's not too short to get alerted when your server is too slow to respond.&lt;/p&gt;

&lt;p&gt;My advice is to start with a large timeout for a few days and then gradually decrease it until you find the right balance. Of course this should be done on a per-url basis, as some resources might be naturally slower than others.&lt;/p&gt;

&lt;p&gt;Some monitoring services will have special configurations for performance monitoring that you could use for this purpose, you should also keep in mind that services will calculate response time differently, and you might get different results from different services, so it's always a good idea to start with a large timeout.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Start with a large timeout and gradually decrease it until you find the right balance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Monitoring frequency
&lt;/h2&gt;

&lt;p&gt;The monitoring frequency is another balancing act. You want to make sure that you get alerted as soon as possible when your website goes down, but without wasting resources on unnecessary checks for your website that is up 99.99% of the time and for our beautiful planet.&lt;/p&gt;

&lt;p&gt;Choose shorter intervals for critical resources and longer intervals for less important things like third-party services or redirections. You could also consider the time of day and monitor more aggressively during your business peak hours.&lt;/p&gt;

&lt;p&gt;Keep in mind the following when choosing the monitoring frequency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every 30 seconds = ~90k requests per month
Every 1 minute = ~45k requests per month
Every 5 minutes = ~9k requests per month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🎓 Choose shorter intervals for critical resources and longer intervals for less important things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Incident confirmations
&lt;/h2&gt;

&lt;p&gt;I would strongly advise against using any monitoring service that does not offer a way to configure a number of confirmations before sending an alert. This is, with multi-region monitoring the most impactful way to avoid false positives.&lt;/p&gt;

&lt;p&gt;The internet is a complex system, and a single network glitch could prevent your monitoring service from reaching your server. It might not seem like a big deal, but the more alert you get, the more you will ignore them, and you will certainly miss a real incident after a few weeks of receiving daily false positives alerts.&lt;/p&gt;

&lt;p&gt;This setting should be configured based on your monitoring frequency, and the criticality of the resource you are monitoring. The more frequent the monitoring, the more confirmations you should require before sending an alert, here is a good rule of thumb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30 seconds monitoring interval -&amp;gt; 2 to 3 confirmations
1 minute monitoring interval -&amp;gt; 2 to 3 confirmations
2 to 10 minutes monitoring interval -&amp;gt; 2 confirmations
Any greater monitoring interval -&amp;gt; 1 to 2 confirmations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🎓 Always require a confirmation before sending an alert.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Multi-region monitoring
&lt;/h2&gt;

&lt;p&gt;Just like incident confirmations, multi-region monitoring is a must-have feature for any monitoring service. It often happens that a request fails temporarily from a specific monitoring endpoint, but it doesn't mean that your website is down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa9qnt76kj2utrpjvrhhs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa9qnt76kj2utrpjvrhhs.jpg" alt="Best practices to configure an uptime monitoring service" width="633" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When checking from multiple regions, uptime monitoring services will usually require a certain number of regions to fail before sending an alert. This is a great way to avoid false positives and make sure that your website is really down for your users.&lt;/p&gt;

&lt;p&gt;You should always monitor all resources from at least 2 regions, and more for critical resources. When possible, choose the regions closest to your users this will give you the best results and accurate performance metrics.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Monitor all resources from at least 2 regions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Alerting
&lt;/h2&gt;

&lt;p&gt;The last thing to consider is how you want to be alerted. Most monitoring services will offer a wide range of alerting options, from email to SMS, to Slack or Discord notifications.&lt;/p&gt;

&lt;p&gt;As we previously established, not all resources are equally important, and you might want to be alerted differently for each of them. Think about the way your company communicates, and how you could integrate the alerts into your existing workflow. You might want to create a dedicated channel for alerts, or use a dedicated email address for alerts. For the most critical resources, you might want to use SMS or Phone notifications, but discuss this topic with your team and make sure that everyone is on the same page. If you configure SMS alerts and the on-call person keeps a phone on silent, that might not be the best idea.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎓 Choose the alerting method adapted to each resource and discuss this topic with your team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In most cases uptime monitoring is a set and forget kind of thing, but I've seen many teams struggle with false positives and alerts fatigue. By following these best practices, you should be able to get the best possible monitoring without false positives, and make sure that you are alerted when your website is really down.&lt;/p&gt;




&lt;p&gt;If you are looking for an uptime monitoring service that helps you implement these best practices, you should check out &lt;a href="https://phare.io" rel="noopener noreferrer"&gt;Phare.io&lt;/a&gt;. It's free to start and scale with your needs.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>uptime</category>
      <category>monitoring</category>
      <category>guide</category>
    </item>
    <item>
      <title>How we run Ghost on Docker with subdirectory routing</title>
      <dc:creator>Nicolas Beauvais</dc:creator>
      <pubDate>Thu, 22 Aug 2024 17:00:17 +0000</pubDate>
      <link>https://dev.to/phare/how-we-run-ghost-on-docker-with-subdirectory-routing-5b20</link>
      <guid>https://dev.to/phare/how-we-run-ghost-on-docker-with-subdirectory-routing-5b20</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fihy48qgcs0qdhlo7kuqe.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fihy48qgcs0qdhlo7kuqe.jpg" alt="How we run Ghost on Docker with subdirectory routing" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deciding on the right blog platform is always a bit of a hassle, whether it's for my personal blog or my company. I often have to resist the urge to build something from scratch, which inevitably means sinking the next two weeks into coding yet another blog from the ground up.&lt;/p&gt;

&lt;p&gt;When it came to setting up a blog for &lt;a href="https://phare.io" rel="noopener noreferrer"&gt;Phare.io&lt;/a&gt;, I made a conscious effort to minimize the time spent on setup. After some research, I decided on &lt;a href="https://ghost.org" rel="noopener noreferrer"&gt;Ghost&lt;/a&gt;, a well-regarded content platform that seemed to meet all our needs. Self-hosting looked straightforward, and the documentation mentioned support for subdirectory routing, which was a key requirement for our SEO strategy.&lt;/p&gt;

&lt;p&gt;But as is often the case, things weren't quite as simple as they first appeared. Hence, this blog post to guide anyone looking to do something similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Ghost on Docker
&lt;/h2&gt;

&lt;p&gt;To keep things organized, the plan was to isolate Ghost on its own server. For this, I spun up a new VPS instance on &lt;a href="https://www.hetzner.com/cloud/" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; running a Docker-CE image.&lt;/p&gt;

&lt;p&gt;This instance runs on a private network without a public IP, and the firewall is configured to accept traffic only from Phare's NGINX server on port 8080.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26av6vvcue3uw3uilapc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26av6vvcue3uw3uilapc.png" alt="How we run Ghost on Docker with subdirectory routing" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This setup might be a bit over the top for hosting a blog, but it was quick to implement and significantly reduces the attack surface, so there’s no reason not to do it.&lt;/p&gt;

&lt;p&gt;With the server ready, the next step was to write a Docker Compose file to configure Ghost's &lt;a href="https://hub.docker.com/_/ghost" rel="noopener noreferrer"&gt;Docker image&lt;/a&gt; on port &lt;code&gt;8080&lt;/code&gt; along with a MySQL database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ghost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghost:5-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8080:2368&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database__client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
      &lt;span class="na"&gt;database __connection__ host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;database __connection__ user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
      &lt;span class="na"&gt;database __connection__ password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ghost_db_password&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;database __connection__ database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghost&lt;/span&gt;
      &lt;span class="na"&gt;mail__transport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp&lt;/span&gt;
      &lt;span class="na"&gt;mail __options__ host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ghost_mail_host&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;mail __options__ port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ghost_mail_port&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;mail __options__ auth__user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ghost_mail_user&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;mail __options__ auth__pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ghost_mail_password&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;mail __options__ secure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://phare.io/blog&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ghost:/var/lib/ghost/content&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.0&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ghost_db_password&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ghost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are some key points to note in that file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;ghost&lt;/code&gt; service binds to port 8080, which is the one we opened on the firewall.&lt;/li&gt;
&lt;li&gt;Both services use persistent storage, making backups straightforward.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;url&lt;/code&gt; environment variable should be set to the public URL where your blog will be hosted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the configuration is complete, you can start the services with Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case this step is automated with an Ansible playbook task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;community.docker.docker_compose_v2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;project_src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/docker/ghost&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose-ghost.yml&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that, we have a running Ghost instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Subdirectory Routing with NGINX
&lt;/h2&gt;

&lt;p&gt;Phare.io uses an NGINX server to manage load balancing, headers, and a few other tasks. Our setup involves complex routing to allow users to &lt;a href="https://phare.io/products/uptime" rel="noopener noreferrer"&gt;create status pages&lt;/a&gt; on &lt;code&gt;*.status.phare.io&lt;/code&gt; or their own domains.&lt;/p&gt;

&lt;p&gt;For the blog, we wanted it to be accessible only on our main &lt;code&gt;phare.io&lt;/code&gt; domain, so the first step was to adjust our configuration to ensure only &lt;code&gt;phare.io&lt;/code&gt; was served, excluding any subdomains.&lt;/p&gt;

&lt;p&gt;With that in place, I created a location block to route all &lt;code&gt;/blog&lt;/code&gt; traffic to the Ghost instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;http2&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;phare.io&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# location / {&lt;/span&gt;
        &lt;span class="c1"&gt;# Configuration for our Laravel app&lt;/span&gt;
    &lt;span class="c1"&gt;# }&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;^~&lt;/span&gt; &lt;span class="n"&gt;/blog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;10G&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$http_host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://10.0.1.2:8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(jpg|jpeg|webp|png|svg|gif|ico|css|js|eot|ttf|woff)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="mi"&gt;1M&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I removed a few irrelevant lines, here are the important details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As recommended by the Ghost documentation, set a high &lt;code&gt;client_max_body_size&lt;/code&gt; to allow large file uploads via the Ghost admin panel.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;^~&lt;/code&gt; directive in the location block ensures no other location block takes precedence, which is crucial to prevent interference with the caching rules further down that could break asset loading.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;proxy_pass&lt;/code&gt; directive points to our Docker server's private IP &lt;code&gt;10.0.1.2&lt;/code&gt; and the previously opened port &lt;code&gt;8080&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accessing the blog
&lt;/h2&gt;

&lt;p&gt;With everything set up, the blog is now accessible at &lt;code&gt;phare.io/blog&lt;/code&gt;, and the admin panel at &lt;code&gt;phare.io/blog/ghost&lt;/code&gt;. Our Ghost Docker instance runs securely on a private network.&lt;/p&gt;

&lt;p&gt;To speed up asset loading and caching, we use &lt;a href="https://bunny.net/" rel="noopener noreferrer"&gt;bunny.net&lt;/a&gt; on the &lt;code&gt;phare.io&lt;/code&gt; domain. Most of our existing rules worked seamlessly on the blog, but I hit a snag when Ghost couldn’t create a session cookie, preventing me from signing in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fchclm07286sjpzm66dcd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fchclm07286sjpzm66dcd.png" alt="How we run Ghost on Docker with subdirectory routing" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem was that cookies were disabled on the domain. Changing this setting solved the issue without affecting the rest of the site, as Phare only uses session cookies on the &lt;code&gt;app.phare.io&lt;/code&gt; domain. However, a potential improvement could be moving Ghost's admin panel to its own subdomain, which would allow this setting to be re-enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hosting a Ghost blog on a &lt;code&gt;/blog&lt;/code&gt; subdirectory path using NGINX is a practical solution when you want to seamlessly integrate your blog with your main website. While it requires some configuration, the benefits for SEO and branding make the effort worthwhile.&lt;/p&gt;

&lt;p&gt;I hope this post helps you in setting up your own Ghost blog. The Phare team is delighted with the platform so far, and I’m glad I didn’t spend weeks building a half-baked in-house solution.&lt;/p&gt;




&lt;p&gt;Would you like to make sure your blog or any other part of your website stays online? &lt;a href="https://app.phare.io/register" rel="noopener noreferrer"&gt;Create a Phare account for free&lt;/a&gt; and start monitoring your website today.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>ghost</category>
      <category>nginx</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Downsampling time series data</title>
      <dc:creator>Nicolas Beauvais</dc:creator>
      <pubDate>Mon, 29 Jul 2024 09:50:00 +0000</pubDate>
      <link>https://dev.to/phare/downsampling-time-series-data-4e0p</link>
      <guid>https://dev.to/phare/downsampling-time-series-data-4e0p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs49jwu4iineussi8ycza.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs49jwu4iineussi8ycza.jpg" alt="Downsampling time series data" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://phare.io/products/uptime" rel="noopener noreferrer"&gt;Phare uptime&lt;/a&gt; we allow you to view your monitor's performance data with up to 90 days of history. While this might not seem like a lot, a monitor running every minute will span about 130 thousand data points during that time frame, for a single region.&lt;/p&gt;

&lt;p&gt;Showing that amount of data in a graph would be slow and impossible to understand due to the sheer number of data points. Finding the best solution requires the right balance between user experience, data quality, and performance. We need to tell you the story of your monitor's performance in a way that is easy to understand, fast, and accurate.&lt;/p&gt;

&lt;p&gt;In this article, we dive into a few techniques that we used to downsample the data by 99.4% while keeping the most important information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raw data
&lt;/h2&gt;

&lt;p&gt;We start with the raw data collected from monitoring the Phare.io dashboard in the last 90 days. The performance varied a lot during this time frame thanks to a noisy CPU neighbor, which made it the perfect candidate for this article.&lt;/p&gt;

&lt;p&gt;If we plot the raw data with 130 thousand points, we get a chart that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbuswd7742mdzavlfhhk5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbuswd7742mdzavlfhhk5.webp" alt="Downsampling time series data" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The performance is not terrible, loading the data takes about 230ms, and rendering the chart is done under 100ms, which is already better than a lot of other charts out there. But the real problem is that the chart is unreadable, we can't see any patterns, and it's hard to understand what's going on.&lt;/p&gt;

&lt;p&gt;The confusion does not only come from the number of data points but also from the scale difference in the data. The vast majority of the requests will be performed under 200ms, but about 0.1% of them will be slower. It does not matter how fast your website and our monitoring infrastructure are, there will always be a few requests that will be slower due to network latency, congestion, or other factors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing anomalies
&lt;/h2&gt;

&lt;p&gt;Our first step is to remove the anomalies from our data set. A single 4-second request among a few thousand will skew the data and make it hard to read. We need to remove these anomalies in a way that will keep any sustained decline in performance visible.&lt;/p&gt;

&lt;p&gt;To solve this problem, we can use one of two techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard deviation: We can calculate the standard deviation of the dataset and remove any data points that are outside a certain multiple of the standard deviation.&lt;/li&gt;
&lt;li&gt;Percentile: We can calculate the 99th percentile of the data set and remove any data points that are above that value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both solutions offer similar results, but need to be applied on a rolling window to make sure that we don't remove any sustained decline in performance and keep anomalies in periods of high performance.&lt;/p&gt;

&lt;p&gt;We use the formula &lt;code&gt;rolling mean + (3 x rolling standard deviation)&lt;/code&gt; to remove any data points that are three standard deviations above the rolling mean, and chose a rolling window of 30 (15 points before and after the current point):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7azqcbgmthygj3fnfk24.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7azqcbgmthygj3fnfk24.webp" alt="Downsampling time series data" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By zooming in on the few remaining spikes, we can see that they are not isolated anomalies but sustained periods of lower performance which we want to keep in our data set.&lt;/p&gt;

&lt;p&gt;The following is a zoomed-in view of the tallest spike on the right, we can see that the spike is followed by a series of slower requests over 1h30, this is exactly the kind of information we want to keep:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s1ahy7fu8snyf07uogc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s1ahy7fu8snyf07uogc.webp" alt="Downsampling time series data" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we did not calculate the deviation or quantile in a rolling window, we would get the following chart, which removes everything above 600ms while keeping many anomalies in the lower performance range:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpecxgqucqp1tjlw0thyx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpecxgqucqp1tjlw0thyx.webp" alt="Downsampling time series data" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling window average
&lt;/h2&gt;

&lt;p&gt;The next step is to smooth the data with a rolling window average. This technique will slightly reduce the gap between two adjacent data points and make the chart more readable while allowing us to detect trends in the data.&lt;/p&gt;

&lt;p&gt;For this example, we use a rolling window of 10 data points (5 points before and after the current point) to smooth the curve without losing too much information:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk07a7uq1wayuqtwr34rw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk07a7uq1wayuqtwr34rw.webp" alt="Downsampling time series data" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Downsampling
&lt;/h2&gt;

&lt;p&gt;Our data is now clean and smooth, but we still have 130 thousand data points to display which cost bandwidth and rendering performance. To reduce the number of data points without losing too much information, we can use the largest triangle three buckets (LTTB) algorithm.&lt;/p&gt;

&lt;p&gt;The LTTB algorithm is a downsampling technique that finds the most important points in the data set by dividing the data into buckets and selecting the point with the largest triangle area in each bucket. In simpler words, the algorithm will only keep the points that are the most representative of the data set so that the overall shape of the curve is preserved.&lt;/p&gt;

&lt;p&gt;By applying the LTTB algorithm to our data set, we can reduce the number of data points from 130 thousand to 750, which is a reduction of almost 99.5%. In the form of a JSON payload, we go from 1.53 MB to 13 KB, which is a significant reduction in bandwidth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9dzmwejiku3b9cq4upp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9dzmwejiku3b9cq4upp.webp" alt="Downsampling time series data" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the chart looks almost identical to the full data set, but uses only a fraction of the data points.&lt;/p&gt;

&lt;p&gt;It is important to carefully prepare the data before applying the LTTB algorithm, as the algorithm is specifically designed to preserve the overall shape of the curve it will keep any anomalies into the final data set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation with ClickHouse
&lt;/h2&gt;

&lt;p&gt;ClickHouse is a powerful column-oriented database optimized for analytical queries that offers unparalleled performance for time series data. We extensively use it at Phare to store and analyze the performance data of your monitors.&lt;/p&gt;

&lt;p&gt;All the techniques described in this article can be implemented in a single ClickHouse query using the &lt;a href="https://clickhouse.com/docs/en/sql-reference/aggregate-functions/reference/largestTriangleThreeBuckets" rel="noopener noreferrer"&gt;largestTriangleThreeBuckets&lt;/a&gt; function, as well as &lt;a href="https://clickhouse.com/docs/en/sql-reference/window-functions" rel="noopener noreferrer"&gt;window functions&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Apply the LTTB algorithm to the data set&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;largestTriangleThreeBuckets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;750&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;
    &lt;span class="nv"&gt;`cleaned_results`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`timestamp`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;`cleaned_results`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`time`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;-- Smooth the remaining data with a rolling window average&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="nv"&gt;`raw_results`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`timestamp`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`raw_results`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`time`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;FOLLOWING&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`time`&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;-- Select the raw data&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt;
          &lt;span class="nv"&gt;`timestamp`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;`time`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="c1"&gt;-- Calculate the rolling window average and standard deviation&lt;/span&gt;
          &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`time`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;`timestamp`&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;FOLLOWING&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;stddevSamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`time`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;`timestamp`&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;FOLLOWING&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;anomalies&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="nv"&gt;`performance_table`&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`raw_results`&lt;/span&gt;
    &lt;span class="c1"&gt;-- Filter out anomalies&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;
      &lt;span class="nv"&gt;`raw_results`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`time`&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;`raw_results`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`anomalies`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`cleaned_results`&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Drawing the data
&lt;/h2&gt;

&lt;p&gt;Phare uses &lt;a href="https://github.com/leeoniya/uPlot" rel="noopener noreferrer"&gt;uPlot&lt;/a&gt; to draw charts in the frontend. uPlot is a small, fast, and flexible charting library, which perfectly fits our needs. It allows us to draw charts with a large number of data points with the best possible performance, where other libraries would struggle.&lt;/p&gt;

&lt;p&gt;Keep in mind that uPlot is a low-level library, which means that you will need to spend a good amount of time configuring it to get the desired result. But the performance and flexibility it offers are worth the effort.&lt;/p&gt;

&lt;p&gt;Because we already processed the data with ClickHouse, we only need to separate the data in two arrays, one for the x-axis and one for the y-axis, and pass them to uPlot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;uPlot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// x-axis values&lt;/span&gt;
  &lt;span class="nx"&gt;times&lt;/span&gt; &lt;span class="c1"&gt;// y-axis values&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, Phare implementation is more complex than that, we need to handle responsive, live-streaming, and displaying uptime incidents, but this goes beyond the scope of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By removing anomalies, smoothing the data with a rolling window average, and downsampling the data with the LTTB algorithm, we were able to reduce the amount of data by 99.4% while keeping the most important information.&lt;/p&gt;

&lt;p&gt;The chart is now readable, fast to load, and easy to understand.&lt;/p&gt;




&lt;p&gt;Would you like to see this performance chart for your own website? &lt;a href="https://app.phare.io/register" rel="noopener noreferrer"&gt;Sign up for free&lt;/a&gt; and start monitoring your website today.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>sql</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
