<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Expat Geek Diaries]]></title><description><![CDATA[Read about everyday challenges in business, entrepreneurship, open-source projects, tech-heavy tutorials, and more.]]></description><link>https://www.expatgeekdiaries.com</link><image><url>https://substackcdn.com/image/fetch/$s_!6HlX!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8dcc6655-3dbd-482a-9547-1ccc15804a90_315x315.png</url><title>Expat Geek Diaries</title><link>https://www.expatgeekdiaries.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 03 Apr 2026 20:16:44 GMT</lastBuildDate><atom:link href="https://www.expatgeekdiaries.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Gábor Boros]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[newsletter@expatgeekdiaries.com]]></webMaster><itunes:owner><itunes:email><![CDATA[newsletter@expatgeekdiaries.com]]></itunes:email><itunes:name><![CDATA[Gábor Boros]]></itunes:name></itunes:owner><itunes:author><![CDATA[Gábor Boros]]></itunes:author><googleplay:owner><![CDATA[newsletter@expatgeekdiaries.com]]></googleplay:owner><googleplay:email><![CDATA[newsletter@expatgeekdiaries.com]]></googleplay:email><googleplay:author><![CDATA[Gábor Boros]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Why GitHub Releases Isn’t Enough for Binary Distribution]]></title><description><![CDATA[GitHub Releases is fine for weekend projects. But if you care about your users, you need something better.]]></description><link>https://www.expatgeekdiaries.com/p/why-github-releases-isnt-enough-for</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/why-github-releases-isnt-enough-for</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Wed, 03 Sep 2025 15:02:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ae2e250d-8db9-400e-adfb-5be80ee31d21_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you&#8217;re like me, you&#8217;ve shipped binaries through GitHub Releases because it is right there in the repo. It looks simple, it&#8217;s free, and for many open source projects it&#8217;s the default.</p><p>The problem is, GitHub Releases was never really designed for proper binary distribution. It works fine for attaching a zip or tarball, but as soon as your project starts relying on binaries, cracks begin to show.</p><p>Here are the main pain points I&#8217;ve run into:</p><p><strong>1. Poor user experience</strong></p><p>Users have to click through the release page and manually pick files. Direct URLs exist but are long and brittle. If you want someone to install via a package manager, GitHub Releases gives you nothing.</p><p><strong>2. No download insights</strong></p><p>You cannot see which versions are popular or how many times a binary has been downloaded. If you&#8217;re shipping multiple builds, you are essentially guessing which ones people actually need.</p><p><strong>3. Limited automation</strong></p><p>GitHub Actions lets you upload artifacts, but that&#8217;s it. There&#8217;s no API for stats, no built-in APT or YUM repo support, and you end up writing extra scripts to fill gaps that should not exist.</p><h3>What developers really need</h3><p>For projects shipping binaries, the essentials are:</p><ul><li><p>Easy hosting and sharing of binaries</p></li><li><p>Real download analytics</p></li><li><p>Support for package managers like APT and YUM</p></li><li><p>CI/CD integration without friction</p></li><li><p>A service built specifically for binaries</p></li></ul><p>That&#8217;s why I built <strong><a href="https://www.zipzen.dev/?utm_source=substack&amp;utm_medium=blog&amp;utm_campaign=i6mp">ZipZen</a>. </strong>It is lightweight release hosting for developers. You get reliable binary hosting, zero-config APT/YUM repositories, and download statistics (more detailed analytics coming soon).</p><p>GitHub Releases will always exist as a basic option, but if you care about your users and your workflow, the cracks are impossible to ignore. If you&#8217;ve been frustrated with binary distribution, ZipZen might be worth checking out.</p><p>I&#8217;d love to hear from you: <strong>how do you currently handle distributing binaries for your projects?</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.expatgeekdiaries.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Expat Geek Diaries! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How to Create Branded Short Links in Seconds]]></title><description><![CDATA[Using Ziplink to Keep Your Sanity.]]></description><link>https://www.expatgeekdiaries.com/p/how-to-create-branded-short-links</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/how-to-create-branded-short-links</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Wed, 13 Aug 2025 15:46:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/de1f8a72-bf25-4cdb-b6c9-093e8a01ff41_2016x1344.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's be real&#8212;long, clunky URLs aren't exactly inviting to click. Branded short links, on the other hand, not only look cleaner but also build trust, boost clicks, and reinforce your brand. Instead of a random, forgettable link, you get something that represents <strong>you</strong> and your content.</p><p>With <strong>Ziplink</strong>, creating branded short links is ridiculously easy. After adding your own domain, in just a few seconds, you can generate trackable, custom URLs that look great and help you stand out. Here is a step-by-step guide on how to do it.</p><p>First things first&#8212;head over to <a href="https://ziplink.click/pricing">Ziplink</a>, select the "<strong>Basic</strong>" or "<strong>Growth</strong>" package and either sign up or log in. It takes just a few moments!</p><h2>Prerequisites</h2><p>You can't add a custom domain unless you own one. If you don't have a domain yet, you'll need to register one first. To do so, you may visit <a href="https://namecheap.com">namecheap.com</a> or <a href="https://www.godaddy.com">godaddy.com</a>.</p><p>Once your domain is ready, decide whether you want to share your links with or without a subdomain.</p><p>If you choose to use a subdomain (<strong>recommended</strong>), add the following DNS record to your domain settings:</p><p><strong>Name</strong>: subdomain.yourdomain.com.<br><strong>Type</strong>: CNAME<br><strong>Value</strong>: zipl.ink.<br><strong>TTL</strong>: 300</p><p>In the case of a root domain (so called, APEX domain), add the follwing DNS record to point your domain to Ziplink.</p><p><strong>Name</strong>: yourdomain.com.<br><strong>Type</strong>: A<br><strong>Value</strong>: 3.72.197.199<br><strong>TTL</strong>: 3600</p><h2>Add your domain to Ziplink</h2><p>Once you're in, you'll see the dashboard. From there, follow these steps:</p><ul><li><p>Navigate to your <a href="https://app.ziplink.click/settings/domains">workspace's domain settings</a></p></li><li><p>Above the table, click on "Add domain"</p></li><li><p>Optionally, fill the "Alias" field with an easy to remember name</p></li><li><p>Add the desired domain you already configured with your DNS provider</p></li><li><p>Click "Submit"</p></li></ul><p>Within a few seconds, Ziplink registers your domain in the background.</p><h2>Create your first branded link</h2><p>Now that your domain is added, you can create a link just like you used to, but this time using your custom domain.</p><ul><li><p>Click on "Create new" in the sidebar to add a new link</p></li><li><p>Enter the destination URL where you want your visitors to be redirected</p></li><li><p>Click on the domain dropdown, and select your own domain</p></li><li><p>Optionally, add a custom link alias</p></li><li><p>Click on "Create link" at the bottom of the form</p></li></ul><p>Voil&#224;! You just created a branded link! &#127881;</p><p>Go and share it on your favorite social media platform or maybe with your customers.</p>]]></content:encoded></item><item><title><![CDATA[URL shorteners and why would you need one?]]></title><description><![CDATA[Streamline Your Digital Sharing with Concise, Impactful Links]]></description><link>https://www.expatgeekdiaries.com/p/url-shorteners-and-why-would-you</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/url-shorteners-and-why-would-you</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Tue, 25 Mar 2025 14:03:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/27781cbc-fcaf-4677-a866-28bb84115e4f_1740x998.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ever been mid-scroll on social media and paused at an unwieldy, endless URL that disrupts an otherwise delighting post? You&#8217;re not alone.</p><p>First impressions count&#8212;even online. A link shortener turns a sprawling web address into a concise, shareable snippet that not only looks cleaner but also enhances your message. Let&#8217;s explore what a URL shortener is, why it&#8217;s essential for modern online communication, and how you can quickly create your own short links.</p><h2>What Is a URL Shortener?</h2><p>At its essence, a URL shortener takes a long, clunky web address and transforms it into a neat, compact version. For example, a lengthy URL like:</p><p><code>https://www.example.com/articles/insightful-tips-on-effective-link-sharing?utm_source=expatgeekdiaries.com</code></p><p>can be converted into something as simple as:</p><p><code>https://exmpl.co/a1B2c</code></p><p>This process works by generating a unique key for the original link and storing it in a database. When a user clicks the shortened URL, the system retrieves the original address and redirects them seamlessly. It&#8217;s a straightforward yet powerful tool for decluttering your digital space.</p><p>The mechanics (in the case of this example) are simple. We throw a long URL at the link shortener, and in return, it outputs a short URL.</p><h2>Why Use a URL Shortener?</h2><p>There are many practical reasons to use URL shorteners beyond mere aesthetics:</p><ul><li><p><strong>Clean and attractive links:</strong> A short URL looks far more polished on social media, emails, and even printed materials.</p></li><li><p><strong>Shareability:</strong> On platforms with character limits&#8212;like Twitter, khm&#8230;&#8212;a compact link leaves more room for your message.</p></li><li><p><strong>Analytics:</strong> Many services, such as <a href="https://www.ziplink.click/?utm_medium=blog&amp;utm_source=expatgeekdiaries&amp;utm_campaign=product_launch">Ziplink</a>, offer tracking features, providing valuable data such as click counts, geographic information, and device types.</p></li><li><p><strong>Branding possibilities:</strong> Custom short links can include your brand&#8217;s name, reinforcing your identity every time a link is shared.</p></li><li><p><strong>Improved experience:</strong> A tidy link reduces visual clutter and allows your audience to focus on your content.</p></li></ul><p>These handy tools aren&#8217;t just for tech experts&#8212;they&#8217;re practical in many everyday situations:</p><ul><li><p><strong>Social media campaigns:</strong> Marketers track post performance and engagement with every click.</p></li><li><p><strong>Email marketing:</strong> Shortened links add a level of sophistication to your newsletters and can boost click-through rates.</p></li><li><p><strong>Text and messaging:</strong> With limited space in SMS and chat apps, a compact URL can be the difference between your message being read or overlooked.</p></li><li><p><strong>Printed media:</strong> Business cards, flyers, and posters become more effective when paired with memorable, short links.</p></li><li><p><strong>Content embedding:</strong> In blog posts and articles, shortened links maintain a neat layout without overwhelming your text.</p></li></ul><p>Consider your daily digital interactions. Whether you&#8217;re sharing a recipe on Facebook, promoting an event via email, or even incorporating a QR code in your print materials, a short URL can make the experience smoother and more professional.</p><h2>How to Create Your Own Short Links</h2><p>Using a URL shortener is a breeze. Here&#8217;s a quick guide to get you started:</p><ol><li><p><strong>Choose a shortener:</strong> Pick a link shortener that fits your needs&#8212;many offer options like custom branding, QR code creation, and analytics. <a href="https://www.ziplink.click/?utm_medium=blog&amp;utm_source=expatgeekdiaries&amp;utm_campaign=product_launch">Ziplink</a>, <a href="https://short.io/">Short.io</a>, and <a href="https://bitly.com/">Bitly</a> are all capable of that. If you are looking for something really simple, you may want to check out <a href="https://tinyurl.com/">TinyURL</a> as well.</p></li><li><p><strong>Enter Your URL:</strong> Depending on the shortener, you need to sign up beforehand, but in essential: input the long web address you want to shorten.</p></li><li><p><strong>Customize (Optional):</strong> Some services let you add a personalized alias/back-half to the short link. This can be a great option, but be aware: the aliases must be unique. So in the case of <code>https://exmpl.co/a1B2c</code>, no two <code>a1B2c</code> alias can exist at the same time.</p></li><li><p><strong>Generate and share:</strong> Once created, your new link is ready to be shared across your digital channels.</p></li></ol><h2>Final Thoughts</h2><p>Simplicity can significantly enhance your communication. URL shorteners do more than just reduce character count&#8212;they refine your links and improve how you connect with your audience. Whether you&#8217;re crafting a social media post, sending out a newsletter, or printing promotional materials, consider the impact a clean, efficient link can have on your message.</p><p>Spread the word, hopefully we see less clunky URLs in the future!</p>]]></content:encoded></item><item><title><![CDATA[You Must Revise Your Infrastructure Spending]]></title><description><![CDATA[Your employees are working hard to deliver new features to customers. The user base is growing very nicely. Based on the positive user feedback, the management has new feature ideas, and the board is happy to see the increase in profit. The company is investing more in its software engineering teams, which are getting bigger budgets. What's wrong with that? Well, not much. At least while the company grows dynamically.]]></description><link>https://www.expatgeekdiaries.com/p/you-must-revise-your-infrastructure</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/you-must-revise-your-infrastructure</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Wed, 14 Feb 2024 08:49:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9wVm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9wVm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9wVm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 424w, https://substackcdn.com/image/fetch/$s_!9wVm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 848w, https://substackcdn.com/image/fetch/$s_!9wVm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 1272w, https://substackcdn.com/image/fetch/$s_!9wVm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9wVm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp" width="800" height="512" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:512,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34950,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9wVm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 424w, https://substackcdn.com/image/fetch/$s_!9wVm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 848w, https://substackcdn.com/image/fetch/$s_!9wVm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 1272w, https://substackcdn.com/image/fetch/$s_!9wVm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6d70c93-5b7a-4261-9f44-a0d52a07c566_800x512.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Picture this: Your employees are working hard to deliver new features to customers. The user base is growing very nicely. Based on the positive user feedback, the management has new feature ideas, and the board is happy to see the increase in profit. The company is investing more in its software engineering teams, which are getting bigger budgets.</p><p>It holds true for almost any other software company with a good product to sell. That's good, isn't it? What's wrong with that? Well, not much. At least while the company grows dynamically. My favorite mother-in-law has an excellent saying:</p><blockquote><p>A company either grows dynamically or sinks irretrievably.</p></blockquote><p>This is especially true for IT companies that are operating on elevated costs. Of course, what high is objective and depends on many factors, though one thing is common: they shouldn&#8217;t have to pay as much as they do.</p><p>How do you know that you are on the wrong track, and you need to invest a bit into taking care of what you have done so far? Here are seven signs and suggestions.</p><h2>1. Excessive Spendings</h2><p>Let&#8217;s start with the most obvious, though often overlooked sign, elevated spendings. Why is it so easy to overlook or ignore this sign if it&#8217;s so frequent? Mostly due to lack of monitoring and the human nature. We tend to find reasons for questionable things, even if we shouldn&#8217;t. The issue is realized when the company starts to stagnate, or the bills are going way above the budget.</p><p>The solution is relatively easy, but requires investigation. The first step is to go down the rabbit hole and track down all cost drivers. This can include checking all billing items on the bill, checking the infrastructure usage, or literally anything that tracks the resource usages.</p><p>By knowing which components of the infrastructure contribute the most to the bills, several actions can be taken. From this point, it is highly dependent on the infrastructure what to do next. However, making sure that the right-sized computing resources are used, correct VAT settings are applied on billing accounts, or dangling resources are cleaned up easily can result in tens of thousands of dollars savings annually.</p><h2>2. High Maintenance Costs</h2><p>It is challenging to pinpoint the cause of increased maintenance costs, as it can be somewhat natural in the case of complex systems. Many times, the processes the teams are following are inefficient. In other cases, the system is poorly or overengineered. Also, it can happen that not the right tools are used or not the right team is doing the maintenance work.</p><p>In this case, all problems need a unique solution. There is no one fit for all. A good first step is to revise the processes in collaboration with the teams that are participating in maintenance. In many cases, they are not raising issues with the processes, or they don&#8217;t even realize that their processes are inefficient.</p><p>Then, check for resource usage. Are the resources utilized as they should have been? Is there any under or over provisioned computing instances?</p><p>If you found no resource utilization issues, the processes in place are efficient, but the maintenance cost is still high, it may be worth investigating if you are using the right technology. For example, setting up Kubernetes for a website just not makes sense.</p><h2>3. Inefficient Processes</h2><p>How can processes influence IT expenses? If you don&#8217;t know the answer to this straight off, you need to get in touch with me.</p><p>A company without efficient processes is doomed. That&#8217;s true for its teams as well. No, we are not talking about micromanagement here. It is about defining the way a team works and keeps itself accountable to other teams.</p><p>If the infrastructure team (or any other teams) has no processes for regular, scheduled, and most importantly, uninterrupted maintenance time, no matter how clever the team or well established the infrastructure, it will get rusty.</p><p>The solution is introducing dedicated maintenance time with a plan the team can stick to. However, everyone in the company should respect this maintenance time. Even the CEO should not intervene in that. Why? Because otherwise it is a wasted time if the team is dragged back to do the regular feature shipping or bug fixing.</p><p>Of course, there are scenarios when this cannot be avoided (like an outage), but that should be rare to be able to count on one hand during the course of the year.</p><h2>4. Lack of Standardization</h2><p>Is the &#8220;not invented here (NIH) syndrome&#8221; familiar to you? NIH is basically the tendency for management or teams to reject any idea that did not originate within the organization.</p><p>In the beginning, custom tooling may cost less and comes with less maintenance, though after some time, this trend will turn to the opposite. Training people for in-house solutions, writing extra documentation, and not using the benefits provided by using standardized solutions, which are mostly open-sourced.</p><p>Nowadays, open-source communities provide solutions to almost anything a company may need. These communities are taking care of bug fixes, shipping new features, maintaining the documentation, keeping the tooling up-to-date, and so much more.</p><p>Think about Docker or Kubernetes. If a company had to ship the containerization or orchestration environment, how many teams would have to work on that for how long? What about Prometheus or Grafana? These products are maintained by hundreds of developers every day. Does your company have these <em>spare</em> resources?</p><p>Letting the fear of not inventing everything go, and starting using standardized and open-source tooling, will definitely help lower the costs. Sure, not all open-source products are completely free. Open-source, per se, doesn&#8217;t mean it is free on any scale with no restrictions. However, still saves a company a lot of effort, time, and money.</p><h2>5. Obsolete Technology</h2><p>The mantra of &#8220;what works doesn't need to be changed&#8221; is not true in this case. Sticking with outdated technology means more fixing and upkeep. When parts get old and the vendors stop lending a hand, you end up shelling out more just to keep things running.</p><p>Technology moves fast, and old systems can't always keep up. This can force companies to splurge on pricey upgrades or come up with clever hacks to make sure everything still plays nice together, all adding to the bill.</p><p>Without dusting off and regularly patching old technology, the company-wide open to hackers. Also, it can really put the brakes on your team's productivity. Slow interfaces, laggy processing, and compatibility issues with newer tools can turn work into a slog.</p><p>The solution to the problem is simple, but time-consuming. Start upgrading the old tech stack. It comes with planning, resource allocations, focused work, increased maintenance time, but keep in mind: at some point, the tech debt must be paid.</p><p>Acknowledging and being conscious about the tech debt is not a shame. It is simply responsible thinking. Nothing is wrong with that. </p><h2>6. Uncontrolled Growth</h2><p>Let's talk about the wild side of growth &#8212; the uncontrolled kind. When things start booming without a leash, it's like the wild west out there.</p><p>Uncontrolled growth tends to spread like wild weeds. It's like trying to manage an overgrown garden, where you spend all your time cutting back excess instead of nurturing what's important. When growth happens too fast, the company can find itself scrambling to keep up with rising demands, whether it's for customers, supplies, staff, or even office space.</p><p>Quality control and peer review becomes shaky amidst unchecked expansion. With all the focus on getting bigger, there's a risk of letting craftsmanship slip or dropping the ball on multiple fields.</p><p>For the infrastructure, this means bad decisions, such as renting cloud computing resources that are not needed, forgetting about unused temporary instances, over-provisioned instances, poor resource utilization and so much more.</p><p>Dealing with uncontrolled growth requires a joined effort of multiple teams, and the issue should be mitigated together. While mitigating the issue, make sure to look out for bad practices, over provisioned instances, dangling resources, and so on. These can be cleaned up easily.</p><h2>7. Overestimated Growth</h2><p>In contrast to uncontrolled growth, the other side of the coin is overestimated growth. When a company is releasing a product, it is often believing that it will get a huge traction from day one. However, be honest, it&#8217;s not the truth.</p><p>The estimated user base is communicated to the engineering teams, who are designing the infrastructure accordingly. And here is the key word: accordingly. It is not their role to (always) second guess the management&#8217;s expectations, so they will scale the systems up as needed.</p><p>The upscaled resources evidently result in higher bills. By simply revising the actual user base, the system can be scaled down as required, lowering the bills. It must be noted that this requires some sort of monitoring and reporting. Without knowing the exact usage of the systems, this cannot be done.</p><h2>Conclusion</h2><p>Running a software company is like steering through a constantly changing environment. Success comes from balancing new ideas and careful management. As your customer base grows and profits increase, it might be tempting to expand fast. But there are risks you need to watch out for that could mess up your plans.</p><p>Dealing with overspending, high costs to keep things running, and inefficient ways of doing things needs more than just looking into the problem. You have to really work at making things smoother. Using standard methods and getting rid of old ways of doing things can make your company more effective. And if your company is growing too fast, you need to rethink your strategy.</p><p>Success isn't just about moving forward. It's also about managing your resources wisely and creating a workplace where being able to change and plan ahead is important. By keeping a close eye on things and being ready to change direction, software companies can keep growing and coming up with new ideas sustainably.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.expatgeekdiaries.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Expat Geek Diaries! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Guide to Building Future-Ready Roadmaps]]></title><description><![CDATA[Seven rules to keep in mind when crafting an IT roadmap.]]></description><link>https://www.expatgeekdiaries.com/p/guide-to-building-future-ready-roadmaps</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/guide-to-building-future-ready-roadmaps</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Mon, 05 Feb 2024 07:30:18 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" width="3000" height="1803" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1803,&quot;width&quot;:3000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;asphalt road between trees&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="asphalt road between trees" title="asphalt road between trees" srcset="https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1471958680802-1345a694ba6d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxyb2FkbWFwfGVufDB8fHx8MTcwNzA1NjczOHww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@foxxmd">Matt Duncan</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>In today's business landscape, leveraging technology effectively is not just an option but a necessity for sustainable growth. As consultants, our role is pivotal in steering organizations toward success through well-thought-out IT initiatives. In this in-depth guide, we'll explore key strategies to develop impactful IT roadmaps that align with business goals and drive lasting results.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.expatgeekdiaries.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Expat Geek Diaries! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Understanding the Client's Needs</h2><p><strong>Rule #1: The client knows what it wants until you ask questions.</strong>&nbsp;After that, everything falls apart. Most of the time.</p><p>The journey toward a successful IT roadmap begins with gaining a profound understanding of your client's business objectives and long-term goals. Aligning IT initiatives with the broader vision ensures a tailored roadmap that meets specific needs. Successful consultants start by asking the right questions to uncover the unique challenges and opportunities that technology can address.</p><p>Begin by fostering an environment of open communication. Schedule comprehensive meetings with key stakeholders, decision-makers, and department heads. Create a platform where everyone feels comfortable sharing their insights and concerns. By engaging in dialogue, you uncover nuances that might not be apparent in formal documentation.</p><p>Do not forget to involve the team leads of the affected teams. In many companies, the details are easily falling through the cracks, resulting in misdelivered information. You will get most context on the specifics from them. For high-level, company or department-wide questions, turn to the department heads.</p><p>Digging into the existing pain points is crucial. What challenges does the client currently face in their day-to-day operations? Are there bottlenecks in communication, inefficiencies in processes, or gaps in technology utilization? Identifying these pain points provides a clear roadmap for how technology can be strategically employed to address immediate concerns and lay the foundation for future growth.</p><p>Understanding the client's needs is not a one-time event; it's an ongoing process. Maintain continuous engagement throughout the roadmap development. Regular follow-ups, feedback sessions, and updates ensure that the evolving needs of the client are always in focus. This proactive approach helps in adapting the roadmap to changing circumstances and ensures that it remains a dynamic tool rather than a static document.</p><h2>Conducting a Comprehensive Assessment</h2><p><strong>Rule #2: Setting up a plan on unknowns will lead to a complete disaster, ever-growing costs, and endless discussions. </strong>Measure twice and cut once.</p><p>The second crucial phase in crafting a successful strategic IT roadmap is conducting a comprehensive assessment. This isn't merely a surface-level scan of the existing IT infrastructure, but a meticulous exploration that delves into every nook and cranny of the organization's technological landscape. This thorough assessment lays the groundwork for informed decision-making, identifying both strengths and weaknesses, opportunities, and potential threats.</p><p>Although every project is different, a comprehensive assessment begins with a detailed analysis of the current IT infrastructure. This involves scrutinizing hardware, software, networks, and data storage systems. Consultants meticulously map out the existing technology ecosystem, ensuring a holistic understanding of the organization's digital footprint. Are there outdated systems causing bottlenecks? Is the network architecture scalable? These are the questions that guide the initial stages of the assessment.</p><p>The assessment, however, should be taken only for the related infrastructure. By the end of the day, all clients would like to pay for what it asked for. And only for that. On the other hand, it is unavoidable to assess services that are not affected by the final roadmap. Make notes about all the nitty-gritty details of the infrastructure you discovered, and hand them over to the client by the end of the project (or as you are progressing) as gratis. For you, it costs nothing, for the client it may carry a valuable insight on their systems.</p><h2>Setting Clear Objectives</h2><p><strong>Rule #3: Find clear, objective, and measurable goals. Otherwise, it will be a never ending jurney.</strong> The &#8220;there will always be something&#8221; strategy is not a strategy.</p><p>To ensure success, it is crucial to establish clear and achievable objectives for the IT roadmap. Defining specific milestones and key performance indicators (KPIs) helps in tracking progress and keeping the project on course. Clarity in objectives is the roadmap's North Star, guiding the organization toward its technology-driven future.</p><p>The collaboration must take place hands in hand with stakeholders to ensure that these objectives are not only aligned with the overall business goals, but are also realistic and measurable. It is a dynamic and ongoing dialogue between the consultant and key stakeholders. Also, depending on the client&#8217;s business, they may change their priorities and want to cross-out some KPIs or add new ones. Knowing about these early can result in the roadmap&#8217;s success or failure.</p><p>Open communication channels are vital to promptly address any changes in the client's business landscape, allowing for the modification of objectives as needed. By fostering this continuous collaboration, consultants can create an agile and responsive environment that positions the IT roadmap for sustained success.</p><h2>Prioritizing Initiatives</h2><p><strong>Rule #4: Assign priorities to the projects on the roadmap</strong>, so if something hits the fan, you know where to cut loose ends.</p><p>In the realm of IT initiatives, not all endeavors are born equal, underscoring the critical role of a consultant's expertise. </p><p>The prioritization process is a nuanced task, demanding a keen understanding of the organizational landscape. Consultants employ a strategic approach, categorizing projects into high, medium, or low priority based on their direct impact on overarching goals. Through a meticulous evaluation of each initiative against predefined criteria, consultants provide organizations with a roadmap for resource allocation that aligns seamlessly with their strategic objectives. </p><p>This strategic prioritization ensures an efficient use of resources, maximizing value and propelling the organization toward its desired outcomes with accelerated progress. In essence, the consultant's ability to discern the varying significance of IT initiatives and strategically prioritize them plays a pivotal role in optimizing the overall impact and success of the organizational roadmap.</p><p>Last but not least, if the scope must be cut, both the stakeholders and the consultant can pinpoint the least important or too time-consuming tasks or projects.</p><h2>Building Flexibility Into the Roadmap</h2><p><strong>Rule #5: Anything that can go wrong will go wrong.</strong> This applies to roadmaps as well, not just your kitchen appliances.</p><p>As mentioned earlier, due to the high chance that something will change during the execution, flexibility is non-negotiable. </p><p>An agile approach allows for swift adjustments in response to market changes or internal requirements. Consultants need to ensure that the IT roadmap is not a rigid plan, but a dynamic framework that can adapt to evolving circumstances. This adaptability is crucial for resilience in the face of uncertainties, ensuring that the organization can navigate through challenges and seize emerging opportunities.</p><p>However, keep in mind that no plan is bulletproof. There are certain situations, changes, or circumstances that can turn a rock-solid plan into a hazy vision.</p><div class="poll-embed" data-attrs="{&quot;id&quot;:143287}" data-component-name="PollToDOM"></div><h2>Communicating Effectively</h2><p><strong>Rule #6: Most projects goes wrong on communication.</strong> Let&#8217;s put the telephone game aside.</p><p>Effective communication is the linchpin of successful IT roadmap implementation. As stressed out many times, it involves keeping stakeholders informed of progress, changes, and challenges through open dialogue and transparency. </p><p>Building trust and securing buy-in from key players are vital for the roadmap's smooth execution. Consultants act as the bridge between technical complexities and non-technical stakeholders, translating jargon into clear, actionable insights. This communication strategy fosters a collaborative environment where everyone is on the same page, working towards shared goals.</p><h2>Monitoring and Evaluating Progress</h2><p><strong>Rule #7: The entire roadmap is based on intuition and leads to second-guessing, if execution is not closely monitored.</strong> Measuring the progress is as important as assessing the infrastructure.</p><p>Regularly monitoring and evaluating progress is another cornerstone of successful IT roadmap implementation, ensuring that the journey stays aligned with the destination. Employing various tools and techniques, consultants establish a robust framework for ongoing assessment, feedback, and refinement. Some of these with the lack of completeness are:</p><ul><li><p>Utilizing advanced project management software facilitates real-time tracking of tasks, timelines, and milestones.</p></li><li><p>Strategic selection and continuous monitoring of KPIs are instrumental in gauging progress.</p></li><li><p>Embracing agile methodologies allows for iterative development and continuous improvement.</p></li><li><p>Scheduled review meetings provide a dedicated forum for comprehensive assessments.</p></li><li><p>Implementing risk management strategies is integral to progress monitoring.</p></li></ul><p>Although every roadmap is different and unique, in a nutshell, these techniques and methods can keep everything on track.</p><h2>Conclusion</h2><p>In conclusion, navigating the intricate terrain of IT roadmap development requires a blend of strategic acumen and adaptive prowess. As technology becomes an indispensable catalyst for organizational growth, consultants play a pivotal role in steering businesses toward success through meticulously crafted IT initiatives.</p><p>Understanding the client's needs forms the bedrock of this journey, demanding ongoing engagement and an open channel for dialogue. A comprehensive assessment, delving into every facet of the organization's technological landscape, sets the stage for informed decision-making. Clear and achievable objectives, collaboratively defined with stakeholders, act as the North Star guiding the organization's technological evolution.</p><p>The prioritization of initiatives, undertaken with nuanced expertise, ensures optimal resource allocation, maximizing impact. Building flexibility into the roadmap acknowledges the dynamic nature of business landscapes, allowing for agile responses to unforeseen challenges and opportunities. Effective communication, a bridge between technical intricacies and non-technical stakeholders, fosters collaboration and trust.</p><p>Finally, the continuous monitoring and evaluation of progress, utilizing tools, methodologies, and strategic reviews, culminate in a roadmap that not only adapts but thrives in the ever-evolving realm of technology. In essence, the success of an IT roadmap lies in its ability to be both a dynamic framework and a strategic guide in an era where change is the only constant.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.expatgeekdiaries.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.expatgeekdiaries.com/subscribe?"><span>Subscribe now</span></a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.expatgeekdiaries.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Expat Geek Diaries! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Visualizing IoT Data with MQTT, QuestDB, and Grafana]]></title><description><![CDATA[Monitoring Time-series IoT Device Data]]></description><link>https://www.expatgeekdiaries.com/p/visualizing-iot-data-with-mqtt-questdb-and-grafana</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/visualizing-iot-data-with-mqtt-questdb-and-grafana</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Mon, 10 Jul 2023 09:20:27 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d898a19a-898a-4121-bda1-46ccf084bc3e_816x451.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Monitoring Time-series IoT Device Data</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aYs9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aYs9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 424w, https://substackcdn.com/image/fetch/$s_!aYs9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 848w, https://substackcdn.com/image/fetch/$s_!aYs9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!aYs9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aYs9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Visualizing IoT Data with MQTT, QuestDB, and Grafana&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Visualizing IoT Data with MQTT, QuestDB, and Grafana" title="Visualizing IoT Data with MQTT, QuestDB, and Grafana" srcset="https://substackcdn.com/image/fetch/$s_!aYs9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 424w, https://substackcdn.com/image/fetch/$s_!aYs9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 848w, https://substackcdn.com/image/fetch/$s_!aYs9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!aYs9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86248403-5bdb-4e98-b6b8-4bb0afcfbe3a_816x451.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>Time-series data is crucial for IoT device monitoring and data visualization in industries such as agriculture, renewable energy, and meteorology. It enables trend analysis, anomaly detection, and predictive analytics, empowering<br>businesses to optimize performance and make data-driven decisions. Thanks to technological advancements and the accessibility of open-source tools, gathering and analyzing data from IoT devices has become easier than ever before.</p><p>In this tutorial, we will guide you through the process of setting up a monitoring system for IoT device data. We will use historical electricity consumption data from some European countries, captured at a 15-minute resolution. The data is sent to an MQTT-compatible message broker called Mosquitto, and then channeled to QuestDB through Telegraf, a highly efficient data collector. To visualize the results, we will connect Grafana to QuestDB using its Postgres interface.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hYT3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hYT3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 424w, https://substackcdn.com/image/fetch/$s_!hYT3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 848w, https://substackcdn.com/image/fetch/$s_!hYT3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 1272w, https://substackcdn.com/image/fetch/$s_!hYT3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hYT3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp" width="710" height="437" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:437,&quot;width&quot;:710,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53512,&quot;alt&quot;:&quot;Overview&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Overview" title="Overview" srcset="https://substackcdn.com/image/fetch/$s_!hYT3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 424w, https://substackcdn.com/image/fetch/$s_!hYT3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 848w, https://substackcdn.com/image/fetch/$s_!hYT3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 1272w, https://substackcdn.com/image/fetch/$s_!hYT3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9b197a7-35d4-4bc0-b680-9728089f3b8f_710x437.webp 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Since we don't have our own sensors to collect the data and send it directly to the broker, we will create the scenario by having a script that gathers the electricity consumption data from <a href="https://data.open-power-system-data.org/time_series/2020-10-06">Open Power System Data</a> and sends it to Mosquitto.</p><h2>Prerequisites</h2><p>Before delving into the system setup, please make sure that you have installed the following:</p><ul><li><p><a href="https://www.docker.com/">Docker</a></p></li><li><p>The latest <a href="https://go.dev/dl/">Go</a> version</p></li></ul><h2>Setting up the prerequisites</h2><p>To get started, clone the prepared <a href="https://github.com/questdb/questdb-mock-power-sensor">GitHub repository</a>:</p><pre><code>$ git clone https://github.com/questdb/questdb-mock-power-sensor mock-sensor</code></pre><p>This repository contains all the configuration files and mock sensor scripts we will need during the tutorial.</p><p>Furthermore, we are going to need a new Docker network named "tutorial" to enable communication between containers without installing additional software on the host system. To create the new network, execute the following command:</p><pre><code>$ docker network create tutorial</code></pre><h2>Setting up an MQTT broker</h2><p>To enable communication between IoT devices and the monitoring system, we need a message broker such as Eclipse Mosquitto, which implements the lightweight MQTT protocol. By using Eclipse Mosquitto, we establish a reliable and efficient communication channel for IoT devices.</p><p>To run Eclipse Mosquitto, we are going to use Docker. The default configuration for Mosquitto does not allow unauthenticated clients. However, no default credentials are set. To fix this, let's use the <code>conf/mosquitto/mosquitto.conf</code> to override the default settings:</p><pre><code>allow_anonymous true
listener 1883 0.0.0.0</code></pre><p>To start the Eclipse Mosquitto broker using Docker, you can execute the following command from the repository root:</p><pre><code>$ docker run --rm -dit -p 1883:1883 -p 9001:9001 \
 -v "$(pwd)"/conf/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf \
 --network=tutorial --name=mosquitto eclipse-mosquitto</code></pre><p>By running this command, the Eclipse Mosquitto broker will start within a Docker container, configured with the settings from the <code>mosquitto.conf</code> file. The specified ports will be exposed to enable MQTT communication and the container will be connected to the tutorial Docker network, allowing it to communicate with other containers in the network.</p><h2>Tunneling data into Mosquitto</h2><p>Now that the message broker is running, in a new terminal window, navigate to the repository root and start the script that will populate the data for us:</p><pre><code>$ cd script
