Log Analysis
Linux Logs and System Logs and dmesg covered where logs live and how they get collected. The next step is turning that raw data into a practical conclusion. Skimming a single log file by eye is easy; a production server writing several million lines a day is not something anyone reads line by line. This article applies the tools already covered in Searching Files and Text and Rewriting Text: sed and awk specifically to log analysis, and adds journalctl's structured output formats to the toolkit.
Lab Data
The following commands build a test file that mimics a real production log, but stays fully under your control:
mkdir -p ~/lab/logs && cd ~/lab/logs
cat > app.log <<'EOF'
2026-07-24 09:12:01 INFO ip=10.0.0.5 path=/api/login status=200 duration=42ms
2026-07-24 09:12:03 ERROR ip=10.0.0.9 path=/api/orders status=500 duration=1381ms
2026-07-24 09:12:05 INFO ip=10.0.0.5 path=/api/orders status=200 duration=88ms
2026-07-24 09:12:07 ERROR ip=10.0.0.9 path=/api/orders status=500 duration=1290ms
2026-07-24 09:12:10 WARN ip=10.0.0.7 path=/api/login status=401 duration=15ms
2026-07-24 09:12:12 ERROR ip=10.0.0.9 path=/api/orders status=500 duration=1455ms
2026-07-24 09:12:20 INFO ip=10.0.0.5 path=/api/profile status=200 duration=30ms
EOF
Every command below runs against this ~/lab/logs/app.log file and is read-only — it has no effect on any real system.
Extracting the Right Events with grep
2026-07-24 09:12:03 ERROR ip=10.0.0.9 path=/api/orders status=500 duration=1381ms
2026-07-24 09:12:07 ERROR ip=10.0.0.9 path=/api/orders status=500 duration=1290ms
2026-07-24 09:12:12 ERROR ip=10.0.0.9 path=/api/orders status=500 duration=1455ms
To search for several conditions at once, use extended regular expressions (-E):
Counting matches:
Viewing surrounding context — what happened just before and after an error line:
-B1 shows one line before the match, -A1 one line after — useful for understanding the event's surrounding context.
Searching Rotated and Compressed Logs
Real incidents rarely stay inside the current file. As Linux Logs showed, logrotate renames older logs to syslog.1 and compresses everything beyond that to syslog.2.gz, syslog.3.gz, and so on — and plain grep cannot read a .gz file. The z-prefixed variants decompress on the fly:
To sweep the entire rotated set — current file plus every archive — in one pass, zgrep accepts the glob directly and reads plain and compressed files together:
Forgetting this is a classic mistake: grep ERROR /var/log/syslog finds nothing from before the last rotation and quietly gives an incomplete picture, when the evidence you need is sitting one .gz file over.
Extracting Columnar Data with awk
Pulling only the ip= value out of every ERROR line:
This two-stage approach is clear, but awk alone can do the same job in one command:
Finding which IP address produced the most errors — one of the most common patterns in practice:
The chain works as follows: grep -oE extracts only the matching fragment (ip=...), sort groups identical values together, uniq -c counts how many times each one repeats, and the final sort -rn orders the result from highest count to lowest — the most frequent value ends up first.
Analyzing response time (duration) — for example, finding the three slowest requests:
Tip
awk has no built-in sort, so numeric sorting is normally done by extracting the values with awk and finishing the job with sort -n — chaining the two tools together is a standard pattern, not a workaround.
Getting journalctl in Structured Form
A grep/awk chain over plain text requires knowing the log format in advance. journalctl entries are already structured, and can be pulled out in JSON:
{
"__CURSOR" : "s=8f2a...",
"_SYSTEMD_UNIT" : "ssh.service",
"PRIORITY" : "6",
"MESSAGE" : "Accepted publickey for ali from 10.0.0.5 port 51422",
"__REALTIME_TIMESTAMP" : "1753349532000000"
}
This format is convenient for scripted processing, since the fields (MESSAGE, PRIORITY, _SYSTEMD_UNIT) are known in advance and can be referenced without worrying about format changes. If jq is installed, for example:
This prints only the message text of entries at err severity or more severe (PRIORITY <= 3, matching the table in System Logs and dmesg).
Correlating Multiple Sources by Time
Diagnosing a real problem often takes more than one source. For instance, if there's an application error (ERROR in app.log), it helps to know what else was happening on the system at that exact moment:
This applies the --since/--until filter, covered in journalctl, to the exact window when an error occurred. If a systemd message about memory pressure or a service restart shows up in that window, it may well explain the real cause of the application error — resource-related situations like this are covered in more depth in Resource Troubleshooting.
Practical Scenario: Which Endpoint Is Causing the Most Trouble
awk '$3 ~ /ERROR|WARN/ { for (i=1;i<=NF;i++) if ($i ~ /^path=/) print $i }' app.log \
| sort | uniq -c | sort -rn
Interpretation: /api/orders is logged with three ERROR entries, /api/login with one WARN — so attention should go to /api/orders first. Rather than accepting this conclusion blindly, it is worth cross-checking the automatically computed count against the source file:
This verification step confirms the automated count against the raw log — a step that should never be skipped in professional analysis.
Interview angle
A common interview probe is: "how do you know your grep/awk chain isn't silently dropping or double-counting lines?" The honest answer is that any pipeline built from independent tools can behave unexpectedly on malformed input — a missing field, an unescaped character, an encoding issue. That is exactly why the verification step above (re-checking a computed count against grep on the raw source) is not optional polish; it is the difference between a number you can act on and a number that merely looks convincing.
Common Mistakes
Not Escaping Regex Special Characters
In grep '10.0.0.9', the dot (.) means "any character" in a regular expression, not a literal dot. To search for an exact IP address, use grep -F '10.0.0.9' (literal search) or grep '10\.0\.0\.9' (escaped) — otherwise a string like 10a0b0c9 could match when it shouldn't.
Not Verifying Computed Statistics Against the Source
A sort | uniq -c chain with correct syntax but the wrong column or filter can produce a wrong, yet confident-looking, number. Always cross-check an important conclusion against the raw log at least once before acting on it.
Treating journalctl's Plain-Text Format as Stable for Parsing
journalctl's default (text) output can shift slightly in formatting between versions. For anything a script depends on, explicit formats like --output=json or --output=cat are far more reliable than parsing plain text with awk/sed.
Exercises
- In the lab file above, use
grepto extract all lines withstatus=500and count them with-c. - Using
awk, extract thedurationvalue from each line and compute the average (using asum/countvariable pattern inawk). - Run
journalctl -u ssh.service -n 20 --output=json-prettyand locate at least four fields (MESSAGE,PRIORITY,_PID,__REALTIME_TIMESTAMP). - Invent your own scenario (for example, "which user produced the most 401 errors") and solve it against the lab file above using a
grep/awkchain.
Verification criterion: for exercise 1, your computed count must match the number of lines you get from independently reading the raw file — if it doesn't, the filter condition is wrong, not the data.
References
- man7.org:
grep(1): https://man7.org/linux/man-pages/man1/grep.1.html - GNU Awk User's Guide: https://www.gnu.org/software/gawk/manual/gawk.html
- man7.org:
journalctl(1)—--outputformats: https://man7.org/linux/man-pages/man1/journalctl.1.html - jq Manual: https://jqlang.org/manual/