<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: François</title>
    <description>The latest articles on Forem by François (@francoislp).</description>
    <link>https://forem.com/francoislp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F129039%2Fa36c5110-cc59-4e9b-94ae-4fdc25fa2e53.jpg</url>
      <title>Forem: François</title>
      <link>https://forem.com/francoislp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/francoislp"/>
    <language>en</language>
    <item>
      <title>My new programming language is English</title>
      <dc:creator>François</dc:creator>
      <pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/my-new-programming-language-is-english-1gla</link>
      <guid>https://forem.com/francoislp/my-new-programming-language-is-english-1gla</guid>
      <description>&lt;h2&gt;
  
  
  My new programming language is not TypeScript, Python, Go, or even Rust. It is English.
&lt;/h2&gt;

&lt;p&gt;I spend most of my time explaining to an agent what I want, why I want it, and what constraints matter. I give exact descriptions when needed, plus links to the right docs. Sometimes those docs are full of typos, and that is OK. Then I review what comes out. For a huge part of my day-to-day engineering work, this is simply faster.&lt;/p&gt;

&lt;p&gt;And if we are honest, in most systems, the Pareto rule applies.&lt;/p&gt;

&lt;p&gt;For 80% of what we build, I do not care that much whether the agent used &lt;code&gt;map&lt;/code&gt; or a &lt;code&gt;for&lt;/code&gt; loop, OOP or functional style, or some "very elegant" abstraction. Is it an API endpoint? A DB query? A CRUD flow? Fine. What I care about is the output. Is it correct? Is it safe? Can we ship it?&lt;/p&gt;

&lt;p&gt;It is also very likely I will not be the one maintaining that code. Another agent probably will.&lt;/p&gt;

&lt;p&gt;Now, this only works if your engineering basics are strong. Actually, they need to be stronger than before. Tribal knowledge does not work with agents starting fresh on each run.&lt;/p&gt;

&lt;p&gt;Because "English is my new programming language" does not mean "standards do not matter anymore." It means standards and guardrails matter so much more.&lt;/p&gt;

&lt;p&gt;You need CI/CD that blocks bad changes. You need tests: unit, integration, end-to-end. You need gradual rollouts, smoke tests, monitoring, alerting. You need pull request review. You need context awareness on the scope of the change and the criticality of what you touch.&lt;/p&gt;

&lt;p&gt;And locally, you need fast feedback loops: linter, formatter, fast builds. Shift-left has never been this true. If feedback is fast, the agent iterates fast and can run its loop locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents are like super smart interns.
&lt;/h2&gt;

&lt;p&gt;Very capable. Very fast. Sometimes impressive. Still not autonomous in the way people fantasize about. I would trust a good intern to implement a lot when motivated. I would absolutely not skip reviewing their changes. Same here.&lt;/p&gt;

&lt;p&gt;If the intern breaks production, blame the game, not the player. Same for code created by an agent, in my opinion.&lt;/p&gt;

&lt;p&gt;Review is still a thing. Maybe the thing nowadays. We spend more time writing specs and reviewing PRs, and I still do not think we have reached a turning point where we can skip checking generated code for production-scale projects.&lt;/p&gt;

&lt;p&gt;When things go wrong, you need to know the structure of the codebase and where to look. Maybe this is just engineering curiosity, maybe it is risk management, probably both. But the day prod is down and &lt;a href="https://status.claude.com/" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; plus &lt;a href="https://status.openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt; are down too, you still need to fix it yourself.&lt;/p&gt;

&lt;p&gt;During review, I am not only checking low-level style comments like "you used &lt;code&gt;let&lt;/code&gt; instead of &lt;code&gt;const&lt;/code&gt;" or "careful, you mutate state here."&lt;/p&gt;

&lt;p&gt;What I really want to check is whether we are using current best practices. Is this API using the new decorator the team agreed on? Is ownership clear? Is observability in place? Are we respecting patterns we know scale in production? Is this internal and low risk, or is this critical path code where every generated line deserves deeper review?&lt;/p&gt;

&lt;p&gt;And when I find myself repeating these rules, questions, and comments, that is usually a signal: write them down.&lt;/p&gt;

&lt;p&gt;That is why skills (like &lt;a href="https://claude.com/skills" rel="noopener noreferrer"&gt;Claude Skills&lt;/a&gt;) are a big deal.&lt;/p&gt;

&lt;p&gt;Skills are how standards get distributed: how to create a service, define ownership, model a domain, and speak business language in the codebase. Less tribal memory, less repeating yourself in PR comments, more consistency by default.&lt;/p&gt;

&lt;p&gt;Same story for documentation.&lt;/p&gt;

&lt;p&gt;To code well in this era, good documentation is not "nice to have." It is core infrastructure. API specs, READMEs, architecture docs, sharp examples, and links to the few pages that actually matter. What I think is core to good AI-driven development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/strong&gt; (for your developers too!) - Here I would expect to find the getting started instructions, a summary of the project, and links to relevant docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; - Keep it very short here. Only give the big lines (main tech, quick repo structure). I like Matt Pocock's approach here: &lt;a href="https://www.aihero.dev/a-complete-guide-to-agents-md" rel="noopener noreferrer"&gt;https://www.aihero.dev/a-complete-guide-to-agents-md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The ideal AGENTS.md is small, focused, and points elsewhere. It gives the agent just enough context to start working, with breadcrumbs to more detailed guidance."&lt;/em&gt; &lt;strong&gt;Matt Pocock.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Skills&lt;/code&gt;&lt;/strong&gt; - Basic AI skills on how to work with your repo and your libs: what you expect for commits, pull requests, testing, and ways of working with specific libs. If you work with Cloudflare, add their &lt;a href="https://github.com/cloudflare/skills" rel="noopener noreferrer"&gt;skills&lt;/a&gt; to your repo! That'll enable anyone working with the repo to produce less garbage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Type-safe schemas&lt;/code&gt;&lt;/strong&gt; - If you use end-to-end type-safe schemas to build your clients and backend APIs, agents usually work better with that than with API specs. But you should still generate OpenAPI specs from the schemas, for instance. And make it clear with a skill that this is automated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Tests&lt;/code&gt;&lt;/strong&gt; - Tests are documentation. Get powerful smoke/e2e tests and you'll save yourself a lot of headaches when shipping faster. Regression is your biggest enemy here; you don't want to break existing flows by adding new features. Again, it's nothing new in our SDLC world.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that, you spend the same amount of time re-explaining your system to the agent as you would with a junior engineer. Again and again and again.&lt;/p&gt;

&lt;p&gt;The thin line now is deciding what should be a skill, what should be a README, and what should just be a code comment.&lt;/p&gt;

&lt;p&gt;Explaining a task to an agent feels a lot like leaving instructions to myself for one month later (I am pretty sure you know that comment you left somewhere in the codebase and you stumble into it from time to time, thanking your past self for clearly describing what the issue was 😅).&lt;/p&gt;

&lt;p&gt;I know I will forget context. The agent never had it. So I need clear explanations of what should be done, how to do it, and where the source of truth lives.&lt;/p&gt;

&lt;p&gt;It is funny when you think about it. We used to say good engineering practices were documentation, tests, and clear code comments. Now we are saying it again like it is brand new.&lt;/p&gt;

&lt;p&gt;In the end, using AI agents feels a lot like scaling your engineering team. In the past, if your team grew x10, you needed more process, more test coverage, and better documentation. Working with agents feels very similar, just faster.&lt;/p&gt;

&lt;p&gt;So yeah, my new programming language is English.&lt;/p&gt;

&lt;p&gt;Not because code disappeared into a black box. Because the highest leverage moved one level up: from typing instructions for computers to writing intent and constraints for systems that generate implementation.&lt;/p&gt;

&lt;p&gt;And in that world, the teams that win are not the teams that type fastest. They are the teams that specify clearly, review rigorously, and document like context is always missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agentic</category>
      <category>documentation</category>
      <category>dx</category>
    </item>
    <item>
      <title>I wanted the best GitHub notifications in Slack, so I built it</title>
      <dc:creator>François</dc:creator>
      <pubDate>Tue, 17 Mar 2026 00:30:00 +0000</pubDate>
      <link>https://forem.com/francoislp/i-wanted-the-best-github-notifications-in-slack-so-i-built-it-n2d</link>
      <guid>https://forem.com/francoislp/i-wanted-the-best-github-notifications-in-slack-so-i-built-it-n2d</guid>
      <description>&lt;p&gt;Two things really annoyed me with GitHub notifications.&lt;/p&gt;

