Prometheus Metrics and Grafana Dashboard

Instrument a Node.js API with Prometheus metrics and build a Grafana dashboard.

Introduction

30 Second Summary

When your app goes down in production, the first question everyone asks is "what happened?" Without monitoring, you're flying blind.

In this project, you will instrument a Node.js Express API with Prometheus metrics, configure automated metric collection with Docker Compose, and build a Grafana dashboard that visualizes your app's health in real time.

What You'll Build

A local observability stack consisting of a Node.js Express API, Prometheus for metrics collection, and Grafana for visualization, all orchestrated with Docker Compose.

By the end of this project, you'll have:

  • A Node.js Express app with a /metrics endpoint exposing Prometheus-format metrics including request counts, durations, and active connections.
  • A Docker Compose stack running Prometheus and Grafana alongside your app, with Prometheus scraping metrics every 15 seconds.
  • A Grafana dashboard with 4 panels visualizing request rate, latency percentiles, error rate, and active connections using PromQL queries.
  • Secret Mission: Add a Grafana alert rule that fires when error rate exceeds 10%.

Are there any prerequisites?

You should have basic familiarity with Node.js and Express. You should also be comfortable running terminal commands. No prior experience with Prometheus, Grafana, or Docker is required.

Set Up Your Project and Install Dependencies

Monitoring starts with having something to monitor. In this step, you'll set up a Node.js project with the libraries needed to expose metrics that Prometheus can scrape.

You'll use Express as your web framework and prom-client as your Prometheus metrics library. Together, they'll let your app serve metrics alongside its regular API responses.

In this step, get ready to:
  • Confirm Node.js and Docker Desktop are working.
  • Create the project folder and subdirectories.
  • Initialize the project and install Express and prom-client.
Confirm your tools are ready

Your Node.js runtime and Docker Desktop need to be working before you can build and run the observability stack. Run a quick version check on each to confirm.

  • In your terminal, confirm Node.js is ready by running:
node --version

You should see a version number like v20.x.x or higher printed in the terminal.

  • Confirm Docker is available by running:
docker --version

You should see a version string starting with Docker version followed by a number.

Command not found?

If node --version fails, revisit the Node.js installation for your operating system at nodejs.org.

If docker --version fails, make sure Docker Desktop is running. Look for the Docker whale icon in your system tray or menu bar.

help me troubleshoot why node or docker commands aren't found in my terminal

Create the project folder structure

Your project needs a dedicated folder on your Desktop, plus subdirectories to organize source code and Prometheus configuration separately.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir observability-demo
cd observability-demo
  • Create the subdirectories for your source code and Prometheus config by running:
mkdir src prometheus
  • Confirm the folders exist by running:
ls

You should see prometheus and src listed in the output.

What are these folders for?

The src/ folder holds your Node.js application code. The prometheus/ folder holds the Prometheus configuration file that tells Prometheus where to find your app's metrics.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the observability-demo folder on your Desktop, then click Open.

Don't see the folder?

Make sure you navigate to your Desktop in the file picker. The observability-demo folder should be right there if the mkdir command succeeded.

help me find my observability-demo folder on the Desktop

Initialize the project and install dependencies

Every Node.js project starts with a package.json file that defines its name, entry point, and dependencies. You'll scaffold one with npm and then customize it for this project.

  • Generate a default package.json by running this command in your terminal:
npm init -y

You should see a JSON object printed in the terminal confirming the file was created.

What does npm init -y do?

The npm init command creates a package.json file for your project. The -y flag accepts all defaults so you skip the interactive wizard.

The defaults need a few tweaks to match your project's structure. You'll update the description, entry point, and start script.

  • In VS Code's Explorer sidebar, click package.json to open it.
  • Select all the content in the file and replace it with the following:
{
  "name": "observability-demo",
  "version": "1.0.0",
  "description": "Node.js app with Prometheus metrics and Grafana dashboard",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "express": "4.21.2",
    "prom-client": "15.1.3"
  }
}

What did you change?

  • description tells anyone reading this file what the project does.
  • main points to src/index.js where your Express app will live.
  • scripts.start lets you run the app with npm start instead of typing the full node command.
  • dependencies pins Express at 4.21.2 and prom-client at 15.1.3 so everyone gets the same versions.
  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Install the dependencies by running this command in your terminal:
npm install

You should see output showing packages being added, ending with a summary like added 67 packages (the exact number may vary).

  • Confirm the install worked by running:
ls node_modules

You should see a list of installed package folders including express and prom-client.

