English
Syslog Comparison: syslog vs journald Logging Selection Guide
The Ultimate Showdown of Modern Linux Dual Logging Systems — From Storage Formats to Security Features, and a Complete Implementation of a Unified Golang Read Layer
Introduction: One system, two sets of logs
If you have ever logged into a modern Linux server (Ubuntu 16.04+, CentOS 7+, Debian 8+), you will be surprised to find:
bash
# The logs exist in two places at the same time.
/var/log/syslog # syslog traditional path
journalctl -u sshd # journald query interfaceThis situation of dual existence, for Security product developer means:
Which one should my Agent read?
What is the data relationship between the two systems?
Which is more suitable for security audit scenarios?
How can a single codebase be compatible with both?
This article will thoroughly answer these questions.
1. The Current Situation of Coexisting Dual Logging Systems
1.1 The Inevitability of Historical Evolution
1.2 Default Behavior of Current Mainstream Distributions
| Distribution | Default logging system | journald status | rsyslog status |
|---|---|---|---|
| Ubuntu 20.04+ | journald + rsyslog | Enable | Enable (Forwarding Mode) |
| CentOS 7/8/9 | journald + rsyslog | Enable | Enable |
| Debian 11+ | journald + rsyslog | Enable | Enable |
| RHEL 8/9 | primarily journald | Enable | Optional installation |
| Fedora | journald as the primary | Enable | Optional installation |
1.3 Core Configuration of Data Flow
In modern systems, the default data flow is:
Application logs → systemd-journald → (forwarding) → rsyslog → /var/log/*This repost is by /etc/rsyslog.d/50-default.conf Configuration implementation in:
apache
# Module for reading logs from journald
module(load="imjournal" # Input module: read journal
StateFile="/var/lib/rsyslog/imjournal.state"
RateLimit.Burst="0") # Disable rate limiting
# Then rsyslog writes to files according to traditional rules.
auth,authpriv.* /var/log/auth.log
*.*;auth,authpriv.none -/var/log/syslog2. Comprehensive Comparison of syslog vs systemd-journald
2.1 Core Features Comparison Table
| dimension | syslog (rsyslog) | systemd-journald | Security scenario impact |
|---|---|---|---|
| storage format | plain text file | binary database | Text is easy to read but easy to tamper with; binary has checksums. |
| Query capability | grep/awk | journalctl Structured query | journald query efficiency is 10-100 times higher |
| Index support | None | Automatic indexing (time, unit, priority) | journald is suitable for searching over large time ranges. |
| Write performance | Synchronous write (configurable asynchronous) | Asynchronous batch write | journald has a smaller impact on application performance. |
| Reliability | Data may be lost during a crash. | SyncIntervalSec control | syslog is more controllable |
| log rotation | logrotate (external tool) | Built-in automatic rotation (by size/time) | journald works out of the box |
| compression | Configurable (logrotate + compress) | Default compression (xz) | journald saves 70-90% of space |
| Tamper-proof | Dependent file permissions | Forward Secure Sealing (FSS) | journald native support |
| Remote transmission | Native support for TCP/TLS/RELP | Additional forwarding required | syslog is more mature |
| structured field | Limited (customizable template) | Native support for 50+ standard fields | journald is better suited for automated analysis |
| Resource usage | Memory~20MB | Memory~50-100MB | journald slightly higher |
2.2 In-Depth Comparison of Query Capabilities
Scenario: Query the logs of failed SSH logins
syslog method:
bash
# Need to know the log file path and format
grep "Failed password" /var/log/auth.log | grep "sshd"
# By time range (requires an external tool)
awk '/Oct 11/ && /Failed password/' /var/log/auth.logjournald method:
bash
# Structured query, precise and efficient
journalctl -u sshd --since="2024-10-11 00:00:00" --until="2024-10-11 23:59:59" \
-p err --grep="Failed password"Actual performance test(1 million log entries):
| Query operation | syslog + grep | journalctl | Performance multiplier |
|---|---|---|---|
| By time range | 2.3 seconds | 0.08 seconds | 28x |
| By service unit | N/A (requires grep) | 0.05 seconds | - |
| By priority | N/A (requires grep) | 0.03 seconds | - |
2.3 Reliability Comparison: Crash Scenario Testing
bash
# Simulate system crash (forced power loss)
echo "Test message" | logger
# Force restart immediately
# syslog: logs from the last few seconds may be lost (buffer not flushed)
tail -n 10 /var/log/syslog # Maybe there was no message just now.
# journald: The default sync interval is 5 minutes; there is also a risk of loss, but it is more controllable.
journalctl --since="1 minute ago" # Depends on SyncIntervalSecReliability engineering trade-offs:
| Configuration | Performance impact | Reliability |
|---|---|---|
| syslog synchronization mode | Flushing to disk on every write causes poor performance. | Extremely high |
| syslog asynchronous mode | Good performance, but data may be lost on crash | Medium |
| journald default | Batch writes, flush to disk every 5 minutes | Medium |
journald SyncIntervalSec=1 | Performance degrades, and crashes cause little data loss. | High |
3. Comparison of security features between syslog and journald
3.1 Anti-tampering capability
journald's Forward Secure Sealing (FSS):
bash
# Enable FSS (Generate Key)
sudo journalctl --setup-keys
# Verify that the log has not been tampered with
sudo journalctl --verify --key=./fss-keyFSS principle: use One-way hash chain, each log entry contains the hash value of the next entry, and any tampering will break the chain.
Tamper-proofing of syslog:
Rely on file system permissions (
chmod 600)Can be accessed via
rsyslogSignature module (omrelp+ TLS) implement transport encryptionNo built-in integrity verification after storage
3.2 Audit Compliance Comparison
| Compliance requirements | syslog | journald | Explanation |
|---|---|---|---|
| Integrity | Partially satisfied | Satisfy (FSS) | journald can prove that logs have not been tampered with |
| Non-repudiation | External signature required | Satisfy (FSS) | journald provides cryptographic proof |
| Confidentiality | TLS encrypted transmission | journal-remote TLS | Both support transmission encryption. |
| Availability | Log rotation | Automatic rotation | All need to monitor disk space. |
| audit trail | Dependency configuration | Built-in _AUDIT_SESSION | journald natively supports auditd integration |
3.3 Field Richness of Security Events
journald built-in fields for an SSH login failure record:
bash
journalctl -u sshd -o verbose | head -20Output example:
MESSAGE=Failed password for root from 192.168.1.100 port 54321 ssh2
PRIORITY=3 # err
_SYSLOG_FACILITY=10 # authpriv
_SOURCE_REALTIME_TIMESTAMP=1728641655000000
_PID=1234
_UID=0
_GID=0
_COMM=sshd
_EXE=/usr/sbin/sshd
_CMDLINE=sshd: [accepted]
_SYSTEMD_UNIT=sshd.service
_SYSTEMD_SLICE=system.slice
_SELINUX_CONTEXT=unconfined
_AUDIT_SESSION=12345
_AUDIT_LOGINUID=1000
_CAP_EFFECTIVE=1fffffffff
_HOSTNAME=webserver-01syslog fields for the same record:
<38>Oct 11 22:14:15 webserver-01 sshd[1234]: Failed password for root from 192.168.1.100 port 54321 ssh2Comparison conclusion: journald provides more than 10 times as much metadata, and its value for security analysis is self-evident.
4. Data Flow Relationships: In-Depth Understanding of the Forwarding Mechanism
4.1 Complete Data Flow Architecture
4.2 Detailed Explanation of Key Forwarding Configurations
journald forwarding settings(/etc/systemd/journald.conf):
ini
[Journal]
# Forward to /dev/console (console)
ForwardToConsole=no
# Forward to the syslog daemon (rsyslog)
ForwardToSyslog=yes # Key: enabled by default
# Forward to wall (all logged-in users)
ForwardToWall=yes
# Maximum log size (when persisting)
SystemMaxUse=2G
# Compression
Compress=yesrsyslog configuration for reading from journald(/etc/rsyslog.d/imjournal.conf):
apache
# Load journal input module
module(load="imjournal"
StateFile="/var/lib/rsyslog/imjournal.state"
# Important: Avoid duplicate reads
IgnorePreviousMessages="off"
# Rate limiting configuration (requires tuning in production environment)
RateLimit.Interval="1"
RateLimit.Burst="10000")4.3 Recommendation Data Pipeline for Security Scenarios
Regarding security products, the following link is recommended:
yaml
Recommendation pipeline:
Collection end:
- Read journald directly (via the sd_journal API)
- Advantages: access the richest metadata, no need to parse text
Archive side:
- journald persistence + rsyslog text backup
- Advantages: journald for querying, text files for external tools
Remote transmission:
- journald via ForwardToSyslog=yes → rsyslog TLS forwarding
- Advantages: Leverage journald's integrity + rsyslog's mature transport5. Adaptation Strategies for Security Products
5.1 Decision Trees for Three Scenarios
5.2 Adaptation Strategy Quick Reference Table
| Scene | Recommended solution | performance | Completeness | Implementation complexity |
|---|---|---|---|---|
| Rapid Prototyping | Read /var/log/auth.log | middle | Low | low |
| Production Agent | sd_journal API (CGO) | High | High | middle |
| Compliance audit | journald FSS + remote backup | High | extremely high | High |
| Mixed environment | Unified abstraction layer (see next section) | Middle | Middle | Middle |
| Container environment | configuration log-driver=journald | middle | middle | low |
6. Golang Unified Log Reading Layer: Complete Implementation
6.1 Design Objectives
Provide one Unified interface, automatically select the optimal reading method based on the environment:
With journald → use CGO +
sd_journal(Highest performance)No journald → fall back to file tail
Pure Go implementation (non-CGO) → Use
journalctl -o json
6.2 Core Interface Definition
go
// LogReader Unified Log Reading Interface
type LogReader interface {
// GetEntries retrieves log entries matching the specified conditions.
GetEntries(filter Filter) ([]LogEntry, error)
// StreamEntries reads logs in real time via streaming
StreamEntries(filter Filter, callback func(LogEntry)) error
// GetAvailableFields Get the list of available fields
GetAvailableFields() []string
// Close Close reader
Close() error
}
// Filter query filter conditions
type Filter struct {
Since time.Time
Until time.Time
Unit string // Service unit, such as "sshd.service"
Priority int // 0-7, 0=emerg, 7=debug
Match map[string]string // Field matching, e.g. {"_UID": "0"}
Limit int
Reverse bool
}
// LogEntry standardized log entry
type LogEntry struct {
Timestamp time.Time
Hostname string
Message string
Priority int
Facility int
Unit string
PID int
UID int
Comm string
Raw string
Fields map[string]string // Other extended fields
}6.3 Option 1: CGO + sd_journal (Highest Performance)
go
//go:build cgo && linux
// +build cgo,linux
package journald
/*
#cgo pkg-config: libsystemd
#include <systemd/sd-journal.h>
#include <stdlib.h>
#include <string.h>
// sd_journal is an opaque pointer, and we operate on it through C functions.
*/
import "C"
import (
"fmt"
"time"
"unsafe"
)
type JournalReader struct {
journal *C.sd_journal
}
func NewJournalReader() (*JournalReader, error) {
var j *C.sd_journal
if ret := C.sd_journal_open(&j, C.SD_JOURNAL_LOCAL_ONLY); ret < 0 {
return nil, fmt.Errorf("sd_journal_open failed: %d", ret)
}
return &JournalReader{journal: j}, nil
}
// AddFilter Add filter conditions
func (r *JournalReader) AddFilter(field, value string) error {
cField := C.CString(field)
cValue := C.CString(value)
defer C.free(unsafe.Pointer(cField))
defer C.free(unsafe.Pointer(cValue))
if ret := C.sd_journal_add_match(r.journal, cField, C.size_t(len(field))); ret < 0 {
return fmt.Errorf("add match failed: %d", ret)
}
// Add value matching (the actual implementation needs to be more complex)
_ = cValue
return nil
}
// SeekTo seeks to the specified time
func (r *JournalReader) SeekTo(t time.Time) error {
ts := C.uint64_t(t.UnixNano() / 1000) // μs
if ret := C.sd_journal_seek_realtime_usec(r.journal, ts); ret < 0 {
return fmt.Errorf("seek failed: %d", ret)
}
return nil
}
// Next Read the next log entry
func (r *JournalReader) Next() (*LogEntry, error) {
ret := C.sd_journal_next(r.journal)
if ret <= 0 {
return nil, nil // EOF
}
entry := &LogEntry{
Fields: make(map[string]string),
}
// Read MESSAGE
var data *C.char
var length C.size_t
if ret := C.sd_journal_get_data(r.journal, C.CString("MESSAGE"), &data, &length); ret >= 0 {
entry.Message = C.GoStringN(data, C.int(length))
}
// Read TIMESTAMP
var ts C.uint64_t
C.sd_journal_get_realtime_usec(r.journal, &ts)
entry.Timestamp = time.Unix(0, int64(ts)*1000)
// Read more fields...
// _PID, _UID, _COMM, _SYSTEMD_UNIT, etc.
return entry, nil
}
func (r *JournalReader) Close() error {
C.sd_journal_close(r.journal)
return nil
}6.4 Option 2: Pure Go + journalctl command
go
package journald
import (
"bufio"
"encoding/json"
"os/exec"
"time"
)
type CommandReader struct {
cmd *exec.Cmd
stdout *bufio.Scanner
}
func NewCommandReader(filter Filter) (*CommandReader, error) {
args := []string{
"--output=json",
"--since", filter.Since.Format("2006-01-02 15:04:05"),
}
if !filter.Until.IsZero() {
args = append(args, "--until", filter.Until.Format("2006-01-02 15:04:05"))
}
if filter.Unit != "" {
args = append(args, "-u", filter.Unit)
}
if filter.Priority != 0 {
args = append(args, "-p", fmt.Sprintf("%d", filter.Priority))
}
if filter.Limit > 0 {
args = append(args, "-n", fmt.Sprintf("%d", filter.Limit))
}
cmd := exec.Command("journalctl", args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
return &CommandReader{
cmd: cmd,
stdout: bufio.NewScanner(stdout),
}, nil
}
func (r *CommandReader) Next() (*LogEntry, error) {
if !r.stdout.Scan() {
return nil, nil
}
var raw map[string]interface{}
if err := json.Unmarshal(r.stdout.Bytes(), &raw); err != nil {
return nil, err
}
return parseFromMap(raw), nil
}
func (r *CommandReader) Close() error {
return r.cmd.Process.Kill()
}6.5 Solution 3: File tail fallback (compatibility mode)
go
package filetail
import (
"bufio"
"os"
"regexp"
"time"
)
type FileReader struct {
file *os.File
scanner *bufio.Scanner
path string
}
func NewFileReader(path string) (*FileReader, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
// Jump to the end of the file (tail -f)
file.Seek(0, 2)
return &FileReader{
file: file,
scanner: bufio.NewScanner(file),
path: path,
}, nil
}
// Traditional syslog line parser
var syslogPattern = regexp.MustCompile(
`^<(\d+)>(?:\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\S+?):\s+(.*)$`,
)
func (r *FileReader) Next() (*LogEntry, error) {
if !r.scanner.Scan() {
return nil, nil
}
line := r.scanner.Text()
matches := syslogPattern.FindStringSubmatch(line)
if len(matches) != 5 {
return &LogEntry{Message: line, Raw: line}, nil
}
return &LogEntry{
Priority: parseInt(matches[1]) % 8,
Facility: parseInt(matches[1]) / 8,
Hostname: matches[2],
Comm: matches[3],
Message: matches[4],
Raw: line,
}, nil
}6.6 Unified Adapter (Automatic Selection)
go
package logadapter
import (
"os"
"path/filepath"
)
type AutoAdapter struct {
reader LogReader
readerType string
}
func NewAutoAdapter() (*AutoAdapter, error) {
// Check whether journald is present (check /run/log/journal or /var/log/journal)
journalPaths := []string{"/run/log/journal", "/var/log/journal"}
hasJournald := false
for _, p := range journalPaths {
if info, err := os.Stat(p); err == nil && info.IsDir() {
hasJournald = true
break
}
}
if hasJournald {
// Prefer CGO (if compiled), otherwise fall back to the command
reader, err := NewJournalReader()
if err == nil {
return &AutoAdapter{reader: reader, readerType: "cgo"}, nil
}
reader, err := NewCommandReader(Filter{})
if err == nil {
return &AutoAdapter{reader: reader, readerType: "command"}, nil
}
}
// Fall back to the syslog file
syslogPaths := []string{"/var/log/syslog", "/var/log/messages"}
for _, p := range syslogPaths {
if _, err := os.Stat(p); err == nil {
reader, err := NewFileReader(p)
if err == nil {
return &AutoAdapter{reader: reader, readerType: "file"}, nil
}
}
}
return nil, fmt.Errorf("no log source found")
}
func (a *AutoAdapter) GetEntries(filter Filter) ([]LogEntry, error) {
return a.reader.GetEntries(filter)
}
func (a *AutoAdapter) GetReaderType() string {
return a.readerType
}6.7 Usage Example
go
func main() {
// Automatically adapt to the environment
adapter, err := NewAutoAdapter()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Using reader: %s\n", adapter.GetReaderType())
// Query the SSH service error logs
entries, err := adapter.GetEntries(Filter{
Since: time.Now().Add(-1 * time.Hour),
Unit: "sshd.service",
Priority: 3, // err
Limit: 100,
})
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Printf("[%s] %s\n", entry.Timestamp.Format("15:04:05"), entry.Message)
}
}7. Selection Summary and Recommendations
7.1 Decision Matrix
| Scene | Recommended Solution | reason |
|---|---|---|
| Newly developed security Agent | journald (sd_journal API) | Rich metadata, efficient querying, native tamper-proofing |
| Need to integrate with the existing SIEM | syslog (rsyslog forwarding) | Mature ecosystem, standard protocols, easy to integrate |
| Compliance audit scenario | journald + FSS | Cryptographic proof that the log has not been tampered with |
| Container/K8s environment | journald + forwarding | Docker supports the journald driver |
| Legacy system compatibility | Unified abstraction layer (automatic adaptation) | One codebase adapts to all environments |
| Extreme performance requirements | CGO + sd_journal | Directly call the systemd API, with no overhead |
| Pure Go deployment (no CGO) | Command mode (journalctl -o json) | No CGO required, but performance is slightly lower. |
7.2 Final Recommendations
For Golang Security Product Developer, the recommended core strategy:
First choice: sd_journal API, highest performance and richest metadata
Downgrade: journalctl command, pure Go implementation
Compatible: file tail, supports legacy systems
Transmission: rsyslog TLS, encrypted and reliable transmission
Best practice combination:
Collection layer: Use CGO +
sd_journalRead journald directlyArchive layer Configure rsyslog to read from journald and write to a text file
Transport Layer: Forward to the log center using rsyslog TLS
Analysis layer: Prefer using journald's JSON output to integrate with ELK/SIEM
This achieves both the performance and integrity of journald while retaining the readability and ecosystem compatibility of the syslog text format.
8. Summary
This article provides a comprehensive comparison of syslog and journald:
Architectural differences Text vs binary: each has its advantages and disadvantages.
Query performance: journald wins by 10–100x on structured queries
Security features: journald's FSS provides native tamper resistance
Data flow The two are synergistic rather than substitutive.
Adaptation strategy: Choose the optimal solution based on the scenario
Unified implementation: Provide a complete Golang unified read layer
Series Summary
This concludes the three-part syslog series:
| Table of contents | Core content | Amount of code |
|---|---|---|
| Basic Section | Protocol format, rsyslog configuration, basic parsing | ~150 lines |
| Practical Guide | TLS certificates, production-grade collectors, SIEM integration | ~500 lines |
| Comparison Chapter(This article) | journald comparison, unified read layer, selection recommendations | ~400 lines |
Trinity helps you become an expert in syslog + systemd-journald, enabling you to confidently handle the log collection requirements of any security product.