summaryrefslogtreecommitdiff
path: root/gemfeed/2025-11-02-perl-new-features-and-foostats.html
blob: 85c36f60fb6fab0e266dbe9479897b1d7e532b52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Perl New Features and Foostats</title>
<link rel="shortcut icon" type="image/gif" href="/favicon.ico" />
<link rel="stylesheet" href="../style.css" />
<link rel="stylesheet" href="style-override.css" />
</head>
<body>
<p class="header">
<a href="https://foo.zone">Home</a> | <a href="https://codeberg.org/snonux/foo.zone/src/branch/content-md/gemfeed/2025-11-02-perl-new-features-and-foostats.md">Markdown</a> | <a href="gemini://foo.zone/gemfeed/2025-11-02-perl-new-features-and-foostats.gmi">Gemini</a>
</p>
<h1 style='display: inline' id='perl-new-features-and-foostats'>Perl New Features and Foostats</h1><br />
<br />
<span class='quote'>Published at 2025-11-01T16:10:35+02:00</span><br />
<br />
<span>Perl recently reached rank 10 in the TIOBE index. That headline made me write this blog post as I was developing the Foostats script for simple analytics of my personal websites and Gemini capsules (e.g. <span class='inlinecode'>foo.zone</span>) and there were a couple of new features added to the Perl language over the last releases. The book *Perl New Features* by brian d foy documents the changes well; this post shows how those features look in a real program that runs every morning for my stats generation.</span><br />
<br />
<a class='textlink' href='https://developers.slashdot.org/story/25/09/14/0134239/is-perl-the-worlds-10th-most-popular-programming-language'>Perl re-enters the top ten</a><br />
<a class='textlink' href='https://perlschool.com/books/perl-new-features/'>Perl New Features by Joshua McAdams and brian d foy</a><br />
<br />
<pre>
$b="24P7cP3dP31P3bPaP28P24P64P31P2cP24P64P32P2cP24P73P2cP24P67P2cP24P7
2P29P3dP28P22P31P30P30P30P30P22P2cP22P31P30P30P30P30P30P22P2cP22P4aP75
P7                                                                  3P
74                                                                  P2
0P  41P6eP6fP74P     68P65P72P20P50 P65P72P6cP2     0P48P           61
P6  3P6bP65P72P22P   29P3bPaP40P6dP 3dP73P70P6cP6   9P74P           20
P2  fP2fP    2cP22P  2cP2eP3aP21P2  bP2aP    30P4f  P40P2           2P
3b  PaP24      P6eP3 dP6c           P65P6      eP67 P74P6           8P
20  P24P7      3P3bP aP24           P75P3      dP22 P20P2           2P
78  P24P6      eP3bP aPaP           70P72      P69P 6eP74           P2
0P  22P5c    P6eP20  P20P           24P75    P5cP7  2P22P           3b
Pa  PaP66P6fP72P2    8P24P7aP20P    3dP20P31P3bP    20P24           P7
aP  3cP3dP24P6       eP3bP20P24     P7aP2bP2bP      29P20           P7
bP  aPaP9            P77P28P24P6    4P31P29P        3bPaP           9P
24  P72P3            dP69           P6eP74P28       P72P6           1P
6e  P64P2            8P24           P6eP2 9P29P     3bPaP           9P
24  P67P3            dP73           P75P6  2P73P    74P72           P2
0P  24P73            P2cP24P72P2cP  31P3b   PaP9P   24P67P20P3fP20  P6
4P  6fP20            P9P7bP20PaP9P9 P9P9P    9P66P  6fP72P20P28P24  P6
bP  3dP30            P3bP24P6bP3cP3 9P3bP    24P6bP 2bP2bP29P20P7b  Pa
P9                                                                  P9
P9                                                                  P9
P9  P9P73P75P6     2P73   P74P  72P2       8P24P75P2c     P24P72    P2
cP  31P29P3dP24P   6dP5   bP24  P6bP       5dP3bP20Pa   P9P9  P9P9  P9
P9  P70P    72P69  P6eP   74P2  0P22       P20P20P24P  75P      5cP 72
P2  2P3b      PaP9 P9P9   P9P9  P9P7       7P28       P24        P6 4P
32  P29P      3bPa P9P9   P9P9  P9P7       dPaP       9P9           P9
P9  P9P7      3P75 P62P   73P7  4P72       P28P        24P7         5P
2c  P24P    72P2c  P31P   29P3  dP24       P67P3bP20P   aP9P9       P9
P9  P7dP20PaP9P    9P3a   P20P  72P6       5P64P6fP3b      PaP9     P7
3P  75P62P73P      74P7   2P28  P24P       73P2cP24P7        2P2c   P3
1P  29P3dP2        2P30   P22P  3bPa       P9P7                0P7  2P
69  P6eP74P2       0P22   P20P  20P2       4P75                 P5c P7
2P  22P3 bPaPa     P7dP   aPaP  77P2       0P28                 P24 P6
4P  32P2  9P3bP    aP70   P72P  69P6       eP74       P2        0P2 2P
20  P20P   24P75   P20P21P5cP7  2P22P3bPaP 73P6cP65P6 5P7     0P20  P3
2P  3bPa    P70P7  2P69P6eP74P  20P22P20P2 0P24P75P20  P21P  5cP6   eP
22  P3bP     aPaP7  3P75P62P2   0P77P20P7b PaP9P24P6c    P3dP73     P6
8P                                                                  69
P6                                                                  6P
74P3bPaP9P66P6fP72P28P24P6aP3dP30P3bP24P6aP3cP24P6cP3bP24P6aP2bP2bP29P
7bP7dPaP7dP";$b=~s/\s//g;split /P/,$b;foreach(@_){$c.=chr hex};eval $c