npm install failed?

Make sure your terminal is inside the observability-demo folder. Run pwd to check your current directory.

If you see a JSON parse error, double-check that package.json is valid JSON. A missing comma or bracket is the most common culprit.

help me fix my npm install error in the observability-demo project

✔️ Awesome, I've got everything!

Great. Double check you've saved package.json and your node_modules folder is present.

ⓧ I'd like to double check the full code

Here's what your package.json should look like:

{
  "name": "observability-demo",
  "version": "1.0.0",
  "description": "Node.js app with Prometheus metrics and Grafana dashboard",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "express": "4.21.2",
    "prom-client": "15.1.3"
  }
}

Your project skeleton is ready with Express and prom-client installed. Next up, you'll write the metrics module and Express app that actually exposes Prometheus-format data.

Instrument Your Express App with Prometheus Metrics

Your project folder is ready with Express and prom-client installed. Now you need a way to answer the question "is my app healthy?" at any given moment.

In this step, you'll create a dedicated metrics module that defines three Prometheus metric types. Then you'll wire them into an Express app using middleware that automatically records data about every request.

In this step, get ready to:
  • Create a metrics module with a Counter, Histogram, and Gauge.
  • Build Express middleware that records request method, route, status code, and duration.
  • Add a /metrics endpoint that exposes all data in Prometheus format.
Create the metrics module

Before your app can report its health, you need to define what "health" means in numbers. The prom-client library gives you three metric types that together cover the RED methodology: Rate, Errors, and Duration.

  • Create a new file called metrics.js inside the src/ folder of your observability-demo project.
  • Paste the following code into src/metrics.js:
const client = require('prom-client');

const register = new client.Registry();

client.collectDefaultMetrics({ register, prefix: 'nodejs_' });

const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
  registers: [register],
});

const httpActiveConnections = new client.Gauge({
  name: 'http_active_connections',
  help: 'Number of active HTTP connections',
  registers: [register],
});

module.exports = {
  register,
  httpRequestsTotal,
  httpRequestDuration,
  httpActiveConnections,
};

What does this code do?

This module defines three custom metrics that map to the RED methodology:

  • Counter (http_requests_total): a number that only goes up. Tracks total requests, sliced by method, route, and status code.
  • Histogram (http_request_duration_seconds): sorts each request's duration into buckets. This lets you calculate percentiles like p50, p95, and p99 later.
  • Gauge (http_active_connections): a value that goes up and down. Tracks how many requests are in-flight right now.

The Registry is the central collection point for all metrics. collectDefaultMetrics adds Node.js-specific metrics like event loop lag and memory usage automatically.

  • Save src/metrics.js.
  • You won't see a change yet. The next file (index.js) imports this module and uses it.

Seeing a red underline in your editor?

If your editor shows a warning about require not being defined, that is normal. Some editor configurations expect ES module syntax. The code runs fine in Node.js as-is because package.json does not set "type": "module", so Node.js defaults to CommonJS.

Help me fix the red underline in my metrics.js file.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/metrics.js file for comparison:

const client = require('prom-client');

const register = new client.Registry();

client.collectDefaultMetrics({ register, prefix: 'nodejs_' });

const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
  registers: [register],
});

const httpActiveConnections = new client.Gauge({
  name: 'http_active_connections',
  help: 'Number of active HTTP connections',
  registers: [register],
});

module.exports = {
  register,
  httpRequestsTotal,
  httpRequestDuration,
  httpActiveConnections,
};
Build the Express app with metrics middleware

Now you need an Express app that uses these metrics. The key is middleware that sits between every incoming request and your route handlers, automatically measuring everything.

  • Create a new file called index.js inside the src/ folder.
  • Paste the following code into src/index.js:
const express = require('express');
const {
  register,
  httpRequestsTotal,
  httpRequestDuration,
  httpActiveConnections,
} = require('./metrics');

const app = express();
const PORT = 3001;

// Metrics middleware
app.use((req, res, next) => {
  httpActiveConnections.inc();
  const end = httpRequestDuration.startTimer();

  res.on('finish', () => {
    httpActiveConnections.dec();
    const route = req.route ? req.route.path : req.path;
    const labels = {
      method: req.method,
      route: route,
      status_code: res.statusCode,
    };
    httpRequestsTotal.inc(labels);
    end(labels);
  });

  next();
});

// Application routes
app.get('/', (req, res) => {
  res.json({ message: 'Hello from the observability demo!' });
});

app.get('/api/users', (req, res) => {
  const delay = Math.random() * 200;
  setTimeout(() => {
    res.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  }, delay);
});