&lt;p&gt;First: when I merge my own PR and break CI/CD on &lt;code&gt;main&lt;/code&gt;, I am not alerted.&lt;/p&gt;

&lt;p&gt;Second: existing notification solutions often spam me with bot comments and status updates (Vercel, Cloudflare, Wiz, CodeRabbit, and more), while human review signal gets buried under these notifications.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;a href="https://www.gitnotifier.com?utm_source=dev-to&amp;amp;utm_medium=article&amp;amp;utm_campaign=why-i-built-gitnotifier&amp;amp;utm_content=intro-link" rel="noopener noreferrer"&gt;GitNotifier&lt;/a&gt;: GitHub notifications in your Slack DMs. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: it alerts you when your PR breaks &lt;code&gt;main&lt;/code&gt;, on comments written by human on your PRs, you can mute bots, etc ;) (and more! keep reading eheh)&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot: post-merge CI failures on main
&lt;/h2&gt;

&lt;p&gt;Most GitHub notifications are PR-centric. That sounds fine until you realize what happens after merge.&lt;/p&gt;

&lt;p&gt;If your PR gets merged, and then CI/CD fails on &lt;code&gt;main&lt;/code&gt;, you often miss it. The branch is red, production might be blocked, and you discover it too late.&lt;/p&gt;

&lt;p&gt;I even wrote a workaround years ago using GitHub Actions and Slack webhooks:&lt;br&gt;
&lt;a href="https://blog.lepape.me/sending-slack-notifications-with-github-actions/" rel="noopener noreferrer"&gt;Sending slack notifications with github actions&lt;/a&gt;. It's quite manual because you need to edit your GitHub Actions in all your repos and configure the right tokens... but it did the job for a while.&lt;/p&gt;

&lt;p&gt;It still felt like duct tape. I wanted something that works out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bots now talk more than humans
&lt;/h2&gt;

&lt;p&gt;On many PRs, I get more bot comments than human comments.&lt;/p&gt;

&lt;p&gt;You know the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vercel preview updates&lt;/li&gt;
&lt;li&gt;Cloudflare checks&lt;/li&gt;
&lt;li&gt;Wiz security notes&lt;/li&gt;
&lt;li&gt;Bito.ai suggestions or CodeRabbit AI review comments&lt;/li&gt;
&lt;li&gt;and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are inherently bad. I actually like reading them (sometimes) when I review a PR. The problem is they should not be notifications. Kind of like with a service level objective (SLO): you only want alerts on symptoms, not for every small CPU burst in your VMs.&lt;/p&gt;

&lt;p&gt;In 2026, reviewing code often takes longer than writing code (thanks Claude, ChatGPT &amp;amp; co). If the most important part of the workflow is human review, then human comments should have the highest signal.&lt;/p&gt;

&lt;p&gt;Here is a graph from the GitNotifier analytics page to show this clearly in the repos I work with: bot comment volume often exceeds human comment volume (crazy, right?).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F92m4aavlfcnuz8z80wri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F92m4aavlfcnuz8z80wri.png" alt="Bot comments often outnumber human comments in PRs" width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing tools did not really solve it for me
&lt;/h2&gt;

&lt;p&gt;I tried a lot of options. Most had one or more of these issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too expensive for what they do&lt;/li&gt;
&lt;li&gt;Hidden inside enterprise bundles&lt;/li&gt;
&lt;li&gt;Focused on employee monitoring or performance tracking&lt;/li&gt;
&lt;li&gt;Poor UX (some honestly felt vibe-coded)&lt;/li&gt;
&lt;li&gt;Limited customization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I "just" want good GitHub notifications in my DMs, not a full-blown team performance tracking system costing me a house per month. Notifications that are actually useful in daily engineering work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most PR reviews involve 2 people, so why spam a channel?
&lt;/h2&gt;

&lt;p&gt;A PR review is usually between:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the author&lt;/li&gt;
&lt;li&gt;the reviewer&lt;/li&gt;
&lt;li&gt;(a review agent? but they are noisy 😁)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes 3 people. More than that is rare.&lt;/p&gt;

&lt;p&gt;So sending every update to a shared Slack channel is often the wrong default. It creates ambient noise, trains people to mute channels, and increases alert fatigue for everyone. The fewer messages, the better.&lt;/p&gt;

&lt;p&gt;IMHO, the right destination for most PR events, such as comments, is personal Slack DMs, not public channels.&lt;/p&gt;

&lt;p&gt;Shared channels should stay for shared context. Direct actions should go to directly involved people.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted instead
&lt;/h2&gt;

&lt;p&gt;I built GitNotifier around a few principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signal over volume&lt;/li&gt;
&lt;li&gt;Human-first notifications&lt;/li&gt;
&lt;li&gt;Strong customization&lt;/li&gt;
&lt;li&gt;DM-first delivery&lt;/li&gt;
&lt;li&gt;Actionable alerts, not passive spam&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: fewer notifications and better notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do things from Slack, do not just get pinged there
&lt;/h2&gt;

&lt;p&gt;Another frustration: every notification forces context switching.&lt;/p&gt;

&lt;p&gt;I wanted to stay in Slack and continue working, so GitNotifier lets me do things directly from Slack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;react to comments&lt;/li&gt;
&lt;li&gt;reply to comments&lt;/li&gt;
&lt;li&gt;merge PRs that are ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple: reduce tab-jumping, keep momentum. As a big fan of Raycast, I want to be more ✨productive✨ when I use a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  DX matters: setup should be boring
&lt;/h2&gt;

&lt;p&gt;Dev tools die when setup is painful.&lt;/p&gt;

&lt;p&gt;The flow I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin installs 2 apps (GitHub and Slack)&lt;/li&gt;
&lt;li&gt;Users join in 1 click from Slack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it.&lt;/p&gt;

&lt;p&gt;No heavy rollout project. No setup overhead. No long onboarding docs. Just connect and start receiving useful notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for me
&lt;/h2&gt;

&lt;p&gt;After using GitNotifier daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I miss fewer critical CI issues&lt;/li&gt;
&lt;li&gt;I focus faster on human review threads&lt;/li&gt;
&lt;li&gt;PR handling is faster because actions happen in Slack, and PRs get merged faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not need more notifications. I needed better ones 😁&lt;/p&gt;




&lt;p&gt;If you want to give it a try, check out &lt;a href="https://www.gitnotifier.com?utm_source=dev-to&amp;amp;utm_medium=article&amp;amp;utm_campaign=why-i-built-gitnotifier&amp;amp;utm_content=final-cta" rel="noopener noreferrer"&gt;GitNotifier&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54387xbafntqvdgvx6vq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54387xbafntqvdgvx6vq.png" alt="Gitnotifier Github PRs slack notifs screenshot" width="719" height="812"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are dealing with the same GitHub + Slack notification fatigue, I would love your feedback!&lt;/p&gt;

</description>
      <category>github</category>
      <category>slack</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop committing with the wrong email: Multiple Git Configs for GitHub</title>
      <dc:creator>François</dc:creator>
      <pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/stop-committing-with-the-wrong-email-multiple-git-configs-for-github-5e9i</link>
      <guid>https://forem.com/francoislp/stop-committing-with-the-wrong-email-multiple-git-configs-for-github-5e9i</guid>
      <description>&lt;p&gt;As a developer working on both personal and professional projects, you might find yourself needing to use different SSH keys and email addresses when committing to GitHub. This is especially common when you want to keep your work and personal contributions separate, or when you need to use different SSH keys for different organizations.&lt;/p&gt;

&lt;p&gt;In this guide, I'll show you how to set up multiple Git configurations using separate config files, allowing you to automatically use the right SSH key and email based on which directory you're working in.&lt;/p&gt;