The above Perl script prints out "Just Another Perl Hacker !" in an
animation of sorts.

</pre>
<br />
<h2 style='display: inline' id='table-of-contents'>Table of Contents</h2><br />
<br />
<ul>
<li><a href='#perl-new-features-and-foostats'>Perl New Features and Foostats</a></li>
<li>⇢ <a href='#motivation'>Motivation</a></li>
<li>⇢ <a href='#why-i-used-perl'>Why I used Perl</a></li>
<li>⇢ <a href='#inside-foostats'>Inside Foostats</a></li>
<li>⇢ ⇢ <a href='#log-pipeline'>Log pipeline</a></li>
<li>⇢ ⇢ <a href='#foooddstxt'><span class='inlinecode'>fooodds.txt</span></a></li>
<li>⇢ ⇢ <a href='#feed-kinds'>Feed kinds</a></li>
<li>⇢ ⇢ <a href='#aggregation-and-output'>Aggregation and output</a></li>
<li>⇢ ⇢ <a href='#command-line-entry-points'>Command-line entry points</a></li>
<li>⇢ <a href='#packages-as-real-blocks'>Packages as real blocks</a></li>
<li>⇢ ⇢ <a href='#scoped-packages'>Scoped packages</a></li>
<li>⇢ <a href='#postfix-dereferencing-keeps-data-structures-tidy'>Postfix dereferencing keeps data structures tidy</a></li>
<li>⇢ ⇢ <a href='#clear-dereferencing'>Clear dereferencing</a></li>
<li>⇢ <a href='#say-is-the-default-voice-now'><span class='inlinecode'>say</span> is the default voice now</a></li>
<li>⇢ <a href='#lexical-subs-promote-local-reasoning'>Lexical subs promote local reasoning</a></li>
<li>⇢ <a href='#reference-aliasing-makes-intent-explicit'>Reference aliasing makes intent explicit</a></li>
<li>⇢ <a href='#persistent-state-without-globals'>Persistent state without globals</a></li>
<li>⇢ ⇢ <a href='#rate-limiting-state'>Rate limiting state</a></li>
<li>⇢ ⇢ <a href='#de-duplicated-logging'>De-duplicated logging</a></li>
<li>⇢ <a href='#subroutine-signatures'>Subroutine signatures</a></li>
<li>⇢ <a href='#defined-or-assignment-for-defaults-without-boilerplate'>Defined-or assignment for defaults without boilerplate</a></li>
<li>⇢ <a href='#cleanup-with-defer'>Cleanup with <span class='inlinecode'>defer</span></a></li>
<li>⇢ <a href='#builtins-and-booleans'>Builtins and booleans</a></li>
<li>⇢ <a href='#conclusion'>Conclusion</a></li>
</ul><br />
<h2 style='display: inline' id='motivation'>Motivation</h2><br />
<br />
<span>I&#39;ve been running <span class='inlinecode'>foo.zone</span> for a while now, but I&#39;ve never looked into visitor statistics or analytics. I value privacy—not just my own, but also the privacy of others (the visitors of this site) — so I hesitated to use any off-the-shelf analytics plugins. All I wanted to collect were:</span><br />
<br />
<ul>
<li>Which blog posts had the most (unique) visitors</li>
<li>Exclude, if possible, any bots and scrapers from the stats</li>
<li>Track only anonymized IP addresses, never store raw addresses</li>
</ul><br />
<span>With Foostats I&#39;ve created a Perl script which does that for my highly opinionated website/blog setup, which consists of:</span><br />
<br />
<a class='textlink' href='https://foo.zone/gemfeed/2021-06-05-gemtexter-one-bash-script-to-rule-it-all.html'>Gemtexter, my static site and Gemini capsule generator</a><br />
<a class='textlink' href='https://foo.zone/gemfeed/2024-04-01-KISS-high-availability-with-OpenBSD.html'>How I host this site highly-available using OpenBSD</a><br />
<br />
<h2 style='display: inline' id='why-i-used-perl'>Why I used Perl</h2><br />
<br />
<span>Even though nowadays I code more in Go and Ruby, I stuck with Perl for Foostats for four simple reasons:</span><br />
<br />
<ul>
<li>I wanted an excuse to explore the newer features of my first programming love.</li>
<li>Sometimes, I miss Perl.</li>
<li>Perl ships with OpenBSD (the operating system on which my sites run) by default.</li>
<li>It really does live up to its Practical Extraction and Report Language (that&#39;s what the name Perl means) for this kind of log grinding I did with Foostats.</li>
</ul><br />
<h2 style='display: inline' id='inside-foostats'>Inside Foostats</h2><br />
<br />
<span>Foostats is simply a log file analyser, which analyses the OpenBSD httpd and relayd logs.</span><br />
<br />
<a class='textlink' href='https://man.openbsd.org/httpd.8'>https://man.openbsd.org/httpd.8</a><br />
<a class='textlink' href='https://man.openbsd.org/relayd.8'>https://man.openbsd.org/relayd.8</a><br />
<br />
<h3 style='display: inline' id='log-pipeline'>Log pipeline</h3><br />
<br />
<span>A CRON job starts Foostats, reads OpenBSD httpd and relayd access logs, and produces the numbers published at <span class='inlinecode'>https://stats.foo.zone</span> and <span class='inlinecode'>gemini://stats.foo.zone</span>. The dashboards are humble because traffic on my sites is still light, yet the trends are interesting for spotting patterns. The script is opinionated (I am repeating myself here, I know), and I will probably be the only one ever using it for my own sites. However, the code demonstrates how Perl&#39;s newer features help keep a small script like this exciting and fun!</span><br />
<br />
<a class='textlink' href='https://stats.foo.zone'>Foostats (HTTP)</a><br />
<a class='textlink' href='gemini://stats.foo.zone'>Foostats (Gemini)</a><br />
<br />
<span>On OpenBSD, I&#39;ve configured the job via the <span class='inlinecode'>daily.local</span> on both of my OpenBSD servers (<span class='inlinecode'>fishfinger.buetow.org</span> and <span class='inlinecode'>blowfish.buetow.org</span> - note one is the master server, the other is the standby server, but the script runs on both and the stats are merged later in the process):</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre>fishfinger$ grep foostats /etc/daily.<b><u><font color="#000000">local</font></u></b>
perl /usr/local/bin/foostats.pl --parse-logs --replicate --report
</pre>
<br />
<span>Internally, <span class='inlinecode'>Foostats::Logreader</span> parses each line of the log files <span class='inlinecode'>/var/log/daemon*</span> and <span class='inlinecode'>/var/www/logs/access_log*</span>, turns timestamps into <span class='inlinecode'>YYYYMMDD/HHMMSS</span> values, hashes IP addresses with SHA3 (for anonymization), and hands a normalized event to <span class='inlinecode'>Foostats::Filter</span>. The filter compares the URI against entries in <span class='inlinecode'>fooodds.txt</span>, tracks how many times an IP address requests within the exact second, and drops anything suspicious (e.g., from web crawlers or malicious attackers). Valid events reach <span class='inlinecode'>Foostats::Aggregator</span>, which counts requests per protocol, records unique visitors for the Gemtext and Atom feeds, and remembers page-level IP sets. <span class='inlinecode'>Foostats::FileOutputter</span> writes the result as gzipped JSON files—one per day and per protocol—with IPv4/IPv6 splits, filtered counters, feed readership, and hashes for long URLs.</span><br />
<br />
<h3 style='display: inline' id='foooddstxt'><span class='inlinecode'>fooodds.txt</span></h3><br />
<br />
<span><span class='inlinecode'>fooodds.txt</span> is a plain text list of substrings of URLs to be blocked, making it quick to shut down web crawlers. Foostats also detects rapid requests (an indicator of excessive crawling) and blocks the IP. Audit lines are written to <span class='inlinecode'>/var/log/fooodds</span>, which can later be reviewed for false or true positives (I do this around once a month). The <span class='inlinecode'>Justfile</span> even has a <span class='inlinecode'>gather-fooodds</span> target that collects suspicious paths from remote logs so new patterns can be added quickly.</span><br />
<br />
<h3 style='display: inline' id='feed-kinds'>Feed kinds</h3><br />
<br />
<span>There are different kinds of feeds being tracked by Foostats:</span><br />
<br />
<ul>
<li>The Atom web-feed</li>
<li>The same feed via Gemini</li>
<li>The Gemfeed (a special format popular in the Geminispace)</li>
</ul><br />
<h3 style='display: inline' id='aggregation-and-output'>Aggregation and output</h3><br />
<br />
<span>As mentioned, Foostats merges the stats from both hosts, master and standby. For the master-standby setup description, read:</span><br />
<br />
<a class='textlink' href='./2024-04-01-KISS-high-availability-with-OpenBSD.html'>KISS high-availability with OpenBSD</a><br />
<br />
<span>Those gzipped files land in <span class='inlinecode'>stats/</span>. From there, <span class='inlinecode'>Foostats::Replicator</span> can pull matching files from the partner host (<span class='inlinecode'>fishfinger</span> or <span class='inlinecode'>blowfish</span>) so the view covers both servers, <span class='inlinecode'>Foostats::Merger</span> combines them into daily summaries, and <span class='inlinecode'>Foostats::Reporter</span> rebuilds Gemtext and HTML reports.</span><br />
<br />
<span>Those are the raw stats files:</span><br />
<br />
<a class='textlink' href='https://blowfish.buetow.org/foostats/'>https://blowfish.buetow.org/foostats/</a><br />
<a class='textlink' href='https://fishfinger.buetow.org/foostats/'>https://fishfinger.buetow.org/foostats/</a><br />
<br />
<span>These are the 30-day reports generated (already linked earlier in this post, but adding here again for clarity):</span><br />
<br />
<a class='textlink' href='gemini://stats.foo.zone'>stats.foo.zone Gemini capsule dashboard</a><br />
<a class='textlink' href='https://stats.foo.zone'>stats.foo.zone HTTP dashboard</a><br />
<br />
<h3 style='display: inline' id='command-line-entry-points'>Command-line entry points</h3><br />
<br />
<span><span class='inlinecode'>foostats_main</span> is the command entry point. <span class='inlinecode'>--parse-logs</span> refreshes the gzipped files, <span class='inlinecode'>--replicate</span> runs the cross-host sync, and <span class='inlinecode'>--report</span> rebuilds the HTML and Gemini report pages. <span class='inlinecode'>--all</span> performs everything in one go. Defaults point to <span class='inlinecode'>/var/www/htdocs/buetow.org/self/foostats</span> for data, <span class='inlinecode'>/var/gemini/stats.foo.zone</span> for Gemtext output, and <span class='inlinecode'>/var/www/htdocs/gemtexter/stats.foo.zone</span> for HTML output. Replication always forces the three most recent days&#39; worth of data across HTTPS and leaves older files untouched to save bandwidth.</span><br />
<br />
<span>The complete source lives on Codeberg here:</span><br />
<br />
<a class='textlink' href='https://codeberg.org/snonux/foostats'>Foostats on Codeberg</a><br />
<br />
<span>Now let&#39;s go to some new Perl features:</span><br />
<br />
<h2 style='display: inline' id='packages-as-real-blocks'>Packages as real blocks</h2><br />
<br />
<h3 style='display: inline' id='scoped-packages'>Scoped packages</h3><br />
<br />
<span>Recent Perl versions allow the block form <span class='inlinecode'>package Foo { ... }</span>. Foostats uses it for every package. Imports stay local to the block, helper subs do not leak into the global symbol table, and configuration happens where the code needs it.</span><br />
<br />
<span>The old way:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">package</font></u></b> foo;

<b><u><font color="#000000">sub</font></u></b> hello {
    <b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello from package foo\n"</font>;
}

<b><u><font color="#000000">package</font></u></b> bar;

<b><u><font color="#000000">sub</font></u></b> hello {
    <b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello from package bar\n"</font>;
}
</pre>
<br />
<span>But now it is also possible to do this:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">package</font></u></b> foo {
    <b><u><font color="#000000">sub</font></u></b> hello {
        <b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello from package foo\n"</font>;
    }
}