app.get('/api/slow', (req, res) => {
  const delay = 1000 + Math.random() * 2000;
  setTimeout(() => {
    res.json({ message: 'This was slow on purpose' });
  }, delay);
});

app.get('/api/error', (req, res) => {
  res.status(500).json({ error: 'Something went wrong!' });
});

What does this middleware do?

The app.use function runs before every route handler. When a request arrives, it increments the active connections gauge and starts a timer.

The res.on('finish') callback fires after the response is sent. At that point, the middleware decrements the gauge, captures the final status code, and records both the request count and duration with labels for method, route, and status code.

The four routes simulate different scenarios: a fast response (/), a variable-speed API (/api/users), an intentionally slow endpoint (/api/slow), and a guaranteed error (/api/error).

  • Save src/index.js.
  • You won't see a change yet. The next chunk adds the /metrics endpoint and starts the server.
Add the /metrics endpoint

Prometheus collects data by sending HTTP GET requests to your app on a schedule. You need to expose a /metrics endpoint that returns all recorded metrics in the Prometheus exposition format.

  • In src/index.js, add the following code at the bottom of the file (after the /api/error route):
// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

app.listen(PORT, () => {
  console.log(`App running on http://localhost:${PORT}`);
  console.log(`Metrics at http://localhost:${PORT}/metrics`);
});

What does this code do?

The /metrics route calls register.metrics() to serialize every metric into a text format that Prometheus understands. The register.contentType header tells Prometheus how to parse the response. This header is required by Prometheus 3.0+.

The app listens on port 3001 to avoid conflicts with Grafana, which you'll run on port 3000 later.

  • Save src/index.js.
  • Start the app by running this command in your terminal:
npm start
  • Open your browser and navigate to http://localhost:3001/metrics.
  • You should see a wall of text starting with lines like # HELP and # TYPE followed by metric names and values. This is the Prometheus exposition format.

Not seeing metrics in the browser?

  • Check your terminal for error messages. If you see Cannot find module './metrics', make sure metrics.js is saved inside the src/ folder (not the project root).
  • If you see EADDRINUSE, another process is already using port 3001. Stop it with Ctrl+C in the other terminal, then try npm start again.
  • Make sure both src/metrics.js and src/index.js are saved before running.

My app won't start or I'm not seeing metrics at localhost:3001/metrics.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/index.js file for comparison:

const express = require('express');
const {
  register,
  httpRequestsTotal,
  httpRequestDuration,
  httpActiveConnections,
} = require('./metrics');

const app = express();
const PORT = 3001;

// Metrics middleware
app.use((req, res, next) => {
  httpActiveConnections.inc();
  const end = httpRequestDuration.startTimer();

  res.on('finish', () => {
    httpActiveConnections.dec();
    const route = req.route ? req.route.path : req.path;
    const labels = {
      method: req.method,
      route: route,
      status_code: res.statusCode,
    };
    httpRequestsTotal.inc(labels);
    end(labels);
  });

  next();
});

// Application routes
app.get('/', (req, res) => {
  res.json({ message: 'Hello from the observability demo!' });
});

app.get('/api/users', (req, res) => {
  const delay = Math.random() * 200;
  setTimeout(() => {
    res.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  }, delay);
});

app.get('/api/slow', (req, res) => {
  const delay = 1000 + Math.random() * 2000;
  setTimeout(() => {
    res.json({ message: 'This was slow on purpose' });
  }, delay);
});

app.get('/api/error', (req, res) => {
  res.status(500).json({ error: 'Something went wrong!' });
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

app.listen(PORT, () => {
  console.log(`App running on http://localhost:${PORT}`);
  console.log(`Metrics at http://localhost:${PORT}/metrics`);
});

Your app is now producing Prometheus-compatible metrics on every request. Next up, you'll configure Prometheus to scrape this endpoint automatically every 15 seconds using Docker Compose.

Configure Prometheus to Scrape Your App

Your Express app exposes a /metrics endpoint packed with data about request counts, durations, and active connections. The next challenge is getting something to actually collect that data on a regular schedule.

Prometheus is a time-series database that pulls (scrapes) metrics from your application at fixed intervals. In this step, you'll configure Prometheus to scrape your app every 15 seconds, then use Docker Compose to run both Prometheus and Grafana side by side.

In this step, get ready to:
  • Write a Prometheus configuration file that targets your Node.js app.
  • Create a Docker Compose file to run Prometheus and Grafana.
  • Start the stack and verify Prometheus is collecting metrics.
Write the Prometheus configuration file

Prometheus needs a configuration file that tells it where to find your app's metrics. This file defines the scrape interval and lists the targets Prometheus should poll.

  • Create the Prometheus config inside the prometheus/ directory by adding a new file called prometheus.yml with the following content:
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'nodejs-app'
    static_configs:
      - targets: ['host.docker.internal:3001']
    metrics_path: '/metrics'

What does this configuration do?

The global.scrape_interval tells Prometheus to pull metrics from every target every 15 seconds.

Under scrape_configs, you define a job called nodejs-app. The targets array points to host.docker.internal:3001, which is how a Docker container reaches your host machine's port 3001. The metrics_path tells Prometheus which endpoint to hit.

  • Save prometheus/prometheus.yml.
  • Confirm the file exists by running:
ls prometheus/

You should see prometheus.yml listed in the output.

Don't see the file?

Make sure you created the file inside the prometheus/ folder, not in the project root. The prometheus/ directory should already exist from Step 1.

help me find where my prometheus.yml file ended up

Create the Docker Compose file

Docker Compose lets you define multiple containers in a single file and start them together. You'll define two services: Prometheus for collecting metrics, and Grafana for visualizing them.

  • In the root of your observability-demo folder, create a new file called docker-compose.yml with the following content:
services:
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    extra_hosts:
      - "host.docker.internal:host-gateway"

  grafana:
    image: grafana/grafana-oss
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana

volumes:
  prometheus-data:
  grafana-data:

What does this file do?

  • The prometheus service uses the official prom/prometheus image, exposes port 9090, and mounts your prometheus.yml into the container at /etc/prometheus/prometheus.yml.
  • The grafana service uses grafana/grafana-oss, exposes port 3000, and sets the admin password to admin via an environment variable.
  • The extra_hosts directive maps host.docker.internal to your host machine's IP so Prometheus can reach your Node.js app running outside Docker.
  • Named volumes (prometheus-data and grafana-data) persist data between container restarts.
  • Save docker-compose.yml.

You won't see a visible change yet. The next substep starts the containers and verifies everything is connected.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'nodejs-app'
    static_configs:
      - targets: ['host.docker.internal:3001']
    metrics_path: '/metrics'
services:
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    extra_hosts:
      - "host.docker.internal:host-gateway"

  grafana:
    image: grafana/grafana-oss
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana

volumes:
  prometheus-data:
  grafana-data:
Start the stack and verify Prometheus is scraping

With both config files in place, you can start Prometheus and Grafana. But first, your Node.js app needs to be running so Prometheus has something to scrape.

  • Start your Node.js app by running:
npm start

You should see output confirming the app is running on http://localhost:3001.

  • Open a second terminal tab in VS Code.
  • Make sure you are in the observability-demo folder, then start the Docker Compose stack by running:
docker compose up -d

What does this command do?

The docker compose up command creates and starts all the containers defined in your docker-compose.yml. The -d flag runs them in detached mode so they continue in the background while you keep using your terminal.

You should see output showing Docker pulling the images and starting both containers. This may take a minute on the first run as Docker downloads the Prometheus and Grafana images.

  • Wait about 20 seconds for Prometheus to start scraping, then open your browser to:
http://localhost:9090/targets
  • Look for the nodejs-app job. Its state should show UP in green, confirming Prometheus is successfully scraping your app's metrics.

Seeing a DOWN state or no targets?

If the target shows DOWN in red, make sure your Node.js app is still running in the first terminal tab. Prometheus can only scrape it if the app is actively listening on port 3001.

If you see no targets at all, give it a few more seconds and refresh the page. Prometheus needs a moment to load its configuration after starting.

If port 9090 is not reachable, check that Docker Desktop is running and that no other service is using port 9090 or 3000 on your machine.

help me troubleshoot why Prometheus can't reach my Node.js app

  • Also confirm Grafana is accessible by visiting http://localhost:3000 in your browser.
  • Log in with username admin and password admin.
  • Grafana will prompt you to change the password. You can click Skip for now.

Why are we running the Node.js app outside Docker?

Running Express on your host machine (not inside a container) keeps the development loop fast. You can edit code and restart instantly without rebuilding a Docker image each time.

The host.docker.internal hostname bridges the gap. It resolves to your host machine's IP from inside any Docker container, letting Prometheus reach your app across the network boundary.

Your monitoring infrastructure is live. Next up, you'll connect Prometheus to Grafana and build a dashboard that turns those raw metrics into real-time visualizations.

Build a Grafana Dashboard

Your metrics pipeline is live. Prometheus is scraping your app every 15 seconds, but raw metric data in text format is hard to read at a glance.

In this step, you'll connect Grafana to Prometheus and build a dashboard with four panels that answer the question "is my app healthy?" in real time.

In this step, get ready to:
  • Connect Prometheus as a data source in Grafana.
  • Create 4 dashboard panels using PromQL queries.
  • Generate traffic to see live data in your panels.
Connect Prometheus as a data source

Before Grafana can query your metrics, it needs to know where Prometheus lives. Because both services share the same Docker Compose network, Grafana can reach Prometheus by its service name.

  • Open your browser and navigate to http://localhost:3000.
  • Sign in with username admin and password admin.
  • If Grafana asks you to change the password, click Skip for now.
  • In the left sidebar, click Connections.
  • Click Data sources.
  • Click Add new data source.
  • Search for and select Prometheus.
  • In the Prometheus server URL field, enter http://prometheus:9090.
  • Scroll to the bottom and click Save & test.

Why http://prometheus:9090?

Inside Docker Compose, containers reach each other by service name. The Grafana container can resolve prometheus because that is the service name defined in your docker-compose.yml.

If you used http://localhost:9090 instead, Grafana would look for Prometheus on its own container's localhost and fail.

You should see a green banner saying the data source is working.

Not seeing a success message?

Make sure your Docker Compose stack is still running. Switch back to your terminal and confirm there are no stopped containers.

Double-check the URL is exactly http://prometheus:9090 with no trailing slash.

help me connect Prometheus to Grafana as a data source

Create dashboard panels with PromQL queries

Each dashboard panel runs a PromQL query against Prometheus and visualizes the result. You'll create four panels that cover the most important signals: request rate, latency, errors, and active connections.

  • Click Dashboards in the left sidebar.
  • Click New and select New Dashboard.
  • Click + Add visualization.
  • Select Prometheus as the data source.

The panel editor opens. This is where you will write your first PromQL query.

Panel 1: Request Rate

  • In the query editor at the bottom, click the Code tab to switch to raw PromQL mode.
  • Enter the following query:
rate(http_requests_total[1m])
  • Click Run queries (or press Shift+Enter) to see results.
  • Leave the visualization type as Time series (the default).
  • Under Panel options on the right sidebar, set the title to Request Rate.
  • Click Back to dashboard in the top left to add the panel to your dashboard.

What does rate() do?

The rate() function calculates the per-second average rate of increase of a counter over a time window. Counters only go up, so rate() converts that raw total into a useful "requests per second" number.

The [1m] part tells Prometheus to look at the last 1 minute of data to calculate the rate.

Panel 2: Latency Percentiles (p50/p95/p99)

  • Click + Add visualization again and select Prometheus.
  • Click the Code tab in the query editor.
  • Enter this query for the p50 (median):
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[1m]))
  • Click + Add query to add a second query to the same panel.
  • In the new query row, click Code and enter the p95 query:
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))
  • Click + Add query once more and enter the p99 query:
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[1m]))
  • Click Run queries.
  • Set the panel title to Latency (p50/p95/p99).
  • Click Back to dashboard.