&lt;p&gt;This guide assumes you sort your git directories by folder. Personally, I keep all my private repos in &lt;code&gt;~/Projects/perso&lt;/code&gt; and all my work repos in &lt;code&gt;~/Projects/&amp;lt;company_name&amp;gt;&lt;/code&gt;. To simplify this post, let's use &lt;code&gt;~/perso&lt;/code&gt; and &lt;code&gt;~/pro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that I'll be focusing on mac and some commands may vary between OS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;By default, Git uses a single global configuration (&lt;code&gt;~/.gitconfig&lt;/code&gt;) for all repositories. You might have setup this once when you got your laptop with &lt;code&gt;git config --global user.email ...&lt;/code&gt; for instance. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All commits use the same email address&lt;/li&gt;
&lt;li&gt;All SSH connections use the same key&lt;/li&gt;
&lt;li&gt;You can't easily switch between personal and professional identities (you could configure the git config per repo, but it comes really cumbersome and error prone)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Multiple Git Config Files
&lt;/h2&gt;

&lt;p&gt;We'll create separate Git configuration files for different contexts and use Git's &lt;code&gt;includeIf&lt;/code&gt; directive to automatically load the right config based on your current directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Your SSH Keys (optional)
&lt;/h3&gt;

&lt;p&gt;If you already have several SSH keys, you can skip this section 🙏&lt;/p&gt;

&lt;p&gt;To generate separate SSH keys for personal and professional use we'll use &lt;strong&gt;ed25519&lt;/strong&gt; keys (&lt;a href="https://dev.to/ssh-keys-vs-passwords#use-state-of-art-keys"&gt;have a look here for details about why&lt;/a&gt;). Feel free to use RSA if you like verbosity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Personal SSH key&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"your-personal-email@example.com"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_ed25519_perso