<b><u><font color="#000000">package</font></u></b> bar {
    <b><u><font color="#000000">sub</font></u></b> hello {
        <b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello from package bar\n"</font>;
    }
}
</pre>
<br />
<h2 style='display: inline' id='postfix-dereferencing-keeps-data-structures-tidy'>Postfix dereferencing keeps data structures tidy</h2><br />
<br />
<h3 style='display: inline' id='clear-dereferencing'>Clear dereferencing</h3><br />
<br />
<span>The script handles nested hashes and arrays. Postfix dereferencing (<span class='inlinecode'>$hash-&gt;%*</span>, <span class='inlinecode'>$array-&gt;@*</span>) keeps that readable.</span><br />
<br />
<span>E.g. instead of having to write:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">for</font></u></b> <b><u><font color="#000000">my</font></u></b> $elem (@{$array_ref}) {
    <b><u><font color="#000000">print</font></u></b> <font color="#808080">"$elem\n"</font>;
}
</pre>
<br />
<span>one can now do:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">for</font></u></b> <b><u><font color="#000000">my</font></u></b> $elem ($array_ref-&gt;@*) {
    <b><u><font color="#000000">print</font></u></b> <font color="#808080">"$elem\n"</font>;
}
</pre>
<br />
<span>You see that this feature becomes increasingly useful with nested data structures, e.g. to print all keys of the nested hash:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">print</font></u></b> <b><u><font color="#000000">for</font></u></b> <b><u><font color="#000000">keys</font></u></b> $hash-&gt;{stats}-&gt;%*;
</pre>
<br />
<span>Loops over like <span class='inlinecode'>$stats-&gt;{page_ips}-&gt;{urls}-&gt;%*</span> or <span class='inlinecode'>$merge{$key}-&gt;{$_}-&gt;%*</span> show which level of the structure is in play. The merger in Foostats updates host and URL statistics without building temporary arrays, and the reporter code mirrors the layout of the final tables. Before postfix dereferencing, the same code relied on braces within braces and was harder to read.</span><br />
<br />
<h2 style='display: inline' id='say-is-the-default-voice-now'><span class='inlinecode'>say</span> is the default voice now</h2><br />
<br />
<span><span class='inlinecode'>say</span> became the default once the script switched to <span class='inlinecode'>use v5.38;</span>. It adds a newline to every message printed, comparable to Ruby&#39;s <span class='inlinecode'>puts</span>, making log messages like "Processing $path" or "Writing report to $report_path" cleaner:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">use</font></u></b> v5.<font color="#000000">38</font>;