What does histogram_quantile() do?

The histogram_quantile() function calculates a specific percentile from histogram bucket data. For example, histogram_quantile(0.95, ...) tells you the value below which 95% of observations fall.

This gives you a much better picture of latency than averages. A healthy-looking average can hide the fact that 5% of requests are painfully slow.

Panel 3: Error Rate

  • Click + Add visualization and select Prometheus.
  • Click the Code tab and enter:
rate(http_requests_total{status_code=~"5.."}[1m])
  • Click Run queries.
  • Set the panel title to Error Rate.
  • Click Back to dashboard.

How does label filtering work?

The {status_code=~"5.."} part is a label matcher using a regex. The =~ operator means "matches regex", and 5.. matches any three-character string starting with 5 (like 500, 502, 503).

This filters the counter to only count requests that resulted in a server error, giving you a clear error rate signal.

Panel 4: Active Connections

  • Click + Add visualization and select Prometheus.
  • Click the Code tab and enter:
http_active_connections
  • Click Run queries.
  • In the visualization picker on the right, select Stat.
  • Set the panel title to Active Connections.
  • Click Back to dashboard.

Why use a Stat visualization for the gauge?

A gauge metric represents a single value that can go up and down (like the number of active connections right now). The Stat visualization displays it as a prominent number, making it easy to see the current value at a glance.

Time series charts are better for counters and histograms where you want to see trends over time.

Not seeing data in your panels?

Make sure your Node.js app is still running on port 3001. If you stopped it, restart it by running npm start in your observability-demo folder.

Prometheus needs at least two scrapes (30 seconds) before rate() can calculate anything. Wait a moment and click Run queries again.

help me debug why my Grafana panels show no data

Save the dashboard and generate traffic

Your four panels are ready. Now you need to save the dashboard and send some traffic to your app so the panels display meaningful data.

  • Click the Save dashboard icon at the top of the page.
  • Enter Node.js Observability as the dashboard name.
  • Click Save.
  • Open a new terminal window.
  • Send a burst of normal requests by running:
for i in $(seq 1 20); do curl http://localhost:3001/api/users; done
  • Hit the slow endpoint a few times by running:
for i in $(seq 1 5); do curl http://localhost:3001/api/slow; done
  • Trigger some errors by running:
for i in $(seq 1 10); do curl http://localhost:3001/api/error; done
  • Switch back to your Grafana browser tab.
  • Set the time range to Last 5 minutes using the time picker in the top right corner.
  • Click the refresh icon to reload the panels.

You should see your Request Rate panel showing spikes, the Latency panel with distinct p50/p95/p99 lines, error data in the Error Rate panel, and a number in your Active Connections stat.

Auto-refresh for live data

Click the dropdown arrow next to the refresh icon and select a refresh interval (like 10s). This makes your dashboard update automatically so you can watch metrics change in real time as traffic flows.

Panels still showing flat lines?

Check that Prometheus is scraping successfully. Open http://localhost:9090/targets in your browser. The nodejs-app target should show a state of UP.

If the time range is too wide, the data points may look flat. Narrow it to Last 5 minutes for the best view of recent activity.

help me troubleshoot my Grafana dashboard panels

Your observability stack is complete. You have a Node.js app exposing metrics, Prometheus collecting them, and Grafana visualizing them in real time. Next up, the Secret Mission takes this further with automated alerting.

Secret mission

Add a Grafana Alert Rule

Your dashboard shows you what's happening. But what about when you're not watching? In this secret mission, you'll configure Grafana to actively monitor your error rate and fire an alert when things go wrong.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using Docker containers, so there are no ongoing cloud costs. However, the running containers do consume CPU and RAM on your machine. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Docker containers (Prometheus on port 9090, Grafana on port 3000).
  • Docker volumes (prometheus-data and grafana-data).
  • Node.js app process running on port 3001.
  • Project folder (observability-demo) on your Desktop.

Keep everything running

No action needed. Choose this if you're still actively building or exploring your observability stack.

  • Your Docker containers will keep running in the background, consuming CPU and RAM.
  • Your Node.js app will keep serving metrics on port 3001 as long as the terminal stays open.
  • Grafana dashboards and alert rules persist in the grafana-data volume.
  • Prometheus metric history persists in the prometheus-data volume.

Pause - I'll come back to this later

Shut down running processes to free up CPU and RAM. Your data and project files stay intact so you can pick up where you left off.

  • Stop your Node.js app by pressing Ctrl+C in the terminal where it's running.
  • Stop the Docker containers (without removing them or their data) by running:
docker compose stop

When you're ready to resume, start everything back up by running docker compose start from the observability-demo folder, then run npm start to restart your Node.js app.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your containers, volumes, metric history, and Grafana dashboards permanently.

  • Stop your Node.js app by pressing Ctrl+C in the terminal where it's running.
  • Remove all Docker containers and volumes by running:
docker compose down -v
  • Delete the project folder by running:
cd ~/Desktop
rm -rf observability-demo

Windows users

If you're on Windows, delete the project folder by running this instead:

cd ~\Desktop
rmdir /s /q observability-demo

Nice Work!

Nice Work!

You've built a complete observability pipeline from scratch. Your Node.js app now exposes metrics, Prometheus scrapes them automatically, and Grafana visualizes your app's health in real time.

You've learned how to:

  • Instrumented a Node.js app with 3 Prometheus metric types (Counter, Histogram, Gauge) using prom-client.
  • Configured Prometheus scraping every 15 seconds via Docker Compose, with cross-network access using host.docker.internal.
  • Built a 4-panel Grafana dashboard using PromQL queries including rate(), histogram_quantile(), and label filtering.
  • Secret Mission: Created a Grafana alert rule that fires when error rate exceeds 10%.

Ready to quiz yourself?