Step 1 of 5 · Reading · ~4 min
Read
Production Features
Access Logs & Metrics
A server that works is not the same as a server you can operate. The moment yours runs somewhere you cannot attach a debugger, two questions arrive that only instrumentation answers: what happened to this one request the customer is complaining about, and is the whole thing getting slower. Logs answer the first, metrics the second, and mixing them up is the most common observability mistake. Your program will write the first kind: one exact line per completed request.
The access log line
One line per request, in the Common Log Format that Apache defined and every tool since has copied (with the Referer, User-Agent and latency extensions everybody adds):
203.0.113.9 - alice [17/Mar/2025:09:15:23 +0000] "GET /api/users HTTP/1.1" 200 482 "https://a.test/" "curl/8.5.0" 23ms
| Field | Rule |
|---|---|
| client address | as given |
- | the RFC 1413 ident field, dead for thirty years, always a dash |
| user | the authenticated user, - when anonymous |
[time] | dd/Mon/yyyy:HH:MM:SS +0000, English month names, not the machine's locale |
"request" | the request line as received, quoted |
| status, bytes | the status code, and body bytes sent: - when it is 0 |
"referer", "agent" | quoted; - inside the quotes when absent |
| latency | milliseconds with a unit suffix |
Follow the format exactly. GoAccess, AWStats, Splunk and every log pipeline already parse it for free, and a dash is a placeholder precisely so every line has the same field count. Emit an empty string and every downstream parser shifts a column.
The timestamp, without a date library
Servers record time as seconds since 1970-01-01 00:00:00 UTC. To print 17/Mar/2025:09:15:23 from 1742202923 you need only integer arithmetic:
- Split off the time of day: the remainder after dividing by 86,400 is the seconds into the day, which gives hours, minutes and seconds.
- The quotient is the number of whole days since 1970. Walk years forward from 1970, subtracting 365 days, or 366 in a leap year, while the days left cover the whole year.
- Do the same with the months of that year, using 29 days for February in a leap year.
- The days left plus one is the day of the month.
A year is a leap year when divisible by 4, except centuries, except centuries divisible by 400. So 2000 and 2024 are leap, 1900 and 2100 are not. Keep epochs in a 64-bit integer: 2,147,483,648 is January 2038, and a 32-bit time_t overflows there. The English month abbreviations are Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec, written from a table, never from LC_TIME.
The request line is attacker-controlled
The quoted fields contain bytes the client chose. A client that sends a path containing a quote and a fake status can make one request look like two log lines, or forge a field, and cover its tracks. Before any quoted field reaches the log, escape it:
"becomes\"and\becomes\\;- CR, LF and TAB become
\r,\n,\t; - every other byte below 0x20 or at/above 0x7f becomes
\xplus two lowercase hex digits, soé(UTF-8 bytes c3 a9) is logged as\xc3\xa9.
An empty request line (a client that connected and left) is logged as -.
Metrics, briefly
Logs scale with traffic, metrics do not. A counter incremented a billion times is still one number. http_requests_total{method,route,status} is a counter; its rate gives throughput and the share of 5xx gives the error rate. Latency belongs in a histogram, not an average: a 40 ms mean is consistent with every request taking 40 ms and with 95% at 5 ms next to 5% at 700 ms, and only buckets give a p99. Keep label sets small: a label per user id makes a new time series per value.
The contract
Each input line has nine TAB-separated fields: ip, epoch, user, request, status, bytes, referer, agent, latency_ms. Print the log line described above. An empty user, referer or agent prints -.
Common mistakes
- Printing
0for a zero-byte response instead of-. - A 32-bit
time_t, or aMontable indexed from 1. - Treating 2100 as a leap year.
- Logging the request line raw, so a quote forges fields.
- Escaping bytes only above 0x7f and letting an ESC or DEL through.
Your exercise
The starter splits each line into its nine fields. You format the time, apply the dash rules and escape the quoted fields.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…