$ go get
$ go run ./main.go</code></pre><p>Great! The script will continue running in the background until it is manually stopped or until it runs out of data to publish. Since we are using a historical dataset, the script will override the original timestamps with the current time when sending the records to the MQTT broker.</p><p>With a 1-second delay between records, the script will consistently publish data to the broker, ensuring a steady flow of simulated sensor data for your tutorial. This allows you to progress with the rest of the tutorial without worrying about starting or stopping the script manually.</p><h2>Setting up QuestDB</h2><p>To ensure that all incoming IoT data flowing through Mosquitto is stored in QuestDB by the end of this tutorial, let's start QuestDB in the background. This will allow us to connect to it later using Telegraf.</p><p>Execute the following Docker command to start QuestDB:</p><pre><code>$ docker run --rm -dit -p 8812:8812 -p 9000:9000 \
    -p 9009:9009 -e QDB_PG_READONLY_USER_ENABLED=true \
    --network=tutorial --name=questdb questdb/questdb</code></pre><p>Once the command is executed, QuestDB will start running in the background. To validate that QuestDB is up and running, you can visit <code>http://localhost:9000</code> in your web browser.</p><p>By accessing the provided URL, you should be able to access the QuestDB web interface, indicating that QuestDB has been successfully started.</p><h2>Connecting the dots and adding Telegraf</h2><p>In this setup, Telegraf will play a key role in transferring data between Mosquitto and QuestDB. To enable this seamless data transfer, we will utilize QuestDB's ILP (Influx Line Protocol) interface, as ILP is capable of handling large volumes of data efficiently sent from Telegraf.</p><p>To configure Telegraf, we are using the <code>conf/telegraf/telegraf.conf</code> file, which has the following content:</p><pre><code># Configuration for Telegraf agent
