Skip to content

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 interface

This 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 ​

DistributionDefault logging systemjournald statusrsyslog status
Ubuntu 20.04+journald + rsyslogEnableEnable (Forwarding Mode)
CentOS 7/8/9journald + rsyslogEnableEnable
Debian 11+journald + rsyslogEnableEnable
RHEL 8/9primarily journaldEnableOptional installation
Fedorajournald as the primaryEnableOptional 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/syslog

2. Comprehensive Comparison of syslog vs systemd-journald ​

2.1 Core Features Comparison Table ​

dimensionsyslog (rsyslog)systemd-journaldSecurity scenario impact
storage formatplain text filebinary databaseText is easy to read but easy to tamper with; binary has checksums.
Query capabilitygrep/awkjournalctl Structured queryjournald query efficiency is 10-100 times higher
Index supportNoneAutomatic indexing (time, unit, priority)journald is suitable for searching over large time ranges.
Write performanceSynchronous write (configurable asynchronous)Asynchronous batch writejournald has a smaller impact on application performance.
ReliabilityData may be lost during a crash.SyncIntervalSec controlsyslog is more controllable
log rotationlogrotate (external tool)Built-in automatic rotation (by size/time)journald works out of the box
compressionConfigurable (logrotate + compress)Default compression (xz)journald saves 70-90% of space
Tamper-proofDependent file permissionsForward Secure Sealing (FSS)journald native support
Remote transmissionNative support for TCP/TLS/RELPAdditional forwarding requiredsyslog is more mature
structured fieldLimited (customizable template)Native support for 50+ standard fieldsjournald is better suited for automated analysis
Resource usageMemory~20MBMemory~50-100MBjournald 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.log

journald 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 operationsyslog + grepjournalctlPerformance multiplier
By time range2.3 seconds0.08 seconds28x
By service unitN/A (requires grep)0.05 seconds-
By priorityN/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 SyncIntervalSec

Reliability engineering trade-offs:

ConfigurationPerformance impactReliability
syslog synchronization modeFlushing to disk on every write causes poor performance.Extremely high
syslog asynchronous modeGood performance, but data may be lost on crashMedium
journald defaultBatch writes, flush to disk every 5 minutesMedium
journald SyncIntervalSec=1Performance 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-key

FSS 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 rsyslog Signature module (omrelp+ TLS) implement transport encryption

  • No built-in integrity verification after storage

3.2 Audit Compliance Comparison ​

Compliance requirementssyslogjournaldExplanation
IntegrityPartially satisfiedSatisfy (FSS)journald can prove that logs have not been tampered with
Non-repudiationExternal signature requiredSatisfy (FSS)journald provides cryptographic proof
ConfidentialityTLS encrypted transmissionjournal-remote TLSBoth support transmission encryption.
AvailabilityLog rotationAutomatic rotationAll need to monitor disk space.
audit trailDependency configurationBuilt-in _AUDIT_SESSIONjournald 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 -20

Output 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-01

syslog 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 ssh2

Comparison 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=yes

rsyslog 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 transport

5. Adaptation Strategies for Security Products ​

5.1 Decision Trees for Three Scenarios ​

5.2 Adaptation Strategy Quick Reference Table ​

SceneRecommended solutionperformanceCompletenessImplementation complexity
Rapid PrototypingRead /var/log/auth.logmiddleLowlow
Production Agentsd_journal API (CGO)HighHighmiddle
Compliance auditjournald FSS + remote backupHighextremely highHigh
Mixed environmentUnified abstraction layer (see next section)MiddleMiddleMiddle
Container environmentconfiguration log-driver=journaldmiddlemiddlelow

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 ​

SceneRecommended Solutionreason
Newly developed security Agentjournald (sd_journal API)Rich metadata, efficient querying, native tamper-proofing
Need to integrate with the existing SIEMsyslog (rsyslog forwarding)Mature ecosystem, standard protocols, easy to integrate
Compliance audit scenariojournald + FSSCryptographic proof that the log has not been tampered with
Container/K8s environmentjournald + forwardingDocker supports the journald driver
Legacy system compatibilityUnified abstraction layer (automatic adaptation)One codebase adapts to all environments
Extreme performance requirementsCGO + sd_journalDirectly 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_journal Read journald directly

  • Archive 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 contentsCore contentAmount of code
Basic SectionProtocol format, rsyslog configuration, basic parsing~150 lines
Practical GuideTLS 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.

Reference Resources ​

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]