&lt;span class="c"&gt;# Professional SSH key&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"your-work-email@company.com"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_ed25519_pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted, you can either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Press Enter to create keys without a passphrase (less secure but more convenient)&lt;/li&gt;
&lt;li&gt;Enter a strong passphrase for additional security (if you do so, I'd recommend to add them to your keychain on mac.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Add Keys to SSH Agent
&lt;/h3&gt;

&lt;p&gt;Add both keys to your SSH agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-add ~/.ssh/id_ed25519_perso
ssh-add ~/.ssh/id_ed25519_pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read more about it in the &lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent" rel="noopener noreferrer"&gt;github docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure SSH for Different Hosts
&lt;/h3&gt;

&lt;p&gt;Create or update your &lt;code&gt;~/.ssh/config&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Personal GitHub&lt;/span&gt;
Host github-perso
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_perso
    &lt;span class="c"&gt;# If you use a passkey, you might need more options like UseKeychain, AddKeysToAgent and IdentityAgent.&lt;/span&gt;

&lt;span class="c"&gt;# Professional GitHub&lt;/span&gt;
Host github-pro
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Set Up Git Config Files
&lt;/h3&gt;

&lt;p&gt;By default Git will use &lt;code&gt;~/.gitconfig&lt;/code&gt;. When you're in a directory that matches the &lt;code&gt;gitdir&lt;/code&gt; pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/perso/&lt;/code&gt;&lt;/strong&gt; → will load &lt;code&gt;~/.gitconfig-perso&lt;/code&gt; config override.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/pro/&lt;/code&gt;&lt;/strong&gt; → will load &lt;code&gt;~/.gitconfig-pro&lt;/code&gt; config override.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Main Git Config (&lt;code&gt;~/.gitconfig&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;This is your global configuration that applies to all repositories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[user]&lt;/span&gt;
    &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Your Name&lt;/span&gt;
    &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;your-personal-email@example.com&lt;/span&gt;

&lt;span class="nn"&gt;[core]&lt;/span&gt;
    &lt;span class="py"&gt;editor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;vim&lt;/span&gt;

&lt;span class="nn"&gt;[init]&lt;/span&gt;
    &lt;span class="py"&gt;defaultBranch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="c"&gt;# Include personal config for personal projects
&lt;/span&gt;&lt;span class="nn"&gt;[includeIf "gitdir:~/perso/"]&lt;/span&gt;
    &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;~/.gitconfig-perso&lt;/span&gt;

&lt;span class="c"&gt;# Include professional config for work projects
&lt;/span&gt;&lt;span class="nn"&gt;[includeIf "gitdir:~/pro/"]&lt;/span&gt;
    &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;~/.gitconfig-pro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Personal Git Config (&lt;code&gt;~/.gitconfig-perso&lt;/code&gt;)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[core]&lt;/span&gt;
  &lt;span class="py"&gt;sshCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ssh -i ~/.ssh/id_ed25519_perso -F /dev/null"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Professional Git Config (&lt;code&gt;~/.gitconfig-pro&lt;/code&gt;)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[core]&lt;/span&gt;
  &lt;span class="py"&gt;sshCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ssh -i ~/.ssh/id_ed25519_pro -F /dev/null"&lt;/span&gt;

&lt;span class="nn"&gt;[user]&lt;/span&gt;
    &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;A very profesional name&lt;/span&gt;
    &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-profesional@email.com"&lt;/span&gt;
    &lt;span class="py"&gt;signingkey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/Users/&amp;lt;your_user&amp;gt;/.ssh/id_ed25519_pro.pub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Add SSH Keys to GitHub
&lt;/h3&gt;

&lt;p&gt;Copy your personal &amp;amp; professional public key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pbcopy &amp;lt; ~/.ssh/id_ed25519_perso.pub
pbcopy &amp;lt; ~/.ssh/id_ed25519_pro.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add both keys to your GitHub account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/settings/keys" rel="noopener noreferrer"&gt;GitHub → Settings → SSH and GPG keys&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click "New SSH key"&lt;/li&gt;
&lt;li&gt;Paste each key and give them descriptive names&lt;/li&gt;
&lt;li&gt;(optional) Add both your keys as &lt;code&gt;Authentication keys&lt;/code&gt; and &lt;code&gt;Signing keys&lt;/code&gt;. This will help you to sign your commits using your SSH key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 6: Set Up Commit Signing (Optional but Recommended)
&lt;/h3&gt;

&lt;p&gt;To verify that your commits are authentic, you can sign them using your SSH keys. This provides cryptographic proof that the commit came from you. It also looks nice in the github UI ☺️&lt;/p&gt;

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

&lt;h4&gt;
  
  
  Configure Git for SSH Signing
&lt;/h4&gt;

&lt;p&gt;Update your Git configs to use SSH signing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main config (&lt;code&gt;~/.gitconfig&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[gpg]&lt;/span&gt;
    &lt;span class="py"&gt;format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ssh&lt;/span&gt;
&lt;span class="nn"&gt;[commit]&lt;/span&gt;
    &lt;span class="py"&gt;gpgsign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus (hide your email) 🥷
&lt;/h2&gt;

&lt;p&gt;In some cases, you might want to hide your email on Github. To do so, you can head to &lt;a href="https://github.com/settings/emails" rel="noopener noreferrer"&gt;https://github.com/settings/emails&lt;/a&gt; and enable "Keep my email addresses private".&lt;/p&gt;

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

&lt;p&gt;By using the random email github generates, here &lt;code&gt;11248623+Lp-Francois@users.noreply.github.com&lt;/code&gt; for my account, Github will hide your personal email.&lt;/p&gt;

&lt;p&gt;To use it with your git config, put it in this block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[user]&lt;/span&gt;
    &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Francois Le Pape&lt;/span&gt;
    &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"11248623+Lp-Francois@users.noreply.github.com"&lt;/span&gt; &lt;span class="c"&gt;# email found in https://github.com/settings/emails"
&lt;/span&gt;    &lt;span class="s"&gt;signingkey = /Users/francois/.ssh/id_ed25519_perso.pub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;With this setup, you now have a clean separation between your personal and professional Git identities. No more accidentally committing with the wrong email or SSH key!&lt;/p&gt;

&lt;p&gt;The beauty of this approach is that it's completely automatic - Git will detect which directory you're in and apply the appropriate configuration. You can focus on coding instead of remembering to switch Git configs manually.&lt;/p&gt;

&lt;p&gt;Hope this helps ✨&lt;/p&gt;

</description>
      <category>git</category>
      <category>github</category>
      <category>mac</category>
    </item>
    <item>
      <title>Stop using "GitOps" to sell your products</title>
      <dc:creator>François</dc:creator>
      <pubDate>Tue, 20 Aug 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/stop-using-gitops-to-sell-your-products-3lff</link>
      <guid>https://forem.com/francoislp/stop-using-gitops-to-sell-your-products-3lff</guid>
      <description>&lt;p&gt;Hello there, and welcome to the first rant on this blog. No, deploying infrastructure from Git doesn't mean you are doing "GitOps". It's called Infrastructure-as-Code on Git.&lt;/p&gt;

&lt;p&gt;GitOps is IaC, but IaC is not necessarily GitOps.&lt;/p&gt;

&lt;p&gt;So what is &lt;em&gt;GitOps&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;Well, there is a specification, a standard, named &lt;a href="https://opengitops.dev/" rel="noopener noreferrer"&gt;OpenGitOps&lt;/a&gt; that defines it the following way (the main focus of this post is on the 4th principle):&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Declarative
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;system&lt;/strong&gt; managed by GitOps must have its desired state expressed &lt;strong&gt;declaratively&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well, there is not much to add, it shouldn't be a &lt;code&gt;for-loop&lt;/code&gt; in a bash script that uses an API, but just plain declarative configuration code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Versioned and Immutable
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Desired state is &lt;strong&gt;stored&lt;/strong&gt; in a way that enforces immutability, versioning and retains a complete version history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the &lt;em&gt;Git&lt;/em&gt; part of GitOps! Although it started with Git (and that's where the name comes from), now you can find another state store like OCI repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pulled Automatically
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Software agents automatically pull the desired state declarations from the source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The "software agents" are the tools like &lt;a href="https://fluxcd.io/" rel="noopener noreferrer"&gt;Flux&lt;/a&gt;, &lt;a href="https://argo-cd.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;ArgoCD&lt;/a&gt;, etc. that will pull the desired state from the source (Git or OCI repository). It shouldn't be manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Continuously Reconciled
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Software agents &lt;strong&gt;continuously&lt;/strong&gt; observe actual system state and &lt;strong&gt;attempt to apply&lt;/strong&gt; the desired state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's to me &lt;strong&gt;THE&lt;/strong&gt; point where people get GitOps wrong (or on purpose wrong, to sell you more buzzwords).&lt;/p&gt;

&lt;p&gt;If the state of the system changes, you should expect the software agent to reconcile the state to the desired state. In other words, what is in Git should be reflected in the Kubernetes Cluster all the time.&lt;/p&gt;

&lt;p&gt;Adding a CI/CD pipeline to your repository that runs a &lt;code&gt;kubectl apply&lt;/code&gt; is not GitOps. GitOps is to have a tool checking for drift detection and re-applying the desired state when needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's take an example:&lt;/strong&gt; The new intern - let's name him Kevin - gets production access, and for debug purposes sets the replica number of a Kubernetes deployment to 0 instead of 3. Well, the GitOps software agent should see that the state of the cluster is different from the desired state and re-apply the desired state, causing the deployment to be back to the desired state: 3. it's a very powerful feature that brings production robustness to manual errors and drift detection!&lt;/p&gt;

&lt;p&gt;Do you want to see a super powerful and strong GitOps agent? Here is mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;do &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/kubernetes/website/snapshot-initial-v1.31/content/en/examples/pods/simple-pod.yaml&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;15&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;pod/nginx created
pod/nginx unchanged
pod/nginx unchanged
pod/nginx unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's declarative, versioned and immutable in Git (Pod specs are in YAML &lt;a href="https://raw.githubusercontent.com/kubernetes/website/snapshot-initial-v1.31/content/en/examples/pods/simple-pod.yaml" rel="noopener noreferrer"&gt;here&lt;/a&gt;). It's pulled from Git, and it's continuously reconciled every 15 seconds. It's GitOps!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;This post is written by a "DevOps" engineer, so don't pay too much attention to buzzwords after all. I hope I shed some light on the GitOps principles at the cost of a clickbait title (and maybe a few Twitter dramas).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenGitOps principles - &lt;a href="https://opengitops.dev/" rel="noopener noreferrer"&gt;https://opengitops.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;4 Core Principles of GitOps - The new stack - &lt;a href="https://thenewstack.io/4-core-principles-of-gitops/" rel="noopener noreferrer"&gt;https://thenewstack.io/4-core-principles-of-gitops/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How We Are Moving from GitOps to Kubernetes Resource Model in 5G Core - &lt;a href="https://www.youtube.com/watch?v=crmTnB6Zwt8&amp;amp;t=302s" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=crmTnB6Zwt8&amp;amp;t=302s&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Flux Beyond Git: Harnessing the Power of OCI - Stefan Prodan &amp;amp; Hidde Beydals, Weaveworks  - &lt;a href="https://www.youtube.com/watch?v=gKR95Kmc5ac&amp;amp;t=1215s" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=gKR95Kmc5ac&amp;amp;t=1215s&lt;/a&gt; (Using OCI repository instead of Git)&lt;/li&gt;
&lt;li&gt;and many others on the internet!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;:wq&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>gitops</category>
    </item>
    <item>
      <title>How to increase AWS ELB keep-alive timeout when deployed from Traefik</title>
      <dc:creator>François</dc:creator>
      <pubDate>Wed, 08 May 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/how-to-increase-aws-elb-keep-alive-timeout-when-deployed-from-traefik-5doo</link>
      <guid>https://forem.com/francoislp/how-to-increase-aws-elb-keep-alive-timeout-when-deployed-from-traefik-5doo</guid>
      <description>&lt;p&gt;Let's see together how to change the Load balancer settings when created from an ingress controller like Traefik on Kubernetes.&lt;/p&gt;

&lt;p&gt;The default timeout of the AWS Elastic Load Balancer (ELB) is 60 seconds. For some use cases (like long LLM calls) this might be too short and you might want to increase it.&lt;/p&gt;

&lt;p&gt;When deploying Traefik on Kubernetes, it might be hard to find the right configuration to increase the keep-alive timeout on the ELB, as Traefik is the one creating the ELB.&lt;/p&gt;

&lt;p&gt;Well, after some searches it is pretty easy to change it. You need to add the right annotation on the Kubernetes Service with the type Load Balancer.&lt;/p&gt;

&lt;p&gt;Here is the documentation with available annotations: &lt;a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/guide/service/annotations/" rel="noopener noreferrer"&gt;https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/guide/service/annotations/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the Traefik Helm chart, it is as simple as updating this &lt;code&gt;values.yaml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;#    https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/guide/service/annotations/#resource-attributes&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 2 minutes&lt;/span&gt;
    &lt;span class="na"&gt;service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some examples of configuration used with nginx ingress controller: &lt;a href="https://github.com/zephyrproject-rtos/infrastructure/blob/4b767ceec69f725614ded3dcb715c8ebffeadc83/terraform/zephyr-aws-blueprints/helm_values/nginx-values.yaml#L4" rel="noopener noreferrer"&gt;https://github.com/zephyrproject-rtos/infrastructure/blob/4b767ceec69f725614ded3dcb715c8ebffeadc83/terraform/zephyr-aws-blueprints/helm_values/nginx-values.yaml#L4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>traefik</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>NodeJS Best Practices: Redacting Secrets from Your Pino Logs</title>
      <dc:creator>François</dc:creator>
      <pubDate>Sat, 30 Mar 2024 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/nodejs-best-practices-redacting-secrets-from-your-pino-logs-1eik</link>
      <guid>https://forem.com/francoislp/nodejs-best-practices-redacting-secrets-from-your-pino-logs-1eik</guid>
      <description>&lt;p&gt;Learn to mitigate potential security issues by redacting sensitive data (like user credentials or secrets) from your logs using Pino logger for Node.js. Consider this a crucial part of your logging strategy.&lt;/p&gt;

&lt;p&gt;In today's world where data breaches are increasingly common, secure logging is not just important - it's absolutely essential. One area that developers often overlook is the logging of sensitive data, like user credentials or secrets. Such data leaks can lead to serious security vulnerabilities and violations of GDPR compliance if you're not careful.&lt;/p&gt;

&lt;p&gt;This article will guide you through redacting such sensitive data from your logs when using Pino, a performant and lightweight logger for Node.js, aiding your application in maintaining GDPR compliance and reducing the risk of privileging escalation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pino
&lt;/h3&gt;

&lt;p&gt;When instantiating a pino instance, you usually start with a similar piece of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You can find the Gist here: https://gist.github.com/Lp-Francois/3d1b36907de8283a8a3eedca31c9cfc3&lt;/span&gt;

&lt;span class="c1"&gt;// file:`example.js`&lt;/span&gt;
&lt;span class="c1"&gt;// install packages: `npm i pino-http --save`&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pino&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// removes pid and hostname from the logs&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[i] running on http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and when running it, then sending requests to it, you can&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# terminal window 1&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;node example.js
&lt;span class="c"&gt;# terminal window 2&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:3000 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"authorization: bearer my-super-secret"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;

&lt;span class="c"&gt;# logs in terminal 1:&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;i] running on http://localhost:3000
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"level"&lt;/span&gt;:30,&lt;span class="s2"&gt;"time"&lt;/span&gt;:1711808000315,&lt;span class="s2"&gt;"req"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;:1,&lt;span class="s2"&gt;"method"&lt;/span&gt;:&lt;span class="s2"&gt;"GET"&lt;/span&gt;,&lt;span class="s2"&gt;"url"&lt;/span&gt;:&lt;span class="s2"&gt;"/"&lt;/span&gt;,&lt;span class="s2"&gt;"headers"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;:&lt;span class="s2"&gt;"localhost:3000"&lt;/span&gt;,&lt;span class="s2"&gt;"user-agent"&lt;/span&gt;:&lt;span class="s2"&gt;"curl/8.4.0"&lt;/span&gt;,&lt;span class="s2"&gt;"accept"&lt;/span&gt;:&lt;span class="s2"&gt;"*/*"&lt;/span&gt;,&lt;span class="s2"&gt;"authorization"&lt;/span&gt;:&lt;span class="s2"&gt;"bearer my-super-secret"&lt;/span&gt;,&lt;span class="s2"&gt;"content-type"&lt;/span&gt;:&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"remoteAddress"&lt;/span&gt;:&lt;span class="s2"&gt;"::1"&lt;/span&gt;,&lt;span class="s2"&gt;"remotePort"&lt;/span&gt;:63547&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"msg"&lt;/span&gt;:&lt;span class="s2"&gt;"my request"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"level"&lt;/span&gt;:30,&lt;span class="s2"&gt;"time"&lt;/span&gt;:1711808000322,&lt;span class="s2"&gt;"req"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;:1,&lt;span class="s2"&gt;"method"&lt;/span&gt;:&lt;span class="s2"&gt;"GET"&lt;/span&gt;,&lt;span class="s2"&gt;"url"&lt;/span&gt;:&lt;span class="s2"&gt;"/"&lt;/span&gt;,&lt;span class="s2"&gt;"headers"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;:&lt;span class="s2"&gt;"localhost:3000"&lt;/span&gt;,&lt;span class="s2"&gt;"user-agent"&lt;/span&gt;:&lt;span class="s2"&gt;"curl/8.4.0"&lt;/span&gt;,&lt;span class="s2"&gt;"accept"&lt;/span&gt;:&lt;span class="s2"&gt;"*/*"&lt;/span&gt;,&lt;span class="s2"&gt;"authorization"&lt;/span&gt;:&lt;span class="s2"&gt;"bearer my-super-secret"&lt;/span&gt;,&lt;span class="s2"&gt;"content-type"&lt;/span&gt;:&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"remoteAddress"&lt;/span&gt;:&lt;span class="s2"&gt;"::1"&lt;/span&gt;,&lt;span class="s2"&gt;"remotePort"&lt;/span&gt;:63547&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"res"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"statusCode"&lt;/span&gt;:200,&lt;span class="s2"&gt;"headers"&lt;/span&gt;:&lt;span class="o"&gt;{}}&lt;/span&gt;,&lt;span class="s2"&gt;"responseTime"&lt;/span&gt;:7,&lt;span class="s2"&gt;"msg"&lt;/span&gt;:&lt;span class="s2"&gt;"request completed"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;authorization&lt;/code&gt; header containing the secret in the logs 😢: &lt;code&gt;..."accept":"*/*","authorization":"bearer my-super-secret"...&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Redaction
&lt;/h3&gt;

&lt;p&gt;Pino has a "hidden" (hard to find) &lt;a href="https://github.com/pinojs/pino/blob/master/docs/redaction.md" rel="noopener noreferrer"&gt;documentation on redacting secrets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is possible to pass to your pino logger an array containing paths to keys holding sensible data.&lt;/p&gt;

&lt;p&gt;To remove our &lt;code&gt;authorization&lt;/code&gt; header from the &lt;code&gt;req&lt;/code&gt; object, we just have to append this to our config: &lt;code&gt;redact: ['req.headers.authorization']&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the full example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pino&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pino-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pino&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ⚠️ The next line is the important one:&lt;/span&gt;
  &lt;span class="na"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;req.headers.authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running on http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outputing the following logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;i] running on http://localhost:3000
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"level"&lt;/span&gt;:30,&lt;span class="s2"&gt;"time"&lt;/span&gt;:1711824390585,&lt;span class="s2"&gt;"req"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;:1,&lt;span class="s2"&gt;"method"&lt;/span&gt;:&lt;span class="s2"&gt;"GET"&lt;/span&gt;,&lt;span class="s2"&gt;"url"&lt;/span&gt;:&lt;span class="s2"&gt;"/"&lt;/span&gt;,&lt;span class="s2"&gt;"headers"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;:&lt;span class="s2"&gt;"localhost:3000"&lt;/span&gt;,&lt;span class="s2"&gt;"user-agent"&lt;/span&gt;:&lt;span class="s2"&gt;"curl/8.4.0"&lt;/span&gt;,&lt;span class="s2"&gt;"accept"&lt;/span&gt;:&lt;span class="s2"&gt;"*/*"&lt;/span&gt;,&lt;span class="s2"&gt;"authorization"&lt;/span&gt;:&lt;span class="s2"&gt;"[Redacted]"&lt;/span&gt;,&lt;span class="s2"&gt;"content-type"&lt;/span&gt;:&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"remoteAddress"&lt;/span&gt;:&lt;span class="s2"&gt;"::1"&lt;/span&gt;,&lt;span class="s2"&gt;"remotePort"&lt;/span&gt;:58055&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"msg"&lt;/span&gt;:&lt;span class="s2"&gt;"my request"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"level"&lt;/span&gt;:30,&lt;span class="s2"&gt;"time"&lt;/span&gt;:1711824390594,&lt;span class="s2"&gt;"req"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;:1,&lt;span class="s2"&gt;"method"&lt;/span&gt;:&lt;span class="s2"&gt;"GET"&lt;/span&gt;,&lt;span class="s2"&gt;"url"&lt;/span&gt;:&lt;span class="s2"&gt;"/"&lt;/span&gt;,&lt;span class="s2"&gt;"headers"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;:&lt;span class="s2"&gt;"localhost:3000"&lt;/span&gt;,&lt;span class="s2"&gt;"user-agent"&lt;/span&gt;:&lt;span class="s2"&gt;"curl/8.4.0"&lt;/span&gt;,&lt;span class="s2"&gt;"accept"&lt;/span&gt;:&lt;span class="s2"&gt;"*/*"&lt;/span&gt;,&lt;span class="s2"&gt;"authorization"&lt;/span&gt;:&lt;span class="s2"&gt;"[Redacted]"&lt;/span&gt;,&lt;span class="s2"&gt;"content-type"&lt;/span&gt;:&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"remoteAddress"&lt;/span&gt;:&lt;span class="s2"&gt;"::1"&lt;/span&gt;,&lt;span class="s2"&gt;"remotePort"&lt;/span&gt;:58055&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="s2"&gt;"res"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"statusCode"&lt;/span&gt;:200,&lt;span class="s2"&gt;"headers"&lt;/span&gt;:&lt;span class="o"&gt;{}}&lt;/span&gt;,&lt;span class="s2"&gt;"responseTime"&lt;/span&gt;:9,&lt;span class="s2"&gt;"msg"&lt;/span&gt;:&lt;span class="s2"&gt;"request completed"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;authorization&lt;/code&gt; header now has a &lt;code&gt;[Redacted]&lt;/code&gt; value!&lt;/p&gt;

&lt;p&gt;Note the keys are &lt;em&gt;case sensitive&lt;/em&gt;! If you want to add to the redact list an uppercase header like &lt;code&gt;req.headers.CREDENTIALS&lt;/code&gt; be careful as it would ignore the lowercase &lt;code&gt;credentials&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;It is also an option to drop both value and key (&lt;code&gt;redact.remove&lt;/code&gt;), or change the value of the redacted field to something else with the &lt;code&gt;censor&lt;/code&gt; option (I'll let you read the &lt;a href="https://github.com/pinojs/pino/blob/master/docs/redaction.md#redaction" rel="noopener noreferrer"&gt;documentation for that&lt;/a&gt; 😉).&lt;/p&gt;

&lt;p&gt;I didn't find many resources online that covered this tip specifically, so I hope this insight proves valuable to you!&lt;/p&gt;

&lt;p&gt;Happy safe logging with Pino!&lt;/p&gt;

</description>
      <category>security</category>
      <category>node</category>
    </item>
    <item>
      <title>Good bye sleep() and delay() with NodeJS, hello setTimeout()</title>
      <dc:creator>François</dc:creator>
      <pubDate>Mon, 27 Nov 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/good-bye-sleep-and-delay-with-nodejs-hello-settimeout-4ppp</link>
      <guid>https://forem.com/francoislp/good-bye-sleep-and-delay-with-nodejs-hello-settimeout-4ppp</guid>
      <description>&lt;p&gt;Stop to use home-made utilities sleep() and delay(), and use the native nodeJS timers API: setTimeout().&lt;/p&gt;

&lt;p&gt;I have seen and added many times in nodeJS codebase the following snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, &lt;a href="https://github.com/nestjs/terminus/pull/2422" rel="noopener noreferrer"&gt;today I learned&lt;/a&gt; that nodeJS has a native API for that: &lt;code&gt;setTimeout()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// https://nodejs.org/api/timers.html#timerspromisessettimeoutdelay-value-options&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:timers/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// sleep 2s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So time to stop to use home-made utilities &lt;code&gt;sleep()&lt;/code&gt; and &lt;code&gt;delay()&lt;/code&gt;, and use the native nodeJS timers API!&lt;/p&gt;

</description>
      <category>node</category>
    </item>
    <item>
      <title>How to use OpenTelemetry to expose custom Prometheus metrics from nodeJS applications</title>
      <dc:creator>François</dc:creator>
      <pubDate>Wed, 08 Nov 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/how-to-use-opentelemetry-to-expose-custom-prometheus-metrics-from-nodejs-applications-2jf</link>
      <guid>https://forem.com/francoislp/how-to-use-opentelemetry-to-expose-custom-prometheus-metrics-from-nodejs-applications-2jf</guid>
      <description>&lt;p&gt;A standard way of exposing metrics for a nodeJS application is to use the &lt;a href="https://github.com/siimon/prom-client" rel="noopener noreferrer"&gt;prom-client&lt;/a&gt; package, which has everything one would need. But we are in 2023, almost 2024 and it is time to use industry standards, and here I am talking about OpenTelemetry. Let’s answer the question together on how to use open-telemetry JS to expose custom metrics.&lt;/p&gt;

&lt;p&gt;I don’t think there is a need to present the OpenTelemetry project anymore. &lt;a href="https://www.cncf.io/blog/2023/01/11/a-look-at-the-2022-velocity-of-cncf-linux-foundation-and-top-30-open-source-projects/" rel="noopener noreferrer"&gt;2nd CNCF project with highest velocity&lt;/a&gt; at the time of writing, vendor-neutral and open-source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example nodeJS project
&lt;/h2&gt;

&lt;p&gt;The following commands help to setup a nodeJS project with some OpenTelemetry packages. You might notice when using open telemetry package that the list in package.json grows quite fast. But be assured, most of them are lightweight. For instance &lt;code&gt;@opentelemetry/exporter-prometheus&lt;/code&gt; is 24.4&lt;br&gt;
kB minified and gzipped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;otel-prom &lt;span class="nb"&gt;cd &lt;/span&gt;otel-prom
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;touch &lt;/span&gt;index.js

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save&lt;/span&gt; @opentelemetry/exporter-prometheus @opentelemetry/api @opentelemetry/sdk-metrics

&lt;span class="c"&gt;# Note: I am using the following versions:&lt;/span&gt;
&lt;span class="c"&gt;# "@opentelemetry/api": "^1.7.0",&lt;/span&gt;
&lt;span class="c"&gt;# "@opentelemetry/exporter-prometheus": "^0.45.0",&lt;/span&gt;
&lt;span class="c"&gt;# "@opentelemetry/sdk-metrics": "^1.18.0",&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@opentelemetry/sdk-metrics" rel="noopener noreferrer"&gt;&lt;code&gt;@opentelemetry/sdk-metrics&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@opentelemetry/exporter-prometheus" rel="noopener noreferrer"&gt;&lt;code&gt;@opentelemetry/exporter-prometheus&lt;/code&gt;&lt;/a&gt; are required to register metrics and export them in prometheus format.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@opentelemetry/api" rel="noopener noreferrer"&gt;&lt;code&gt;@opentelemetry/api&lt;/code&gt;&lt;/a&gt; is optional, and contain some useful utilities &amp;amp; interfaces.&lt;/li&gt;
&lt;li&gt;You could even add &lt;a href="https://www.npmjs.com/package/@opentelemetry/sdk-node" rel="noopener noreferrer"&gt;&lt;code&gt;@opentelemetry/sdk-node&lt;/code&gt;&lt;/a&gt; to get some native nodejs metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The Otel packages shown in this blog are currently considered experimental packages under active development. New releases may include breaking changes.&lt;/p&gt;

&lt;p&gt;Then proceed to edit the file &lt;code&gt;index.js&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ValueType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrometheusExporter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/exporter-prometheus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MeterProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/sdk-metrics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Mock a potential database call to query data&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryDatabaseStuff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startMetricsExporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`starting prometheus metrics server`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// you can choose in the options the port, and if you want to start a webserver&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrometheusExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meterProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MeterProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;meterProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMetricReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;meterProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prometheus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// create the gauge&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outdatedDataCountGauge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObservableGauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outdated_data_count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;valueType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ValueType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outdated data count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// callbacks are executed when the /metrics endpoint is hit&lt;/span&gt;
  &lt;span class="nx"&gt;outdatedDataCountGauge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outdatedDataCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;queryDatabaseStuff&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outdatedDataCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// simulate a normal webserver, it could be fastify, express, etc.&lt;/span&gt;
&lt;span class="c1"&gt;// Just to illustrate you could run the exporter along a classic webserver&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startNormalWebServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`starting normal web server`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;http&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8089&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;startMetricsExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;startNormalWebServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the application with &lt;code&gt;node index.js&lt;/code&gt; to see 2 web servers. You can go to &lt;code&gt;localhost:8089&lt;/code&gt; to see the normal web server, and &lt;code&gt;localhost:9100/metrics&lt;/code&gt; to see the metrics endpoint.&lt;/p&gt;

&lt;p&gt;Here are more detailed explanations of what the code is doing: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;queryDatabaseStuff()&lt;/code&gt; is to mock a DB call that would probably fetch some data count, like outdated data.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;meter.createObservableGauge&lt;/code&gt; then records a Gauge, where you can pass a description and the expected value type. The part not very well documented in OTel is the &lt;code&gt;addCallback&lt;/code&gt; function, which allow you to run a function when the metrics endpoint is called. &lt;/p&gt;

&lt;p&gt;Be aware this approach is optimal for relatively light database queries. In situations where your query examines multiple databases, executing complex aggregations that can slow down the system, a more effective strategy is to run the query at predetermined intervals in the background. The recent results are stored and merely retrieved during the callback, rather than executing a comprehensive query each time. This tactic essentially minimizes the time-consuming database operations during the metrics' collection and maintains a responsive and efficient metrics endpoint.&lt;/p&gt;

&lt;p&gt;About the options passed to &lt;code&gt;PrometheusExporter&lt;/code&gt;, I would advise keeping the default which is to start a web server on a different port. Therefore you are sure you won’t expose the metrics by mistake to the outside world.&lt;/p&gt;

&lt;h2&gt;
  
  
  and... with Kubernetes? ☸️
&lt;/h2&gt;

&lt;p&gt;If you use Kubernetes as your underlying platform, you can then have a service exposing 2 ports, with only one of them being for the ingress of the application web server, and the other one for the &lt;code&gt;ServiceMonitor&lt;/code&gt; watching the metrics web server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="c1"&gt;# skip the boring part&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8089&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
            &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9100&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metrics&lt;/span&gt;
            &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8089&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metrics&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9100&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metrics&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring.coreos.com/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceMonitor&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-servicemonitor&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-web-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metrics&lt;/span&gt;
  &lt;span class="na"&gt;jobLabel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;my-web-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It makes it way easier to prevent exposing the metrics endpoint to the outside, you then don't have to juggle with ingress path routing to exclude &lt;code&gt;/metrics&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;OpenTelemetry is a great project, and I am glad to see it growing so fast, but it can be uneasy to get familiar with the Otel terms. I hope this blog post will help you as documentation is still missing for observability with nodeJS in general.&lt;/p&gt;

&lt;p&gt;Hope this post helps, and won’t be deprecated too fast!&lt;/p&gt;

</description>
      <category>node</category>
      <category>observability</category>
      <category>prometheus</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to stream big JSON files with low-memory footprint in nodeJS</title>
      <dc:creator>François</dc:creator>
      <pubDate>Thu, 17 Aug 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/how-to-stream-big-json-files-with-low-memory-footprint-in-nodejs-25g4</link>
      <guid>https://forem.com/francoislp/how-to-stream-big-json-files-with-low-memory-footprint-in-nodejs-25g4</guid>
      <description>&lt;p&gt;For production-grade applications, actions like reading a big file in memory is not ideal as they might cause OOM crash (Out Of Memory), and require a lot of resources for sometimes a really short duration. It would be a waste of resources and money to have a big server to only handle rare cases, and run at 5% of the capacity the rest of the time.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Legend:&lt;/strong&gt; Example of a container with average memory consumption between 50 mb to 150 mb, when processing files it spikes to 4GB of RAM, leading to OOM crash. &lt;/p&gt;

&lt;p&gt;Most of the time, especially with applications running on Kubernetes (like the one from the screenshot), a best practice is to stream data instead of loading it in memory. Here is how you can do it using Stream Transformers, a less-known Node.js feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming big objects with nodejs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Generate data
&lt;/h3&gt;

&lt;p&gt;Generate dummy data with this one-liner to paste in a terminal, to simulate a big JSON file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
require('node:fs').writeFile('dummy.json', JSON.stringify(
  new Array(process.argv[2] || 50).fill({ id: 0, name: 'dummy' })
), _ =&amp;gt; console.log('done'))
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  JSON stream NPM Module
&lt;/h2&gt;

&lt;p&gt;To help with processing JSON, we can use a tiny library like &lt;code&gt;stream-json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As written on the GitHub README:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;stream-json is a micro-library of node.js stream components with minimal dependencies for creating custom data processors oriented on processing huge JSON files while requiring a minimal memory footprint. It can parse JSON files far exceeding available memory. Even individual primitive data items (keys, strings, and numbers) can be streamed piece-wise.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Install the json stream module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;stream-json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Transform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;streamArray&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream-json/streamers/streamArray&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream-json/utils/Batch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream-json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Memory usage: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
      &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MB`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customTransformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;objectMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// to accept arrays&lt;/span&gt;
  &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;logMemory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chunk length: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;// read from file&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dummy.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// parse JSON&lt;/span&gt;
  &lt;span class="nf"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="c1"&gt;// takes an array of objects and produces a stream of its components&lt;/span&gt;
  &lt;span class="nf"&gt;streamArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="c1"&gt;// batches items into arrays&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Batch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nx"&gt;customTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pipeline failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pipeline succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it should give an output similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  node-stream-demo node index.js
Memory usage: 4.6 MB
chunk length:  2
Memory usage: 4.64 MB
chunk length:  2
&lt;span class="o"&gt;[&lt;/span&gt;...]
chunk length:  2
Memory usage: 4.32 MB
chunk length:  2
Memory usage: 4.33 MB
chunk length:  2
Memory usage: 4.33 MB
chunk length:  2
Pipeline succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2 interesting parts are in the &lt;code&gt;pipeline()&lt;/code&gt; function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;new Batch({ batchSize: 2 })&lt;/code&gt; grouping data 2 by 2, very convenient to process data in batches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;customTransformer&lt;/code&gt; to do async operations on the batches, like inserting data in a database by X.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hope the example helps and provides ideas on how to use Node Stream Transformers!&lt;/p&gt;

</description>
      <category>node</category>
    </item>
    <item>
      <title>How to debug RDS database from a Kubernetes cluster</title>
      <dc:creator>François</dc:creator>
      <pubDate>Sat, 05 Aug 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/how-to-debug-rds-database-from-a-kubernetes-cluster-2220</link>
      <guid>https://forem.com/francoislp/how-to-debug-rds-database-from-a-kubernetes-cluster-2220</guid>
      <description>&lt;p&gt;When running Kubernetes workflows, most people tend to keep the stateless applications like applications on the cluster, and the stateful applications like databases outside of the cluster, on the same VPC. This is a good practice, but it makes debugging a bit more complicated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: I use AWS as an example, but this can be applied to any cloud provider with managed database &amp;amp; K8s using private networks&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I assume your database is &lt;strong&gt;private&lt;/strong&gt; and only accessible from a VPC. Maybe for some obscure reasons, you still need to access this database to debug something (Staging database of course &lt;em&gt;cough cough&lt;/em&gt;). Well, it might be a bit annoying. &lt;/p&gt;

&lt;p&gt;One way would be to spawn an EC2 instance inside the same VPC, install a bunch of tools like &lt;code&gt;netcat&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt;, &lt;code&gt;postgres&lt;/code&gt; and debug from the VM. Not very convenient, you might left resources running and it's not very reproducible.&lt;/p&gt;

&lt;p&gt;What I would advise, is to use a "reverse-proxy" container inside your cluster. Being on the same VPC as your managed database, you will be able to access it, and this container port can be port-forwarded to your machine. Of course, all the following expect you to have enough privileges to create a pod inside your cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;----------        -------        ------------&lt;/span&gt;
| Laptop |  &lt;span class="nt"&gt;&amp;lt;--&amp;gt;&lt;/span&gt;  | Pod |  &lt;span class="nt"&gt;&amp;lt;--&amp;gt;&lt;/span&gt;  | Database |
&lt;span class="p"&gt;----------        -------        ------------
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will need 2 terminal windows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One to create the pod opening the connection to the database&lt;/li&gt;
&lt;li&gt;One to port-forward the pod port to your local machine
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DB_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5432
&lt;span class="nv"&gt;DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;64eddd8e4b61-postgresql.993191f72591.eu-central-1.rds.amazonaws.com

kubectl run psql-tunnel &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alpine/socat &lt;span class="nt"&gt;--tty&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--expose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DB_PORT&lt;/span&gt; tcp-listen:&lt;span class="nv"&gt;$DB_PORT&lt;/span&gt;,fork,reuseaddr tcp-connect:&lt;span class="nv"&gt;$DB_HOST&lt;/span&gt;:&lt;span class="nv"&gt;$DB_PORT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is an explanation of each argument that might be useful to understand what is happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;socat&lt;/code&gt;: This image allows to establish two bidirectional byte streams and transfers data between them, perfect for our use case!&lt;/li&gt;
&lt;li&gt; &lt;code&gt;--rm&lt;/code&gt;: Removes the container automatically once it exits. (It helps in cleanup and avoids leaving unused containers.)&lt;/li&gt;
&lt;li&gt; &lt;code&gt;tcp-listen:$DB_PORT,fork,reuseaddr&lt;/code&gt;: Configures &lt;code&gt;socat&lt;/code&gt; to listen on TCP port &lt;code&gt;5432&lt;/code&gt; inside the container. The &lt;code&gt;fork&lt;/code&gt; option allows handling multiple connections, and &lt;code&gt;reuseaddr&lt;/code&gt; allows reusing the address.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;tcp-connect:$DB_HOST:$DB_PORT&lt;/code&gt;: Specifies the destination address and port where the incoming connections to &lt;code&gt;5432&lt;/code&gt; inside the container will be forwarded. It connects to the host &lt;code&gt;64eddd8e4b61-postgresql.993191f72591.eu-central-1.rds.amazonaws.com&lt;/code&gt; on port &lt;code&gt;5432&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the second command to port-forward the container port to localhost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward psql-tunnel 54323:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now use any GUI to connect to the DB and visualize what is happening (TablePlus, DBeaver, PhpMyAdmin, etc)!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Sending slack notifications with github actions</title>
      <dc:creator>François</dc:creator>
      <pubDate>Sun, 02 Jul 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/sending-slack-notifications-with-github-actions-2gc7</link>
      <guid>https://forem.com/francoislp/sending-slack-notifications-with-github-actions-2gc7</guid>
      <description>&lt;p&gt;This is a small post about sending neat slack notifications from Github Actions. If it looks good, you get developers to click on the notification, trust me 😄&lt;/p&gt;

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

&lt;p&gt;Here are the steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new slack app from &lt;a href="https://api.slack.com/apps" rel="noopener noreferrer"&gt;https://api.slack.com/apps&lt;/a&gt;, named &lt;code&gt;Github actions&lt;/code&gt; and with a cool github logo&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;Incoming webhooks&lt;/code&gt;, create a webhook, and save the webhook as a Github Actions &lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer"&gt;repository secret&lt;/a&gt; with the name SLACK_NOTIFICATIONS_TOKEN&lt;/li&gt;
&lt;li&gt;Add the newly created app to your channel: Channel settings &amp;gt; Integrations &amp;gt; Apps &amp;gt; Add apps. You should see a message on the channel: &lt;em&gt;&lt;code&gt;John Doe added an integration to this channel: Github Actions&lt;/code&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;From the same setting page, get the Channel ID, you will need to set it in the YAML.&lt;/li&gt;
&lt;li&gt;Tweak your existing YAML manifest in &lt;code&gt;.github/workflows&lt;/code&gt; to add the following snippet:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# [...]&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Deployment'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# - name: Deploy&lt;/span&gt;
      &lt;span class="c1"&gt;#   run: deploy.sh&lt;/span&gt;
      &lt;span class="c1"&gt;# [...]&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notify slack&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slackapi/slack-github-action@v1.24.0&lt;/span&gt;
        &lt;span class="c1"&gt;# https://api.slack.com/apps/&amp;lt;your-app-id&amp;gt;/incoming-webhooks&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ failure() }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;channel-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;F04D4MXFY3R'&lt;/span&gt; &lt;span class="c1"&gt;# slack channel #alerts-test&lt;/span&gt;
          &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"text": "GitHub Action failed",&lt;/span&gt;
              &lt;span class="s"&gt;"blocks": [&lt;/span&gt;
                &lt;span class="s"&gt;{&lt;/span&gt;
                  &lt;span class="s"&gt;"type": "section",&lt;/span&gt;
                  &lt;span class="s"&gt;"text": {&lt;/span&gt;
                    &lt;span class="s"&gt;"type": "mrkdwn",&lt;/span&gt;
                    &lt;span class="s"&gt;"text": "*Production deployment check failed* :fire_engine:\n High error rate detected after deployment! Rolling back..."&lt;/span&gt;
                  &lt;span class="s"&gt;}&lt;/span&gt;
                &lt;span class="s"&gt;},&lt;/span&gt;
                &lt;span class="s"&gt;{&lt;/span&gt;
                  &lt;span class="s"&gt;"type": "actions",&lt;/span&gt;
                  &lt;span class="s"&gt;"elements": [&lt;/span&gt;
                    &lt;span class="s"&gt;{&lt;/span&gt;
                      &lt;span class="s"&gt;"type": "button",&lt;/span&gt;
                      &lt;span class="s"&gt;"text": {&lt;/span&gt;
                        &lt;span class="s"&gt;"type": "plain_text",&lt;/span&gt;
                        &lt;span class="s"&gt;"text": ":github: Failed action"&lt;/span&gt;
                      &lt;span class="s"&gt;},&lt;/span&gt;
                      &lt;span class="s"&gt;"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"&lt;/span&gt;
                    &lt;span class="s"&gt;},&lt;/span&gt;
                    &lt;span class="s"&gt;{&lt;/span&gt;
                      &lt;span class="s"&gt;"type": "button",&lt;/span&gt;
                      &lt;span class="s"&gt;"text": {&lt;/span&gt;
                        &lt;span class="s"&gt;"type": "plain_text",&lt;/span&gt;
                        &lt;span class="s"&gt;"text": ":grafana: Grafana dashboard"&lt;/span&gt;
                      &lt;span class="s"&gt;},&lt;/span&gt;
                      &lt;span class="s"&gt;"url": "https://&amp;lt;your_organization&amp;gt;.grafana.net/d/my-app-dashboard?from=now-1h&amp;amp;to=now"&lt;/span&gt;
                    &lt;span class="s"&gt;}&lt;/span&gt;
                  &lt;span class="s"&gt;]&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;]&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLACK_NOTIFICATIONS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;SLACK_WEBHOOK_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;INCOMING_WEBHOOK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feel free to customize the "mrkdwn" part as you want, you can learn more about formatting here: &lt;a href="https://api.slack.com/reference/surfaces/formatting" rel="noopener noreferrer"&gt;https://api.slack.com/reference/surfaces/formatting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the buttons, I like to add the failed action that sent the notification, and a link to the production app grafana dashboard.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note 1:&lt;/strong&gt; You can notice I use custom slack emojis to ✨beautify✨ the slack message: &lt;code&gt;github&lt;/code&gt; and &lt;code&gt;grafana&lt;/code&gt;. &lt;br&gt;
Check this link on how to add custom emojis if you don't know yet: &lt;a href="https://slack.com/help/articles/206870177-Add-custom-emoji-and-aliases-to-your-workspace" rel="noopener noreferrer"&gt;https://slack.com/help/articles/206870177-Add-custom-emoji-and-aliases-to-your-workspace&lt;/a&gt;&lt;br&gt;
You can go to &lt;a href="https://slackmojis.com/" rel="noopener noreferrer"&gt;https://slackmojis.com/&lt;/a&gt; to find the Github and the Grafana ones.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note 2:&lt;/strong&gt; You can use &lt;code&gt;if: ${{ failure() }}&lt;/code&gt; on the step level to only send the notification when the previous step (the deployment) failed :D&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;2026 updates&lt;/strong&gt;: I got tired of hacky solutions so I built my own tool for getting the best GitHub notifications in your Slack DMs. Give it a try! &lt;a href="https://www.gitnotifier.com?utm_source=dev-to" rel="noopener noreferrer"&gt;gitnotifier.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>slack</category>
      <category>devops</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>How to add a MySQL Planetscale database as a grafana cloud datasource</title>
      <dc:creator>François</dc:creator>
      <pubDate>Fri, 19 May 2023 00:00:00 +0000</pubDate>
      <link>https://forem.com/francoislp/how-to-add-a-mysql-planetscale-database-as-a-grafana-cloud-datasource-5486</link>
      <guid>https://forem.com/francoislp/how-to-add-a-mysql-planetscale-database-as-a-grafana-cloud-datasource-5486</guid>
      <description>&lt;p&gt;The grafana cloud free plan is amazing for most of personal projects! I use it a lot on &lt;a href="https://fixtheops.dev" rel="noopener noreferrer"&gt;https://fixtheops.dev&lt;/a&gt;, and recently I wanted to display some data from my platnetscale mysql database to visualise the user growth. Platnetscale free plan being amazing as well, I highly advise the platform (&lt;a href="https://planetscale.com/" rel="noopener noreferrer"&gt;https://planetscale.com/&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;First you need to add a new datasource, in our case: mysql.&lt;/p&gt;

&lt;p&gt;Then head over to Planetscale, and create a new password with &lt;strong&gt;read-only rights&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This part is important, as from grafana you can run DROP commands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then in the datasource add the host, user, password. As Planetscale is using SSL, make sure to toggle "With CA cert" option.&lt;/p&gt;

&lt;p&gt;In authentication details, you need to provide the TLS/SSL root certificate. Here is how to obtain it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# if you have pbcopy on mac:&lt;/span&gt;
curl https://letsencrypt.org/certs/isrgrootx1.pem | pbcopy

&lt;span class="c"&gt;# otherwise, copy paste what you get from the curl command&lt;/span&gt;
curl https://letsencrypt.org/certs/isrgrootx1.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How it should look like:&lt;/p&gt;

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

&lt;p&gt;And here you are! All good to build beautiful dashboards for free ✨&lt;/p&gt;

</description>
      <category>grafana</category>
      <category>mysql</category>
    </item>
  </channel>
</rss>