<b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello, world!\n"</font>;    <i><font color="silver"># old way</font></i>
say <font color="#808080">"Hello, world!"</font>;        <i><font color="silver"># new way</font></i>
</pre>
<br />
<h2 style='display: inline' id='lexical-subs-promote-local-reasoning'>Lexical subs promote local reasoning</h2><br />
<br />
<span>Lexical subroutines keep helpers close to the code that needs them. In <span class='inlinecode'>Foostats::Logreader::parse_web_logs</span>, functions such as <span class='inlinecode'>my sub parse_date</span> and <span class='inlinecode'>my sub open_file</span> live only inside that scope.</span><br />
<br />
<span>This is an example of a lexical sub named <span class='inlinecode'>trim</span>, which is only visible within the outer sub named <span class='inlinecode'>process_lines</span>:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">use</font></u></b> v5.<font color="#000000">38</font>;

<b><u><font color="#000000">sub</font></u></b> process_lines (@lines) {
    <b><u><font color="#000000">my</font></u></b> <b><u><font color="#000000">sub</font></u></b> trim ($str) {
        $str =~ <b><u><font color="#000000">s</font></u></b><font color="#808080">/^\s+|\s+$//</font>gr;
    }
    <b><u><font color="#000000">return</font></u></b> [ <b><u><font color="#000000">map</font></u></b> { trim($_) } @lines ];
}

