NET-103 · Lesson 9 of 12 · 28 min

Logs, monitoring and troubleshooting

▶ Video coming soon A video version of this lesson is on the way.

At 08:05 on a Saturday in November, Rosa texted Sam: the shop was showing an error page, and Saturday morning is when the week's biggest orders arrive. Sam was out, so Jordan logged in. Within ten minutes he had found the cause, a disk filled by a single runaway log file, freed space, brought the shop back and set up a rule so that it could not happen again. He could do it because Linux records almost everything that happens in logs, and because he knew where to look and which commands summarize a server's health. This lesson follows that morning, and teaches the logging and monitoring tools behind it.

It covers where Linux keeps its logs, the systemd journal and journalctl, the traditional log files and their priorities, log rotation, the commands that show a server's health at a glance, the basics of continuous monitoring, and a method for troubleshooting that works for problems you have never seen before.

Where logs live

Linux has two logging systems that usually run side by side. The systemd journal collects messages from the kernel, from every service systemd starts and from programs that use the standard logging interface, and stores them in a structured binary format under /var/log/journal. The traditional syslog service, rsyslog on Ubuntu, receives the same messages and writes them to plain text files in /var/log, sorted by kind. Many applications also write their own log files, usually under /var/log in a directory of their own.

Where to find the logs on ridge-web
LogWhat it records
journalctl (the journal)Everything from the kernel and services, searchable by service, time and priority
/var/log/syslogGeneral system messages (on Debian and Ubuntu; RHEL uses /var/log/messages)
/var/log/auth.logLogins, SSH, sudo and other authentication events (RHEL: /var/log/secure)
/var/log/kern.logKernel messages, including hardware and disk errors
/var/log/apt/history.logEvery package installed, upgraded or removed, with the command used
/var/log/nginx/access.log and error.logEvery web request, and web server errors
/var/log/postgresql/Database server messages
/var/log/ridgeline/The shop application's own logs

Each message has a priority, a number from 0 to 7 inherited from the original syslog design of the 1980s, and a facility, which says what kind of program sent it. Knowing the priorities lets you filter out routine chatter and see only what matters.

Syslog priorities, from most to least severe
NumberNameMeaning
0emergThe system is unusable
1alertAction must be taken immediately
2critCritical conditions, such as hardware failure
3errErrors
4warningSomething unusual that may become a problem
5noticeNormal but significant events
6infoRoutine information
7debugDetail for developers, usually switched off

The journal and journalctl

The journalctl command reads the journal. On its own it shows every message since records began, oldest first, which is rarely what you want; its options narrow the view by service, time, priority and boot. Members of the adm or systemd-journal groups, and sudo users, can read the whole journal.

Useful journalctl filters
CommandShows
journalctl -u ridgeline-shopMessages from one service
journalctl -u nginx --since "2026-11-07 07:30" --until "08:30"One service in a time window
journalctl -p err -bErrors and worse since the current boot
journalctl -b -1Everything from the previous boot, useful after a crash
journalctl -kKernel messages, the same as dmesg
journalctl -f -u ridgeline-shopFollow new messages live, like tail -f
journalctl -eJump to the end of the journal
journalctl --disk-usageHow much space the journal uses
The first command Jordan ran on Saturday morning, and the answer
> journalctl -u ridgeline-shop -p err --since today
Nov 07 07:58:31 ridge-web gunicorn[1391]: OSError: [Errno 28] No space left on device: '/var/log/ridgeline/shop-debug.log'
Nov 07 07:58:31 ridge-web gunicorn[1391]: [ERROR] Exception in worker process
Nov 07 07:58:32 ridge-web systemd[1]: ridgeline-shop.service: Main process exited, code=exited, status=1/FAILURE
Nov 07 07:58:32 ridge-web systemd[1]: ridgeline-shop.service: Failed with result 'exit-code'.
Nov 07 07:58:37 ridge-web systemd[1]: ridgeline-shop.service: Scheduled restart job, restart counter is at 1.

The first error line names the problem precisely: Errno 28, no space left on device, while writing to a debug log. The following lines show systemd doing its job, restarting the service five seconds after each failure as the unit file asked, but every restart failed for the same reason. Starting with the failing service's errors, rather than reading logs from the top, is what turned a potentially long investigation into a short one.

Following the clue

The error pointed at storage, so the next commands were df and du from lesson 7, followed by a look at the file itself.

The root file system is full, and one debug log is 38 GB
> df -h /var/log /srv
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda2        78G   78G     0 100% /
/dev/vdb1        98G   41G   53G  44% /srv
> sudo du -sh /var/log/* 2>/dev/null | sort -h | tail -n 3
412M    /var/log/journal
1.1G    /var/log/nginx
38G     /var/log/ridgeline
> ls -lh /var/log/ridgeline/
-rw-r----- 1 shop adm 38G Nov  7 07:58 shop-debug.log
-rw-r----- 1 shop adm 21M Nov  7 07:58 shop.log

A developer had switched on debug logging while testing a new checkout page on Wednesday and forgotten to switch it off, and nothing was rotating the file. Three days of logging every database query had filled the root file system. Note that /srv, where Ridgeline's data lives, was fine: the problem was on the system disk, because the shop writes its logs to /var/log.

There is a trap here that catches many administrators. If you delete a file that a process still has open, the name disappears but the space is not freed until the process closes the file, because the data is still in use. So rm shop-debug.log would have left the disk full while the shop kept running. Jordan instead emptied the file in place with truncate, which keeps it open but releases the space, and only then turned off debug logging.

Freeing the space safely, checking for deleted-but-open files, and restoring the shop
> sudo truncate -s 0 /var/log/ridgeline/shop-debug.log
> df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda2        78G   40G   35G  54% /
> sudo lsof +L1 | head -n 3
COMMAND   PID USER  FD  TYPE DEVICE SIZE/OFF NLINK  NODE NAME
> sudo sed -i 's/^LOG_LEVEL=DEBUG/LOG_LEVEL=INFO/' /etc/ridgeline/shop.env
> sudo systemctl restart ridgeline-shop
> curl -s -o /dev/null -w "%{http_code}\n" https://shop.ridgeline.example/
200

The command lsof +L1 lists open files whose link count is zero, meaning deleted but still open. It came back empty, so no hidden space was being held. The curl command at the end prints only the HTTP status code, and 200 confirmed the shop was back at 08:16, eleven minutes after Rosa's text. Jordan posted a short note to Rosa and Sam: what happened, what he did, and what still needed fixing.

Log rotation

Logs grow forever unless something trims them. On Ubuntu, logrotate runs daily from a systemd timer and handles this for every file it has a rule for: it renames the current log, starts a fresh one, compresses old ones and deletes the oldest. Packages install their own rules in /etc/logrotate.d, which is why nginx's and PostgreSQL's logs never caused trouble. The shop's logs had no rule because nobody had written one. The fix, the real cure rather than the emergency truncate, was a rule of a few lines.

The log rotation rule Jordan added for the shop's logs
# /etc/logrotate.d/ridgeline
/var/log/ridgeline/*.log {
    daily
    rotate 14
    maxsize 500M
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
    su shop adm
}

Each setting has a purpose. daily and rotate 14 keep two weeks of logs. maxsize 500M rotates a file early if it grows past 500 MB, even mid-day, which would have caught the debug log on Wednesday. compress and delaycompress gzip old logs but leave yesterday's uncompressed for easy reading. copytruncate copies the log and then empties it in place, the same technique as the emergency fix, for applications that keep their log file open. Testing the rule with sudo logrotate --debug /etc/logrotate.d/ridgeline shows what it would do without doing it. The journal has its own limits, set with SystemMaxUse in /etc/systemd/journald.conf.

A server's health at a glance

Beyond logs, a few commands show the current state of a server. Running them in sequence takes under a minute and answers most first questions about a slow or troubled machine.

A quick health check
CommandWhat it tells you
uptimeTime since boot and the load average over 1, 5 and 15 minutes
free -hMemory and swap in use; the available column is what matters
df -h and df -iSpace and inodes on each file system
top or htopThe busiest processes, live
vmstat 5Processor, memory, swapping and disk activity every five seconds
iostat -x 5Per-disk activity and wait times (from the sysstat package)
ss -sA summary of network connections
systemctl --failedAny failed services
journalctl -p err -bErrors since boot

The load average is the average number of processes running or waiting to run, including those waiting for disk. It should be read against the number of processors: on ridge-web, with two virtual processors, a load of 2 means the processors are fully busy, and a load that stays well above 2 means work is queuing. A high load with low processor use usually means processes are waiting for slow storage. In free's output, Linux uses spare memory as a disk cache, so used memory always looks high; the available column shows how much programs could actually use.

Interpreting the load average against the number of processors
def load_report(uptime_line, cpus):
    loads = uptime_line.split("load average:")[1].split(",")
    one, five, fifteen = (float(x) for x in loads)
    status = "ok"
    if five > cpus:
        status = "overloaded"
    elif five > 0.7 * cpus:
        status = "busy"
    trend = "rising" if one > fifteen * 1.2 else "falling" if one < fifteen * 0.8 else "steady"
    return f"5-min load {five} on {cpus} CPUs: {status}, {trend}"

print(load_report(" 09:20:41 up 3 days,  2:11,  1 user,  load average: 0.02, 0.05, 0.01", 2))
print(load_report(" 08:01:02 up 9 days,  1:03,  2 users,  load average: 3.91, 2.62, 0.88", 2))
print(load_report(" 13:40:10 up 9 days,  6:42,  1 user,  load average: 1.10, 1.52, 1.95", 2))
Output
5-min load 0.05 on 2 CPUs: ok, rising
5-min load 2.62 on 2 CPUs: overloaded, rising
5-min load 1.52 on 2 CPUs: busy, falling

Continuous monitoring and alerting

Jordan found the Saturday problem quickly, but only after customers had already seen the error page. Monitoring turns that around: a program checks the server constantly and alerts someone as soon as a value crosses a threshold, ideally before customers notice. For a small business, a few checks cover most risks: is the shop's home page answering, is any file system's Use figure above 80, are any services failed, is the load or memory unusual, and are backups recent.

Tools range from simple to elaborate. An external uptime service checks the shop's address every minute from the internet and sends a text if it fails, which catches network and DNS problems as well as server ones. On the server, Prometheus with its node exporter collects metrics such as disk, memory and processor use, Grafana draws dashboards from them, and Alertmanager sends alerts; Netdata and Zabbix are popular all-in-one alternatives. Cloud providers offer their own monitoring too. Sam sets Ridgeline up with an external uptime check and a small script, written in lesson 11, that emails Jordan when a disk's Use figure passes 80 or a service fails.

A timeline of the Saturday incident. Wednesday at 14:00 debug logging is switched on. The root file system's Use figure climbs steadily from 18 to 100 over three days, crossing a dashed alert line at 80 on Friday afternoon, where a note says an alert here would have given a day of warning. Saturday at 07:58 the disk is full and the shop fails; at 08:05 Rosa texts; at 08:16 the shop is restored after truncating the log; at 08:40 a logrotate rule and monitoring are added.
The Saturday incident: a disk alert at 80 would have given more than a day of warning

Who did what? Reading auth.log

The authentication log answers questions about access: who logged in, from where, who used sudo and what for, and who tried and failed. On any server reachable from the internet it also shows a constant background of automated attempts to guess SSH passwords. Monday's review of ridge-web's auth.log found thousands of them.

Password-guessing attempts and the sudo audit trail in auth.log
> sudo grep "Failed password" /var/log/auth.log | tail -n 3
Nov  9 03:12:44 ridge-web sshd[20211]: Failed password for root from 192.0.2.201 port 51022 ssh2
Nov  9 03:12:47 ridge-web sshd[20213]: Failed password for invalid user admin from 192.0.2.201 port 51040 ssh2
Nov  9 03:13:02 ridge-web sshd[20220]: Failed password for invalid user oracle from 203.0.113.140 port 40112 ssh2
> sudo grep "sudo:" /var/log/auth.log | grep COMMAND | tail -n 2
Nov  9 09:31:10 ridge-web sudo:   jordan : TTY=pts/0 ; PWD=/home/jordan ; USER=root ; COMMAND=/usr/bin/apt update
Nov  9 09:33:52 ridge-web sudo:   jordan : TTY=pts/0 ; PWD=/home/jordan ; USER=root ; COMMAND=/usr/bin/systemctl restart nginx

The failed attempts are a reason to act, not to panic. With the firewall from lesson 8 only allowing SSH from the office and the roastery, these attempts came from the brief window before ufw was enabled; fail2ban, which bans addresses after repeated failures, and key-only logins in lesson 10 close the door completely. The sudo lines show the audit trail: every privileged command, with the user, directory and full command line. Counting failed attempts by address is a classic first script for a new administrator.

A troubleshooting method

Every problem is different, but a consistent method works for problems you have never met. First, define the symptom precisely: what fails, for whom, since when, and what changed recently. Second, check the obvious, because most outages have dull causes: is the service running, is a disk full, did an update or configuration change happen just before? Third, read the logs for the failing component, errors first, around the time the trouble started. Fourth, form one hypothesis, test it with one change, and observe; changing several things at once leaves you not knowing which one worked. Finally, write down what happened and fix the underlying cause, not just the symptom, so it does not return.

Worked problem

Writing up the Saturday incident

Sam asks Jordan for a short incident report that Rosa can read and that will help prevent repeats. What should it contain?

Show solution
  1. Summary: the shop was unavailable from 07:58 to 08:16 on Saturday 7 November, about 18 minutes, during the busiest time of the week.
  2. Cause: debug logging left on after testing on Wednesday filled the root file system with a 38 GB log, and the shop's logs had no rotation rule.
  3. Detection: Rosa noticed the error page; no monitoring alerted anyone.
  4. Resolution: truncated the log, switched the log level back to INFO, restarted the shop and confirmed with curl.
  5. Follow-up actions with owners: logrotate rule with maxsize (Jordan, done), disk and uptime alerts (Sam, Monday), a checklist step to switch off debug logging after testing (developer), and a separate /var/log volume to consider later.
  6. Tone: blameless, describing what the system allowed to happen rather than blaming the developer, so that people report mistakes freely.

Answer: A blameless report with timeline, cause, detection, fix and owned follow-up actions.

Try it yourself

Count failed SSH logins

Write failed_by_address(log_text) that finds every line in an auth.log containing Failed password and returns a dictionary mapping each source address (the word after from) to the number of failures.

def failed_by_address(log_text):
    counts = {}
    return counts

Try it yourself

Only the serious messages

Journal lines exported for analysis start with a priority number in angle brackets, such as <3> for err. Write serious(lines, worst=3) that returns the messages (the text after the closing bracket, stripped) whose priority number is less than or equal to worst, keeping their order.

def serious(lines, worst=3):
    return lines

Try it yourself

Is the server overloaded?

Write load_status(uptime_line, cpus) that reads the 5-minute load average from a line of uptime output and returns overloaded if it is greater than cpus, busy if it is greater than 0.7 times cpus, and ok otherwise.

def load_status(uptime_line, cpus):
    return 'ok'

Common mistakes

  • MistakeReading logs from the top and getting lost.

    InsteadFilter first: the failing service, errors and worse, around the time the trouble began.

  • MistakeDeleting a large log that a process still has open.

    InsteadTruncate it in place, or restart the process; deleted-but-open files keep using space.

  • MistakeFixing the symptom and moving on.

    InsteadFind and fix the cause, such as a missing logrotate rule, and write it down.

  • MistakeJudging the load average without considering the number of processors.

    InsteadCompare the load with the CPU count; 2 is full on a two-processor machine.

  • MistakeTreating high used memory in free as a problem.

    InsteadLook at the available column; Linux uses spare memory for disk caching.

  • MistakeRelying on customers to report outages.

    InsteadSet up uptime checks and alerts for disks, services and backups.

Summary and key terms

  • Logs go to the systemd journal and to text files in /var/log, such as syslog and auth.log; applications often keep their own.
  • Priorities run from 0 (emerg) to 7 (debug); filter by priority to see what matters.
  • journalctl filters the journal by service (-u), priority (-p), time (--since) and boot (-b), and follows it with -f.
  • Deleting an open log does not free space; truncate it instead, and use lsof +L1 to find hidden deleted files.
  • logrotate rules in /etc/logrotate.d rotate, compress and delete logs; every application log needs one.
  • uptime, free, df, top, vmstat and systemctl --failed give a quick health check; read the load against the CPU count.
  • Monitoring and alerting catch problems before customers do.
  • Troubleshoot methodically: define the symptom, check the obvious, read the logs, change one thing at a time, then fix the cause and write it up.
systemd journal
The structured log store that collects messages from the kernel and services.
journalctl
The command that reads and filters the systemd journal.
rsyslog
The service that writes traditional text log files in /var/log.
priority
A syslog severity level from 0 (emerg) to 7 (debug).
log rotation
Regularly renaming, compressing and deleting old log files.
load average
The average number of processes running or waiting to run over 1, 5 and 15 minutes.
monitoring
Continuously measuring a system's health and availability.
alerting
Notifying people automatically when a measurement crosses a threshold.
incident report
A written account of an outage: timeline, cause, fix and follow-up actions.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Write a journalctl command showing errors and worse from nginx since 06:00 today.

    Show answer

    journalctl -u nginx -p err --since 06:00

  2. On a four-processor server the 15-minute load average is 3.2. Is it overloaded?

    Show answer

    No, but it is busy: 3.2 is below 4 processors, though above 0.7 times 4.

  3. After deleting a 10 GB log, df still shows the disk full. Why, and what do you do?

    Show answer

    A process still has the file open; restart that process or truncate through /proc, and next time truncate instead of deleting.

  4. Which logrotate setting rotates a file early when it gets too big?

    Show answer

    maxsize, for example maxsize 500M.

  5. Where would you look to see who ran sudo commands yesterday?

    Show answer

    /var/log/auth.log (or journalctl with _COMM=sudo).

  6. Name two things an external uptime check catches that a check on the server cannot.

    Show answer

    DNS failures and network or provider outages, because the server cannot report when it is unreachable.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz