Logs and Monitoring
Finding out what a system is doing: log files and the journal, disk and memory usage, load and uptime.
Why logs matter
A server has no screen that shows error dialogs. When a service stops, a disk fails or someone tries a thousand passwords, the evidence is in the logs: timestamped lines written by the kernel, by services and by the system itself. Logs answer the questions of every incident — what happened, when, and in which order — and they are how you notice problems before users do.
Monitoring is the other half: measuring resources (disk, memory, processor load) so that "the server is slow" becomes "the root file system is 96% full" or "the load is three times the number of cores". This page shows the command-line tools for both. Larger environments send logs and measurements to central systems, but those collect exactly what these tools show. How that works — log shippers, central log stores, metrics and alerting — is the subject of Observability & SRE.
The systemd journal
The journal is the central log of a
system that runs systemd, kept by the systemd-journald
daemon. It collects kernel messages, messages from the boot process,
everything that services write to their output, and messages sent
with the syslog protocol, and stores them in an indexed binary
format with extra fields such as the unit, the process ID (PID) and
the priority.
Where it is stored decides whether it survives a reboot. With the
default setting (Storage=auto in
/etc/systemd/journald.conf) the journal is written to
/var/log/journal/ if that directory exists, and
otherwise only to /run/log/journal/, which is lost at
every reboot. journalctl --list-boots shows how many
boots are kept; if it shows only one, you cannot look at the boot
before a crash.
Reading it with journalctl
journalctl is the command that reads and filters the journal. Without options it shows everything, oldest first, in a pager; the options below turn that into an answer. The unit-level basics are also on Services and systemd.
| Option | Shows | Example |
|---|---|---|
-u unit |
Only one unit | journalctl -u nginx |
-p err |
Only this priority and worse (emerg,
alert, crit, err,
warning, notice,
info, debug, or 0–7)
|
journalctl -p err -b |
-b, -b -1 |
The current boot; the previous boot | journalctl -b -1 -p warning |
-f |
Follow new messages live, like tail -f |
journalctl -f -u inventory-api |
--since, --until |
A time window | journalctl --since "1 hour ago" |
-n 20 |
Only the last 20 lines | journalctl -n 20 -u ssh |
-k |
Only kernel messages (like dmesg) |
journalctl -k -b |
-e, -x |
Jump to the end of the pager; add explanations to known messages | journalctl -xe |
--disk-usage |
How much space the journal takes | journalctl --disk-usage |
The priorities are the same eight severity levels that syslog uses (see severity levels). Options combine, so the question "what went wrong since this morning's boot?" is one command:
$ sudo journalctl -p err -b --no-pager
Sep 30 08:13:55 web01 kernel: ACPI BIOS Error (bug): Could not resolve symbol [\_SB.PC00.PEG1.PEGP], AE_NOT_FOUND (20230628/dswload2-162) Sep 30 10:02:17 web01 (python)[2311]: inventory-api.service: Failed at step EXEC spawning /opt/inventory/venv/bin/python: No such file or directory Sep 30 10:02:31 web01 kernel: I/O error, dev sdb, sector 1953456 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 2
Each line has the same shape: timestamp, host name, the program (with its PID in brackets) and the message. Not every error matters equally: harmless firmware complaints like the first line appear on many machines at every boot. That is why it pays to know what a normal boot of your servers looks like. The last line, a read error on a disk, does matter.
Classic log files in /var/log
Before the journal, every service wrote plain text files in
/var/log, many of them through a syslog daemon (today
usually rsyslog). Most distributions still keep that
setup next to the journal, so the same message can be in both. The
file names differ between Debian and Ubuntu on one side and Red Hat
Enterprise Linux (RHEL) and Fedora on the other:
| Contents | Debian / Ubuntu | RHEL / Fedora |
|---|---|---|
| General system messages | /var/log/syslog |
/var/log/messages |
Logins, sudo, Secure Shell (SSH) authentication
|
/var/log/auth.log |
/var/log/secure |
| Kernel messages | /var/log/kern.log |
in /var/log/messages |
| Package changes |
/var/log/dpkg.log,
/var/log/apt/history.log
|
/var/log/dnf.log, dnf history
|
| Scheduled jobs | in /var/log/syslog |
/var/log/cron |
| Application logs |
Their own directory, for example
/var/log/nginx/access.log and
error.log, /var/log/postgresql/
|
|
| Login history (binary) |
/var/log/wtmp and btmp, read with
last and sudo lastb
|
|
Text logs are read with the tools from
Viewing and Editing Text
(less, tail -f) and searched with
grep. Because every line has
the same structure, a short pipeline turns a log into a summary.
This example builds a small syslog-style file and counts the error
lines per program, then the failed logins per source address:
cat > syslog <<'EOF'
Sep 30 10:00:01 web01 CRON[2240]: (www-data) CMD (php /var/www/shop/cron.php)
Sep 30 10:00:14 web01 sshd[2251]: Failed password for invalid user admin from 203.0.113.45 port 51122 ssh2
Sep 30 10:00:16 web01 sshd[2251]: Failed password for invalid user admin from 203.0.113.45 port 51122 ssh2
Sep 30 10:01:40 web01 sshd[2263]: Accepted publickey for ana from 192.0.2.15 port 50412 ssh2: ED25519 SHA256:q1Lx0Vc8…
Sep 30 10:02:17 web01 inventory-api[2311]: ERROR could not connect to database at 127.0.0.1:5432: Connection refused
Sep 30 10:02:22 web01 inventory-api[2318]: ERROR could not connect to database at 127.0.0.1:5432: Connection refused
Sep 30 10:02:28 web01 inventory-api[2325]: ERROR could not connect to database at 127.0.0.1:5432: Connection refused
Sep 30 10:02:31 web01 kernel: [183021.442310] I/O error, dev sdb, sector 1953456 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 2
Sep 30 10:03:01 web01 postfix/smtp[2402]: 4F2A81C0B3: to=<orders@example.com>, relay=none, delay=30, status=deferred (connect to mail.example.com[198.51.100.25]:25: Connection timed out)
Sep 30 10:04:12 web01 sshd[2270]: Failed password for root from 198.51.100.77 port 40022 ssh2
EOF
echo "Error lines per program:"
grep -iE 'error|fail' syslog |
awk '{ prog = $5; sub(/\[.*/, "", prog); sub(/:$/, "", prog); print prog }' |
sort | uniq -c | sort -rn
echo "Failed SSH logins per source address:"
grep 'Failed password' syslog | awk '{ print $(NF-3) }' | sort | uniq -c
Error lines per program:
3 sshd
3 inventory-api
1 kernel
Failed SSH logins per source address:
1 198.51.100.77
2 203.0.113.45
-
grep -iE 'error|fail'keeps lines that contain "error" or "fail" in any case — a rough but useful first filter. -
In
awk,$5is the fifth field (sshd[2251]:); the twosub()calls cut off the PID and the colon, leaving the program name. -
sort | uniq -ccounts identical lines (they must be adjacent, hence the sort);sort -rnputs the highest count first. -
In a "Failed password" line the source address is always the
fourth field from the end,
$(NF-3), whatever the user name looks like.
On a real Ubuntu server the same pipeline runs on
/var/log/syslog or /var/log/auth.log. The
timestamp format may differ: Ubuntu 24.04's rsyslog writes full
dates such as 2026-09-30T10:00:14.512031+00:00, which
is one field instead of three, so the program name moves from
$5 to $3; $(NF-3) still works
because it counts from the end.
Kernel messages: dmesg
dmesg prints the kernel ring buffer: the messages the kernel itself has written since boot — hardware it found, drivers it loaded, disks and USB (Universal Serial Bus) devices that appeared or failed, and processes it killed when memory ran out. The buffer has a fixed size, so the oldest messages drop off on a long-running system; the journal keeps them.
$ sudo dmesg -T | tail -n 6
[Wed Sep 30 09:41:12 2026] usb 2-1: new SuperSpeed USB device number 3 using xhci_hcd [Wed Sep 30 09:41:12 2026] sd 6:0:0:0: [sdc] 61440000 512-byte logical blocks: (31.5 GB/29.3 GiB) [Wed Sep 30 09:41:12 2026] sdc: sdc1 [Wed Sep 30 10:02:31 2026] I/O error, dev sdb, sector 1953456 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 2 [Wed Sep 30 10:27:05 2026] Out of memory: Killed process 3312 (java) total-vm:8123456kB, anon-rss:3921344kB, file-rss:0kB, shmem-rss:0kB, UID:1002 pgtables:8124kB oom_score_adj:0 [Wed Sep 30 10:27:05 2026] oom_reaper: reaped process 3312 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Without -T, timestamps are seconds since boot
([183021.442310]). Useful variations:
dmesg -w follows new messages (handy while plugging in
a device), dmesg -l err,warn filters by level, and
journalctl -k -b -1 shows the kernel messages of the
previous boot. On Debian and Ubuntu, reading the buffer needs root
(dmesg: read kernel buffer failed: Operation not permitted
otherwise).
Two lines in that output deserve an immediate ticket: an I/O (input/output) error on a disk (check its health and your backups) and the out-of-memory (OOM) killer, which ends a process when the system has run out of memory (see out of memory). A service that "randomly stops" is often an OOM kill.
Log rotation
Logs grow forever unless something stops them. A disk that fills up
with logs stops databases, mail and logins.
Log rotation solves that: at a
fixed interval or size, the current log file is renamed and a fresh
one started, older files are compressed, and the oldest are deleted.
On most systems logrotate does this, started once a day
by a systemd timer (logrotate.timer) or by cron,
following the rules in /etc/logrotate.conf and one file
per package in /etc/logrotate.d/:
/var/log/nginx/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
create 0640 www-data adm
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
Rotate daily and keep 14 old files; don't complain if a log is
missing or rotate it when empty; compress old files, but not the
most recent one (delaycompress, so a program that still
writes to it briefly is not disturbed); create the new file with
these permissions; and afterward tell nginx to reopen its log files.
The result in the directory:
access.log ← today, being written access.log.1 ← yesterday, not yet compressed access.log.2.gz ← two days ago access.log.3.gz … access.log.14.gz ← oldest; deleted at the next rotation
Compressed logs cannot be read with cat or
grep directly, but each has a z version
that decompresses on the fly: zcat,
zless and zgrep. zgrep also
reads uncompressed files, so it can search all generations at once:
printf '%s\n' "Sep 28 03:12:40 web01 sshd[901]: Failed password for root from 198.51.100.77 port 39001 ssh2" > auth.log.2
printf '%s\n' "Sep 29 22:05:13 web01 sshd[1774]: Failed password for invalid user test from 203.0.113.45 port 50110 ssh2" \
"Sep 29 22:05:15 web01 sshd[1774]: Failed password for invalid user test from 203.0.113.45 port 50110 ssh2" > auth.log.1
printf '%s\n' "Sep 30 10:04:12 web01 sshd[2270]: Failed password for root from 198.51.100.77 port 40022 ssh2" > auth.log
gzip auth.log.2
ls -1 auth.log*
echo "---"
zgrep -h 'Failed password' auth.log* | wc -l
zcat auth.log.2.gz | cut -d' ' -f1-3
auth.log auth.log.1 auth.log.2.gz --- 4 Sep 28 03:12:40
-h leaves out the file names that
zgrep would otherwise put in front of each match. The
journal rotates itself: it keeps its total size under a limit (SystemMaxUse=
in journald.conf, by default 10% of the file system,
capped at 4 GB), and
sudo journalctl --vacuum-time=4weeks or
--vacuum-size=500M trims it by hand.
Disk usage: df and du
Two commands with confusingly similar names answer two different
questions. df (disk free) asks each mounted file system
how full it is. du (disk usage) adds up the sizes of
files and directories you point it at. -h makes both
print human-readable sizes.
$ df -h
Filesystem Size Used Avail Use% Mounted on tmpfs 392M 1.2M 391M 1% /run /dev/sda2 48G 44G 2.1G 96% / tmpfs 2.0G 0 2.0G 0% /dev/shm /dev/sda1 974M 182M 725M 21% /boot /dev/sdb1 492G 401G 66G 86% /srv/data
Read the Use% and Mounted on columns: the
root file system / is 96% full. Size minus Used is more
than Avail because ext4 reserves about 5% for root, so that the
system keeps working a little longer when users fill the disk.
tmpfs lines are file systems in memory and can usually
be ignored. A monitoring check does exactly this, with a threshold:
cat > df.txt <<'EOF'
Filesystem Size Used Avail Use% Mounted on
tmpfs 392M 1.2M 391M 1% /run
/dev/sda2 48G 44G 2.1G 96% /
tmpfs 2.0G 0 2.0G 0% /dev/shm
/dev/sda1 974M 182M 725M 21% /boot
/dev/sdb1 492G 401G 66G 86% /srv/data
EOF
awk 'NR > 1 { use = $5; sub(/%/, "", use); if (use + 0 >= 80) print $6, "is", $5, "full" }' df.txt
/ is 96% full /srv/data is 86% full
On a live system you would pipe df -h into the same
awk. use + 0 turns the text
96 into a number, so the comparison is numeric.
Out of inodes
A file system can also run out of
inodes, the records that
describe each file, while plenty of space is left. The symptom is
"No space left on device" although df -h shows free
space; the cause is usually millions of tiny files (session files, a
mail queue, cache entries). df -i shows it:
$ df -i /
Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda2 3145728 3145728 0 100% /
Finding what uses the space
du -sh gives one total per argument (-s,
summarize). To find the culprit on a full disk, list the size of
each subdirectory, sort by size, and descend into the biggest:
$ sudo du -xh --max-depth=1 /var | sort -h | tail -n 5
$ sudo du -sh /var/log/*.gz
212M /var/cache 1.1G /var/lib 2.3G /var/log 14G /var/backups-old 18G /var 4.0K /var/log/alternatives.log.2.gz 28K /var/log/dpkg.log.2.gz …
-
--max-depth=1(short:-d 1): one line per direct subdirectory plus the total. -
-x: stay on this file system, so a mounted data disk under the directory is not counted. -
sort -hunderstandsK,MandG; a plainsort -nwould put 14G before 212M. -
sudois needed to read every directory; without it you get "Permission denied" lines and a total that is too low.
Here a forgotten backup directory is the answer.
du on a large tree can take minutes and cause a lot of
disk activity — on a busy server, start from the most likely
directories (/var/log, /var/lib,
/home, /tmp) rather than /.
Memory: free
$ free -h
total used free shared buff/cache available Mem: 3.8Gi 1.9Gi 214Mi 12Mi 1.9Gi 1.7Gi Swap: 2.0Gi 256Ki 2.0Gi
The free column looks alarming: only 214 MiB. It is
not. Linux uses otherwise idle memory as a cache for disk contents
(buff/cache) and gives it back the moment a program
needs it. The number that answers "how much memory can programs
still get?" is available: 1.7 GiB here. Low free is normal and healthy; low
available together with growing swap use is the real
warning sign. How virtual memory and swapping work is explained in
Memory Management.
Load average and uptime
$ uptime
$ nproc
10:42:07 up 42 days, 3:15, 2 users, load average: 3.91, 2.14, 1.20 4
uptime shows the time, how long the system has run
since the last boot, how many users are logged in, and three load
figures. The load average is the
average number of tasks that were running, waiting for a CPU
(central processing unit) core, or waiting uninterruptibly for disk
I/O, over the last 1, 5 and 15 minutes. It is not a percentage:
compare it with the number of cores, which
nproc prints. The background is explained in
CPU Scheduling.
loadavg="3.91 2.14 1.20 5/214 5120" # the contents of /proc/loadavg
cores=4 # what nproc printed
echo "$loadavg" | awk -v cores="$cores" '{
printf "per core: 1 min %.2f 5 min %.2f 15 min %.2f\n", $1 / cores, $2 / cores, $3 / cores
}'
per core: 1 min 0.98 5 min 0.54 15 min 0.30
Per core, around 1.0 means fully busy; well above 1.0 means work is
queuing. The trend matters as much as the value: 0.30 → 0.54 → 0.98
from the 15- to the 1-minute figure means the load is rising right
now; the reverse means a busy period is ending. A high load with an
idle CPU points to processes waiting for disk or network storage,
which is where vmstat and iostat help. To
see which processes cause the load, use top or
htop (see
Processes and Jobs). Load is a
saturation signal; how to plan with utilization and saturation is on
Capacity Planning and Performance.
vmstat and iostat
vmstat prints one line of system-wide figures per
interval — here every 2 seconds, 3 times. The first line is the
average since boot; the following lines are live.
$ vmstat 2 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 256 219140 61220 1934800 0 0 12 30 210 380 6 2 91 1 0 0 6 256 215880 61220 1935120 0 0 8420 112 1320 2410 3 4 22 71 0 0 5 256 214300 61224 1935400 0 0 7984 96 1288 2302 2 4 25 69 0
-
r: processes running or waiting for a CPU;b: processes blocked, waiting for I/O. -
si/so: memory swapped in and out per second. Anything above zero for a longer time means memory is short. -
us,sy,id,wa: CPU time in user programs, in the kernel, idle, and waiting for I/O. The newervmstaton Ubuntu 24.04 adds agucolumn (time spent running virtual machine guests).
In the live lines, six processes are blocked (b) and
the CPU spends about 70% of its time waiting for I/O
(wa): the disk is the bottleneck, not the processor.
iostat -xz 2 (from the sysstat package)
then shows which disk: look for a device with
%util near 100 and a high r_await /
w_await (milliseconds per request).