<b><u><font color="#000000">my</font></u></b> @raw = (<font color="#808080">"  foo  "</font>, <font color="#808080">" bar"</font>, <font color="#808080">"baz "</font>);
<b><u><font color="#000000">my</font></u></b> $cleaned = process_lines(@raw);
say <b><u><font color="#000000">for</font></u></b> @$cleaned; <i><font color="silver"># prints "foo", "bar", "baz"</font></i>
</pre>
<br />
<h2 style='display: inline' id='reference-aliasing-makes-intent-explicit'>Reference aliasing makes intent explicit</h2><br />
<br />
<span>Reference aliasing can be enabled with <span class='inlinecode'>use feature qw(refaliasing)</span> and helps communicate intent more clearly (if you remember the Perl syntax, of course—otherwise, it can look rather cryptic). The filter starts with <span class='inlinecode'>\my $uri_path = \$event-&gt;{uri_path}</span> so any later modification touches the original event. This is an example with ref aliasing in action:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">use</font></u></b> feature <b><u><font color="#000000">qw</font></u></b>(refaliasing);

<b><u><font color="#000000">my</font></u></b> $hash = { foo =&gt; <font color="#000000">42</font> };
\<b><u><font color="#000000">my</font></u></b> $foo = \$hash-&gt;{foo};