[agent]
  ## Default data collection interval for all inputs
  interval = "1s"

[[inputs.mqtt_consumer]]
  servers = ["tcp://mosquitto:1883"]
  topics = ["sensor"]
  data_format = "influx"
  client_id = "telegraf"
  data_type = "string"

# Write results to QuestDB
[[outputs.socket_writer]]
  # Write metrics to a local QuestDB instance over TCP
  address = "tcp://questdb:9009"</code></pre><p>Let&#8217;s start the Telegraf container by executing:</p><pre><code>$ docker run --rm -it \
    -v "$(pwd)"/conf/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf \
    --network=tutorial --name=telegraf telegraf</code></pre><p>Once Telegraf is up and running, the sensor data is automatically tunneled into QuestDB, and a new table called "sensor" is created to store the incoming data. To verify that the data is successfully flowing into QuestDB, you can execute the following query on the QuestDB console, accessible at <code>http://localhost:9000</code>:</p><pre><code>SELECT * FROM sensor;</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vizS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vizS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 424w, https://substackcdn.com/image/fetch/$s_!vizS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 848w, https://substackcdn.com/image/fetch/$s_!vizS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 1272w, https://substackcdn.com/image/fetch/$s_!vizS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vizS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp" width="900" height="592" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:592,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:322078,&quot;alt&quot;:&quot;QuestDB Console Overview&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="QuestDB Console Overview" title="QuestDB Console Overview" srcset="https://substackcdn.com/image/fetch/$s_!vizS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 424w, https://substackcdn.com/image/fetch/$s_!vizS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 848w, https://substackcdn.com/image/fetch/$s_!vizS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 1272w, https://substackcdn.com/image/fetch/$s_!vizS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1aa2890b-ffb2-4f96-9ecb-e13e6fbbe0c6_900x592.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In the next steps of the tutorial, we will set up Grafana, which is a powerful data visualization and monitoring tool. We will connect Grafana to QuestDB and create compelling visualizations based on the received sensor data.</p><h2>Visualizing the dataset with Grafana</h2><p>To proceed with creating a Grafana container and connecting it to QuestDB, execute the following command:</p><pre><code>$ docker run --rm -dit -p 3000:3000 \
    --network=tutorial --name=grafana grafana/grafana</code></pre><p>This command will start a Grafana container within the same tutorial network. To access the Grafana login page, navigate to <code>http://localhost:3000</code> in your web browser. You will be prompted to log in using the default credentials:</p><ul><li><p>Username: admin</p></li><li><p>Password: admin</p></li></ul><p>To connect Grafana with QuestDB and establish a data source, follow these steps:</p><ol><li><p>Open your web browser and go to <code>http://localhost:3000/datasources/new</code>.</p></li><li><p>On the "New data source" page, select "PostgreSQL" as the data source type from the list of available options.</p></li></ol><p>Fill in the form with the following details:</p><ul><li><p>Name: QuestDB</p></li><li><p>Host: questdb:8812</p></li><li><p>Database: qdb</p></li><li><p>User: user</p></li><li><p>Password: quest</p></li><li><p>TLS/SSL mode: disable</p></li></ul><p>Once you have filled in the form with the correct information, click on the "Save &amp; Test" button at the bottom of the page. Grafana will attempt to connect to QuestDB using the provided credentials and verify the connection. If everything is set up correctly, you should see a success message indicating that the data source was added successfully.</p><p>Now that Grafana is successfully connected to QuestDB, you can begin creating insightful dashboards to visualize the electricity consumption data. Follow these steps to create a new dashboard:</p><ol><li><p>In your browser and navigate to the <a href="http://localhost:3000/dashboard/new?orgId=1">new dashboard</a> page.</p></li><li><p>Once you're on the new dashboard page, click on the "Add visualization" button.</p></li></ol><p>On the popup panel, select &#8220;QuestDB&#8221; data source.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xena!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xena!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 424w, https://substackcdn.com/image/fetch/$s_!xena!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 848w, https://substackcdn.com/image/fetch/$s_!xena!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 1272w, https://substackcdn.com/image/fetch/$s_!xena!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xena!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp" width="900" height="552" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:552,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:142221,&quot;alt&quot;:&quot;Grafana Edit View&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Grafana Edit View" title="Grafana Edit View" srcset="https://substackcdn.com/image/fetch/$s_!xena!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 424w, https://substackcdn.com/image/fetch/$s_!xena!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 848w, https://substackcdn.com/image/fetch/$s_!xena!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 1272w, https://substackcdn.com/image/fetch/$s_!xena!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba0101a8-8884-4c4a-b25a-5f92d19f2406_900x552.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now you're ready to start building your dashboard and visualizing the electricity consumption data from QuestDB. Grafana provides a wide range of visualization options, including graphs, charts, tables, and more. You can customize the panel settings, apply different visualization types, and configure data queries to create meaningful visual representations of the data.</p><p>To visualize the electricity consumption data from QuestDB, we will use the "Time series" chart type, which is the default selected chart based on our data. The "Time series" chart is ideal for displaying data over time and is well-suited for analyzing trends and patterns.</p><p>In the query builder panel, switch to &#8220;Code&#8221; and paste the following SQL query:</p><pre><code>SELECT
    date_trunc('minute', timestamp) AS minute,
    AVG(load_actual) AS avg_load_actual,
    AVG(load_forecast) AS avg_load_forecast
FROM sensor
WHERE country = 'NL'
GROUP BY minute
ORDER BY minute ASC</code></pre><p>By executing this query, we will obtain a result set that provides minutely aggregated data, showing the average, actual, and forecasted energy load for each minute. Feel free to adjust the sampling or grouping interval if you wish. If you followed this tutorial successfully, you should see similar results.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hSEK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hSEK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 424w, https://substackcdn.com/image/fetch/$s_!hSEK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 848w, https://substackcdn.com/image/fetch/$s_!hSEK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 1272w, https://substackcdn.com/image/fetch/$s_!hSEK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hSEK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp" width="1000" height="703" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:703,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:679413,&quot;alt&quot;:&quot;Dashboard Panel View&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Dashboard Panel View" title="Dashboard Panel View" srcset="https://substackcdn.com/image/fetch/$s_!hSEK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 424w, https://substackcdn.com/image/fetch/$s_!hSEK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 848w, https://substackcdn.com/image/fetch/$s_!hSEK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 1272w, https://substackcdn.com/image/fetch/$s_!hSEK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8608dcf6-2606-40dc-a1f6-3e829f19a1ae_1000x703.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To make the changes persistent while the container is running, rename the title of the graph on the right-hand side properties pane, and save it by clicking on the &#8220;Apply&#8221; button in the top right corner.</p><p>The time series chart we have created, with the average actual and forecasted energy consumption, provides valuable insights into the accuracy of the forecasting logic. By analyzing this chart, we can easily identify any discrepancies between the actual and forecasted values, helping you pinpoint areas where improvements can be made.</p><h2>Cleaning up resources</h2><p>Upon finishing the tutorial, you may want to clean up any dangling resources, such as the tutorial network we created. Removing a network requires the removal of existing resources attached to that first. To remove the containers, the network, and the images used during this tutorial, run the following commands:</p><pre><code>$ docker ps -qa --filter="network=tutorial" | xargs -n1 docker kill
$ docker network rm tutorial
$ docker rmi telegraf eclipse-mosquitto grafana/grafana questdb/questdb</code></pre><h2>Summary</h2><p>In this tutorial, we explored the setup and configuration of a monitoring system for IoT device data using MQTT, Telegraf, QuestDB, and Grafana. Through a series of steps, we established communication between IoT devices and the monitoring system using Eclipse-Mosquitto as the MQTT broker.</p><p>We simulated data collection using a script that gathered electricity consumption from Open Power System Data and sent it to the MQTT broker. We stored this data in QuestDB and visualized it using Grafana. By connecting Grafana to QuestDB, we created informative dashboards to gain insights.</p><p>The tutorial showcased the power of time-series data and its visualization, enabling us to compare actual and forecasted energy consumption.</p>]]></content:encoded></item><item><title><![CDATA[Loading Pandas DataFrames into QuestDB]]></title><description><![CDATA[Learn how to improve your time series analysis capability by using the QuestDB Python package to ingest Pandas DataFrames.]]></description><link>https://www.expatgeekdiaries.com/p/loading-pandas-dataframes-into-questdb</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/loading-pandas-dataframes-into-questdb</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Mon, 13 Mar 2023 19:12:30 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2d2db6e4-7bf0-471b-90a9-033ee12a9da4_800x450.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LIR9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LIR9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 424w, https://substackcdn.com/image/fetch/$s_!LIR9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 848w, https://substackcdn.com/image/fetch/$s_!LIR9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 1272w, https://substackcdn.com/image/fetch/$s_!LIR9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LIR9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Loading Pandas DataFrames into QuestDB&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Loading Pandas DataFrames into QuestDB" title="Loading Pandas DataFrames into QuestDB" srcset="https://substackcdn.com/image/fetch/$s_!LIR9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 424w, https://substackcdn.com/image/fetch/$s_!LIR9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 848w, https://substackcdn.com/image/fetch/$s_!LIR9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 1272w, https://substackcdn.com/image/fetch/$s_!LIR9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30aae2aa-6b01-4c7f-9aa0-bfb9d7c78596_800x450.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>Learn how to improve your time series analysis capability by using the QuestDB Python package to ingest Pandas DataFrames.</p><h2>Introduction</h2><p>Pandas is an open-source data analysis and data manipulation library for Python that has become an essential tool for data scientists and analysts. It provides a simple and intuitive way to manipulate data, making it a popular choice for data analysis tasks. However, while Pandas is excellent for small to medium-sized datasets, it can struggle with large datasets that exceed the available memory of the machine it is running on. This is where QuestDB excels, designed specifically for high-performance operations in such scenarios, making it the go-to solution for demanding data analysis tasks. By loading Pandas DataFrames into QuestDB, we can leverage <a href="https://github.com/questdb/py-tsbs-benchmark#serialization-network-send--data-insertion-into-questdb">powerful data processing capabilities</a> of the database, allowing you to scale your analysis and data manipulation operations to large datasets. We will learn how to load large Pandas dataframes into QuestDB. We use yellow and green taxi trip records published by NYC Taxi &amp; Limousine Commission as our data source.</p><p>In this tutorial, we will learn how to load large Pandas dataframes into QuestDB. We use yellow and green taxi trip records published by NYC Taxi &amp; Limousine Commission as our data source.</p><h2>Prerequisites</h2><p>For this tutorial, it is recommended to have a basic understanding of Python, and SQL. Also, you will need to have the following installed on your machine:</p><ul><li><p>Docker</p></li></ul><h2>Getting the data to ingest</h2><p>Before we begin loading the data into QuestDB, we need to obtain the data that we will be working with. As mentioned above, we will use the NYC TLC&#8217;s records of yellow and green taxi trips. Let&#8217;s download the data:</p><ol><li><p>Create a new directory called <code>pandas-to-questdb</code> and a <code>data</code> directory inside that.</p></li><li><p>Edit and execute the following command in your terminal to download the parquet files:</p></li></ol><pre><code>curl -L -o ./data/yellow_tripdata_2022-&lt;MONTH&gt;.parquet https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2022-&lt;MONTH&gt;.parquet
</code></pre><p>Make sure you replace <code>&lt;MONTH&gt;</code> with the zero-prefixed number of the month you wish to download (between 01 and 11, the 12th month is not available at the time of writing).</p><p>Now, we have the data to ingest. It is time to try loading it using Pandas.</p><h2>Loading records into memory</h2><p>You may already have noticed that the downloaded files are in Parquet format. Parquet is a columnar storage format commonly used for big data processing. They are optimized for use with modern big data processing engines and provide efficient storage and retrieval of data compared to traditional row-based storage formats like CSV and JSON.</p><p>Before being able to load any data, we are going to set up a simulation production environment in which we can easily test what happens if Pandas cannot load the Parquet files into memory. In production, we often meet situations where we have to deal with memory constraints, and this environment can reflect that.</p><p>Run the following command to create a new docker container running with 1GiB memory limit. If the container reaches that limit, either docker will kill it, or the OS will OOM kill the process we are running.</p><pre><code>docker run -it -m 1g -v "$(pwd)":/tutorial -w /tutorial --net host python:3.11.1-slim-bullseye /bin/bash
</code></pre><p>Right, we have an Ubuntu-based Python 3.11 docker container. Let&#8217;s install our requirements. Create a <code>requirements.txt</code> file with the content below:</p><pre><code>pandas&gt;=1.5.3
psycopg[binary]&gt;=3.1.8
pyarrow&gt;=11.0.0
questdb&gt;=1.1.0
</code></pre><p>Now, execute <code>pip install -r requirements.txt</code> within the container. Pip will install the python requirements.</p><p>At this point we have a test environment in which we can load the data. Create a new file, called <code>data_loader.py</code>, with the following content:</p><pre><code># data_loader.py

