I wanted to try my new Prometheus setup with some useful data, and since I want to learn how to use Prometheus, I thought it would be fun and useful to do this.

In case you're not familiar with it, Prometheus is a server app that gathers metrics in a database. Metrics that are then displayed in some specialized web app such as Grafana (the most popular for this usage).

I won't go into details of how to install prometheus, since this can be found in many places on the Web.

The JavaScript

Here's the JS code I've put on all my pages (in all.js at the top):

const worker = new Worker('/javascripts/worker.js');
worker.postMessage({ page: window.location.pathname });

and here's the content of my worker.js:

self.onmessage = async (e) => {
    const { page } = e.data;
    await fetch('/prom_collector.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url: page }),
        keepalive: true, // survives page unload
    });
};

BTW, why use JavaScript and not the backend (PHP) directly?

Because as you may know, we live in an era where bots who crawl web pages are legion. And those bots don't normally operate within the context of a (costly) headless browser. Some do -- like Googlebot -- but at the time of writing this, they don't execute workers, because that might be even costlier, for marginal gains.

In short, I want metrics about real humans visiting my site. Otherwise I would've used my Apache logs.

The PHP

Here's the PHP code (prom_collector.php) that's triggered by the worker.js:

<?php
/*
 * This page is requested by the javascript on tracked pages
 */

$db = new PDO('sqlite:' . __DIR__ . '/pageviews.sqlite3');

$json = file_get_contents('php://input');
$data = json_decode($json, false);

// Only execute this when deploying to a new site:
// $db->exec('CREATE TABLE IF NOT EXISTS views (page TEXT PRIMARY KEY, count INTEGER DEFAULT 0)');

$page = $data->url ?? 'unknown';
if ($page === 'unknown') {
  die("unknown page");
}
$stmt = $db->prepare('INSERT INTO views(page, count) VALUES(?, 1)
                      ON CONFLICT(page) DO UPDATE SET count = count + 1');
$stmt->execute([$page]);

echo json_encode(['ok' => true]);

And here's the code for the metrics.php page:

<?php
/*
 * Prometheus scrapes this page
 */

$db = new PDO('sqlite:' . __DIR__ . '/pageviews.sqlite3');
$rows = $db->query('SELECT page, count FROM views')->fetchAll(PDO::FETCH_ASSOC);

header('Content-Type: text/plain; version=0.0.4');

// Begin prometheus-formatted document:
echo "# HELP page_views_total Number of times a page was accessed\n";
echo "# TYPE page_views_total counter\n";
foreach ($rows as $row) {
    $safe = addslashes($row['page']);
    echo "page_views_total{page=\"{$safe}\"} {$row['count']}\n";
}

So, for each page that received at least one visit, we display the number of visits it got so far.

Prometheus

Finally, here's the config that I added to my prometheus.yml, under scrape_config:

  - job_name: 'juliendesrosiers_com'
    scrape_interval: 60s
    static_configs:
      - targets: ['juliendesrosiers.com']
    metrics_path: '/metrics.php'

Then, restart prometheus.

The rest is ClickOps:

  1. login to your grafana
  2. go to "Dashboards", then create a new dashboard -- if needed: grafana dashboards section
  3. click "Add visualization": grafana add visualization
  4. choose "Prometheus" as the data source: grafana data source
  5. now, scroll down and click "Select metric": grafana select metric field
  6. you should find the page_views_total (see metrics.php): grafana metric selection
  7. select a label, if you want to group only for one site: grafana instance filtering
  8. you can try the query by clicking "Run query", and if you're like me, you have many page in your site. So each of those get assigned its own color: grafana run query preview
  9. which of course should get crowdy pretty quickly. so let's add another operation, to aggregate those into only one metric (the total for all the pages): grafana SUM aggregation
  10. "Run query" again, and you should now see something like this: grafana preview, sum of pages
  11. Make sure you save your work... grafana save dashboard button
  12. Take the time to give a proper name for this dashboard: grafana name dashboard
  13. Now go back to your dashboard view and take a look. Nice!: grafana dashboard

Downside of this approach

  • As it is right now, it won't allow much more granularity, when you compare to tools meant for tracking visits, like Google Analytics. For example, adding viewport size and user agent wouldn't scale well here.
  • I'm hosting Prometheus myself, so it's more expensive than using Google's solution. (Which uses Cookies, which would necessitate adding one of those pesky cookie acceptance popups)