Lab: log detective
A web app had a bad morning, and you have its logs. Find out what went wrong with grep and pipes, watch a log live, catch a program holding a port and stop it, then solve the same case again with SysVitals and LogLens.
Level: beginner · Time: 45 to 60 minutes · Before this: the six lessons of this track
What you need
- A Linux computer or virtual machine, with two terminal windows or tabs open. Everything here runs as your normal user. You never need
sudo. - Python 3, which almost every Linux distribution includes. Check with
python3 --version. - For part 5, SysVitals downloaded and executable. For part 6, LogLens installed from source, with its virtual environment. Parts 1 to 4 need neither.
Everything happens in one practice folder, ~/detective, which you delete at the end.
Part 1: set up the scene
Make the practice folder and go into it:
bashmkdir ~/detective cd ~/detectiveThe prompt now ends in
~/detective$.Create the app's log. Paste this whole block and press Enter:
bashfor i in $(seq 0 47); do t=$(printf '10:%02d:00' "$i") if [ $((i % 10)) -eq 7 ]; then echo "2026-11-09 $t ERROR database connection refused" elif [ $((i % 15)) -eq 4 ]; then echo "2026-11-09 $t ERROR payment service timeout" elif [ $((i % 6)) -eq 0 ]; then echo "2026-11-09 $t WARN slow response (1200 ms)" else echo "2026-11-09 $t INFO request ok" fi done > app.logYou do not need to follow every detail. It is a loop: it counts
ifrom 0 to 47, writes one log line per minute from 10:00 to 10:47, and picks the level from the remainder (%) ofidivided by 10, 15 or 6. The> app.logat the end sends all 48 lines into the file. Nothing is printed.Create the web server's access log the same way:
bashfor i in $(seq 0 47); do case $((i % 12)) in 5) code=500 ;; 3|9) code=404 ;; *) code=200 ;; esac printf '192.168.1.%d - - [09/Nov/2026:10:%02d:00 +0000] "GET /page/%d HTTP/1.1" %d 512 "-" "curl/8.5.0"\n' $((10 + i % 7)) "$i" "$i" "$code" done > access.logEach line is one web request in the common "combined" format: the client's address, the time, the request, the status code, the size, and the client program.
Check what you made:
textsam@laptop:~/detective$ wc -l *.log 48 access.log 48 app.log 96 total sam@laptop:~/detective$ head -n 3 app.log 2026-11-09 10:00:00 WARN slow response (1200 ms) 2026-11-09 10:01:00 INFO request ok 2026-11-09 10:02:00 INFO request ok sam@laptop:~/detective$ head -n 3 access.log 192.168.1.10 - - [09/Nov/2026:10:00:00 +0000] "GET /page/0 HTTP/1.1" 200 512 "-" "curl/8.5.0" 192.168.1.11 - - [09/Nov/2026:10:01:00 +0000] "GET /page/1 HTTP/1.1" 200 512 "-" "curl/8.5.0" 192.168.1.12 - - [09/Nov/2026:10:02:00 +0000] "GET /page/2 HTTP/1.1" 200 512 "-" "curl/8.5.0"
Part 2: question the logs
Answer each question with one command line. Try to build it yourself before you look at the answer under it.
How many errors did the app log?
textsam@laptop:~/detective$ grep -c ERROR app.log 8When did the trouble start?
-m 1makesgrepstop after the first match:textsam@laptop:~/detective$ grep -m 1 ERROR app.log 2026-11-09 10:04:00 ERROR payment service timeoutWhich error happens most? Keep the error lines, cut off the date, time and level, then count the repeats:
textsam@laptop:~/detective$ grep ERROR app.log | cut -d' ' -f4- | sort | uniq -c | sort -rn 5 database connection refused 3 payment service timeoutThe database is the main suspect.
How healthy is the log overall? Count each level, the third word on every line:
textsam@laptop:~/detective$ awk '{print $3}' app.log | sort | uniq -c 8 ERROR 32 INFO 8 WARNWhat did the web server answer? In
access.logthe status code is the ninth word:textsam@laptop:~/detective$ awk '{print $9}' access.log | sort | uniq -c | sort -rn 36 200 8 404 4 500Four requests failed on the server side (500), and eight asked for pages that do not exist (404).
Which client got the most 404s?
awkcan filter too:$9 == 404keeps only those lines, and{print $1}prints the client's address:textsam@laptop:~/detective$ awk '$9 == 404 {print $1}' access.log | sort | uniq -c | sort -rn | head -n 1 2 192.168.1.13Write up the evidence. Put the app's errors and the server's 5xx lines in one report:
textsam@laptop:~/detective$ grep ERROR app.log > report.txt sam@laptop:~/detective$ awk '$9 >= 500' access.log >> report.txt sam@laptop:~/detective$ wc -l report.txt 12 report.txtThe first command uses
>to start the file; the second uses>>to add to it. With>twice, you would keep only the last four lines.
Part 3: watch it live
In the first terminal, follow the app log and show only errors:
bashtail -f app.log | grep ERRORtail -fstarts with the last 10 lines, so you see one old error, then nothing more. The command keeps running and waits:text2026-11-09 10:47:00 ERROR database connection refusedIn the second terminal, go to the folder and play the app: write one error and one normal line.
bashcd ~/detective echo "2026-11-09 10:48:00 ERROR disk full" >> app.log echo "2026-11-09 10:48:30 INFO request ok" >> app.logIn the first terminal, only
2026-11-09 10:48:00 ERROR disk fullappears. TheINFOline was written to the file too, butgrepfiltered it out.In the first terminal, press Ctrl + C to stop following. You get your prompt back, and
wc -l app.lognow says50 app.log.
Part 4: the program on port 8099
Now a mystery program is holding a port. You will play both parts: start it, then track it down.
In the first terminal, start a small web server in the background, with its output going to a log of its own:
textsam@laptop:~/detective$ python3 -u -m http.server 8099 --bind 127.0.0.1 > server.log 2>&1 & [1] 4410[1]is the job number and4410the PID; yours will differ.-umakes Python write each line straight away instead of saving them up, and> server.log 2>&1sends both output streams toserver.log.--bind 127.0.0.1keeps the server reachable from this computer only.Find out what is listening on port 8099:
textsam@laptop:~/detective$ ss -tlnp | grep 8099 LISTEN 0 5 127.0.0.1:8099 0.0.0.0:* users:(("python3",pid=4410,fd=3))The program is
python3, its PID is 4410, and it listens on127.0.0.1only.Visit it. Open
http://127.0.0.1:8099/in a web browser on the same computer: you see a list of the files in~/detective. If you havecurl,curl -s http://127.0.0.1:8099/ | head -n 3does the same in the terminal. Then read the server's log:textsam@laptop:~/detective$ tail -n 2 server.log Serving HTTP on 127.0.0.1 port 8099 (http://127.0.0.1:8099/) ... 127.0.0.1 - - [09/Nov/2026 17:46:35] "GET / HTTP/1.1" 200 -Stop it politely with
SIGTERM, using the PID you found (not 4410, unless that is yours):bashkill 4410Press Enter once more and the shell reports
[1]+ Terminatedwith the command.Check that the port is free:
textsam@laptop:~/detective$ ss -tlnp | grep 8099 sam@laptop:~/detective$No output means nothing is listening on 8099 any more.
Part 5: the same case in SysVitals
Start the server again. Press ↑ until you reach the
python3 -u -m http.server …line and press Enter. Note the new PID.In the second terminal, go to the folder that holds
sysvitals.pyand start it with./sysvitals.py. You start on the Processes tab.Press 3 for the Ports tab. Press /, type
8099and press Enter.You see one
tcprow: address127.0.0.1, port8099, your user, and your PID withpython3under PID/PROGRAM. It is not yellow, because it listens on the loopback address only.Press 1 for Processes. Press /, type the PID and press Enter. The table narrows to the
python3process with that PID.Press x. The bottom line asks
Signal PID … (python3)?with[t] SIGTERM [k] SIGKILL [Esc] cancel. Press t.The bottom line shows
sent SIGTERM toand the PID: the same signal thatkillsent in part 4.Press q to quit SysVitals. In the first terminal, press Enter: the job is reported as
Terminated.
TIP
./sysvitals.py --dump prints all four views once as text, so you can search it with the tools from part 2. While the server runs, ./sysvitals.py --dump | grep 8099 prints its line from the --- Listening ports --- section, ending in the PID and python3.
See Network connections and ports and Watch and stop processes for more.
Part 6: the same case in LogLens
In the second terminal, go to your cloned
loglensfolder and activate its virtual environment withsource .venv/bin/activate, as in Install.Start a third log that keeps growing, five lines a second, using the sample script that comes with LogLens:
bashpython3 scripts/genlog.py ~/detective/live.log --format generic --rate 5 &genlog.pyempties the file you name before it writes, so always give it a new name such aslive.log, neverapp.log.Open all three logs in one view:
bashloglens ~/detective/app.log ~/detective/access.log ~/detective/live.logThe status bar at the top shows one coloured label per file. LogLens loads the last 50 lines of each file at the start, which is all of
app.logandaccess.log, then newlive.loglines keep arriving.Press e for errors only. You see the
ERRORlines fromapp.log, the500lines fromaccess.log(LogLens counts a 5xx status as an error), and errors with their tracebacks fromlive.log. The status bar showsERRORS. Press e again to see everything.Press /, type
refusedand press Enter. Only lines containingrefusedstay, from every file, likegrepon all three at once. Press Esc to clear the filter.Press c to open the error clusters panel. Look for the group for
database connection refusedwith[×5]and the group forpayment service timeoutwith[×3]: the same counts your pipeline found in part 2. Thelive.loggroups keep growing while you watch. Press Esc to close the panel.Press q to quit LogLens. Stop the sample script with
kill %1in the same terminal.
See Track down errors for the histogram and for exporting what you find.
Break it, try it
Each of these takes a minute or two and shows you a mistake on purpose, where it is safe.
Two servers, one port. Start the server from part 4 in the background, then start the same command again in the foreground, without the & and the redirection. The second one fails with OSError: [Errno 98] Address already in use. Find the first with ss -tlnp | grep 8099 and stop it with kill.
Open to the network. Start the server without --bind 127.0.0.1. ss -tlnp | grep 8099 now shows 0.0.0.0:8099, and in SysVitals's Ports tab the row is yellow: other machines on your network could reach the files in ~/detective. Try this only on a network you trust, and stop the server at once.
The overwrite trap. Make a copy and sort it into itself:
sam@laptop:~/detective$ cp app.log copy.log
sam@laptop:~/detective$ sort copy.log > copy.log
sam@laptop:~/detective$ wc -l copy.log
0 copy.logThe shell emptied copy.log for the > before sort started reading it. That is why you never redirect into the file you are reading.
Locked out. Take away every permission and try to read the report:
sam@laptop:~/detective$ chmod 000 report.txt
sam@laptop:~/detective$ cat report.txt
cat: report.txt: Permission denied
sam@laptop:~/detective$ chmod 644 report.txtYou own the file, so chmod gives the permissions back. No sudo needed.
Careful with pkill. pkill -f http.server stops every process whose full command line contains http.server, which can include processes you did not mean, such as someone else's server or a shell running a script with those words in it. Prefer ss to find the exact PID, and kill that.
Clean up
Make sure nothing from the lab is still running. Both of these should print nothing:
bashss -tlnp | grep 8099 pgrep -af genlogIf either prints a line, stop that PID with
kill.Look at what you are about to delete, then delete it:
bashcd ~ ls ~/detective rm -r ~/detectivelsshould list only the lab files, such asaccess.log,app.log,live.log,report.txtandserver.log.rm -rremoves the folder for good.Keep SysVitals and the LogLens folder if you want to use them again. To remove them, see their install pages.
Check yourself
Question 1 of 6
Which command line lists the most common error message first?
What you learned
grep,cut,awk,sort,uniq -candwc -lanswer real questions about a log, one pipe at a time.>starts a file and>>adds to it; redirecting into the file you are reading empties it.tail -f | grepwatches a log for the lines you care about.ss -tlnpfinds the program on a port, andkillsends itSIGTERM.- SysVitals shows the same port and process and stops it with the same signal; LogLens merges several logs and counts repeated errors for you.