import pandas as pd

df = pd.read_parquet("./data/yellow_tripdata_2022-01.parquet")
print(df.head())
</code></pre><p>Now, execute it within the docker container by running <code>python data_loader.py</code>. The program runs successfully and we should see the following:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6G_H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6G_H!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 424w, https://substackcdn.com/image/fetch/$s_!6G_H!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 848w, https://substackcdn.com/image/fetch/$s_!6G_H!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 1272w, https://substackcdn.com/image/fetch/$s_!6G_H!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6G_H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp" width="1456" height="411" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:411,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:28544,&quot;alt&quot;:&quot;Data Loader&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Data Loader" title="Data Loader" srcset="https://substackcdn.com/image/fetch/$s_!6G_H!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 424w, https://substackcdn.com/image/fetch/$s_!6G_H!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 848w, https://substackcdn.com/image/fetch/$s_!6G_H!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 1272w, https://substackcdn.com/image/fetch/$s_!6G_H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F076043e3-b284-4299-9c19-300190e4b67c_1920x542.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We just loaded the taxi trip records for 2022 January! Let&#8217;s try to load more data. Replace the content of <code>data_loader.py</code> by the code below to load all files from the data directory and execute the program again:</p><pre><code># data_loader.py

import os
import glob
import pandas as pd

records = glob.glob(os.path.join("data", "*.parquet"))

df = pd.concat((pd.read_parquet(r) for r in records), ignore_index=True)

print(df.head())
</code></pre><p>When executing the <code>data_loader.py</code> you should get an error message: &#8220;Killed&#8221;. As you may assume, OOM killer terminated the process. We were not able to load the dataset, therefore we cannot work with that. We need a different approach.</p><h2>Ingesting to QuestDB</h2><p>In a new terminal window, start a QuestDB container by executing:</p><pre><code>docker run --rm -it -p 8812:8812 -p 9009:9009 --net host --name questdb questdb/questdb
</code></pre><p>The database is now ready to receive the data. Update the data_loader.py to ingest data into QuestDB using the questdb package that uses the <a href="https://www.gaboros.hu/docs/reference/api/ilp/overview/">InfluxDB Line Protocol (ILP)</a> over TCP for maximum throughput.</p><p>To handle large datasets, we will read files one by one and transfer their contents to QuestDB. Then, we will use QuestDB to query the data and load the results back into Pandas DataFrames. Refactor the data loader based on the above:</p><pre><code># data_loader.py

import os
import glob
import pandas as pd
from questdb.ingress import Sender

def main():
   files = glob.glob(os.path.join("data", "*.parquet"))

   with Sender("127.0.0.1", 9009) as sender:
       for file in files:
           df = pd.read_parquet(file)
           print(f"ingesting {len(df.index)} rows from {file}")
           sender.dataframe(df, table_name="trips", at="tpep_pickup_datetime")

if __name__ == "__main__":
   main()
</code></pre><p>Let's start from the beginning. The first major change you'll notice is that we need to specify the hostname and port number in the script in order to run it.</p><p>Then we iterate over the parquet files and load them into memory using Pandas. After that, utilizing QuestDB&#8217;s Python client, we are ingesting to QuestDB directly from Pandas DataFrames.</p><p>In the Python container, run <code>python data_loader.py</code>. The script will ingest one parquet file at a time.</p><h2>Working with trip data</h2><p>So far, we have prepared the dataset and loaded it into QuestDB. It&#8217;s time to execute some queries and load the result into DataFrames. Using the whole dataset, we want to know what was the average total amount paid by passengers grouped by the passengers.</p><p>Create a new file, called <code>query_amount.py</code> with the following content:</p><pre><code># query_amount.py

import pandas as pd
import psycopg

QUERY = """
SELECT passenger_count, avg(total_amount)
   FROM 'trips'
   WHERE passenger_count &gt; 0
   GROUP BY passenger_count
"""

if __name__ == "__main__":
   conn = psycopg.connect(
       dbname="questdb",
       host="127.0.0.1",
       user="admin",
       password="quest",
       port=8812,
   )

   df = pd.read_sql_query(QUERY, conn)

   print(df.head(10))
</code></pre><p>Similarly to the data loader script, this script requires the host and port too.</p><p>In the script above, we are using the Postgresql Python client and connecting to QuestDB using that. In the Python container, execute python <code>query_amount.py</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FGrJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FGrJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 424w, https://substackcdn.com/image/fetch/$s_!FGrJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 848w, https://substackcdn.com/image/fetch/$s_!FGrJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 1272w, https://substackcdn.com/image/fetch/$s_!FGrJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FGrJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp" width="1456" height="391" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:391,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:17082,&quot;alt&quot;:&quot;Query Results&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Query Results" title="Query Results" srcset="https://substackcdn.com/image/fetch/$s_!FGrJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 424w, https://substackcdn.com/image/fetch/$s_!FGrJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 848w, https://substackcdn.com/image/fetch/$s_!FGrJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 1272w, https://substackcdn.com/image/fetch/$s_!FGrJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F58e5db35-1eee-4a19-a62d-852494b43961_2688x722.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When the script finishes, you should see the average total amount paid by passengers. Interestingly, there is a huge difference in the average between passenger count 6 and 7, the average almost 2.5x for 7 passengers compared to 6.</p><p>By further analyzing the data, it may turn out what was the possible root cause increase, but probably it is bound to human nature: we like to share the cost of rides if we are going on a longer trip.</p><h2>Summary</h2><p>In this tutorial, we have learned how to load large datasets into QuestDB using Pandas DataFrames. By transferring data from Pandas to QuestDB, we have taken advantage of the database's powerful data processing capabilities, enabling us to scale our analysis and data manipulation operations to handle large datasets.</p><p>The approach outlined in this tutorial is just one way to work with big data using Pandas and QuestDB. You can customize this method to suit your specific needs and continue to explore the possibilities of these powerful tools. The end goal is to make data analysis and manipulation easier and more efficient, regardless of the size of the dataset.</p>]]></content:encoded></item><item><title><![CDATA[Real-time stock price dashboard using QuestDB, Python and Plotly]]></title><description><![CDATA[Why Plotly and Dash are useful for real-time applications]]></description><link>https://www.expatgeekdiaries.com/p/real-time-stock-price-dashboard-using-questdb-python-and-plotly</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/real-time-stock-price-dashboard-using-questdb-python-and-plotly</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Thu, 04 Nov 2021 14:59:02 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9eabf7ba-61d6-4bdf-a508-ca24bed2ac7f_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Why Plotly and Dash are useful for real-time applications</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mNti!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mNti!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!mNti!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!mNti!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!mNti!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mNti!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Real-time stock price dashboard using QuestDB, Python and Plotly&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Real-time stock price dashboard using QuestDB, Python and Plotly" title="Real-time stock price dashboard using QuestDB, Python and Plotly" srcset="https://substackcdn.com/image/fetch/$s_!mNti!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!mNti!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!mNti!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!mNti!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ec15668-24fc-4db5-8525-128d8541a076_1200x630.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>If you're working with large amounts of data, efficiently storing raw information will be your first obstacle. The next challenge is to make sense of the data utilizing analytics. One of the fastest ways to convey the state of data is through charts and graphs.</p><p>In this tutorial, we will create a real-time streaming dashboard using QuestDB, Celery, Redis, Plotly, and Dash. It will be a fun project with excellent charts to quickly understand the state of a system with beautiful data visualizations.</p><p>Plotly defines itself as "the front end for ML and data science models", which describes it really well. Plotly has an "app framework" called Dash which we can use to create web applications quickly and efficiently. Dash abstracts away the boilerplate needed to set up a web server and several handlers for it.</p><h2>Project overview</h2><p>The project will be built from two main components:</p><ul><li><p>a backend that periodically fetches user-defined stock data from <a href="https://finnhub.io/">Finnhub</a>, and</p></li><li><p>a front-end that utilizes Plotly and Dash to visualize the gathered data on interactive charts</p></li></ul><p>For this tutorial, you will need some experience in Python and basic SQL knowledge. We will use Celery backed by Redis as the message broker and QuestDB as storage to periodically fetch data.</p><p>Let's see the prerequisites and jump right in!</p><h3>Prerequisites</h3><ul><li><p>Python 3.8</p></li><li><p>Docker &amp; Docker Compose</p></li><li><p>Finnhub account and sandbox API key</p></li><li><p>Basic SQL skills</p></li></ul><p>The source code for this tutorial is available at the corresponding <a href="https://github.com/gabor-boros/questdb-stock-market-dashboard">GitHub repository</a>.</p><h2>Environment setup</h2><h3>Create a new project</h3><p>First of all, we are going to create empty directories for our project root and the Python module:</p><pre><code>mkdir -p streaming-dashboard/app
# streaming-dashboard
# &#9492;&#9472;&#9472; app
</code></pre><h3>Installing QuestDB &amp; Redis</h3><p>To install the services required for our project, we are using Docker and Docker Compose to avoid polluting our host machine. Within the project root, let's create a file, called docker-compose.yml. This file describes all the necessary requirements the project will use; later on we will extend this file with other services too.</p><pre><code>version: "3"

volumes:
  questdb_data: {}

services:
  redis:
    image: "redis:latest"
    ports:
      - "6379:6379"

  questdb:
    image: "questdb/questdb:latest"
    volumes:
      - questdb_data:/root/.questdb/db
    ports:
      - "9000:9000"
      - "8812:8812"
</code></pre><p>Here we go! When you run <code>docker-compose up</code>, QuestDB and Redis will fire up. After starting the services, we can access QuestDB's interactive console on <a href="http://127.0.0.1:9000/">http://127.0.0.1:9000</a>.</p><h3>Create the database table</h3><p>We could create the database table later, but we will take this opportunity and create the table now since we have already started QuestDB. Connect to QuestDB's interactive console, and run the following SQL statement:</p><pre><code>CREATE TABLE
      quotes(stock_symbol SYMBOL CAPACITY 5 CACHE INDEX, -- we are in fact just checking 3
             current_price DOUBLE,
             high_price DOUBLE,
             low_price DOUBLE,
             open_price DOUBLE,
             percent_change DOUBLE,
             tradets TIMESTAMP, -- timestamp of the trade
             ts TIMESTAMP)      -- time of insert in our table
      timestamp(ts)
PARTITION BY DAY;
</code></pre><p>After executing the command, we will see a success message in the bottom left corner, confirming that the table creation was successful and the table appears on the right-hand side's table list view.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bfJN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bfJN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 424w, https://substackcdn.com/image/fetch/$s_!bfJN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 848w, https://substackcdn.com/image/fetch/$s_!bfJN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 1272w, https://substackcdn.com/image/fetch/$s_!bfJN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bfJN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png" width="880" height="213" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:213,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:12881,&quot;alt&quot;:&quot;QuestDB Console&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="QuestDB Console" title="QuestDB Console" srcset="https://substackcdn.com/image/fetch/$s_!bfJN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 424w, https://substackcdn.com/image/fetch/$s_!bfJN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 848w, https://substackcdn.com/image/fetch/$s_!bfJN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 1272w, https://substackcdn.com/image/fetch/$s_!bfJN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcad9bc37-922c-4d5d-97b0-52633fa1be42_880x213.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Voil&#225;! The table is ready for use.</p><h2>Creating workers using Celery</h2><h3>Define Python dependencies</h3><p>As mentioned, our project will have two parts. For now, let's focus on the routine jobs that will fetch the data from Finnhub. As is the case of every standard Python project, we are using requirements.txt to define the dependencies the project will use. Place the requirements.txt in your project root with the content below:</p><pre><code>finnhub-python==2.4.5   # The official Finnhub Python client
pydantic[dotenv]==1.8.2 # We will use Pydantic to create data models
celery[redis]==5.1.2    # Celery will be the periodic task executor
psycopg2==2.9.1         # We are using QuestDB's PostgreSQL connector
sqlalchemy==1.4.2       # SQLAlchemy will help us executing SQL queries
dash==2.0.0             # Dash is used for building data apps
pandas==1.3.4           # Pandas will handle the data frames from QuestDB
plotly==5.3.1           # Plotly will help us with beautiful charts
</code></pre><p>We can split the requirements into two logical groups:</p><ol><li><p>requirements for fetching the data, and</p></li><li><p>requirements needed to visualize this data</p></li></ol><p>For the sake of simplicity, we did not create two separate requirements files, though in a production environment we would do. Create a virtualenv and install the dependencies:</p><pre><code>$ virtualenv -p python3.8 virtualenv
$ source virtualenv/bin/activate
$ pip install -r requirements.txt
</code></pre><h3>Setting up the DB connection</h3><p>Since the periodic tasks would need to store the fetched quotes, we need to connect to QuestDB. Therefore, we create a new file in the <code>app</code> package, called <code>db.py</code>. This file contains the <code>SQLAlchemy</code> engine that will serve as the base for our connections.</p><pre><code>from sqlalchemy import create_engine

from app.settings import settings

engine = create_engine(
    settings.database_url, pool_size=settings.database_pool_size, pool_pre_ping=True
)
</code></pre><h3>Define the worker settings</h3><p>Before we jump right into the implementation, we must configure Celery. To create a configuration used by both the workers and the dashboard, create a <code>settings.py</code> file in the <code>app</code> package. We will use <code>pydantic</code>'s BaseSettings to define the configuration. This helps us to read the settings from a <code>.env</code> file, environment variable, and prefix them if needed.</p><p>Ensuring that we do not overwrite any other environment variables, we will set the prefix to <code>SMD</code> that stands for "stock market dashboard", our application. Below you can see the settings file:</p><pre><code>from typing import List

from pydantic import BaseSettings


class Settings(BaseSettings):
    """
    Settings of the application, used by workers and dashboard.
    """

    # Celery settings
    celery_broker: str = "redis://127.0.0.1:6379/0"

    # Database settings
    database_url: str = "postgresql://admin:quest@127.0.0.1:8812/qdb"
    database_pool_size: int = 3

    # Finnhub settings
    api_key: str = ""
    frequency: int = 5  # default stock data fetch frequency in seconds
    symbols: List[str] = list()

    # Dash/Plotly
    debug: bool = True
    graph_interval: int = 10

    class Config:
        """
        Meta configuration of the settings parser.
        """

        env_file = ".env"
        # Prefix the environment variable not to mix up with other variables
        # used by the OS or other software.
        env_prefix = "SMD_"  # SMD stands for Stock Market Dashboard


settings = Settings()
</code></pre><p>In the settings, you can notice we already defined the <code>celery_broker</code> and <code>database_url</code> settings with unusual default values.</p><p>Some bits are missing at the moment. We still have to define the correct settings and run the worker in a Docker container. Get started with the settings!</p><p>To keep our environment separated, we will use a <code>.env</code> file. One of <code>pydantic</code> based settings' most significant advantage is that it can read environment variables from <code>.env</code> files.</p><p>Let's create a <code>.env</code> file in the project root, next to <code>docker-compose.yml</code>:</p><pre><code>SMD_API_KEY = "&lt;YOUR SANDBOX API KEY&gt;"
SMD_FREQUENCY = 10
SMD_SYMBOLS = ["AAPL","DOCN","EBAY"]
</code></pre><p>As you may assume, you will need to get your API key for the sandbox environment at this step. To retrieve the key, the only thing you have to do is sign up to Finnhub, and your API key will appear on the dashboard after login.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3ro2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3ro2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 424w, https://substackcdn.com/image/fetch/$s_!3ro2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 848w, https://substackcdn.com/image/fetch/$s_!3ro2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 1272w, https://substackcdn.com/image/fetch/$s_!3ro2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3ro2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png" width="880" height="507" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:507,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11477,&quot;alt&quot;:&quot;Finnhub API Keys&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Finnhub API Keys" title="Finnhub API Keys" srcset="https://substackcdn.com/image/fetch/$s_!3ro2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 424w, https://substackcdn.com/image/fetch/$s_!3ro2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 848w, https://substackcdn.com/image/fetch/$s_!3ro2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 1272w, https://substackcdn.com/image/fetch/$s_!3ro2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9b244c5-4ec9-4c18-a392-654e4f115a21_880x507.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Create the periodic task</h3><pre><code>import finnhub
from celery import Celery
from sqlalchemy import text
from app.db import engine
from app.settings import settings

