<?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: André Escobar</title>
    <description>The latest articles on Forem by André Escobar (@andre_escobar).</description>
    <link>https://forem.com/andre_escobar</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%2F2904611%2Fd9003948-b89a-4831-b3cc-e36964cdd70c.jpeg</url>
      <title>Forem: André Escobar</title>
      <link>https://forem.com/andre_escobar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/andre_escobar"/>
    <language>en</language>
    <item>
      <title>The Ultimate Guide to Web Application Observability</title>
      <dc:creator>André Escobar</dc:creator>
      <pubDate>Wed, 02 Apr 2025 01:23:01 +0000</pubDate>
      <link>https://forem.com/andre_escobar/the-ultimate-guide-to-web-application-observability-2ebc</link>
      <guid>https://forem.com/andre_escobar/the-ultimate-guide-to-web-application-observability-2ebc</guid>
      <description>&lt;p&gt;In modern web apps, keeping an eye on how things are running is a big deal. Tools like Prometheus, Grafana, and Alloy give us a way to monitor our apps, check performance, and track down issues before they become a problem.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll walk you through how to set up observability for a Nuxt-based server-side app that runs in a Docker container. We’ll use Prometheus to collect metrics and Grafana to visualize them. Let’s dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Observability?
&lt;/h2&gt;

&lt;p&gt;Simply put, observability is all about understanding what’s happening inside your system based on what it’s outputting like logs, metrics, and traces. With these, you can spot problems, troubleshoot, and make sure everything’s running smoothly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tools We’ll Use
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus: This open-source toolkit helps us gather performance data over time and analyze it with PromQL (a fancy query language). It’s like our system’s health monitor.&lt;/li&gt;
&lt;li&gt;Grafana: It connects to Prometheus and gives us nice charts and dashboards to visualize our app’s performance. Think of it as the app’s “health dashboard.”&lt;/li&gt;
&lt;li&gt;Alloy: Alloy makes it easy to integrate Prometheus and Grafana to monitor web apps, simplifying the setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We won’t go super deep into how these tools work internally, but we’ll cover what they do and how to set them up for your Nuxt app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Prometheus Registry in Nuxt
&lt;/h2&gt;

&lt;p&gt;Let’s start by adding a Prometheus registry to the backend of our Nuxt app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Prometheus Client
&lt;/h3&gt;

&lt;p&gt;We need the prom-client package. Go ahead and install it:&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;prom-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Metrics Service
&lt;/h3&gt;

&lt;p&gt;Inside your Nuxt app’s server folder, create a file &lt;code&gt;server/services/prometheus.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;collectDefaultMetrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Counter&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;prom-client&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;registry&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;Registry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Collect default metrics like CPU usage, memory usage, etc.&lt;/span&gt;
&lt;span class="nf"&gt;collectDefaultMetrics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a custom metric for counting HTTP requests&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;httpRequestsTotal&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;Counter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&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_requests_total&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Total number of HTTP requests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;labelNames&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;method&lt;/span&gt;&lt;span class="dl"&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;route&lt;/span&gt;&lt;span class="dl"&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;status_code&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerMetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;httpRequestsTotal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sets up the Prometheus registry, collects some basic metrics (like CPU and memory), and also adds a custom metric to count HTTP requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expose the Metrics
&lt;/h3&gt;

&lt;p&gt;Next, let’s make those metrics available through an API route. Create &lt;code&gt;server/routes/metrics.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;registry&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;./metrics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &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="o"&gt;=&amp;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;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metrics&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;Once the app is up and running, head to &lt;a href="http://localhost:3000/metrics" rel="noopener noreferrer"&gt;http://localhost:3000/metrics&lt;/a&gt;, and you should see Prometheus-formatted metrics.&lt;/p&gt;

&lt;p&gt;It’ll look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter

# HELP process_cpu_system_seconds_total Total system CPU time spent in seconds.
# TYPE process_cpu_system_seconds_total counter

...

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/api/metrics",status_code="200"} 42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to secure this endpoint, you can add basic authentication or restrict access by IP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Set Up Alloy for Grafana Scraping
&lt;/h2&gt;

&lt;p&gt;Now we need to set up Alloy to scrape those metrics from your Nuxt app. Alloy will help Grafana get the data it needs to visualize it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Alloy Config
&lt;/h3&gt;