$foo = <font color="#000000">99</font>;
<b><u><font color="#000000">print</font></u></b> $hash-&gt;{foo}; <i><font color="silver"># prints 99</font></i>
</pre>
<br />
<span>The aggregator in Foostats aliases <span class='inlinecode'>$self-&gt;{stats}{$date_key}</span> before updating counters, so the structure remains intact. Combined with subroutine signatures, this makes it obvious when a piece of data is shared instead of copied, preventing silent bugs. This enables having shorter names for long nested data structures.</span><br />
<br />
<h2 style='display: inline' id='persistent-state-without-globals'>Persistent state without globals</h2><br />
<br />
<span>A Perl state variable is declared with <span class='inlinecode'>state $var</span> and retains its value between calls to the enclosing subroutine. Foostats uses that for rate limiting and de-duplicated logging.</span><br />
<br />
<span>This is a small example demonstrating the use of a state variable in Perl:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">sub</font></u></b> counter {
    state $count = <font color="#000000">0</font>;
    $count++;
    <b><u><font color="#000000">return</font></u></b> $count;
}

say counter(); <i><font color="silver"># 1</font></i>
say counter(); <i><font color="silver"># 2</font></i>
say counter(); <i><font color="silver"># 3</font></i>
</pre>
<br />
<span>Hash and array state variables have been supported since <span class='inlinecode'>state</span> arrived in Perl 5.10. Scalar state variables were already supported previously.</span><br />
<br />
<h3 style='display: inline' id='rate-limiting-state'>Rate limiting state</h3><br />
<br />
<span>In Foostats, <span class='inlinecode'>state</span> variables store run-specific state without using package globals. <span class='inlinecode'>state %blocked</span> remembers IP hashes that already triggered the odd-request filter, and <span class='inlinecode'>state $last_time</span> and <span class='inlinecode'>state %count</span> track how many requests an IP makes in the exact second.</span><br />
<br />
<h3 style='display: inline' id='de-duplicated-logging'>De-duplicated logging</h3><br />
<br />
<span><span class='inlinecode'>state %dedup</span> keeps the log output of the suspicious calls to one warning per URI. Early versions utilized global hashes for the same tasks, producing inconsistent results during tests. Switching to <span class='inlinecode'>state</span> removed those edge cases.</span><br />
<br />
<h2 style='display: inline' id='subroutine-signatures'>Subroutine signatures</h2><br />
<br />
<span>Perl now supports subroutine signatures like other modern languages do. Foostats uses them everywhere. Examples:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><i><font color="silver"># Old way</font></i>
<b><u><font color="#000000">sub</font></u></b> greet_old { <b><u><font color="#000000">my</font></u></b> $name = <b><u><font color="#000000">shift</font></u></b>; <b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello, $name!\n"</font> }

<i><font color="silver"># Another old way</font></i>
<b><u><font color="#000000">sub</font></u></b> greet_old2 ($) { <b><u><font color="#000000">my</font></u></b> $name = <b><u><font color="#000000">shift</font></u></b>; <b><u><font color="#000000">print</font></u></b> <font color="#808080">"Hello, $name!\n"</font> }

<i><font color="silver"># New way</font></i>
<b><u><font color="#000000">sub</font></u></b> greet ($name) { say <font color="#808080">"Hello, $name!"</font>; }

greet(<font color="#808080">"Alice"</font>); <i><font color="silver"># prints "Hello, Alice!"</font></i>
</pre>
<br />
<span>In Foostats, constructors declare <span class='inlinecode'>sub new ($class, $odds_file, $log_path)</span>, anonymous callbacks expose <span class='inlinecode'>sub ($event)</span>, and helper subs list the values they expect, e.g.:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">my</font></u></b> $anon = <b><u><font color="#000000">sub</font></u></b> ($name) {
    say <font color="#808080">"Hello, $name!"</font>;
};