client = finnhub.Client(api_key=settings.api_key)
celery_app = Celery(broker=settings.celery_broker)

@celery_app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    """
    Setup a periodic task for every symbol defined in the settings.
    """
    for symbol in settings.symbols:
        sender.add_periodic_task(settings.frequency, fetch.s(symbol))


@celery_app.task
def fetch(symbol: str):
    """
    Fetch the stock info for a given symbol from Finnhub and load it into QuestDB.
    """

    quote: dict = client.quote(symbol)
    # https://finnhub.io/docs/api/quote
    #  quote = {'c': 148.96, 'd': -0.84, 'dp': -0.5607, 'h': 149.7, 'l': 147.8, 'o': 148.985, 'pc': 149.8, 't': 1635796803}
    # c: Current price
    # d: Change
    # dp: Percent change
    # h: High price of the day
    # l: Low price of the day
    # o: Open price of the day
    # pc: Previous close price
    # t: when it was traded
    query = f"""
    INSERT INTO quotes(stock_symbol, current_price, high_price, low_price, open_price, percent_change, tradets, ts)
    VALUES(
        '{symbol}',
        {quote["c"]},
        {quote["h"]},
        {quote["l"]},
        {quote["o"]},
        {quote["pc"]},
        {quote["t"]} * 1000000,
        systimestamp()
    );
    """

    with engine.connect() as conn:
        conn.execute(text(query))
</code></pre><p>Going through the code above:</p><pre><code>import finnhub
from celery import Celery
from sqlalchemy import text

from app.db import engine
from app.settings import settings

# [...]
</code></pre><p>In the first few lines, we import the requirements that are needed to fetch and store the data.</p><p>After importing the requirements, we configure the Finnhub client and Celery to use the Redis broker we defined in the application settings.</p><pre><code># [...]

client = finnhub.Client(api_key=settings.api_key)
celery_app = Celery(broker=settings.celery_broker)

# [...]
</code></pre><p>To fetch the data periodically per stock symbol, we need to programmatically<br>create a periodic task for every symbol we defined in the settings.</p><pre><code># [...]

@celery_app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    """
    Setup a periodic task for every symbol defined in the settings.
    """
    for symbol in settings.symbols:
        sender.add_periodic_task(settings.frequency, fetch.s(symbol))

# [...]
</code></pre><p>The snippet above will register a new periodic per stock symbol after Celery is connected to the broker.</p><p>The last step is to define the <code>fetch</code> task that does the majority of the work.</p><pre><code># [...]

@celery_app.task
def fetch(symbol: str):
    """
    Fetch the stock info for a given symbol from Finnhub and load it into QuestDB.
    """

    quote: dict = client.quote(symbol)
    # https://finnhub.io/docs/api/quote
    #  quote = {'c': 148.96, 'd': -0.84, 'dp': -0.5607, 'h': 149.7, 'l': 147.8, 'o': 148.985, 'pc': 149.8, 't': 1635796803}
    # c: Current price
    # d: Change
    # dp: Percent change
    # h: High price of the day
    # l: Low price of the day
    # o: Open price of the day
    # pc: Previous close price
    # t: when it was traded

    query = f"""
    INSERT INTO quotes(stock_symbol, current_price, high_price, low_price, open_price, percent_change, tradets, ts)
    VALUES(
        '{symbol}',
        {quote["c"]},
        {quote["h"]},
        {quote["l"]},
        {quote["o"]},
        {quote["pc"]},
        {quote["t"]} * 1000000,
        systimestamp()
    );
    """

    with engine.connect() as conn:
        conn.execute(text(query))
</code></pre><p>Using the Finnhub <code>client</code>, we get a quote for the given symbol. After the quote is retrieved successfully, we prepare a SQL query to insert the quote into the database. At the end of the function, as the last step, we open a connection to QuestDB and insert the new quote.</p><p>Congratulations! The worker is ready for use; let's try it out!</p><p>Execute the command below in a new terminal window within the virtualenv, and wait some seconds to let Celery kick in:</p><pre><code>python -m celery --app app.worker.celery_app worker --beat -l info -c 1
</code></pre><p>Soon, you will see that the tasks are scheduled, and the database is slowly<br>filling.</p><h3>Checking in on what we've built so far</h3><p>Before proceeding to the visualization steps, let's have a look at what we have built so far:</p><ol><li><p>we created the project root</p></li><li><p>a <code>docker-compose.yml</code> file to manage related services</p></li><li><p><code>app/settings.py</code> that handles our application configuration</p></li><li><p><code>app/db.py</code> configuring the database engine, and</p></li><li><p>last but not least, <code>app/worker.py</code> that handles the hard work, fetches, and stores the data.</p></li></ol><p>At this point, we should have the following project structure:</p><pre><code>&#9500;&#9472;&#9472; app
&#9474;   &#9500;&#9472;&#9472; __init__.py
&#9474;   &#9500;&#9472;&#9472; db.py
&#9474;   &#9500;&#9472;&#9472; settings.py
&#9474;   &#9492;&#9472;&#9472; worker.py
&#9492;&#9472;&#9472; docker-compose.yml
</code></pre><h2>Visualize the data with Plotly and Dash</h2><h3>Getting static assets</h3><p>This tutorial is not about writing the necessary style sheets or collecting static assets, so you only need to copy-paste some code. As the first step, create an <code>assets</code> directory next to the <code>app</code> package with the structure below:</p><pre><code>&#9500;&#9472;&#9472; app
&#9474;   &#9500;&#9472;&#9472; __init__.py
&#9474;   &#9500;&#9472;&#9472; db.py
&#9474;   &#9500;&#9472;&#9472; settings.py
&#9474;   &#9492;&#9472;&#9472; worker.py
&#9500;&#9472;&#9472; assets
&#9500;&#9472;&#9472; .env
&#9500;&#9472;&#9472; docker-compose.yml
</code></pre><p>The <code>style.css</code> will define the styling for our application. As mentioned above, Dash will save us from boilerplate code, so the <code>assets</code> directory will be used by default in conjunction with the stylesheet in it.</p><p>Download the <code>style.css</code> file to the <code>assets</code> directory, this can be done using <code>curl</code>:</p><pre><code>curl -s -Lo ./assets/style.css https://raw.githubusercontent.com/gabor-boros/questdb-stock-market-dashboard/main/assets/style.css
</code></pre><h3>Setting up the application</h3><p>This is the most interesting part of the tutorial. We are going to visualize the data we collect. Create a <code>main.py</code> file in the <code>app</code> package, and let's begin with the imports:</p><pre><code>from datetime import datetime, timedelta

import dash
import pandas
from dash import dcc, html
from dash.dependencies import Input, Output
from plotly import graph_objects

from app.db import engine
from app.settings import settings

# [...]
</code></pre><p>After having the imports in place, we are defining some helper functions and constants.</p><pre><code># [...]

GRAPH_INTERVAL = settings.graph_interval * 1000

TIME_DELTA = 5  # last T hours of data are looked into as per insert time

COLORS = [
    "#1e88e5",
    "#7cb342",
    "#fbc02d",
    "#ab47bc",
    "#26a69a",
    "#5d8aa8",
]


def now() -&gt; datetime:
    return datetime.utcnow()


def get_stock_data(start: datetime, end: datetime, stock_symbol: str):
    def format_date(dt: datetime) -&gt; str:
        return dt.isoformat(timespec="microseconds") + "Z"

    query = f"quotes WHERE ts BETWEEN '{format_date(start)}' AND '{format_date(end)}'"

    if stock_symbol:
        query += f" AND stock_symbol = '{stock_symbol}' "

    with engine.connect() as conn:
        return pandas.read_sql_query(query, conn)

# [...]
</code></pre><p>In the first few lines, we define constants for setting a graph update frequency (<code>GRAPH_INTERVAL</code>) and colors that will be used for coloring the graph (<code>COLORS</code>).</p><p>After that, we define two helper functions, <code>now</code> and <code>get_stock_data</code>. While <code>now</code> is responsible only for getting the current time in UTC (as Finnhub returns the date in UTC too), the <code>get_stock_data</code> does more. It is the core of<br>our front-end application, it fetches the stock data from QuestDB that workers inserted.</p><p>Define the initial data frame and the application:</p><pre><code># [...]

df = get_stock_data(now() - timedelta(hours=TIME_DELTA), now(), "")

app = dash.Dash(
    __name__,
    title="Real-time stock market changes",
    assets_folder="../assets",
    meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}],
)

# [...]
</code></pre><p>As you can see above, the initial data frame (<code>df</code>) will contain the latest 5 hours of data we have. This is needed to pre-populate the application with some data we have. The application definition <code>app</code> describes the application's title, asset folder, and some HTML meta tags used during rendering.</p><p>Create the application layout that will be rendered as HTML. We won't write HTML, we will use Dash's helpers for that:</p><pre><code># [...]

app.layout = html.Div(
    [
        html.Div(
            [
                html.Div(
                    [
                        html.H4("Stock market changes", className="app__header__title"),
                        html.P(
                            "Continually query QuestDB and display live changes of the specified stocks.",
                            className="app__header__subtitle",
                        ),
                    ],
                    className="app__header__desc",
                ),
            ],
            className="app__header",
        ),
        html.Div(
            [
                html.P("Select a stock symbol"),
                dcc.Dropdown(
                    id="stock-symbol",
                    searchable=True,
                    options=[
                        {"label": symbol, "value": symbol}
                        for symbol in df["stock_symbol"].unique()
                    ],
                ),
            ],
            className="app__selector",
        ),
        html.Div(
            [
                html.Div(
                    [
                        html.Div(
                            [html.H6("Current price changes", className="graph__title")]
                        ),
                        dcc.Graph(id="stock-graph"),
                    ],
                    className="one-half column",
                ),
                html.Div(
                    [
                        html.Div(
                            [html.H6("Percent changes", className="graph__title")]
                        ),
                        dcc.Graph(id="stock-graph-percent-change"),
                    ],
                    className="one-half column",
                ),
            ],
            className="app__content",
        ),
        dcc.Interval(
            id="stock-graph-update",
            interval=int(GRAPH_INTERVAL),
            n_intervals=0,
        ),
    ],
    className="app__container",
)

# [...]
</code></pre><p>This snippet is a bit longer, though it has only one interesting part, <code>dcc.Interval</code>. The interval is used to set up periodic graph refresh.</p><p>We are nearly finished with our application, but the last steps are to define two callbacks that will listen to input changes and the interval discussed above. The first callback is for generating the graph data and rendering the<br>lines per stock symbol.</p><pre><code># [...]

@app.callback(
    Output("stock-graph", "figure"),
    [Input("stock-symbol", "value"), Input("stock-graph-update", "n_intervals")],
)
def generate_stock_graph(selected_symbol, _):
    data = []
    filtered_df = get_stock_data(now() - timedelta(hours=TIME_DELTA), now(), selected_symbol)
    groups = filtered_df.groupby(by="stock_symbol")

    for group, data_frame in groups:
        data_frame = data_frame.sort_values(by=["ts"])
        trace = graph_objects.Scatter(
            x=data_frame.ts.tolist(),
            y=data_frame.current_price.tolist(),
            marker=dict(color=COLORS[len(data)]),
            name=group,
        )
        data.append(trace)

    layout = graph_objects.Layout(
        xaxis={"title": "Time"},
        yaxis={"title": "Price"},
        margin={"l": 70, "b": 70, "t": 70, "r": 70},
        hovermode="closest",
        plot_bgcolor="#282a36",
        paper_bgcolor="#282a36",
        font={"color": "#aaa"},
    )

    figure = graph_objects.Figure(data=data, layout=layout)
    return figure

# [...]
</code></pre><p>The other callback is very similar to the previous one; it will be responsible for updating the percentage change representation of the stocks or a given<br>stock.</p><pre><code># [...]

@app.callback(
    Output("stock-graph-percent-change", "figure"),
    [
        Input("stock-symbol", "value"),
        Input("stock-graph-update", "n_intervals"),
    ],
)
def generate_stock_graph_percentage(selected_symbol, _):
    data = []
    filtered_df = get_stock_data(now() - timedelta(hours=TIME_DELTA), now(), selected_symbol)
    groups = filtered_df.groupby(by="stock_symbol")

    for group, data_frame in groups:
        data_frame = data_frame.sort_values(by=["ts"])
        trace = graph_objects.Scatter(
            x=data_frame.ts.tolist(),
            y=data_frame.percent_change.tolist(),
            marker=dict(color=COLORS[len(data)]),
            name=group,
        )
        data.append(trace)

    layout = graph_objects.Layout(
        xaxis={"title": "Time"},
        yaxis={"title": "Percent change"},
        margin={"l": 70, "b": 70, "t": 70, "r": 70},
        hovermode="closest",
        plot_bgcolor="#282a36",
        paper_bgcolor="#282a36",
        font={"color": "#aaa"},
    )

    figure = graph_objects.Figure(data=data, layout=layout)
    return figure

# [...]
</code></pre><p>The last step is to call <code>run_server</code> on the <code>app</code> object when the script is called from the CLI.</p><pre><code># [...]

if __name__ == "__main__":
    app.run_server(host="0.0.0.0", debug=settings.debug)
</code></pre><p>We are now ready to try our application with actual data. Make sure that the Docker containers are started and execute <code>PYTHONPATH=. python app/main.py</code> from the project root:</p><pre><code>$ PYTHONPATH=. python app/main.py