&lt;p&gt;Create a file called config.alloy in your root project directory (or wherever you keep your Alloy config):&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="err"&gt;logging&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"info"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;prometheus.scrape&lt;/span&gt; &lt;span class="err"&gt;"nuxt_app"&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;[{&lt;/span&gt;
    &lt;span class="py"&gt;__address__&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"host.docker.internal:3000",&lt;/span&gt;
    &lt;span class="py"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"project_name",&lt;/span&gt;
  &lt;span class="err"&gt;}]&lt;/span&gt;
  &lt;span class="py"&gt;metrics_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/metrics"&lt;/span&gt;
  &lt;span class="py"&gt;forward_to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;[prometheus.remote_write.default.receiver]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;prometheus.scrape&lt;/span&gt; &lt;span class="err"&gt;"node_exporter"&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;[{&lt;/span&gt;
    &lt;span class="py"&gt;__address__&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"node-exporter:9100",&lt;/span&gt;
    &lt;span class="py"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"node-exporter",&lt;/span&gt;
    &lt;span class="py"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"project_name",&lt;/span&gt;
    &lt;span class="py"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;env("${CI_ENVIRONMENT_NAME}"),&lt;/span&gt;
  &lt;span class="err"&gt;}]&lt;/span&gt;
  &lt;span class="py"&gt;forward_to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;[prometheus.remote_write.default.receiver]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;prometheus.remote_write&lt;/span&gt; &lt;span class="err"&gt;"default"&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;endpoint&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;env("GRAFANA_CLOUD_API_KEY")&lt;/span&gt;
    &lt;span class="err"&gt;basic_auth&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;env("GRAFANA_CLOUD_USERNAME")&lt;/span&gt;
      &lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;env("GRAFANA_CLOUD_PASSWORD")&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config tells Alloy how to scrape the metrics from your Nuxt app and forward them to Grafana Cloud (or wherever you’re storing the data). It also sets up basic authentication for your Nuxt app’s &lt;code&gt;/metrics&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;Make sure to set the right environment variables for things like basic auth and Grafana credentials.&lt;/p&gt;

&lt;p&gt;If you have one or more enviroment, you can set on &lt;code&gt;prometheus.scrape&lt;/code&gt; a enviroment like &lt;code&gt;enviroment: env('CI_ENVIROMENT_NAME')&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Running Alloy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;We’re going to run both the Nuxt app and Alloy using Docker Compose. It’ll simplify things like networking and orchestration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Docker Compose File
&lt;/h3&gt;

&lt;p&gt;Here’s a simple &lt;code&gt;docker-compose.yml&lt;/code&gt; file to get everything running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nuxt_app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nuxt_app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;node:lts-alpine'&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./:/app'&lt;/span&gt;  &lt;span class="c1"&gt;# Mount the app code&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NUXT_HOST=0.0.0.0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NUXT_PORT=3000&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dev'&lt;/span&gt;

  &lt;span class="na"&gt;alloy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alloy&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grafana/alloy:latest'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CI_ENVIRONMENT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${CI_ENVIRONMENT_NAME}&lt;/span&gt;
      &lt;span class="na"&gt;GRAFANA_CLOUD_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GRAFANA_CLOUD_USERNAME}&lt;/span&gt;
      &lt;span class="na"&gt;GRAFANA_CLOUD_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GRAFANA_CLOUD_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;GRAFANA_CLOUD_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GRAFANA_CLOUD_API_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./config.alloy:/etc/alloy/config.alloy'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12345:12345'&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--server.http.listen-addr=0.0.0.0:12345&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--storage.path=/var/lib/alloy/data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/etc/alloy/config.alloy'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will spin up both services: your Nuxt app and Alloy. The Nuxt app will be available at &lt;code&gt;http://localhost:3000&lt;/code&gt;, and Alloy will be at &lt;code&gt;http://localhost:12345&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Visualizing Metrics with Grafana
&lt;/h2&gt;

&lt;p&gt;Now that Prometheus is scraping metrics and Alloy is running, you can hook Grafana up to your Prometheus instance. From there, you can create dashboards to keep an eye on your app’s health and performance in real time.&lt;/p&gt;