$anon-&gt;(<font color="#808080">"World"</font>); <i><font color="silver"># prints "Hello, World!"</font></i>
</pre>
<br />
<h2 style='display: inline' id='defined-or-assignment-for-defaults-without-boilerplate'>Defined-or assignment for defaults without boilerplate</h2><br />
<br />
<span>The operator <span class='inlinecode'>//=</span> keeps configuration and counters simple. Environment variables may be missing when CRON runs the script, so <span class='inlinecode'>//=</span>, combined with signatures, sets defaults without warnings. Example use of that operator:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">my</font></u></b> $foo;
$foo <font color="#808080">//</font>= <font color="#000000">42</font>;
say $foo; <i><font color="silver"># prints 42</font></i>

$foo <font color="#808080">//</font>= <font color="#000000">99</font>;
say $foo; <i><font color="silver"># still prints 42, because $foo was already defined</font></i>
</pre>
<br />
<h2 style='display: inline' id='cleanup-with-defer'>Cleanup with <span class='inlinecode'>defer</span></h2><br />
<br />
<span>Even though not used in Foostats, this feature (similar to Go&#39;s defer) is neat to have in Perl now.</span><br />
<br />
<span>The <span class='inlinecode'>defer</span> block (<span class='inlinecode'>use feature &#39;defer"</span>) schedules a piece of code to run when the current scope exits, regardless of how it exits (e.g. normal return, exception). This is perfect for ensuring resources, such as file handles, are closed.</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><u><font color="#000000">use</font></u></b> feature <b><u><font color="#000000">qw</font></u></b>(defer);

<b><u><font color="#000000">sub</font></u></b> parse_log_file ($path) {
    <b><u><font color="#000000">open</font></u></b> <b><u><font color="#000000">my</font></u></b> $fh, <font color="#808080">'&lt;'</font>, $path or <b><u><font color="#000000">die</font></u></b> <font color="#808080">"Cannot open $path: $!"</font>;
    defer { <b><u><font color="#000000">close</font></u></b> $fh };

    <b><u><font color="#000000">while</font></u></b> (<b><u><font color="#000000">my</font></u></b> $line = <font color="#808080">&lt;$fh&gt;</font>) {
        <i><font color="silver"># ... parsing logic that might throw an exception ...</font></i>
    }
    <i><font color="silver"># $fh is automatically closed here</font></i>
}
</pre>
<br />
<span>This pattern replaces manual <span class='inlinecode'>close</span> calls in every exit path of the subroutine and is more robust than relying solely on object destructors.</span><br />
<br />
<h2 style='display: inline' id='builtins-and-booleans'>Builtins and booleans</h2><br />
<br />
<span>The script also utilizes other modern additions that often go unnoticed. <span class='inlinecode'>use builtin qw(true false);</span> combined with <span class='inlinecode'>experimental::builtin</span> provides more real boolean values.</span><br />
<br />
<h2 style='display: inline' id='conclusion'>Conclusion</h2><br />
<br />
<span>I want to code more in Perl again. The newer features make it a joy to write small scripts like Foostats. If you haven&#39;t looked at Perl in a while, give it another try! The main thing which holds me back from writing more Perl is the lack of good tooling. For example, there is no proper LSP and tree sitter support available, which would work as good as the ones available for Go and Ruby.</span><br />
<br />
<span class='quote'>A reader pointed out that there&#39;s now a third-party Perl Tree-sitter implementation one could use:</span><br />
<br />
<a class='textlink' href='https://github.com/tree-sitter-perl/tree-sitter-perl'>https://github.com/tree-sitter-perl/tree-sitter-perl</a><br />
<br />
<span>E-Mail your comments to <span class='inlinecode'>paul@nospam.buetow.org</span> :-)</span><br />
<br />
<span>Other related posts are:</span><br />
<br />
<a class='textlink' href='./2026-02-15-loadbars-resurrected-from-perl-to-go.html'>2026-02-15 Loadbars resurrected: From Perl to Go after 15 years</a><br />
<a class='textlink' href='./2025-11-02-perl-new-features-and-foostats.html'>2025-11-02 Perl New Features and Foostats (You are currently reading this)</a><br />
<a class='textlink' href='./2023-05-01-unveiling-guprecords:-uptime-records-with-raku.html'>2023-05-01 Unveiling <span class='inlinecode'>guprecords.raku</span>: Global Uptime Records with Raku</a><br />
<a class='textlink' href='./2022-05-27-perl-is-still-a-great-choice.html'>2022-05-27 Perl is still a great choice</a><br />
<a class='textlink' href='./2011-05-07-perl-daemon-service-framework.html'>2011-05-07 Perl Daemon (Service Framework)</a><br />
<a class='textlink' href='./2008-06-26-perl-poetry.html'>2008-06-26 Perl Poetry</a><br />
<br />
<a class='textlink' href='../'>Back to the main site</a><br />
<p class="footer">
	Generated with <a href="https://codeberg.org/snonux/gemtexter">Gemtexter 3.0.1-develop</a> |
	served by <a href="https://www.OpenBSD.org">OpenBSD</a>/<a href="https://man.openbsd.org/relayd.8">relayd(8)</a>+<a href="https://man.openbsd.org/httpd.8">httpd(8)</a> |
	<a href="https://foo.zone/site-mirrors.html">Site Mirrors</a>
	<br />
	Webring: <a href="https://shring.sh/foo.zone/previous">previous</a> | <a href="https://shring.sh">shring</a> | <a href="https://shring.sh/foo.zone/next">next</a>
</p>
</body>
</html>