Dash is running on http://0.0.0.0:8050/

 * Tip: There are .env or .flaskenv files present. Do "pip install python-dotenv" to use them.
 * Serving Flask app 'main' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses.
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://192.168.0.14:8050/ (Press CTRL+C to quit)
</code></pre><p>Navigate to <a href="http://127.0.0.1:8050/">http://127.0.0.1:8050/</a>, to see the application in action.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TCO2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TCO2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 424w, https://substackcdn.com/image/fetch/$s_!TCO2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 848w, https://substackcdn.com/image/fetch/$s_!TCO2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 1272w, https://substackcdn.com/image/fetch/$s_!TCO2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TCO2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png" width="880" height="405" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:405,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10513,&quot;alt&quot;:&quot;Stock Market Overview&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Stock Market Overview" title="Stock Market Overview" srcset="https://substackcdn.com/image/fetch/$s_!TCO2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 424w, https://substackcdn.com/image/fetch/$s_!TCO2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 848w, https://substackcdn.com/image/fetch/$s_!TCO2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 1272w, https://substackcdn.com/image/fetch/$s_!TCO2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a13300c-58e7-4bba-9594-0dbb744ad098_880x405.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To select only one stock, in the dropdown field choose the desired stock symbol and let the application refresh.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0ZdZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 424w, https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 848w, https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 1272w, https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png" width="880" height="407" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f93ad505-fb6f-4505-bed3-858672ca782f_880x407.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:407,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9256,&quot;alt&quot;:&quot;Stock Overview&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Stock Overview" title="Stock Overview" srcset="https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 424w, https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 848w, https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 1272w, https://substackcdn.com/image/fetch/$s_!0ZdZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff93ad505-fb6f-4505-bed3-858672ca782f_880x407.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Summary</h2><p>In this tutorial, we've learned how to schedule tasks in Python, store data in QuestDB, and create beautiful dashboards using Plotly and Dash. Although we won't start trading just right now; this tutorial demonstrated well how to combine these separately powerful tools and software to create something bigger and more useful.</p><p><em>The source code is available at</em><br><a href="https://github.com/gabor-boros/questdb-stock-market-dashboard">https://github.com/gabor-boros/questdb-stock-market-dashboard</a>.</p>]]></content:encoded></item><item><title><![CDATA[Automating ETL jobs on time series data with QuestDB on Google Cloud Platform]]></title><description><![CDATA[In the world of big data, software developers and data analysts often have to write scripts or complex software collections to process data before sending it to a data store for further analysis.]]></description><link>https://www.expatgeekdiaries.com/p/automating-etl-jobs-on-time-series-data-with-questdb-on-google-cloud-platform</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/automating-etl-jobs-on-time-series-data-with-questdb-on-google-cloud-platform</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Mon, 29 Mar 2021 17:30:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/20592bdd-c7a1-4e1a-b10b-e69d050c17ff_2000x1439.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-fLW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-fLW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-fLW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-fLW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-fLW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-fLW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Automating ETL jobs on time series data with QuestDB on Google Cloud Platform&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Automating ETL jobs on time series data with QuestDB on Google Cloud Platform" title="Automating ETL jobs on time series data with QuestDB on Google Cloud Platform" srcset="https://substackcdn.com/image/fetch/$s_!-fLW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-fLW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-fLW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-fLW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F094c90d6-14ef-4bec-bb61-d1bc325f66f7_2000x1439.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>In the world of big data, software developers and data analysts often have to write scripts or complex software collections to process data before sending it to a data store for further analysis. This process is commonly called ETL, which stands for Extract, Transform and Load.</p><h2>What could we use ETL jobs for?</h2><p>Let's consider the following example: a medium-sized webshop with a few thousand orders per day exports order information hourly. After a while, we would like to visualize purchase trends, and we might want to share the results between departments or even publicly. Since the exported data contains personally identifiable information (PII), we should anonymize it before using or exposing it to the public.</p><p>For the example above, we can use an ETL job to extract the incoming data, remove any PII and load the transformed data into a database used as the data visualization backend later.</p><h2>Prerequisites</h2><p>During this tutorial, we will use Python to write the cloud functions, so basic python knowledge is essential. Aside from these skills, you will need the following resources:</p><ul><li><p>A <a href="https://console.cloud.google.com/getting-started">Google Cloud Platform</a> (GCP) account and a GCP Project.</p></li><li><p>Enable the <a href="https://console.cloud.google.com/marketplace/product/google/cloudbuild.googleapis.com">Cloud Build API</a> - when enabling APIs <strong>ensure that the correct GCP project is selected</strong>.</p></li></ul><h2>Creating an ETL job</h2><p>As an intermediate datastore where the webshop exports the data, we will use Google Storage and use Google Cloud Functions to transform it before loading it into QuestDB.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AVfP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AVfP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 424w, https://substackcdn.com/image/fetch/$s_!AVfP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 848w, https://substackcdn.com/image/fetch/$s_!AVfP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 1272w, https://substackcdn.com/image/fetch/$s_!AVfP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AVfP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png" width="880" height="161" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:161,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7073,&quot;alt&quot;:&quot;Workflow&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Workflow" title="Workflow" srcset="https://substackcdn.com/image/fetch/$s_!AVfP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 424w, https://substackcdn.com/image/fetch/$s_!AVfP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 848w, https://substackcdn.com/image/fetch/$s_!AVfP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 1272w, https://substackcdn.com/image/fetch/$s_!AVfP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb04af64-d1ec-4aba-a0c5-5f181d50c46f_880x161.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>We won't be building a webshop or a data exporter for an existing webshop, but we will use a script to simulate the export to Google Storage.</p><p>In the following sections, we will set up the necessary components on GCP. Ensure the required APIs mentioned in the prerequisites are enabled, and that you have selected the GCP project in which you would like to create the tutorial resources.</p><h3>Create a Compute Engine instance for QuestDB</h3><p>First things first, we start with installing QuestDB on a virtual machine. To get started, navigate to the <a href="https://console.cloud.google.com/compute/instances">Compute Engine console</a>. Visiting this page for the first time will take a few moments to initialize. After the loading indicator has gone, start a new virtual machine:</p><ol><li><p>Click on "create" and give the instance the name <code>questdb-vm</code></p></li><li><p>Select a region close to you</p></li><li><p>Select the first generation "N1" series</p></li><li><p>Choose the <code>f1-micro</code> machine type - in a production environment you would choose a more performant instance, but for tutorial purposes, this is enough</p></li><li><p>In the "Container section, check "Deploy a container image to this VM instance", and type "questdb/questdb:latest" for the "Container image"</p></li><li><p>In the "Firewall" section, click on "Management, security, disks, networking, sole tenancy"</p></li><li><p>In the newly appeared panel, select "Networking" and add <code>questdb</code> as a "Network tag"</p></li><li><p>Leave all other settings with their defaults, and click on "create"</p></li></ol><p>Make sure you note the "External IP" of the instance as we will need that later.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ynNv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ynNv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 424w, https://substackcdn.com/image/fetch/$s_!ynNv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 848w, https://substackcdn.com/image/fetch/$s_!ynNv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 1272w, https://substackcdn.com/image/fetch/$s_!ynNv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ynNv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png" width="880" height="143" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/17927e72-916a-4d85-b754-881d89ada9f7_880x143.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:143,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7283,&quot;alt&quot;:&quot;Virtual Machines&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Virtual Machines" title="Virtual Machines" srcset="https://substackcdn.com/image/fetch/$s_!ynNv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 424w, https://substackcdn.com/image/fetch/$s_!ynNv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 848w, https://substackcdn.com/image/fetch/$s_!ynNv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 1272w, https://substackcdn.com/image/fetch/$s_!ynNv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17927e72-916a-4d85-b754-881d89ada9f7_880x143.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>After a short time, the new instance will be up and running. As soon as the instance is provisioned, we can initiate a remote session to install QuestDB by clicking <strong>ssh</strong> in the VM panel.</p><h3>Allow networking on the instance</h3><p>If we try to open the web console by opening the <code>http://&lt;EXTERNAL_IP&gt;:9000</code> (where <code>&lt;EXTERNAL_IP&gt;</code> is the external IP of your virtual machine) it won't load and we will face a timeout. The reason behind this is that the firewall is not opened for port <code>9000</code> yet.</p><p>To allow port <code>9000</code> used by QuestDB, we must allow the port by adding a new firewall rule on the <a href="https://console.cloud.google.com/networking/firewalls/list">firewall console</a>:</p><ol><li><p>Click on "create firewall rule" at the top of the page</p></li><li><p>Give the rule the name "QuestDBPorts"</p></li><li><p>In the "Target tags" field, write the same tag used for instance creation (<code>questdb</code>)</p></li><li><p>For the "Source IP ranges" field, set <code>0.0.0.0/0</code></p></li><li><p>In the "Protocols and ports" section, select <strong>tcp</strong> and set port to <code>9000,8812</code></p></li><li><p>Click on "create"</p></li></ol><p>Some seconds later, the rule will be applied on every instance with the matching <code>questdb</code> tag and port <code>9000</code> will be open. You may ask what port <code>8812</code> is for; this port will be used by the Cloud Function later to connect to the database.</p><p>If you try to open the interactive console again, you should see the <a href="https://questdb.io/docs/reference/web-console/">QuestDB Web Console</a> and start writing queries.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bpYS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bpYS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 424w, https://substackcdn.com/image/fetch/$s_!bpYS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 848w, https://substackcdn.com/image/fetch/$s_!bpYS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 1272w, https://substackcdn.com/image/fetch/$s_!bpYS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bpYS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png" width="880" height="246" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:246,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3041,&quot;alt&quot;:&quot;QuestDB Console&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="QuestDB Console" title="QuestDB Console" srcset="https://substackcdn.com/image/fetch/$s_!bpYS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 424w, https://substackcdn.com/image/fetch/$s_!bpYS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 848w, https://substackcdn.com/image/fetch/$s_!bpYS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 1272w, https://substackcdn.com/image/fetch/$s_!bpYS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fb9c1f1-1deb-4d51-a6c3-4a7aa16baa52_880x246.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>As our first query, create the table in which the Cloud Function will write the anonymized data. To create the table run the following SQL statement:</p><pre><code>CREATE TABLE
    purchases(buyer STRING, item_id INT, quantity INT, price INT, purchase_date TIMESTAMP)
    timestamp(purchase_date);
</code></pre><p>The query above uses <code>timestamp(purchase_date)</code> to set a designated timestamp on the table so we can easily perform time series analysis in QuestDB.<br>For more information on designated timestamps, see the official <a href="https://questdb.io/docs/concept/designated-timestamp/">QuestDB documentation for timestamp</a>.</p><h3>Create a Storage bucket</h3><p>Now, we create the bucket where we will store the simulated webshop data. Storage buckets are in a single global namespace in GCP, which means that the bucket's name must be unique across all GCP customers. You can read more about Storage and buckets on Google's <a href="https://cloud.google.com/storage/docs">documentation</a> site.</p><p>To create a new bucket:</p><ol><li><p>Navigate to the <a href="https://console.cloud.google.com/storage">cloud storage console</a></p></li><li><p>Select your project if not selected yet</p></li><li><p>Click on "create bucket" and choose a unique name</p></li><li><p>Select the same region as the instance above</p></li><li><p>Leave other settings on default and click on "continue" to create the bucket</p></li></ol><p>If you successfully created the bucket, it should show up in the storage browser as you can see below.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4Z4-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4Z4-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 424w, https://substackcdn.com/image/fetch/$s_!4Z4-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 848w, https://substackcdn.com/image/fetch/$s_!4Z4-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 1272w, https://substackcdn.com/image/fetch/$s_!4Z4-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4Z4-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png" width="880" height="110" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d22136ae-8093-4c89-b2f4-4f0013864390_880x110.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:110,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7190,&quot;alt&quot;:&quot;GCP Storage Buckets&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GCP Storage Buckets" title="GCP Storage Buckets" srcset="https://substackcdn.com/image/fetch/$s_!4Z4-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 424w, https://substackcdn.com/image/fetch/$s_!4Z4-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 848w, https://substackcdn.com/image/fetch/$s_!4Z4-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 1272w, https://substackcdn.com/image/fetch/$s_!4Z4-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd22136ae-8093-4c89-b2f4-4f0013864390_880x110.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>At this point, we don't set any permissions, ACLs, or visibility settings on the bucket, but we will come back to that later.</p><h3>Create a Cloud Function</h3><p>We have the bucket to upload the data, but we have nothing to process the data yet, and for this, we will use Cloud Functions to remove the PII.</p><p>Cloud Functions are functions as a service (FaaS) solution within GCP, similar to AWS Lambda. The functions are triggered by an event that can come from various sources. Our scenario Cloud Functions are convenient since we don't need to pay for a server to run all day, which is mostly idle; the function will be executed when the trigger event is fired, and we only pay for the execution time the number of function calls.</p><p>To create a Cloud Function:</p><ol><li><p>Navigate to <a href="https://console.cloud.google.com/functions/list">cloud functions console</a></p></li><li><p>Click on "create function" and give it the name <code>remove-pii</code></p></li><li><p>Select the region we are using for other resources</p></li><li><p>For "Trigger" select "Cloud Storage" from the dropdown list</p></li><li><p>Set the event type to "Finalise/Create"</p></li><li><p>Choose the bucket created above and click "variables, networking, and advanced settings"</p></li><li><p>Select "environment variables" on the tabbed panel</p></li><li><p>Click on "add variable" right below the "Runtime environment variables" section</p></li><li><p>Add a new variable called <code>DATABASE_URL</code> with the value <code>postgresql://admin:quest@&lt;EXTERNAL_IP&gt;:8812/qdb</code>, where <code>&lt;EXTERNAL_IP&gt;</code> is the external IP of your virtual machine</p></li><li><p>Click "save" then "next"</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lk1l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lk1l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 424w, https://substackcdn.com/image/fetch/$s_!lk1l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 848w, https://substackcdn.com/image/fetch/$s_!lk1l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 1272w, https://substackcdn.com/image/fetch/$s_!lk1l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lk1l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png" width="880" height="628" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:628,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14536,&quot;alt&quot;:&quot;Cloud Function Creation&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Cloud Function Creation" title="Cloud Function Creation" srcset="https://substackcdn.com/image/fetch/$s_!lk1l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 424w, https://substackcdn.com/image/fetch/$s_!lk1l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 848w, https://substackcdn.com/image/fetch/$s_!lk1l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 1272w, https://substackcdn.com/image/fetch/$s_!lk1l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70379518-8ece-4003-9418-fd6fb460e0b2_880x628.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The next step is to select the runtime our function will use and provide the code. On this page, we can choose between numerous runtimes, including multiple versions of Python, NodeJS, Go, Ruby, and even Java.</p><p>Since this tutorial uses Python, select <strong>Python 3.8</strong> as it is the latest non-beta version at the time of writing. Leave the rest of the settings as default, and write the function in the next section. Click "deploy" at the bottom of the page. Some seconds later, you will see that the deployment of the function is in progress.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VqVe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VqVe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 424w, https://substackcdn.com/image/fetch/$s_!VqVe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 848w, https://substackcdn.com/image/fetch/$s_!VqVe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 1272w, https://substackcdn.com/image/fetch/$s_!VqVe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VqVe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png" width="880" height="165" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:165,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10167,&quot;alt&quot;:&quot;List of Cloud Functions&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="List of Cloud Functions" title="List of Cloud Functions" srcset="https://substackcdn.com/image/fetch/$s_!VqVe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 424w, https://substackcdn.com/image/fetch/$s_!VqVe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 848w, https://substackcdn.com/image/fetch/$s_!VqVe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 1272w, https://substackcdn.com/image/fetch/$s_!VqVe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd831caf6-91fb-475e-a989-b1d01740c9ef_880x165.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The deployment may take a while, so we can move on to the next section of the tutorial.</p><h2>Generating and processing data</h2><p>Before moving on, here's a quick recap on what we did so far:</p><ul><li><p>Set up a new Google Storage bucket</p></li><li><p>Created the Cloud Function which will process the data later on</p></li><li><p>Connected the bucket with the function to trigger on a new object is created on the bucket</p></li></ul><p>Now for the fun part of the tutorial: writing the data processing script and loading the data in the database. Let's write the function to remove PII, but first, talk a bit about the data's structure.</p><h3>Inspect the data structure</h3><p>ETL jobs, by their nature, heavily depend on the structure of incoming data. A job may process multiple data sources, and data structure can vary per source.<br>The data structure we will use is simple. We have a CSV file with the following information:</p><ul><li><p>purchase date</p></li><li><p>email address</p></li><li><p>purchased item's ID</p></li><li><p>quantity</p></li><li><p>the price per item</p></li></ul><p>As you see, there is no currency column since we will assume every price is in one currency.</p><p>To generate random data, you can use the <a href="https://github.com/gabor-boros/questdb-etl-jobs/blob/e47b5f3191c8648f486cea207b317c92899c3bd1/data_generator.py">pre-made script</a> written for this tutorial</p><h3>Create the data transformer function</h3><p>By now, we have everything to write the data transformer function, connect the dots and try out the PII removal.</p><p>We will work in the "inline editor" of the cloud function, so as a first step, open the edit the cloud function created above by navigating to the <a href="https://console.cloud.google.com/functions/list">cloud functions console</a> and clicking on the function's name. That will open the details of the function. At the top, click on "edit", then at the bottom, click on "next" to open the editor.</p><p>Let's start with the requirements. On the left-hand side, click on the <code>requirements.txt</code> and paste the following:</p><pre><code>google-cloud-storage==1.36.2
psycopg2==2.8.6
sqlalchemy==1.4.2
</code></pre><p>Here we add the required packages to connect to Google Storage and QuestDB. Next, click on <code>main.py</code>, remove its whole content and start adding the following:</p><pre><code>import csv
import hashlib
import json
import logging
import os
from dataclasses import dataclass
from datetime import datetime
from typing import List

from google.cloud import storage
from sqlalchemy.sql import text
from sqlalchemy.engine import Connection, create_engine

logger = logging.getLogger(__name__)

# Create a database engine
engine = create_engine(os.getenv("DATABASE_URL"))

# ...
</code></pre><p>As you may expect, we start with the imports, but we added two extra lines: one for the logger and one for configuring the database engine. We will need the logger to log warnings and exceptions during the execution, while we will use the engine later to insert anonymized data into the database.</p><p>To make our job easier, we are going to add a data class, called <code>Record</code>. This data class will be used to store the parsed and anonymized CSV data for a line of the uploaded file.</p><pre><code># ...

@dataclass
class Record:
    buyer: str
    item_id: int
    quantity: int
    price: int
    purchase_date: datetime

# ...
</code></pre><p>As we discussed, ETL jobs are validating the data that they receive as input. In our case, we will trigger the function if an object is created on the storage. This means any object, like a CSV, PDF, TXT, PNG file, or even a directory, is created, though we only want to execute CSV files' transformation. To validate the incoming data, we write two simple validator functions:</p><pre><code># ...

def is_event_valid(event: dict) -&gt; bool:
    """
    Validate that the event has all the necessary attributes required for the
    execution.
    """

    attributes = event.keys()
    required_parameters = ["bucket", "contentType", "name", "size"]

    return all(parameter in attributes for parameter in required_parameters)


def is_object_valid(event: dict) -&gt; bool:
    """
    Validate that the finalized/created object is a CSV file and its size is
    greater than zero.
    """

    has_content = int(event["size"]) &gt; 0
    is_csv = event["contentType"] == "text/csv"

    return has_content and is_csv

# ...
</code></pre><p>The first function will validate that event has all the necessary parameters, while the second function checks that the object created and triggered the event is a CSV and has any content.<br>The next function we create is used to get an object from the storage which, in our case, the file triggered the event:</p><pre><code># ...

def get_content(bucket: storage.Bucket, file_path: str) -&gt; str:
    """
    Get the blob from the bucket and return its content as a string.
    """

    blob = bucket.get_blob(file_path)
    return blob.download_as_string().decode("utf-8")

# ...
</code></pre><p>Anonymizing the data, in this scenario, is relatively easy, though we need to ensure we can build statistics and visualizations later based on this data, so the anonymized parts should be consistent for a user. To achieve this, we will hash the buyer's email address, so nobody may track it back to the person owning the email, but we can use it for visualization:</p><pre><code># ...

def anonymize_pii(row: List[str]) -&gt; Record:
    """
    Unpack and anonymize data.
    """

    email, item_id, quantity, price, purchase_date = row

    # Anonymize email address
    hashed_email = hashlib.sha1(email.encode()).hexdigest()

    return Record(
        buyer=hashed_email,
        item_id=int(item_id),
        quantity=int(quantity),
        price=int(price),
        purchase_date=purchase_date,
    )

# ...
</code></pre><p>So far, we have functions to validate the data, get the file's content which triggered the Cloud Function, and anonymize the data. The next thing we need to be able to do is to load the data into our database. Up to this point, every function we have wrote was simple, and this one is no exception:</p><pre><code># ...

def write_to_db(conn: Connection, record: Record):
    """
    Write the records into the database.
    """

    query = """
    INSERT INTO purchases(buyer, item_id, quantity, price, purchase_date)
    VALUES(:buyer, :item_id, :quantity, :price, to_timestamp(:purchase_date, 'yyyy-MM-ddTHH:mm:ss'));
    """

    try:
        conn.execute(text(query), **record.__dict__)
    except Exception as exc:
        # If an error occures, log the exception and continue
        logger.exception("cannot write record", exc_info=exc)

# ...
</code></pre><p>As you see, writing to the database is easy. We get the connection and the record we need to write into the database, prepare the query and execute it. In case of an exception, we don't want to block the whole processing, so we catch the exception, log it and let the script go on. If an exception occurred, we can check it later and fix the script or load the data manually.</p><p>The last bit is the glue code, which brings together these functions. Let's have a look at that:</p><pre><code># ...

def entrypoint(event: dict, context):
    """
    Triggered by a creation on a Cloud Storage bucket.
    """

    # Check if the event has all the necessary parameters. In case any of the
    # required parameters are missing, return early not to waste execution time.
    if not is_event_valid(event):
        logger.error("invalid event: %s", json.dumps(event))
        return

    file_path = event["name"]

    # Check if the created object is valid or not. In case the object is invalid
    # return early not to waste execution time.
    if not is_object_valid(event):
        logger.warning("invalid object: %s", file_path)
        return

    storage_client = storage.Client()
    bucket = storage_client.get_bucket(event["bucket"])

    data = get_content(bucket, file_path)
    reader = csv.reader(data.splitlines())

    # Anonymize PII and filter out invalid records
    records: List[Record] = filter(lambda r: r, [anonymize_pii(row) for row in reader])

    # Write the anonymized data to database
    with engine.connect() as conn:
        for record in records:
            write_to_db(conn, record)
</code></pre><p>In the example above, we call the two validators to ensure it worth processing the data, and we get the file path from the event. After that we initialize the client used to connect to Google Storage, then we get the object's content, parse the CSV file, and anonymize the content of it.</p><p>Last but not least, we connect to the database - defined by the DATABASE_URL configured for the engine and write all records to the database one by one.</p><p>As you see, the entrypoint of the function has been changed as well. In the text box called "Entrypoint" set the entrypoint as a function name to call. The entrypoint is the function that will be called by Cloud Functions when an event is triggered.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JvcO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JvcO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 424w, https://substackcdn.com/image/fetch/$s_!JvcO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 848w, https://substackcdn.com/image/fetch/$s_!JvcO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 1272w, https://substackcdn.com/image/fetch/$s_!JvcO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JvcO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png" width="880" height="401" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:401,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:16203,&quot;alt&quot;:&quot;Overview of The Function&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Overview of The Function" title="Overview of The Function" srcset="https://substackcdn.com/image/fetch/$s_!JvcO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 424w, https://substackcdn.com/image/fetch/$s_!JvcO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 848w, https://substackcdn.com/image/fetch/$s_!JvcO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 1272w, https://substackcdn.com/image/fetch/$s_!JvcO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb41d92a9-c62c-4e3f-a63e-321e7db3546a_880x401.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Connecting the services</h2><p>We are close to finishing this tutorial, so it's time to test our Cloud Function.</p><p>To test the Cloud Function:</p><ol><li><p>Download the <a href="https://github.com/gabor-boros/questdb-etl-jobs/blob/e47b5f3191c8648f486cea207b317c92899c3bd1/data_generator.py">pre-made script</a> and run it to generate random data.</p></li><li><p>Navigate to the bucket you created</p></li><li><p>Above the list of objects in the bucket (which should be empty) click on "upload files"</p></li><li><p>Select and upload the random generated data *1</p></li><li><p>After the file is uploaded, go to <a href="https://console.cloud.google.com/functions/list">Cloud Functions console</a></p></li><li><p>Click on the actions button and select "view logs"</p></li><li><p>Calidate that the script did not encounter any issues</p></li><li><p>Navigate to <code>http://&lt;EXTERNAL_IP&gt;:9000</code>, where <code>&lt;EXTERNAL_IP&gt;</code> is the external IP of your virtual machine</p></li></ol><p>We can now execute the following SQL query:</p><pre><code>SELECT * FROM purchases ORDER BY purchase_date;
</code></pre><p>As you can see, the data is loaded and we have no PII there. By creating a simple chart, we can even observe trends in the generated data, how our imaginary buyers purchased items on the webshop.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zwdz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zwdz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 424w, https://substackcdn.com/image/fetch/$s_!zwdz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 848w, https://substackcdn.com/image/fetch/$s_!zwdz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!zwdz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zwdz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg" width="880" height="432" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:432,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53845,&quot;alt&quot;:&quot;QuestDB Console With Stats&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="QuestDB Console With Stats" title="QuestDB Console With Stats" srcset="https://substackcdn.com/image/fetch/$s_!zwdz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 424w, https://substackcdn.com/image/fetch/$s_!zwdz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 848w, https://substackcdn.com/image/fetch/$s_!zwdz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!zwdz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7b9c3f6-4fcd-4202-adc4-dfa0db81574f_880x432.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>*1 QuestDB at the time of writing does not support "out of order" writes. This means you need to upload data with delay to let the previous function finish processing the data. Also, the uploaded purchase data must be in time order and increasing across the uploads. Example: We are uploading <code>data1.csv</code> and <code>data2.csv</code> the last generated purchase data in <code>data1.csv</code> is <code>2021-03-21T11:59:49</code>, therefor <code>data2.csv</code>'s first purchase order must be greater than or equal to <code>2021-03-21T11:59:49</code>.</p><h2>Summary</h2><p>We've installed QuestDB on Google Cloud Platform, set up a Google Storage bucket to store the simulated purchase data exports, built an ETL job that anonymized our buyers' data, and loaded it into a time series database, QuestDB. Data analysts could write more jobs as Cloud Functions in multiple languages and set up multiple sources. Furthermore, this data could be loaded into a business intelligence (BI) dashboard like Power BI to have a more comprehensive overview of the data as it does not contains PII anymore.</p><p><em>The source code is available at <a href="https://github.com/gabor-boros/questdb-etl-jobs">https://github.com/gabor-boros/questdb-etl-jobs</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Monitoring the uptime of an application with Python, Nuxt.js and QuestDB]]></title><description><![CDATA[Highly available services that serve millions of requests rely on the visibility of the system status for customers and internal teams.]]></description><link>https://www.expatgeekdiaries.com/p/monitoring-the-uptime-of-an-application-with-python-nuxt-js-and-questdb</link><guid isPermaLink="false">https://www.expatgeekdiaries.com/p/monitoring-the-uptime-of-an-application-with-python-nuxt-js-and-questdb</guid><dc:creator><![CDATA[Gábor Boros]]></dc:creator><pubDate>Wed, 13 Jan 2021 00:01:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/458bcf20-5010-4006-b364-04b1ff34746c_2000x1333.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a72X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a72X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 424w, https://substackcdn.com/image/fetch/$s_!a72X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 848w, https://substackcdn.com/image/fetch/$s_!a72X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!a72X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a72X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Monitoring the uptime of an application with Python, Nuxt.js and QuestDB&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" title="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" srcset="https://substackcdn.com/image/fetch/$s_!a72X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 424w, https://substackcdn.com/image/fetch/$s_!a72X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 848w, https://substackcdn.com/image/fetch/$s_!a72X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!a72X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcf63be6-1b75-4fb5-b9ec-4cbf189e1dca_2000x1333.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>Highly available services that serve millions of requests rely on the visibility of the system status for customers and internal teams. This tutorial shows how a lightweight and performant time-series database coupled with queued status checks and a simple UI are key ingredients for robust application monitoring.</p><h2>Why build a status page for an application?</h2><p>Even if we design the most reliable systems, incidents will occur for hard-to-predict reasons. It&#8217;s critical to provide as much information as possible to users, customers, and service teams. The most convenient way to display this is through a status page.</p><p>Although the page&#8217;s responsibility is to provide information, it can reduce the support team&#8217;s load and eliminate duplicate support tickets. Status pages are a crucial part of incident management, and usually, other teams enjoy benefits like client and service owners when they need to refer to SLAs. In this tutorial, I&#8217;ll show you how to build a simple yet powerful status page that scores well on performance and design.</p><h2>What we will build</h2><h3>Overview</h3><p>As mentioned above, we will build a simple status page made of two parts: the backend monitors our service, and a frontend shows our services&#8217; status on an hourly scale.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mE9j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mE9j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 424w, https://substackcdn.com/image/fetch/$s_!mE9j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 848w, https://substackcdn.com/image/fetch/$s_!mE9j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 1272w, https://substackcdn.com/image/fetch/$s_!mE9j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mE9j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Monitoring the uptime of an application with Python, Nuxt.js and QuestDB&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" title="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" srcset="https://substackcdn.com/image/fetch/$s_!mE9j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 424w, https://substackcdn.com/image/fetch/$s_!mE9j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 848w, https://substackcdn.com/image/fetch/$s_!mE9j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 1272w, https://substackcdn.com/image/fetch/$s_!mE9j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cd18f3a-3a2d-4584-947a-7e6ccc8f8be3_2308x481.png 1456w" sizes="100vw"></picture><div></div></div></a></figure></div><p>You will need some experience in Python, JavaScript, and basic SQL knowledge. To build our service, we will use FastAPI, an ultra-fast Python web framework, Celery for scheduling monitoring tasks, QuestDB, the fastest open-source time-series database, to store monitoring results, and NuxtJs to display them.</p><p>There&#8217;s a lot to learn, so let&#8217;s jump right in!</p><h3>Prerequisites</h3><p>You will need to have the following installed on your machine:</p><ul><li><p>Python 3.8</p></li><li><p>NodeJS 14+</p></li><li><p>Docker</p></li><li><p>Docker Compose</p></li></ul><h2>Setting up the environment</h2><h3>Create a new project</h3><p>First things first, we create a directory, called <code>status-page</code>, this is our project root. We also need to create another directory called <code>app</code> which will contain the backend code. After following these steps, you should have a project structure like this.</p><pre><code>status-page (project root)
&#9492;&#9472;&#9472; app (backend service directory)</code></pre><h3>Installing QuestDB &amp; Redis</h3><p>Now, we will install QuestDB and Redis. QuestDB is used to store the HTTP status and the service state of our application over time, and Redis is used as a message broker between the backend application and the workers who will do the scheduled monitoring.</p><p>To install these services, we will use Docker and Docker Compose. We are going to create a <code>docker-compose.yml</code> file within the project root with the following content:</p><pre><code>version: '3'

volumes:
  questdb_data: {}

services:
  redis:
    image: 'redis:latest'
    ports:
      - '6379:6379'

  questdb:
    image: 'questdb/questdb:latest'
    volumes:
      # Map QuestDB's data directory to the host
      - 'questdb_data:/root/.questdb/db'
    ports:
      - '9000:9000'
      - '8812:8812'</code></pre><p>Voila! When we run <code>docker-compose up</code>, QuestDB and Redis start, and we can access QuestDB's interactive console on <a href="http://127.0.0.1:9000/">http://127.0.0.1:9000</a>.</p><h3>Install backend dependencies</h3><p>Now, we have the project structure, and we can run the required services, so we need to set up our backend service to collect data about the website or service we would like to monitor. We will use poetry to manage Python dependencies during this tutorial, so let&#8217;s start by installing that.</p><pre><code>$ pip install poetry</code></pre><p>To define the project requirements, create a <code>pyproject.toml</code> file with the following content:</p><pre><code>[tool.poetry]
name = "status-page"
version = "0.1.0"
description = "QuestDB tutorial for creating a simple status page."
authors = ["Your name &lt;your.email@example.com&gt;"]
license = "MIT"

[tool.poetry.dependencies]
python = "3.8"

[build-system]
requires = ["poetry-core&gt;=1.0.0"]
build-backend = "poetry.core.masonry.api"</code></pre><p>Install the project dependencies by executing the following:</p><pre><code>$ poetry add python fastapi pydantic uvicorn requests \
 psycopg2-binary "databases[postgresql]" "celery[redis]"</code></pre><p>As you may assume by checking the requirements, we will use QuestDB&#8217;s Postgres interface to connect. When <code>poetry</code> finishes its job, it will add the dependencies to <code>pyproject.toml</code> and we can now start to implement the backend service.</p><h2>Create a simple API</h2><p>The time has come, let&#8217;s create the backend service, but step-by-step. Within the <code>app</code> directory, create an <code>__init__.py</code> and <code>main.py</code>. The first one is responsible for making the <code>app</code> directory to a package, while the latter will define the APIs our service exposes. Open <code>main.py</code> for edit and add the following:</p><pre><code># main.py

from fastapi import FastAPI

app = FastAPI(
    title="Status Page",
    description="This service gives back the status of the configured URL.",
    version="0.1.0",
)</code></pre><p>Congratulations! You just created the backend service. You can go and try it out by executing:</p><pre><code>$ poetry run uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
INFO: Uvicorn running on http://127.0.0.1:8000
...
INFO: Application startup complete.</code></pre><p>Although the service does nothing yet, it works and listens for any code change. Add a new endpoint and watch it reload:</p><pre><code># main.py

# ...

@app.get(path="/signals", tags=["Monitoring"])
async def get_signals():
    return {}</code></pre><p>We have now created an API endpoint that will serve the system status data of the monitored URL. If you open <a href="http://127.0.0.1:8000/redoc">http://127.0.0.1:8000/redoc</a>, you can see the generated documentation for the endpoint, or you can check it working at <a href="http://127.0.0.1:8000/signals">http://127.0.0.1:8000/signals</a>, though it won't return any data yet.</p><p>It is time to have fun, we are going to integrate QuestDB with our shiny new backend service.</p><h2>Integrate QuestDB with FastAPI</h2><p>Integrating QuestDB with FastAPI is easier than you think. Thanks to QuestDB&#8217;s <a href="https://questdb.io/docs/develop/connect">Postgres compatibility</a>, you can use any standard or popular third-party libraries of any programming language which implements Postgres wire protocol.</p><h3>Set up the table</h3><p>The very first step is to create the table in QuestDB. As said before, our approach is simple, so that the table is simple, too. QuestDB is running from our docker compose script so, we open the interactive console at <a href="http://127.0.0.1:9000/">http://127.0.0.1:9000</a> and create a new table by running the following query:</p><pre><code>CREATE TABLE
    signals(url STRING, http_status INT, received TIMESTAMP, available BOOLEAN)
    timestamp(received);</code></pre><p>The query executes, and after refreshing the table list on the left, you can see the table we created.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Wt15!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Wt15!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 424w, https://substackcdn.com/image/fetch/$s_!Wt15!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 848w, https://substackcdn.com/image/fetch/$s_!Wt15!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 1272w, https://substackcdn.com/image/fetch/$s_!Wt15!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Wt15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Monitoring the uptime of an application with Python, Nuxt.js and QuestDB&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" title="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" srcset="https://substackcdn.com/image/fetch/$s_!Wt15!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 424w, https://substackcdn.com/image/fetch/$s_!Wt15!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 848w, https://substackcdn.com/image/fetch/$s_!Wt15!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 1272w, https://substackcdn.com/image/fetch/$s_!Wt15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff09c4245-4ec7-40a7-94d1-42fd7c2db524_3114x678.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>Connect QuestDB and FastAPI</h3><p>As we have the table in the database, it is time to connect to QuestDB and query some data to return through the API. To connect, we will use the Postgres interface of QuestDB and SQLAlchemy to connect to it.</p><p>To be able to reuse the engine later on, create a new file in the <code>app</code> package which is responsible for defining how to connect and name it <code>db.py</code>:</p><pre><code># db.py

from sqlalchemy import create_engine

engine = create_engine(
    "postgresql://admin:quest@127.0.0.1:8812/qdb", # Use a the default credentials
    pool_size=5, # Set pool size greater than 1 to not block async requests
    pool_pre_ping=True # Set pre-ping to ensure a connection is opened when sending a query
)</code></pre><p>To set up a schema that represents the table in the database, create a <code>models.py</code> containing the schema definition:</p><pre><code># models.py

from datetime import datetime
from pydantic import BaseModel, Schema


class Signal(BaseModel):
    url: str = Schema(..., description="The monitored URL")
    http_status: int = Schema(..., description="HTTP status code returned by upstream")
    available: bool = Schema(..., description="Represents the service availability")
    received: datetime = Schema(..., description="Timestamp when the signal received")</code></pre><p>Let&#8217;s stop here for a moment and talk through what we did in the last steps:</p><ul><li><p>set up the API which will serve the requests coming from the frontend</p></li><li><p>created a table in QuestDB for our status records and provided connection credentials for Postgres wire</p></li><li><p>implemented the schema which is used to serialize the results returned by the database</p></li></ul><p>The next step is to initiate a connection and return the results from the database. First, import the <code>engine</code> and <code>Signal</code> schema and then extend the function which serves the <code>/signals</code> endpoint:</p><pre><code># main.py

# Other imports ...
from app.db import engine
from app.models import Signal

from typing import List
from pydantic import BaseModel

class SignalResponse(BaseModel):
    url: str
    records: List[Signal]</code></pre><p>After adding the <code>defaultdict</code> import, the implementation of the <code>/signals</code> endpoint should look like this:</p><pre><code># main.py

# Other imports ...
from collections import defaultdict

# ...

@app.get(path="/signals", response_model=List[SignalResponse], tags=["Monitoring"])
async def get_signals(limit: int = 60):
    
    # A simple query to return every record belongs to the website we will monitor
    query = f"""
    SELECT * FROM signals
    WHERE url = 'https://questdb.io' ORDER BY received DESC LIMIT {limit};
    """

    signals = defaultdict(list)

    with engine.connect() as conn: # connect to the database
        for result in conn.execute(query): # execute the SELECT query
            signal = Signal(**dict(result)) # parse the results results returned by QuestDB
            signals[signal.url].append(signal) # add every result per URL

    # Return the response which is validated against the `response_model` schema
    return [
        SignalResponse(url=url, records=list(reversed(records)))
        for url, records in signals.items()
    ]</code></pre><p>Let&#8217;s recap on our code above, starting from the top:</p><ul><li><p>we added <code>defaultdict</code> import (we'll explain that later)</p></li><li><p>extended the function decorator to use <code>response_model=List[SignalResponse]</code>, the response model we defined already</p></li><li><p>changed the function signature to include a <code>limit</code> parameter and set its default value to <code>60</code> since we will monitor HTTP status every minute</p></li><li><p>select the records from the database and prepare a dictionary for the parsed <code>Signal</code>s.</p></li></ul><p>You may ask why to group the returned records per URL. Although we will monitor only one URL for the sake of simplicity, I challenge you to change the implementation later and explore QuestDB to handle the monitoring of multiple URLs.</p><p>In the following lines, we are connecting to the database, executing the query, and populates the dictionary, which we will use in the last four lines to construct the <code>SignalResponse</code>. Our version of <code>main.py</code> at this point looks like the following:</p><pre><code># main.py

from collections import defaultdict
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel

from app.db import engine
from app.models import Signal


# Add a response model to indicate the structure of the signals API response.
class SignalResponse(BaseModel):
    url: str
    records: List[Signal]

      
app = FastAPI(
    title="Status Page",
    description="This service gives back the status of the configured URL.",
    version="0.1.0",
)

@app.get(path="/signals", response_model=List[SignalResponse], tags=["Monitoring"])
async def get_signals(limit: int = 60):
    
    # A simple query to return every record belongs to the website we will monitor
    query = f"""
    SELECT * FROM signals
    WHERE url = 'https://questdb.io' ORDER BY received DESC LIMIT {limit};
    """

    signals = defaultdict(list)

    with engine.connect() as conn: # connect to the database
        for result in conn.execute(query): # execute the SELECT query
            signal = Signal(**dict(result)) # parse the results results returned by QuestDB
            signals[signal.url].append(signal) # add every result per URL

    # Return the response which is validated against the `response_model` schema
    return [
        SignalResponse(url=url, records=list(reversed(records)))
        for url, records in signals.items()
    ]</code></pre><h2>Schedule monitoring tasks</h2><p>For scheduling the monitoring task, we will use Celery Beat, the built-in periodic task scheduler implementation of Celery.</p><h3>Scheduling with Celery</h3><p>Before we schedule any task, we need to configure Celery. In the <code>app</code> package, create a new <code>celery.py</code> which will contain the Celery and beat schedule configuration. Import <code>Celery</code> for creating tasks, and <code>crontab</code> for constructing Unix-like crontabs for our tasks. The task is the dotted path representation of the function which is executed by Celery (<code>app.tasks.monitor</code>) and sent to queues handled by Redis.</p><p>The only thing left is to configure the beat schedule, which is a simple dictionary. We give a name for the schedule, define the dotted path pointing to the task (function), and specify the schedule itself:</p><pre><code># celery.py

from celery import Celery
from celery.schedules import crontab

MONITORING_TASK = "app.tasks.monitor"

celery_app = Celery("tasks", broker="redis://localhost:6379/0")

# Set a queue for task routes
celery_app.conf.task_routes = {
  MONITORING_TASK: "main-queue"
}

# Schedule the monitoring task
celery_app.conf.beat_schedule = {
    "monitor": { # Name of the schedule
        "task": MONITORING_TASK, # Register the monitoring task
        "schedule": crontab(
            minute=f"*/1" # Run the task every minute
        ),
    }
}</code></pre><h3>Create a monitoring task</h3><p>And the last part: creating the monitoring task. In the previous section, we talked about the &#8220;monitoring task&#8221; multiple times, but we didn&#8217;t see the concrete implementation.</p><p>In this final backend related section, you will implement the task which will check the availability of the desired website or service and saves the results as records in QuestDB. The monitoring task is a simple <code>HTTP HEAD</code> request and saving the response to the database. We see the implementation in pieces of the <code>tasks.py</code> referenced in celery as the dotted path before.</p><p>First, we start with imports:</p><pre><code># tasks.py

from datetime import datetime

import requests

from app.celery import celery_app
from app.db import engine
from app.models import Signal

# ...</code></pre><p>We import <code>celery_app</code> which represents the Celery application, an <code>engine</code> to save the results in the database, and finally <code>Signal</code> to construct the record we will save. As the necessary imports are in place, we can define the <code>monitor</code> task.</p><pre><code># tasks.py

# ...

@celery_app.task # register the function as a Celery task
def monitor():
    try:
        response = requests.head("https://questdb.io")
    except Exception as exc: # handle any exception which may occur due to connection errors
        query = f"""
            INSERT INTO signals(received,url,http_status,available)
            VALUES(systimestamp(), 'https://questdb.io', -1, False);
            """

        # Open a connection and execute the query
        with engine.connect() as conn:
            conn.execute(query)

        # Re-raise the exception to not hide issues
        raise exc

# ...</code></pre><p>As you can see, we send a request to the desired website and store the response for later use. In case the website is down and unreachable, an exception will be raised by requests or any underlying packages. As we need to log that the request does not finish, we catch the exception, save a record in the database, and re-raise the exception to not hide anything. Next, we construct a signal to save.</p><pre><code># ...

@celery_app.task
def monitor():
    # ...

    signal = Signal(
        url="https://questdb.io",
        http_status=response.status_code,
        received=datetime.now(),
        available=response.status_code &gt;= 200 and response.status_code &lt; 400,
    )
    
    # ...</code></pre><p>We don&#8217;t do anything special here, though the following step is more interesting: inserting the result in the database. Finally, we prepare and execute the query based on the <code>signal</code>.</p><pre><code># ...

@celery_app.task
def monitor():
    # ...
    
    query = f"""
    INSERT INTO signals(received,url,http_status,available)
    VALUES(systimestamp(), '{signal.url}', {signal.http_status}, {signal.available});
    """

    with engine.connect() as conn:
        conn.execute(query)</code></pre><p>Congratulations! You just arrived at the last part of the backend service implementation. We did many things and built a service that can periodically check the website&#8217;s status, save it in the database, and expose the results through an API.</p><p>The very last thing we need to address is to allow connections initiated by the frontend later on. As it will run on localhost:3000 and we don&#8217;t use domain names, the port is different hence all requests will be rejected with errors related to <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Cross-Origin Resource Sharing</a>.</p><p>To address this issue, add the following middleware to the application which will allow us to connect at <a href="http://localhost:3000/">http://localhost:3000</a>:</p><pre><code># main.py

# Other imports ...
from fastapi.middleware.cors import CORSMiddleware

# app = FastAPI ...

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ...</code></pre><h2>Implement the frontend</h2><h3>Setting up frontend</h3><p>To build the frontend, we will use Nuxt.js. We will use <code>yarn</code> to set up the starter project by running <code>yarn</code> and selecting the answers detailed below.</p><pre><code>$ yarn create nuxt-app frontend

[...]

? Project name: frontend
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Tailwind CSS
? Nuxt.js modules: Axios
? Linting tools: (Press &lt;space&gt; to select, &lt;a&gt; to toggle all, &lt;i&gt; to invert selection)
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: (Press &lt;space&gt; to select, &lt;a&gt; to toggle all, &lt;i&gt; to invert selection)
? What is your GitHub username? gabor-boros
? Version control system: Git</code></pre><p>The project root now looks like this:</p><pre><code>status-page
&#9500;&#9472;&#9472; app/
&#9500;&#9472;&#9472; frontend/
&#9500;&#9472;&#9472; docker-compose.yml
&#9500;&#9472;&#9472; poetry.lock
&#9492;&#9472;&#9472; pyproject.toml</code></pre><h3>Cleaning up generated project</h3><p>Since we don&#8217;t need any styling delivered by the project generation, we need to get rid of them. Open <code>frontend/layouts/default.vue</code> and replace its content with:</p><pre><code>&lt;!-- frontend/layouts/default.vue --&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;Nuxt /&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><p>Now, we will change <code>frontend/pages/index.vue</code> and call the backend service. Let's begin with <code>&lt;scripts&gt;</code>.</p><pre><code>&lt;!-- frontend/pages/index.vue --&gt;

&lt;!-- template ... --&gt;

&lt;script&gt;
const LIMIT = 60;
async function fetchSignals($axios, limit) {
  const signals = await $axios.$get(`http://localhost:8000/signals?limit=${limit}`)
  return signals
}
export default { 
  data() {
    return {
      signals: []
    }
  },
  // Handle initial call
  async asyncData({ $axios }) {
    const signals = await fetchSignals($axios, LIMIT)
    return { signals }
  },
  methods: {
    // Calculate uptime based on the signals belongs to a URL
    uptime: (records) =&gt; {
      let availableRecords = records.filter(record =&gt; record.available).length;
      return ((availableRecords / records.length) * 100).toFixed(2)
    }
  },
  // Set up periodic calls when the component is mounted
  mounted() {
    let that = this
    const axios = this.$axios;
    setInterval(() =&gt; {
      Promise.resolve(fetchSignals(axios, LIMIT)).then((signals) =&gt; {
        that.signals = signals
      });
    }, 1000 * LIMIT);
  }
}
&lt;/script&gt;</code></pre><p>At the first sight, it might look a lot, but if we check the most important parts in pieces everything will be crystal clear.</p><p>We define <code>fetchSignals</code> to reduce code duplication later on. Then, we set up initial <code>signals</code> data, where we will store the periodically fetched responses returned by the backed. After that, as part of <code>asyncData</code>, we initiate an async call towards the backend to get the initial signals to show.</p><p>The last part is to define a periodic call to the backend when the component is <code>mounted</code>. Right, we have the logic which will call backend and keep the data up to date. Now we have to display the results.</p><pre><code>&lt;!-- frontend/pages/index.vue --&gt;

&lt;template&gt;
  &lt;div class="p-8 h-screen text-center"&gt;
    &lt;h1 class="text-2xl font-light"&gt;QuestDB website status&lt;/h1&gt;
    &lt;h2 class="text-lg font-thin"&gt;service uptime in the past 60 minutes&lt;/h2&gt;

    &lt;div class="h-8 mt-12 flex justify-center" v-if="signals.length &gt; 0"&gt;
      
      &lt;!-- Iterate over the signals belongs to records --&gt;
      &lt;div class="w-1/4" v-for="(signal, s) in signals" :key="s"&gt;
        &lt;div class="flex mb-1 text-sm"&gt;
          &lt;p class="flex-1 text-left font-normal"&gt;{{ signal.url }}&lt;/p&gt;
          &lt;p class="flex-1 text-right font-thin"&gt;{{ uptime(signal.records) }}% uptime&lt;/p&gt;
        &lt;/div&gt;
        &lt;div class="grid grid-flow-col auto-cols-max gap-x-1"&gt;
          
          &lt;!-- Draw a green or yellow bar depending on service availability --&gt;
          &lt;div 
            v-for="(signal, r) in signal.records"
            :key="r"
            :class="`w-1 bg-${signal.available ? 'green' : 'yellow'}-700`"
          &gt;&amp;nbsp;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- In case we have no records yet, show an informative message --&gt;
    &lt;div v-else&gt;
      &lt;p&gt;No signals found&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;!-- scripts ... --&gt;</code></pre><h2>Run the project</h2><p>We reached the end of the tutorial. We have both the backend and the frontend. It is time to try everything out. Run the following commands in different shells from the project root:</p><pre><code># Shell 1 - Start the application
$ docker-compose up -d
$ poetry run uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload

# Shell 2 - Start the worker process
$ poetry run celery --app=app.tasks worker --beat -l info -Q main-queue -c 1

# Shell 3 - Start the frontend
$ cd frontend
$ yarn dev</code></pre><p>Navigate to <a href="http://localhost:3000/">http://localhost:3000</a> to see the backend reporting the status of the monitored URL. The first task to check the system status is executed when the scheduler and worker starts and the status of the website over time can be seen after a few minutes on the page or when you check back at a later stage:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UeaB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UeaB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 424w, https://substackcdn.com/image/fetch/$s_!UeaB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 848w, https://substackcdn.com/image/fetch/$s_!UeaB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 1272w, https://substackcdn.com/image/fetch/$s_!UeaB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UeaB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:null,&quot;width&quot;:null,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Monitoring the uptime of an application with Python, Nuxt.js and QuestDB&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" title="Monitoring the uptime of an application with Python, Nuxt.js and QuestDB" srcset="https://substackcdn.com/image/fetch/$s_!UeaB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 424w, https://substackcdn.com/image/fetch/$s_!UeaB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 848w, https://substackcdn.com/image/fetch/$s_!UeaB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 1272w, https://substackcdn.com/image/fetch/$s_!UeaB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa48f1d9-ff64-41ae-9e77-89bc3ef65786_3116x731.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Summary</h2><p>We&#8217;ve successfully built a pretty status page that can be publicly-visible to users or used for internal teams to monitor an application&#8217;s uptime. We&#8217;ve learned how to queue and schedule tasks and store the responses in a time-series database and make use of low-latency queries. Engineers can modify this demo to monitor a website&#8217;s HTTP response code or multiple endpoints or services for a robust overview of an entire system&#8217;s status.</p><p><em>The containerized source code is available at <a href="https://github.com/gabor-boros/questdb-statuspage">https://github.com/gabor-boros/questdb-statuspage</a>.</em></p>]]></content:encoded></item></channel></rss>