Linux Lesson 17 of 47

Logs & Monitoring: journald, rsyslog, logrotate, dmesg & Where to Look When Things Break

When something breaks on a Linux box, the machine has almost always already told you why — you just have to know where it wrote it down. Logs are the flight recorder. A service that refuses to start, an SSH login that’s suddenly rejected, a process that vanished in the night, a disk that filled to 100% — every one of these left a trail. The difference between a beginner and an operator is not that the operator guesses better; it’s that the operator opens the right log, filters to the right ten lines, and reads the answer.

The catch is that modern Linux has two logging worlds running side by side, and beginners routinely look in the wrong one. There’s the systemd journal — a binary, structured, indexed database that you query with journalctl — and there’s the traditional syslog world of plain-text files under /var/log that you read with less, grep, and tail. They overlap, they duplicate each other, and on most distros they’re both live at once. Understand how the two coexist and you’ll never again grep a file that doesn’t have the message while the answer sits in the journal.

This lesson builds that model from the ground up and then makes it practical: journalctl in real depth, the syslog protocol (facilities, severities, and the priority number), rsyslog for routing and central logging, the /var/log tour with the Debian-versus-RHEL naming differences that trip everyone up, dmesg and the kernel ring buffer, logrotate so logs don’t eat the disk, and a “something broke — where do I look” playbook you’ll come back to for years. Type the commands as you go on a throwaway VM or WSL.

Why this matters

Three concrete situations, all of which you will hit:

The mental model to hold onto is a pipeline: sources (your apps and the kernel) emit messages → a store (the journal, and/or text files) keeps them → tools (journalctl, grep, dmesg) let you read them → retention (journald’s self-vacuum, logrotate) stops them growing forever. Everything below is detail on those four stages. This lesson pairs naturally with the deeper systemd material in Systemd units, services, targets & journald — here we go deep on the logs; there you manage the units that produce them.

The two logging worlds and how they coexist

Before systemd, Linux logging was simple to describe and painful to use: a daemon called syslogd (later rsyslogd) listened on a socket, received text messages tagged with a facility and a severity, and appended them as lines to files under /var/log. That’s it. Plain text, one line per event, grep to search. Simple — but there was no structure (every app formatted its line differently), no indexing (searching meant scanning whole files), and no reliable link between a message and the process that produced it.

Systemd introduced systemd-journald (the “journal”), which flips the model. Every message is stored as a structured record — a set of key/value fields — in a binary, indexed file. The human message is just one field (MESSAGE=); alongside it journald automatically records who sent it (_PID=, _UID=, _COMM=, _SYSTEMD_UNIT=), when (__REALTIME_TIMESTAMP), how severe it is (PRIORITY=), which boot it belongs to (_BOOT_ID=), and dozens more. Because it’s indexed, journalctl -u nginx -p err -b answers “errors from nginx this boot” instantly, without you ever naming a file.

Here’s the part beginners miss: on a typical modern distro, both worlds are running. journald is always there (it’s part of systemd and captures everything — stdout/stderr of every service, syslog() calls, and the kernel buffer). rsyslog is often also installed, and it typically reads from the journal and writes the familiar text files so that decades of tooling, scripts, and muscle memory keep working. So the same “Failed password” line can exist in both the journal and /var/log/auth.log at the same time. Neither is wrong; they’re two views of the same stream.

How does that duplication actually happen? By one of two mechanisms. Either rsyslog loads the imjournal module and pulls structured entries straight out of the journal (the RHEL default), or journald is told to push to the classic syslog socket with ForwardToSyslog=yes in journald.conf, which rsyslog reads via imuxsock. Either way the journal is the source of truth and rsyslog is a consumer that produces text files. This also settles a question every operator eventually asks — do I even need rsyslog? On a single instance where you only ever run journalctl, often no: many minimal cloud images ship without it and lose nothing. You want rsyslog the moment you need text files for legacy tooling or forwarding to a central server — the two jobs the journal doesn’t do on its own.

Left-to-right diagram of the Linux logging pipeline: applications emitting via stdout and the syslog() call plus the kernel ring buffer feed systemd-journald, which stores every message as structured queryable fields in a binary journal under /var/log/journal and caps its own disk use; rsyslog reads the journal via imjournal, routes messages by facility.severity into /var/log text files such as messages and secure, and the omfwd action forwards a copy to a central remote log server over TCP or TLS; logrotate rotates, compresses and prunes the text files so the disk stays bounded while the journal self-vacuums, with numbered badges marking the kernel ring buffer, journald being structured and queryable, journald self-vacuuming, rsyslog forwarding centrally, logrotate rotating text logs, and the disk staying capped

Walk the diagram left to right: your apps write to stdout or call syslog(), and the kernel fills its ring buffer; all of it lands in journald, the structured store. From there rsyslog can pull a copy, apply routing rules, drop lines into /var/log text files, and forward them to a central server. logrotate keeps those text files bounded; the journal caps itself. Learn this shape once and every command below has an obvious place to live.