&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;With this setup, you’ve got a streamlined way to monitor your Nuxt app inside Docker using Prometheus, Grafana, and Alloy. It’s an efficient way to track performance and quickly spot issues before they affect your users.&lt;/p&gt;

&lt;p&gt;Hope this guide helped you get things set up! If you have any questions or need some tweaks, feel free to reach out!&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/alloy/latest/" rel="noopener noreferrer"&gt;Grafana - Alloy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/artmizu/nuxt-prometheus" rel="noopener noreferrer"&gt;Nuxt Prometheus - by Artmizu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/siimon/prom-client" rel="noopener noreferrer"&gt;Prometheus client for node.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Fix Hydration Mismatch Errors in Nuxt App's</title>
      <dc:creator>André Escobar</dc:creator>
      <pubDate>Sat, 01 Mar 2025 01:20:45 +0000</pubDate>
      <link>https://forem.com/andre_escobar/how-to-fix-hydration-mismatch-errors-in-nuxt-apps-40oa</link>
      <guid>https://forem.com/andre_escobar/how-to-fix-hydration-mismatch-errors-in-nuxt-apps-40oa</guid>
      <description>&lt;p&gt;If you've ever seen those annoying "Hydration mismatch" errors in your Nuxt app, you know how frustrating they can be. They usually happen when the HTML your server sends doesn't match what Vue expects when it takes over on the client side. But don’t worry! In this post, I’ll walk you through how to track down these issues and fix them, based on my own experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  First off all, what is Hydration?
&lt;/h3&gt;

&lt;p&gt;Hydration is the process that happens after Nuxt renders a page on the server and sends it to the browser. First, Nuxt runs your Vue.js code on the server, generating a fully rendered HTML page. The browser downloads this HTML and displays it instantly, just like a traditional server-rendered page. Then, Vue.js takes over, running the same JavaScript code in the browser and attaching event listeners to make the page interactive. This step is called hydration. Once hydration is complete, the page is fully interactive with features like dynamic updates and smooth transitions.&lt;/p&gt;

&lt;p&gt;However, if the HTML generated on the server doesn't exactly match what Vue tries to render on the client, you get a hydration mismatch error. Let's dive into what causes these errors and how to fix them.&lt;/p&gt;

&lt;p&gt;If you want to get more into the nitty-gritty of rendering modes in Nuxt, check out the &lt;a href="https://nuxt.com/docs/guide/concepts/rendering" rel="noopener noreferrer"&gt;docs&lt;/a&gt; for a deeper dive.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Causes Hydration Mismatch?
&lt;/h3&gt;

&lt;p&gt;A hydration mismatch happens when the server and client render things differently. One common issue is using random values like Math.random(), timestamps, or UUIDs in SSR. Another cause is relying on browser-only APIs, such as &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, or &lt;code&gt;localStorage&lt;/code&gt;, which don’t exist during server rendering.&lt;/p&gt;

&lt;p&gt;Differences in computed properties or reactive state can also lead to mismatches, especially if they depend on data that changes between SSR and CSR. API calls returning different results on the server and client create inconsistencies too.&lt;/p&gt;

&lt;p&gt;Invalid HTML structure is another culprit. Placing a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; inside a &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; tag, for example, leads to an automatic browser correction that changes the DOM structure, causing discrepancies. Similarly, dynamically rendered elements that apply different attributes between SSR and CSR—such as conditional classes or IDs can trigger hydration errors.&lt;/p&gt;

&lt;h4&gt;
  
  
  Here’s a simple Vue component that can cause a hydration mismatch:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;randomNumber&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randomNumber&lt;/span&gt; &lt;span class="o"&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="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this component is server-rendered, &lt;code&gt;randomNumber&lt;/code&gt; is generated once and included in the initial HTML. However, when the client re-renders it, a new random number is generated, leading to a mismatch between the SSR and CSR output.&lt;/p&gt;

&lt;h4&gt;
  
  
  Another Example: Number Formatting Issue I Faced This Week
&lt;/h4&gt;

&lt;p&gt;This past week, I ran into a real-world case of hydration mismatch when formatting numbers differently between the server and client. &lt;/p&gt;

&lt;p&gt;Consider the following function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;floatToLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;floatCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NumberFormatOptions&lt;/span&gt; &lt;span class="o"&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;const&lt;/span&gt; &lt;span class="nx"&gt;floatCurrencyFixed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertCurrencyValueToFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floatCurrency&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;twoPlacedFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;floatCurrencyFixed&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="nf"&gt;toString&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minMaxFractionDigits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floatCurrencyFixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;twoPlacedFloat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt-BR&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;minimumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;minMaxFractionDigits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maximumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;minMaxFractionDigits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BRL&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;config&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;This function takes a float and formats it using &lt;code&gt;toLocaleString()&lt;/code&gt;. The issue arose because &lt;code&gt;toLocaleString()&lt;/code&gt; can produce slightly different outputs depending on the environment (server vs. client). Small rounding variations occurred when converting floating-point numbers, leading to different HTML structures and causing a hydration mismatch.&lt;/p&gt;

&lt;p&gt;To fix this, I ensured that the server and client always returned the same formatted value by rounding it explicitly before formatting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;floatToLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;floatCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NumberFormatOptions&lt;/span&gt; &lt;span class="o"&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;const&lt;/span&gt; &lt;span class="nx"&gt;floatCurrencyFixed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertCurrencyValueToFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floatCurrency&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;roundedValue&lt;/span&gt; &lt;span class="o"&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floatCurrencyFixed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt-BR&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;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BRL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;minimumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maximumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roundedValue&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;By explicitly rounding the value with &lt;code&gt;Math.ceil()&lt;/code&gt;, I eliminated inconsistencies that arose from fractional differences, ensuring the output remained the same between SSR and CSR. This small change saved me a lot of debugging time!&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Debug Hydration Mismatch
&lt;/h3&gt;

&lt;p&gt;One of the simplest ways to catch hydration issues is to disable JavaScript in your browser and compare the raw HTML with the fully rendered page. You can do this by opening DevTools, going to the Network tab, and checking the HTML response under the &lt;code&gt;document&lt;/code&gt; request. If things look different, you’ve found a clue.&lt;/p&gt;

&lt;p&gt;Another useful trick is comparing the page source with the rendered DOM. Right-click on your page, select &lt;code&gt;View Page Source&lt;/code&gt;, and compare it with what’s displayed in the Elements tab of DevTools. If there are differences, something is changing after hydration.&lt;/p&gt;

&lt;p&gt;For an even easier approach, you can use the nuxt-hydration module by &lt;a href="https://github.com/huang-julien/nuxt-hydration" rel="noopener noreferrer"&gt;huang-julien/nuxt-hydration&lt;/a&gt;, which show exactly where hydration mismatches occur. To install it, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn|pnpm|npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; nuxt-hydration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, add it to your &lt;code&gt;nuxt.config.ts&lt;/code&gt;. When enabled, it will show a interface with hydration mismatches, helping you pinpoint the code that is causing the issue.&lt;/p&gt;

&lt;p&gt;While this module can be helpful for identifying which part of your code triggers the hydration error, it’s worth noting that it may not always offer a complete solution. The solution can sometimes be a bit generic, so while it helps you track down the general area of the problem, you may still need to dig deeper to fix the root cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;Hydration mismatches in Nuxt can be frustrating, but they’re definitely fixable. By checking raw HTML, using nuxt-hydration, and ensuring consistency between SSR and CSR, you can avoid these issues and keep your app running smoothly. &lt;/p&gt;

&lt;p&gt;However, it’s important to note that these hydration errors aren’t just annoying—they can escalate into more serious problems, like 500 errors, which may crash your app and significantly disrupt the user experience. I’ve personally run into this, and it can be quite challenging to pinpoint and resolve under pressure.&lt;/p&gt;

&lt;p&gt;So, &lt;strong&gt;don’t ignore hydration errors&lt;/strong&gt;. It’s easy to let them slide, but accumulating these issues over time can make it harder to fix them all at once. Addressing them early will save you time in the long run and ensure a smoother, more reliable app. The more you delay, the more difficult it becomes to track down and resolve each individual issue.&lt;/p&gt;

&lt;p&gt;This is my first post on the DEV.to, and I’m excited to share more of my experiences from my career as a software engineer. I hope to contribute more insights and lessons learned along the way.&lt;/p&gt;

&lt;p&gt;Have you run into hydration mismatches before? What tricks helped you fix them? Drop a comment and let’s talk!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
