Chapter 110·Intermediate·9 min read
Reading Logs: Finding the One Line That Matters
A plain-English guide to reading logs on Linux — what logs are and where they live, watching them live with tail -f, searching with grep and pipes, reading systemd's journal with journalctl, and a calm method for debugging a production incident. Turning 'it's broken' into 'here's why'.
July 17, 2026
Something has gone wrong on a server. The app is returning errors, or it won't start, or it's mysteriously slow — and you can't see inside it, can't pause it, can't step through it with a debugger. What you can do is read what it wrote down. Programs keep a running diary of what they're doing, and when they fail, the reason is almost always already recorded. This final chapter is the skill of reading that diary: finding, in a river of thousands of lines, the one that explains the problem. It's the payoff for every other skill in this guide.
What logs are, and why they're your lifeline
A log is a running record a program writes as it works — a timestamped line for each notable thing it does: a request served, a database query, a warning, an error, a crash. Nobody reads logs for fun; you read them when something breaks, because the log is the evidence. On a production server you can't attach a debugger or add a print and re-run reality — the failure already happened. The logs are what it left behind.
A typical log line packs several facts into one row:
2026-07-17 09:14:22 ERROR db.connection Connection refused: could not connect to postgres:5432Timestamp, severity level, which component, and the message. Those severity levels are your first filter — most logging uses the same ladder:
- DEBUG — fine-grained detail, usually noise unless you're deep in a problem.
- INFO — normal events: "server started," "request handled."
- WARN — something's off but the program coped.
- ERROR — something failed.
- FATAL / CRITICAL — something failed so badly the program is going down.
When you're hunting a problem, you mentally (and often literally, with grep) skip straight to ERROR and WARN. Which brings us to the real skill: logs are enormous, and reading them is entirely about filtering.
Where logs live
There are two homes for logs on a modern Linux system, and you'll meet both.
The traditional home is the /var/log directory — plain text files you already know how to read with the tools from chapter three:
$ ls /var/log
syslog auth.log nginx/ dpkg.log ...nginx/access.log records every web request; auth.log records logins and sudo use; syslog is the system's general diary. Because they're just text files, everything you learned — less, grep, tail, pipes — works on them directly.
The modern home, on most current Linux distributions, is the systemd journal — a structured log that the system service manager keeps for you, read with a dedicated command called journalctl (coming up shortly). Many services log to both, and knowing which to reach for is half the battle. The rule of thumb: for a specific service like nginx or your own app, try journalctl first; for lower-level system and access logs, look in /var/log.
Watching logs live: tail -f
Often the most powerful move is to watch a log as it happens. tail normally prints the last few lines of a file — but add -f (follow) and it stays open, streaming each new line the instant it's written:
$ tail -f /var/log/nginx/access.logNow the terminal is a live feed. Trigger the action you're investigating — reload the page, retry the failing request — and watch precisely what the server records at that moment. There's no faster way to connect "I did this" to "the program logged that." Press Ctrl-C to stop following. A common refinement shows the last 100 lines and then follows:
$ tail -n 100 -f /var/log/app.logFinding the needle: grep and pipes
When the log already holds the event — it happened an hour ago — you search rather than watch, and this is where chapter four's pipes come into their own. A log is exactly the "river of text" that filters were built for.
$ grep -i "error" /var/log/app.log # every error line
$ grep -i "error" app.log | tail -20 # the 20 most recent
$ grep "500" access.log | wc -l # how many 500 errors?
$ grep -i "error" app.log | grep "payment" # errors mentioning paymentThat third line answers "how bad is it?" with a number; the fourth narrows by chaining two greps, keeping only the errors that also mention payments. You can pull the busiest error, or the timeline of a single user's requests, by stacking the same small tools:
$ grep "user_id=4821" app.log | grep -i "error"A genuinely useful one-liner: which errors are most frequent right now?
$ grep -i "error" app.log | cut -d' ' -f4- | sort | uniq -c | sort -rn | headThat strips the timestamp off each error, groups identical messages, counts them, and shows the top offenders — instantly telling you whether you're chasing one repeated failure or a scatter of unrelated ones.
The modern way: journalctl
On systemd-based systems, services managed by the system (your web server, your database, your own app if you set it up as a service) log to the journal, and journalctl is the one tool to read it. Its flags mirror everything you already know:
$ journalctl -u nginx # logs for just the nginx service (-u = unit)
$ journalctl -u nginx -f # follow them live, like tail -f
$ journalctl -u nginx --since "1 hour ago" # only the recent window
$ journalctl -u myapp -p err # only error-priority and worse
$ journalctl -u myapp --since today | grep timeoutThe wins over raw files: -u scopes to exactly one service so you're not sifting through everything at once, --since/--until slice by time (invaluable when you know roughly when it broke), and -p filters by priority level. And because its output is just text on stdout, you can pipe it into grep, wc, and friends exactly like any other command — the journal plays by the same rules as the rest of the shell.
Putting it together: debugging an incident
Here's the calm, methodical approach that turns a panicked "it's down!" into a fix. Suppose your app is throwing 500 errors.
- Confirm the process is alive —
ps aux | grep myapporjournalctl -u myappfor a "started/crashed" line. Sometimes "the app is erroring" is really "the app isn't running," and that changes everything. - Read the recent errors —
journalctl -u myapp -p err --since "30 min ago", orgrep -i error /var/log/app.log | tail -30. Look for the first error in a burst; later ones are often just fallout from the first. - Locate when it began — narrow with
--sinceuntil you find the moment the errors start. That timestamp often lines up with a deploy, a traffic spike, or a dependency going down — which points straight at the cause. - Reproduce while following —
tail -f(orjournalctl -f) the log and trigger the failing request. Watching the exact lines it writes as it fails is frequently the moment the answer appears. - Read the root-cause line and act — a stack trace, a "connection refused," a "permission denied" (now you know exactly what that means, and how to fix it with
chown/chmod). That line is what you came for.
Notice that this final skill uses every chapter before it: you ssh into the server (ch. 7), cd to the logs (ch. 2), check the ps and ports (ch. 6), reason about a "permission denied" (ch. 5), and slice the log with grep and pipes (ch. 3–4). Reading logs isn't a separate topic — it's where all of it comes together under pressure.
Where this leaves you
You began this guide facing a blinking cursor that felt like a test. You now have the full working vocabulary of a Linux terminal: you can move around the filesystem, wrangle files, compose small tools into custom ones with pipes, reason about who's allowed to do what, watch and control running processes and the ports they hold, reach any server in the world over SSH, and — when something breaks — read the logs to find out exactly why.
That's not a collection of trivia to memorise; it's a way of working that every engineering role assumes and every production system demands. The terminal has stopped being a black box and become what it always was for the people who live in it: the most direct, most powerful conversation you can have with a computer. From here, the natural next step in the Common Foundations track is containers — Docker packages an app with its whole Linux environment so it runs the same everywhere — and every command you just learned is exactly what you'll use inside them.