Dimension systemd journal (journald) Traditional syslog (rsyslog → text)
Format Binary, structured (key=value fields) Plain text, one line per event
Read with journalctl (only) less / grep / tail / awk
Location /run/log/journal (volatile) or /var/log/journal (persistent) /var/log/*.log, /var/log/messages, …
Indexed / queryable Yes — by unit, priority, boot, PID, time No — full-text scan every time
Survives reboot Only if persistent storage is enabled Yes (files on disk)
Structured metadata Rich, automatic (_SYSTEMD_UNIT, _UID, …) None — whatever the app printed
Retention Self-managed (SystemMaxUse=, vacuum) External — needs logrotate
Remote / central Not natively (needs a forwarder) Native (omfwd, @@host)
Always present? Yes, on any systemd distro Only if rsyslog/syslog-ng installed

The practical rule: reach for journalctl first — it’s always there, it’s structured, and it can filter to exactly what you need. Drop to /var/log text files when (a) an app writes its own file that journald never sees (nginx access logs, for example), (b) rsyslog is forwarding them somewhere the journal isn’t, or © you’re on an older or minimal system where the journal is volatile and yesterday’s entries are already gone.

journalctl in depth: querying the systemd journal

journalctl is the single most useful command in this lesson. Run it with no arguments and it prints the entire journal, oldest first, paged through less (so / searches, q quits, G jumps to the end). That’s rarely what you want — the power is in the filters, and they compose: unit and priority and time and boot, all at once.

A note on permissions before you start: to read the full system journal you must be root or a member of the systemd-journal group (some distros also grant adm or wheel). As an ordinary user you’ll often see only your own session’s messages, or “No journal files were found.” sudo journalctl is the quick fix; adding your user to systemd-journal is the durable one.

Following, tailing, and jumping to the end

journalctl -e                 # jump straight to the END (newest), still paged
journalctl -n 50              # the last 50 lines then exit (like tail)
journalctl -f                 # FOLLOW — live tail, new lines appear as they arrive
journalctl --no-pager         # dump raw to stdout (for piping into grep/awk/less)
journalctl -r                 # reverse: newest first

-f (follow) is what you leave running in one terminal while you reproduce a problem in another — the logging equivalent of tail -f. -e and -n are how you glance at “what just happened” without scrolling through history.

Filtering by unit — the workhorse

# All logs for one service, this boot, jumped to the end:
journalctl -u nginx.service -e

# Follow a service live (the single most common troubleshooting command):
journalctl -u ssh -f

# Two units at once (repeat -u):
journalctl -u nginx -u php8.2-fpm --since "10 min ago"

-u <unit> restricts to one systemd unit and is how you answer “why did this service fail?” You can drop the .service suffix. On Debian/Ubuntu the SSH unit is ssh; on RHEL/Fedora it’s sshd — a naming gap that catches everyone.

This boot, last boot, and time windows

The journal knows about boots. Each reboot starts a new boot ID, and you can address them by offset — 0 is the current boot, -1 the previous one, and so on. This is invaluable for “the box rebooted itself last night — what happened just before?”

journalctl -b                 # everything since THIS boot
journalctl -b -1              # everything from the PREVIOUS boot (the crash!)
journalctl --list-boots       # table of known boots with their offsets & IDs

# Time windows — natural-language or absolute:
journalctl --since "2026-07-09 08:00:00" --until "2026-07-09 09:00:00"
journalctl --since "1 hour ago"
journalctl --since yesterday --until today
journalctl -u nginx --since "09:30" --until "09:45"   # a tight 15-minute slice

--since/--until accept yesterday, today, now, "2 days ago", "09:00", and full timestamps. Combine a time window with -u and -p and you can zoom to the exact minute a service died.

Priority (severity) filtering

-p filters by priority, which is the syslog severity level (0 = most severe, 7 = least). Give it a single level and journald shows that level and everything more severe (numerically lower). Give it a LOW..HIGH range to bound both ends.

journalctl -p err -b          # errors and worse (0–3), this boot
journalctl -p warning         # warnings and worse (0–4)
journalctl -p err..alert      # only err, crit, alert (a 3..1 range)
journalctl -u nginx -p err --since today   # today's nginx errors only

The level names and numbers (emerg 0 … debug 7) are the syslog severities we tabulate in full in the next section — the same scale journald reuses. A frequent “why is my log empty?” moment is running -p err when the only messages are info/notice: nothing is that severe, so nothing prints.

Kernel messages

journalctl -k                 # kernel (dmesg-style) messages, THIS boot
journalctl -k -b -1           # kernel messages from the PREVIOUS boot
journalctl -k -p err          # kernel errors only

-k is journalctl limited to kernel messages (equivalent to _TRANSPORT=kernel). Unlike dmesg, which reads the volatile in-memory ring buffer that’s wiped on reboot, journalctl -k -b -1 can show you the kernel log from before a crash — provided the journal is persistent. That single capability is why persistent journald matters (next section).

Field filters — the structured superpower

Because every entry is structured, you can match on any field with FIELD=VALUE. Multiple different fields are ANDed; the same field repeated is ORed; a bare + separates OR-groups.

journalctl _PID=1234          # everything logged by PID 1234
journalctl _UID=0             # everything logged as root
journalctl _COMM=sshd         # by command name (the executable)
journalctl _SYSTEMD_UNIT=cron.service    # like -u, but exact
journalctl _UID=1000 _COMM=sudo          # AND: sudo runs by user 1000
journalctl _SYSTEMD_UNIT=nginx.service + _SYSTEMD_UNIT=ssh.service  # OR

journalctl -F _SYSTEMD_UNIT   # LIST every value a field takes (capital -F)
Field Matches Example
_PID= Process ID that logged journalctl _PID=1 (pid 1 = systemd)
_UID= User ID of the sender journalctl _UID=33 (www-data)
_GID= Group ID of the sender journalctl _GID=0
_COMM= Command / executable name journalctl _COMM=sudo
_EXE= Full path to the executable journalctl _EXE=/usr/sbin/sshd
_SYSTEMD_UNIT= Owning systemd unit journalctl _SYSTEMD_UNIT=nginx.service
_HOSTNAME= Host that logged it (useful on a central collector)
_BOOT_ID= Specific boot pair with --list-boots
PRIORITY= Exact severity (0–7) journalctl PRIORITY=3 (only err)
SYSLOG_FACILITY= Syslog facility number journalctl SYSLOG_FACILITY=10 (authpriv)
-F <field> Lists all values of a field journalctl -F _COMM

Output formats and grep

-o changes how each entry is rendered — from the terse default to full structured dumps to machine-readable JSON. -g (grep) filters the MESSAGE field by a regex, and -x adds catalog explanations for known messages.

journalctl -u nginx -o cat            # message text ONLY (no timestamp/host) — clean for piping
journalctl -u nginx -o verbose        # EVERY field of every entry (what -o cat hides)
journalctl -u nginx -o json-pretty    # structured JSON — feed to jq or a log shipper
journalctl -o short-iso               # default layout but with ISO-8601 timestamps
journalctl -g 'timeout|refused'       # grep MESSAGE for a regex (smart-case)
journalctl -u ssh -g 'Failed password' -p info    # grep + unit + priority together
journalctl -xe                        # the classic: end of journal, with -x explanations
-o format Shows Use it for
short (default) time host unit: message Everyday reading
short-iso Same, ISO-8601 timestamps Unambiguous times, cross-TZ
short-precise Microsecond timestamps Ordering fast events
cat message only Piping to grep/awk, clean copy
verbose All fields per entry Discovering fields to filter on
json / json-pretty One JSON object per entry jq, shipping to ELK/Loki
export Binary-safe serialization Backing up / transferring journals

-o verbose is how you discover what you can filter on: run it on one interesting entry, read the field names, then filter by them. For parsing the text files (not the journal) you’ll lean on grep/awk/sed — covered in Text processing: grep, sed, awk, cut, sort.

Here is the flag reference to bookmark:

Flag Meaning Example
-u UNIT One systemd unit journalctl -u nginx
-f Follow (live tail) journalctl -f
-e Jump to end journalctl -e
-n N Last N lines journalctl -n 100
-r Reverse (newest first) journalctl -r
-b / -b -1 This / previous boot journalctl -b -1
--list-boots All known boots
--since / --until Time window --since "1 hour ago"
-p LEVEL Priority ≤ LEVEL (or A..B range) -p err
-k Kernel messages journalctl -k
FIELD=VALUE Structured field filter _UID=0
-F FIELD List a field’s values -F _SYSTEMD_UNIT
-g PATTERN Grep the MESSAGE field -g 'oom'
-o FORMAT Output format -o json-pretty
-x Add catalog explanations journalctl -xe
--no-pager No less, raw stdout for pipes
--disk-usage Space the journal uses
--vacuum-size / --vacuum-time Prune the journal --vacuum-time=2weeks

Persistent vs volatile, disk usage, and rate-limiting

By default on many distros the journal is volatile — it lives in /run/log/journal, which is a tmpfs (RAM), and it is wiped on every reboot. That’s why a fresh Ubuntu box can show you journalctl -b but returns nothing for -b -1: there was no previous boot on disk. A persistent journal lives in /var/log/journal and survives reboots. Whether you get one is controlled by Storage= in /etc/systemd/journald.conf:

Storage= value Where the journal lives Survives reboot?
auto (default) /var/log/journal if that directory exists, else /run Only if the dir exists
persistent /var/log/journal (created if missing) Yes
volatile /run/log/journal (RAM) No
none Nowhere — journald forwards but stores nothing No

To make the journal persistent, the reliable one-liner is to create the directory (with auto, that alone flips it) or set Storage=persistent, then restart journald:

# Enable a persistent journal:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal   # correct owner/mode
sudo systemctl restart systemd-journald
journalctl --list-boots        # now accumulates across reboots

The journal manages its own size — this is the reason it never needs logrotate. Retention is governed by a handful of journald.conf knobs, and you can force cleanup on demand:

journalctl --disk-usage
# Archived and active journals take up 1.2G in the file system.

sudo journalctl --vacuum-size=500M    # shrink the journal to at most 500 MB
sudo journalctl --vacuum-time=2weeks  # delete journal files older than 2 weeks
sudo journalctl --vacuum-files=5      # keep only the 5 most recent journal files
sudo journalctl --rotate              # rotate now (start a fresh active file)
journalctl --verify                   # integrity-check the journal files
journald.conf directive Controls Typical value
Storage= Volatile vs persistent auto / persistent
SystemMaxUse= Max total space (persistent) SystemMaxUse=1G
SystemKeepFree= Min free space to leave on the FS SystemKeepFree=15%
SystemMaxFileSize= Max size per journal file before rotate SystemMaxFileSize=128M
RuntimeMaxUse= Max space for the volatile journal RuntimeMaxUse=200M
MaxRetentionSec= Delete entries older than this MaxRetentionSec=1month
MaxFileSec= Force rotate a file after this age MaxFileSec=1week
ForwardToSyslog= Also hand messages to rsyslog yes

One more journald behaviour that surprises people: rate-limiting. To stop a crash-looping service from filling the disk with the same line a million times a second, journald drops messages beyond a burst threshold and inserts a marker: “Suppressed 3172 messages from …”. The controls live in journald.conf:

Directive Default Meaning
RateLimitIntervalSec= 30s The sliding window
RateLimitBurst= 10000 Max messages per window per service

If you’re legitimately losing important lines, raise RateLimitBurst (or set it to 0 to disable, which you’d only do while debugging). You can also override it per-service in the unit with LogRateLimitIntervalSec=/LogRateLimitBurst=. Seeing “Suppressed N messages” is a symptom, not a bug — it means something is logging far too much.

The syslog world: facilities, severities, rsyslog & central logging

Even in a journald world you have to understand syslog, because it’s the protocol every network device, appliance, and legacy daemon speaks, and it’s the vocabulary journald itself reuses (-p, SYSLOG_FACILITY=). A syslog message carries two small numbers: a facility (roughly who is talking — the subsystem) and a severity (how bad it is). Together they form the priority (PRI).

Facilities — who is talking

Code Keyword Subsystem
0 kern Kernel messages
1 user Generic user-level (the default for logger)
2 mail Mail system (postfix, sendmail)
3 daemon System daemons without their own facility
4 auth Security / authorization
5 syslog Messages from the syslog daemon itself
6 lpr Line-printer subsystem
7 news Usenet news
8 uucp UUCP subsystem
9 cron Clock / cron daemon
10 authpriv Security / authorization, private (sudo, sshd, PAM)
11 ftp FTP daemon
16–23 local0local7 Free for your own apps

The two you’ll use most: authpriv (facility 10) is where authentication events go — sudo, sshd, PAM — which is why the auth log is a separate, tighter-permissioned file. And local0local7 are reserved for you: point your own application at local3 and route it to its own file without colliding with anything built-in.

Severities — how bad it is

Code Keyword Meaning You act…
0 emerg System is unusable Now, everything’s down
1 alert Action needed immediately Now
2 crit Critical condition Urgently
3 err Error condition Soon
4 warning Warning condition Investigate
5 notice Normal but significant Note it
6 info Informational Context
7 debug Debug-level Only when debugging

This is the same 0–7 scale journalctl -p uses. Lower number = more severe. “Show me errors and worse” means -p err, i.e. levels 0–3.

The priority number

The single wire value at the front of a raw syslog message — the <34> you’ll see if you ever tcpdump port 514 — is the priority (PRI), computed as:

PRI = facility × 8 + severity

So an authpriv (10) message at crit (2) severity has PRI = 10 × 8 + 2 = 82, transmitted as <82>. You rarely compute this by hand, but knowing the formula demystifies the numbers in packet captures and rsyslog internals — and it’s a favourite exam question. Watch the terminology: rsyslog’s “priority” is this combined PRI number, while journald’s “priority” (-p, PRIORITY=) is just the severity (0–7). Same word, two meanings.

rsyslog: rules and routing

rsyslog is the modern syslog daemon on most distros. Its job is to receive messages (from the journal, the local socket, the kernel, and the network) and route them to destinations based on rules. Config lives in /etc/rsyslog.conf, with drop-in snippets in /etc/rsyslog.d/*.conf (put your own rules there, not in the main file). The classic rule (still perfectly valid) is a selectorfacility.severity — followed by an action (usually a file path):

# /etc/rsyslog.d/50-custom.conf — selector  <TAB>  action

authpriv.*          /var/log/secure          # all auth-private events → secure
mail.*              -/var/log/maillog        # leading - = async write (don't fsync each line)
cron.*              /var/log/cron
kern.*              /var/log/kern.log
*.info;mail.none;authpriv.none;cron.none  /var/log/messages   # everything info+, minus 3 facilities
*.emerg             :omusrmsg:*              # emergencies → wall to all logged-in users
local3.*            /var/log/myapp.log       # your app's local3 → its own file

Read the selector precisely: mail.info means facility mail, severity info and above (more severe). Modifiers refine it — mail.=info is only info, mail.!=info is everything except info, and .none excludes a facility entirely (that’s how /var/log/messages gets “everything except mail, authpriv, and cron”, which have their own files). *.* is everything.

Selector Matches
mail.info mail facility, info and more severe
mail.=info mail facility, only info
mail.!=info mail facility, everything except info
*.err Any facility, err and worse
authpriv.* All severities of authpriv
cron.none Nothing from cron (used to exclude)
*.* Everything

The daemon loads modules to gather and emit messages. You seldom touch these on a stock box, but you need to recognise them:

Module Role
imuxsock Input: the local /dev/log socket (apps calling syslog())
imjournal Input: read structured messages from the journal
imklog Input: the kernel log
imtcp / imudp Input: receive remote syslog over TCP / UDP
omfile Output: write to a file (the default file action)
omfwd Output: forward to a remote server

After any change, validate then restart — never restart blind:

sudo rsyslogd -N1                 # check config syntax, change nothing
sudo systemctl restart rsyslog    # apply
logger -p local3.info "rsyslog test line"   # emit a test message
tail -n1 /var/log/myapp.log       # confirm it routed

Central logging: forwarding with omfwd

The moment you have more than a couple of servers, you want logs in one place — a central collector you can search across the whole fleet, and that survives a box being wiped. rsyslog does this natively with the omfwd action. The legacy shorthand is compact; the modern RainerScript form is explicit and is what you should write today:

# Legacy shorthand (still works):
*.*  @logserver.example.com:514      # single @  = UDP  (fire-and-forget, can drop)
*.*  @@logserver.example.com:514     # double @@ = TCP  (reliable, ordered)

# Modern RainerScript (preferred — explicit, with a disk queue for reliability):
action(type="omfwd"
       target="logserver.example.com" port="514" protocol="tcp"
       action.resumeRetryCount="-1"                 # retry forever if the server is down
       queue.type="linkedList" queue.size="10000")  # buffer in RAM if it can't send
Syntax Transport Reliability
@host:514 UDP Fire-and-forget — silently drops under load
@@host:514 TCP Reliable, ordered, back-pressured
omfwd + queue.* TCP + on-disk/RAM queue Survives collector downtime
omfwd + gtls TCP over TLS Encrypted + reliable (port 6514)

For anything crossing an untrusted network, wrap it in TLS (RFC 5425, conventionally port 6514) so credentials and internal hostnames aren’t sent in clear text:

global(DefaultNetstreamDriver="gtls"
       DefaultNetstreamDriverCAFile="/etc/rsyslog.d/ca.pem")
action(type="omfwd" target="logserver.example.com" port="6514" protocol="tcp"
       StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name")

On the collector you enable the matching input — module(load="imtcp") input(type="imtcp" port="514") — open the firewall, and route incoming messages into per-host files with a template. Templates let you control the on-disk format and path using message properties:

# On the central server: file per host, per program.
template(name="perHost" type="string"
         string="/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log")
module(load="imtcp")
input(type="imtcp" port="514")
*.*  action(type="omfile" dynaFile="perHost")

Properties like %HOSTNAME%, %PROGRAMNAME%, %timegenerated%, %syslogpriority%, and %msg% are the property replacer — the same mechanism that formats every line rsyslog writes. This one server, with one imtcp input and one templated output, is the seed of a real centralised-logging platform (the next step up being an aggregator like Loki, Graylog, or an ELK/OpenSearch stack that indexes what rsyslog ships it).

The /var/log tour and the kernel ring buffer

Even with the journal, you must know the /var/log neighbourhood — because apps write there directly, because forwarded/central logs land there, and because half of production tooling reads there. The big gotcha is naming: Debian/Ubuntu and RHEL/Fedora call the same logs different things.

Purpose Debian / Ubuntu RHEL / Fedora / Rocky Read with
General system messages /var/log/syslog /var/log/messages less, tail -f
Authentication (sudo, ssh, PAM) /var/log/auth.log /var/log/secure grep, tail -f
Kernel ring-buffer log /var/log/kern.log (folded into messages) less / journalctl -k
Boot-time kernel snapshot /var/log/dmesg /var/log/dmesg less
Service/boot startup /var/log/boot.log /var/log/boot.log less
Cron jobs (in syslog) or /var/log/cron.log /var/log/cron grep CRON
Mail system /var/log/mail.log /var/log/maillog less
Package manager /var/log/dpkg.log, apt/ /var/log/dnf.log (yum.log) less
SELinux / audit (usually absent) /var/log/audit/audit.log ausearch, less
Web server /var/log/nginx/, /var/log/apache2/ /var/log/nginx/, /var/log/httpd/ per-vhost files
Login records (binary) /var/log/wtmp, btmp, lastlog same last, lastb, lastlog

Two things to internalise. First, /var/log/wtmp, /var/log/btmp, and /var/log/lastlog are binarycat them and you get garbage; you read them with dedicated commands (below). Second, the single most-visited troubleshooting file is the auth log, and its name is distro-dependentauth.log on Debian, secure on RHEL. Memorise both. When a file’s contents look identical to something in the journal, that’s expected: rsyslog wrote it from the journal.

dmesg and the kernel ring buffer

The kernel can’t write to a file — the filesystem is userspace and may not even be mounted early in boot — so it logs into a ring buffer: a fixed-size, circular chunk of kernel memory. When it fills, the oldest lines are overwritten. dmesg prints that buffer, and it’s where hardware, driver, filesystem, and out-of-memory events surface.

dmesg -H                 # Human: pager + relative human timestamps + colour
dmesg -T                 # translate timestamps to wall-clock dates
dmesg -w                 # WAIT/follow — live-tail new kernel messages
dmesg -l err,warn        # only error- and warning-level lines
dmesg -x                 # decode the facility & level of each line
dmesg | grep -i -e error -e fail -e oom     # quick scan for trouble
Flag Effect
-H Human-readable: pager, colour, relative times
-T Wall-clock timestamps (can drift after suspend/resume)
-w Follow — wait for and print new messages
-l LEVEL Filter by level (err, warn, crit, …)
-f FAC Filter by facility (kern, daemon, …)
-x Decode facility/level as text prefixes
-c / -C Read-and-clear / clear the buffer

The classic thing you hunt for in dmesg is the OOM killer. When the box runs out of memory, the kernel picks a victim process and kills it to survive, leaving an unmistakable trail:

sudo dmesg -T | grep -i -A1 'killed process'
# [Thu Jul  9 03:14:22 2026] Out of memory: Killed process 2468 (mysqld)
#   total-vm:8419236kB, anon-rss:6291456kB, file-rss:0kB, oom_score_adj:0

If a service “randomly disappeared” overnight and its own logs just stop mid-sentence, this is the first place to look — the kernel killed it, and systemd then logged a Main process exited. Because the ring buffer is volatile, prefer journalctl -k -b -1 to inspect a previous boot’s kernel log; dmesg only ever shows the current one.

⚠️ On Ubuntu (and any box with kernel.dmesg_restrict=1), an unprivileged dmesg fails with read kernel buffer failed: Operation not permitted. Use sudo dmesg, or read journalctl -k instead — the journal copy is readable by the systemd-journal/adm group.

logrotate: keeping text logs from eating the disk

The journal caps itself; text files do not. An app that logs to /var/log/myapp.log will grow it forever until the disk is full and the whole box falls over. logrotate is the standard tool that periodically renames, compresses, prunes, and re-creates those files so they stay bounded. It isn’t a daemon — it’s a program run on a schedule, from /etc/cron.daily/logrotate on older systems or a systemd timer (logrotate.timerlogrotate.service) on modern ones. (Timers are covered in Scheduling: cron, at & systemd timers.)

Global defaults live in /etc/logrotate.conf, and each package drops a snippet into /etc/logrotate.d/ for its own logs. A typical snippet:

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily                 # rotate once a day
    rotate 14             # keep 14 old copies, then delete the oldest
    compress              # gzip rotated copies (.1.gz, .2.gz, …)
    delaycompress         # but don't compress the MOST-recent rotation yet
    missingok             # don't error if the log is absent
    notifempty            # skip rotation if the file is empty
    create 0640 myapp adm # after rotating, make a fresh empty file with these perms
    sharedscripts         # run postrotate once for the whole glob, not per file
    postrotate
        systemctl reload myapp >/dev/null 2>&1 || true   # tell the app to reopen its log
    endscript
}
Directive What it does
daily / weekly / monthly Rotation frequency
rotate N Keep N rotated copies, then discard
size 100M / maxsize / minsize Rotate when the file exceeds a size
compress Gzip rotated files
delaycompress Delay compression by one cycle (pair with reopen-lazy apps)
missingok Don’t fail if the log is missing
notifempty Skip rotation of empty files
create MODE OWNER GROUP Recreate the log after rotating, with these perms
copytruncate Copy-then-truncate in place instead of rename+create
dateext Suffix with a date (-20260709) instead of .1
olddir DIR Move rotated files elsewhere
maxage N Delete rotated files older than N days
su USER GROUP Rotate as a specific user (for locked-down dirs)
postrotate … endscript Run a command after rotating (usually reload the writer)
sharedscripts Run pre/postrotate once per glob, not once per file

The one gotcha that catches everyone: create vs copytruncate

The default rotation strategy is rename + create: logrotate renames myapp.logmyapp.log.1 and creates a new empty myapp.log. But if the writing process is still holding the old file open, it keeps writing to myapp.log.1 (same inode) and the new myapp.log stays empty. The fix is postrotate — signal the app (systemctl reload, or kill -HUP) so it reopens the path.

copytruncate sidesteps that for apps you can’t signal: logrotate copies the contents to .1 and then truncates the original in place, so the process’s open file descriptor stays valid and keeps writing to the same (now-empty) file. The cost is a tiny race window — anything written between the copy and the truncate is lost.

Strategy How it works Use when Risk
create (default) Rename old → .1, create fresh log The app reopens on signal (add postrotate) New file stays empty if app never reopens
copytruncate Copy contents, truncate original in place The app can’t be told to reopen Loses lines written during the copy→truncate race

Testing and forcing a rotation

Never wait a day to find out your rule is wrong — debug it dry:

sudo logrotate -d /etc/logrotate.conf        # DEBUG: dry-run, print the plan, change NOTHING
sudo logrotate -d /etc/logrotate.d/myapp     # debug just one snippet
sudo logrotate -vf /etc/logrotate.conf       # -f FORCE rotation now (+ -v verbose)

-d (debug) implies verbose and makes no changes — read its output to see exactly which files it would rotate and why (or why it would skip them). -f forces a rotation even if the schedule says it isn’t due yet — how you test a fresh snippet immediately. logrotate remembers when it last rotated each file in a state file (/var/lib/logrotate/logrotate.status on RHEL, /var/lib/logrotate/status on Debian); if a rule “won’t rotate,” that state file usually thinks it already did today — -f overrides it. ⚠️ Watch permissions: getting create’s mode/owner wrong (or forgetting su) can leave the new log unreadable by the service, and it silently stops logging.

Writing to logs and tracking logins

You’ll also write to the log stream — from scripts, cron jobs, and deploy hooks — and you’ll audit who logged in. Two small commands handle writing:

logger "deploy started"                       # → journal + syslog, facility user, severity notice
logger -t deploy -p local3.info "build 4821 shipped"   # custom tag + facility.severity
logger -s -p local3.err "migration FAILED"    # -s also echoes to stderr (for scripts)
echo "disk almost full" | logger -t diskcheck # pipe a command's output into the log

wall "Reboot in 5 minutes — save your work"   # broadcast to ALL logged-in terminals
wall < /etc/motd                              # broadcast a file

logger is the correct way for a shell script to log — it goes through the same pipeline as everything else, so journalctl -t deploy finds it and your rsyslog rules route it. wall (“write all”) broadcasts to every logged-in TTY, which is how you warn users before a reboot.

To capture the entire output of a command — both stdout and stderr — into the journal under its own identifier, use systemd-cat. It’s the clean way to make an ad-hoc job’s output land in journalctl alongside everything else, properly tagged and prioritised:

systemd-cat -t backup -p info ./nightly-backup.sh   # both streams → journal, tagged 'backup'
journalctl -t backup -e                             # read exactly that run back

For login history there are four tools reading the binary accounting files:

Command Reads Shows
who /run/utmp Who is logged in right now (user, TTY, login time)
w /run/utmp + /proc Who’s on and what they’re running (+ load average)
last /var/log/wtmp Login/logout history, plus reboots (last reboot)
lastb /var/log/btmp Failed login attempts (root only) — brute-force detector
lastlog /var/log/lastlog The last login time for every account
who                     # vinod  pts/0  2026-07-09 09:02 (10.0.0.5)
w                       # + load average and each session's current command
last -n 10              # last 10 logins/logouts
last reboot             # every boot — a reboot timeline
sudo lastb -n 20        # last 20 FAILED logins — spot a brute-force by IP
journalctl -u ssh -g 'Failed password'   # the same, from the journal, with detail

sudo lastb is your fast brute-force check: a wall of Failed entries from one IP against root or admin is an attack in progress. Cross-reference with journalctl -u ssh -g 'Failed password' for the full context (source IP, username tried, timestamp).

The “something broke — where do I look” playbook

This is the section to internalise. Most incidents fall into a handful of shapes, and each has a first place to look. Memorise the mapping and you’ll diagnose in seconds instead of flailing.

Symptom First place to look Command
A service won’t start / keeps restarting Its journal, jumped to the end journalctl -u <svc> -e (then -xe, -p err)
Service died overnight, logs cut off Kernel OOM killer, then the unit journalctl -k -b -1 -g oomjournalctl -u <svc> -b -1 -e
Can’t SSH in / auth failing The auth log journalctl -u ssh -e · /var/log/auth.log (Deb) · /var/log/secure (RHEL)
Suspected break-in / brute-force Failed logins sudo lastb · journalctl -u ssh -g 'Failed password'
Hardware / disk / driver flakiness Kernel ring buffer sudo dmesg -T -l err,warn · journalctl -k -p err
Disk full because of /var/log Sizes, then vacuum + rotate du -sh /var/log/* · journalctl --disk-usage--vacuum-size=500M; logrotate -f
Cron job didn’t run Cron facility journalctl -u cron · /var/log/cron (RHEL) · grep CRON /var/log/syslog
App error but nothing in the journal The app’s own file ls /var/log/<app>/ · tail -f /var/log/nginx/error.log
“Suppressed N messages” journald rate-limit raise RateLimitBurst= in journald.conf
Reboot cause unknown Boot list + previous boot journalctl --list-bootsjournalctl -b -1 -p err -e

The disk-full case deserves a full walkthrough because it’s the one that pages you:

df -h /var                            # confirm /var (or /) is the full filesystem
sudo du -sh /var/log/* | sort -rh | head   # find the biggest offenders
journalctl --disk-usage               # how much is the journal?
sudo journalctl --vacuum-size=500M    # reclaim journal space immediately
sudo logrotate -vf /etc/logrotate.conf   # force-rotate + compress the text logs
# Then make it permanent: set SystemMaxUse= in journald.conf, and fix the
# logrotate rule for whatever grew (add/lower `rotate`, add `compress`).

Notice the pattern: reclaim now (vacuum, force-rotate), then cap forever (config). Deleting a live log file by hand is the wrong move — if a process still holds it open, the space isn’t freed until the process closes it, and you’ve lost the data with nothing to show for it. Truncate (: > file) or rotate instead.

Hands-on lab

Run this on a throwaway Ubuntu or Rocky/RHEL VM, or WSL. A few steps need sudo. (Bare containers often ship without a running journald or rsyslog — a VM or WSL is the reliable environment.) Each step lists the command, what you should see, and what just happened.

1. See both worlds at once.

journalctl -n 5 --no-pager        # newest 5 journal entries
ls -lh /var/log/ | head           # the text-file world
tail -n 3 /var/log/syslog 2>/dev/null || sudo tail -n 3 /var/log/messages

You should see structured journal lines and familiar text files. What just happened: you looked at the same system through both windows — the binary journal and the plain-text /var/log.

2. Inject a message and find it in both places.

logger -t lab -p local3.warning "hello from the logging lab"
journalctl -t lab -n 3 --no-pager                 # find it in the journal by tag
journalctl -p warning -g 'logging lab' -n 3 --no-pager   # find it by priority + grep

You should see your line come back, tagged lab. What just happened: logger pushed one message into the pipeline; you retrieved it two different ways — by tag and by priority+grep.

3. Slice by unit, time, and priority.

journalctl -u ssh -n 20 --no-pager        # (use -u sshd on RHEL)
journalctl -b -p err --no-pager | tail     # this boot's errors, if any
journalctl --since "10 min ago" -p warning --no-pager | tail

What just happened: you composed filters — unit, boot, time, priority — the core skill of reading the journal.

4. Explore boots and structured fields.

journalctl --list-boots            # how many boots are recorded?
journalctl -o verbose -n 1 --no-pager   # EVERY field of the newest entry
journalctl _COMM=sudo -n 5 --no-pager   # everything sudo logged

If --list-boots shows only boot 0, your journal is volatile. What just happened: you saw the raw fields that make journald queryable, and whether your journal survives reboots.

5. Check journal disk usage and (safely) vacuum.

journalctl --disk-usage
sudo journalctl --vacuum-time=2d       # remove journal data older than 2 days

⚠️ Vacuuming permanently deletes old journal data — fine on a lab box, think twice on a server you might need history from. What just happened: you saw journald’s self-managed retention, the reason it never needs logrotate.

6. Read the kernel ring buffer.

sudo dmesg -H | tail -n 15        # recent kernel messages, human-readable
sudo dmesg -l err,warn | tail     # only errors and warnings

What just happened: you read hardware/driver/kernel events straight from the ring buffer — where OOM kills and disk errors surface.

7. Write and debug a logrotate rule.

sudo mkdir -p /var/log/lab && echo "line 1" | sudo tee /var/log/lab/app.log
sudo tee /etc/logrotate.d/lab >/dev/null <<'EOF'
/var/log/lab/*.log {
    daily
    rotate 3
    compress
    missingok
    notifempty
    copytruncate
}
EOF
sudo logrotate -d /etc/logrotate.d/lab     # DRY-RUN: read the plan, no changes
sudo logrotate -vf /etc/logrotate.d/lab    # FORCE it, verbosely
ls -l /var/log/lab/                        # app.log + app.log.1 (or .1.gz next cycle)

You should see the debug plan, then a forced rotation producing app.log.1. What just happened: you wrote, dry-ran, and force-executed a rotation rule — exactly how you’d validate one in production before trusting the timer.

8. Review login history.

last -n 5           # recent logins + reboots
sudo lastb -n 5     # recent FAILED logins (may be empty on a clean box)
who ; w             # who's on now, and what they're doing

What just happened: you read the binary accounting files through their proper tools — the audit trail for who accessed the box.

9. Clean up. ⚠️ rm -rf deletes recursively with no undo — double-check the paths before you press Enter; these are the two lab artefacts you created above, nothing else.

sudo rm -rf /var/log/lab /etc/logrotate.d/lab

What just happened: removed the lab artefacts so nothing lingers.

Common mistakes and troubleshooting

Symptom Cause Fix
journalctl -b -1 says “no journal files” / only boot 0 exists Journal is volatile (RAM), wiped each reboot mkdir -p /var/log/journal; set Storage=persistent; systemctl restart systemd-journald
journalctl shows nothing / “No journal files” as a normal user Not in the systemd-journal (or adm/wheel) group sudo journalctl, or usermod -aG systemd-journal <user> then re-login
“Suppressed 4213 messages from …” in the log journald rate-limiting a chatty service Raise RateLimitBurst=/RateLimitIntervalSec= (or fix the app spewing logs)
After logrotate, app.log stays empty; app.log.1 keeps growing App holds the old fd open; never reopened the path Add postrotate reload (kill -HUP/systemctl reload), or use copytruncate
dmesg: read kernel buffer failed: Operation not permitted kernel.dmesg_restrict=1 (Ubuntu default) sudo dmesg, or read journalctl -k instead
Disk at 100%, deleting a big /var/log file didn’t free space A process still holds the deleted file open Truncate instead: : > /var/log/big.log, or restart the writer; then cap it
rsyslog rule added but no new file appears Config not reloaded, or a syntax error rsyslogd -N1 to validate, then systemctl restart rsyslog
Central logs never arrive at the collector Firewall/port, UDP vs TCP mismatch, or TLS misconfig Check `ss -tlnp
logrotate “won’t rotate” a growing file State file thinks it already rotated today; or not due logrotate -f; inspect /var/lib/logrotate/ state; check the schedule directive
Auth failures invisible — you grepped the wrong file Distro naming: auth.log (Debian) vs secure (RHEL) Use journalctl -u ssh/SYSLOG_FACILITY=10, which is distro-neutral

Three gotchas that cause the most lost hours:

Volatile journals lie by omission. The single most common “the logs don’t have it” moment is looking for a previous boot on a box whose journal is volatile. There’s nothing there because RAM was cleared on reboot. Always confirm with journalctl --list-boots; if you only ever see boot 0, enable persistence before the incident you’ll wish you had it for. On cloud images this is frequently the default.

The empty-file-after-rotation trap. When a daemon keeps its log file open and logrotate renames it out from under the daemon, the daemon happily keeps writing to the now-renamed inode while the freshly created file stays at zero bytes. You “fixed” nothing and lost visibility. The rule: any rotation with create needs a postrotate that tells the writer to reopen (a reload or HUP); if you can’t signal the writer, use copytruncate and accept its small race. Nginx and most systemd services reload cleanly; some third-party apps only understand copytruncate.

Deleting a held-open log doesn’t free the disk. Under pressure, people rm the giant log file — and df still shows the disk full, because the writing process holds the file descriptor and the kernel won’t reclaim the blocks until that process closes or dies. Use : > file (truncate) or restart/rotate the writer. lsof +L1 lists exactly these “deleted but still open” files when you’re hunting for phantom disk usage.

Cheat-sheet

Task Command
Follow a service live journalctl -u <svc> -f
A service’s recent logs, end journalctl -u <svc> -e (or -xe)
Errors this boot journalctl -p err -b
Previous boot’s log journalctl -b -1
Kernel log (persistent) journalctl -k (prev boot: -k -b -1)
Time window journalctl --since "1 hour ago" --until now
Grep the message field journalctl -g 'pattern'
Filter by user / PID / cmd journalctl _UID=0 · _PID=123 · _COMM=sudo
All fields of an entry journalctl -o verbose -n1
Journal size / vacuum journalctl --disk-usage · --vacuum-size=500M · --vacuum-time=2weeks
Make journal persistent mkdir -p /var/log/journal && systemctl restart systemd-journald
Kernel ring buffer dmesg -H · follow dmesg -w · errors dmesg -l err
Auth log Debian /var/log/auth.log · RHEL /var/log/secure
General log Debian /var/log/syslog · RHEL /var/log/messages
Validate rsyslog config rsyslogd -N1 then systemctl restart rsyslog
Forward to a server (TCP) rsyslog: *.* @@logserver:514
Write to the log logger -t tag -p local3.info "msg"
Broadcast to all TTYs wall "message"
logrotate dry-run / force logrotate -d <conf> · logrotate -vf <conf>
Failed logins / who’s on sudo lastb · last · who · w
PRI number PRI = facility × 8 + severity

Interview and exam questions

Q: What is the fundamental difference between the systemd journal and traditional syslog files? A: The journal is a binary, structured, indexed store — every entry carries named fields (_SYSTEMD_UNIT, _UID, PRIORITY, …) and you query it with journalctl, filtering by unit/priority/boot/time without scanning files. Syslog files are plain text, one line per event, unstructured, read with grep/less. On a modern box both coexist: rsyslog typically reads the journal and writes the familiar text files.

Q: The journal is empty for the previous boot. Why, and how do you fix it? A: The journal is volatile (in /run, RAM) and was wiped on reboot. Make it persistent: mkdir -p /var/log/journal (with Storage=auto that alone flips it) or set Storage=persistent in /etc/systemd/journald.conf, then systemctl restart systemd-journald. Verify with journalctl --list-boots.

Q: Show all error-and-worse messages for nginx from the previous boot. A: journalctl -u nginx -b -1 -p err. -u scopes the unit, -b -1 the previous boot, -p err severity 0–3.

Q: How is a syslog priority (PRI) computed, and how does it differ from journald’s priority? A: PRI = facility × 8 + severity — the combined wire value (e.g. authpriv(10)+crit(2) = <82>). journald’s “priority” (-p, PRIORITY=) is just the severity 0–7. Same word, different scope.

Q: -p err — what exactly does it show, and how would you show only err and warning? A: A single level shows that level and everything more severe (lower number): -p err = severities 0–3. For a bounded set use a range: -p warning..err (i.e. 4…3) shows only warning and err.

Q: A service logs to a file, you set up logrotate, but after rotation the new file stays empty. Why? A: The process still holds the old file descriptor and keeps writing to the renamed inode. Fix with a postrotate that reloads/HUPs the app so it reopens the path, or use copytruncate (copy then truncate in place) if the app can’t be signalled.

Q: Why doesn’t the journal need logrotate, but text logs do? A: journald self-manages retention via SystemMaxUse=/MaxRetentionSec= and on-demand --vacuum-* — it rotates and prunes its own binary files. Text files under /var/log grow unbounded and need an external tool (logrotate) to rename, compress, prune, and re-create them.

Q: (RHCSA-style) Configure rsyslog to forward all logs to logs.example.com over TCP, reliably. A: In /etc/rsyslog.d/60-fwd.conf: legacy *.* @@logs.example.com:514, or RainerScript action(type="omfwd" target="logs.example.com" port="514" protocol="tcp" queue.type="linkedList" action.resumeRetryCount="-1"). Then rsyslogd -N1 and systemctl restart rsyslog. @@ = TCP (reliable), @ = UDP (lossy).

Q: A process died overnight and its own log just stops. Where do you look first? A: The kernel OOM killer: journalctl -k -b -1 -g oom (or dmesg -T | grep -i 'killed process'). If the kernel killed it for memory, you’ll see Out of memory: Killed process …, then the unit’s Main process exited in journalctl -u <svc> -b -1.

Q: How do you find recent failed SSH login attempts? A: sudo lastb (from /var/log/btmp) for the raw list, and journalctl -u ssh -g 'Failed password' for source IPs and usernames. A flood from one IP is a brute-force in progress.

Q: dmesg fails with “Operation not permitted” for a normal user. Why? A: kernel.dmesg_restrict=1 (Ubuntu’s default) restricts the ring buffer to privileged users. Use sudo dmesg, or read the journal’s copy with journalctl -k, which the systemd-journal/adm group can read.

Q: The disk is full from logs. Walk through fixing it without losing more than necessary. A: du -sh /var/log/* | sort -rh to find the offender and journalctl --disk-usage for the journal. Reclaim now: journalctl --vacuum-size=500M and logrotate -vf. Cap forever: SystemMaxUse= in journald.conf, and tune the rotate/compress rule for the file that grew. Never rm a held-open file (df won’t shrink) — truncate with : > file or restart the writer.

Key takeaways

linuxloggingjournaldjournalctlrsyslogsysloglogrotatedmesgsystemdvar-logmonitoringtroubleshootingrhcsalfcs
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments