How to make a visitor counter with just NGINX (... and the filesystem... and nodejs.... and cronjobs...)

a number

It sounds too good to be true right? Gone are the days of having to use a database or SaaS just to store a number!

Let's start with what's already done for us, I use NGINX, and NGINX already collects all the information needed to generate a visitor counter. It has an access log where it also logs IP addresses. If we scromble this a bit, and sort & filter out only unique entries, and count the lines, we should have a fairly ok representation of visitors for that day! Here is a line that does this for yesterdays traffic.

sudo cat /var/log/nginx/access.log.1 | cut -d" " -f 1 | sort -u | wc -l

Now this is only good for one day of log data, and the way NGINX is set up by default is that it rotates these log files on a 14 day basis. So each day gets its own file, where the current day is access.log, and yesterday would be access.log.1... and so on.

people working hard on the computer

Now to preserve this data, we need to store a number, and the best way to store a number on a server is to store it in a file! So let's do just that!

Below is the first of two scripts that together create a wacky system for storing your visitor counter on disk! I called it /home/MYUSERNAME/visitor_counter/fbbtbot.mjs.

#!/usr/bin/env node

import {readFileSync, writeFileSync} from 'node:fs';
import { exec } from 'node:child_process';

const fbbtbot_file = '/home/MYUSERNAME/visitor_counter/from_big_bang_to_beginning_of_today';

exec('cat /var/log/nginx/access.log.1 | cut -d" " -f 1 | sort -u | wc -l', (err, stdout, stderr) => {
    if (err) {
        console.error(`really bad things happened: ${err}`);
        return;
    }

    const visitors_yesterday = parseInt(stdout);

    const visitors_cum_string = readFileSync(fbbtbot_file, 'utf8');
    const visitors_cum = parseInt(visitors_cum_string);

    writeFileSync(fbbtbot_file, `${visitors_yesterday + visitors_cum}`);

    writeFileSync('/home/MYUSERNAME/visitor_counter/runs.log', `fbbtbot ${new Date(Date.now()).toISOString()}\n`, {flag: 'a+'});
});

Now, since it's not a very smart piece of code, and laziness sets in really soon in projects like these, ensure that the file it tries to write the counter to exists before the program tries to run.

echo 0 > /home/MYUSERNAME/visitor_counter/from_big_bang_to_beginning_of_today

Now we only have half the puzzle solved! the other half is this second script (which I decided to call /home/MYUSERNAME/visitor_counter/freshcount.mjs):

#!/usr/bin/env node

import {readFileSync, writeFileSync} from 'node:fs';
import { exec } from 'node:child_process';

const fbbtbot_file = '/home/MYUSERNAME/visitor_counter/from_big_bang_to_beginning_of_today';
const fresh_file = '/home/MYUSERNAME/visitor_counter/from_big_bang';

exec('cat /var/log/nginx/access.log | cut -d" " -f 1 | sort -u | wc -l', (err, stdout, stderr) => {
    if (err) {
        console.error(`really bad things happened: ${err}`);
        return;
    }

    const visitors_today = parseInt(stdout);

    const visitors_cum_string = readFileSync(fbbtbot_file, 'utf8');
    const visitors_cum = parseInt(visitors_cum_string);

    writeFileSync(fresh_file, `${visitors_today + visitors_cum}`);

    writeFileSync('/home/MYUSERNAME/visitor_counter/runs.log', `freshcount ${new Date(Date.now()).toISOString()}\n`, {flag: 'a+'});
});

You should now be able to run both of these scripts (as root nonetheless) and see that they do something! 🎊🎉

sudo ./fbbtbot.mjs
sudo ./freshcount.mjs

wizard at a computer

Next up is to make sure that these scripts run as root on cronjob timers! I decided that I wanted the freshest visitor counter, so i run the freshcount script every minute, and the fbbtbot script first thing in the morning 1 minute after the day starts! (this is to make sure that the nginx stuff has a hot minute to do its logrotation).

sudo crontab -e
1 0 * * * /home/MYUSERNAME/visitor_counter/fbbtbot.mjs
* * * * * /home/MYUSERNAME/visitor_counter/freshcount.mjs

Incredible right?! Now you should be able to see some files with some numbers in them, and the file named from_big_bang is your up-to-the-minute semi-accurate visitor counter!

Now what?!

Well, now we have to expose this visitor counter through NGINX to the rest of the world! (as well as to our own clientside scripts running). To do that I used the following entry in my nginx config file (/etc/nginx/sites-available/anyfilenamegoeshere).

    location /visitor-counter {
        default_type text/plain;
        alias /home/MYUSERNAME/visitor_counter/from_big_bang;
    }

visual depiction of nginx configuration files

Tada!


We're getting really close to the end-goal now! Just a little bit of HTML and javascript and we're there! Hooray! 👏👏👏👏

<html>
    <body>
          ... lots of other stuff...

          <footer> <!-- Remember to use semantic tokens! -->
              <div class="visitor-counter">
                  <p>Visitors: <span></span></p>
              </div>
          </footer>
    </body>
</html>

and the JS snippet to go with it looks like this:

(async () => {
    let visitorCount = await (await fetch('/visitor-counter')).text();
    document.querySelector('.visitor-counter span').innerHTML = visitorCount;
})();

And now it should all work! 👏


Wacky words by

Tegaki