or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-types.mdcontextual-logging.mdcore-logging.mddiode-writer.mdfield-methods.mdglobal-configuration.mdglobal-logger.mdhooks.mdhttp-middleware.mdindex.mdjournald-integration.mdpkgerrors-integration.mdsampling.mdwriters-and-output.md
tile.json

journald-integration.mddocs/

Journald Integration

The journald subpackage provides integration with systemd's journal daemon (journald) for structured logging on Linux systems. This allows zerolog to send logs directly to the system journal, making them accessible via journalctl.

Platform Support

Note: This package is only available on Linux systems. It uses build tags to exclude Windows:

//go:build !windows
// +build !windows

Package Imports

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/journald"
)

Dependencies: Requires github.com/coreos/go-systemd/v22/journal:

go get github.com/coreos/go-systemd/v22/journal

Overview

The journald writer sends zerolog output directly to systemd journal daemon. This provides:

  • Native systemd integration
  • Structured field support
  • Priority mapping (log levels)
  • System-wide log aggregation
  • Query with journalctl

Creating a Journald Writer

// Create writer that sends logs to journald
func NewJournalDWriter() io.Writer

Example:

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/journald"
)

func main() {
    writer := journald.NewJournalDWriter()
    logger := zerolog.New(writer).With().Timestamp().Logger()

    logger.Info().Str("service", "myapp").Msg("application started")
}

Log Level Mapping

Zerolog levels are mapped to journald priorities:

Zerolog LevelJournald Priority
TraceLevelPriDebug (7)
DebugLevelPriDebug (7)
InfoLevelPriInfo (6)
WarnLevelPriWarning (4)
ErrorLevelPriErr (3)
FatalLevelPriCrit (2)
PanicLevelPriEmerg (0)
NoLevelPriNotice (5)

Field Handling

Key transformations:

  • All field keys are converted to uppercase (journald requirement)
  • String values are sent as-is
  • Numeric values are converted to strings
  • Complex values are JSON-encoded
  • Complete JSON log is also sent under JSON field

Special fields:

  • level and time fields are handled specially (not passed as args)
  • message field becomes the journald message text
  • All other fields become journald variables

Example:

logger.Info().
    Str("user", "alice").
    Int("count", 42).
    Msg("user action")

In journald:

MESSAGE=user action
USER=alice
COUNT=42
JSON={"level":"info","user":"alice","count":42,"message":"user action"}

Usage Examples

Basic Usage

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/journald"
)

func main() {
    logger := zerolog.New(journald.NewJournalDWriter()).
        With().
        Timestamp().
        Str("app", "myservice").
        Logger()

    logger.Info().Msg("service started")
    logger.Error().Err(err).Msg("operation failed")
}

View logs:

journalctl -f -o json-pretty

Systemd Service Integration

package main

import (
    "os"

    "github.com/rs/zerolog"
    "github.com/rs/zerolog/journald"
)

func main() {
    // Use journald when running as systemd service
    var writer io.Writer
    if os.Getenv("INVOCATION_ID") != "" {
        // Running under systemd
        writer = journald.NewJournalDWriter()
    } else {
        // Running interactively
        writer = zerolog.ConsoleWriter{Out: os.Stderr}
    }

    logger := zerolog.New(writer).With().
        Timestamp().
        Str("service", "myapp").
        Logger()

    logger.Info().Msg("application started")
}

Structured Field Logging

logger.Info().
    Str("service", "api").
    Str("endpoint", "/users").
    Int("status", 200).
    Dur("duration", time.Since(start)).
    Str("method", "GET").
    Msg("request completed")

Query with journalctl:

# Filter by service
journalctl SERVICE=api

# Filter by endpoint and status
journalctl SERVICE=api ENDPOINT=/users STATUS=200

# View JSON field
journalctl -o json-pretty | jq '.JSON'

Multiple Output Destinations

// Write to both journald and file
file, _ := os.Create("/var/log/myapp.log")

multi := zerolog.MultiLevelWriter(
    journald.NewJournalDWriter(),
    file,
)

logger := zerolog.New(multi).With().Timestamp().Logger()

With Context

logger := zerolog.New(journald.NewJournalDWriter()).With().
    Timestamp().
    Str("app", "myservice").
    Str("version", "1.0.0").
    Int("pid", os.Getpid()).
    Logger()

// All logs include app, version, and pid
logger.Info().Msg("starting")

Error Logging with Context

func handleRequest(logger zerolog.Logger, requestID string) {
    reqLogger := logger.With().
        Str("request_id", requestID).
        Logger()

    err := processRequest()
    if err != nil {
        reqLogger.Error().
            Err(err).
            Str("operation", "process_request").
            Msg("request failed")
    }

    reqLogger.Info().Msg("request completed")
}

Query errors by request ID:

journalctl REQUEST_ID=abc123 PRIORITY=3

Querying Logs with journalctl

Basic Queries

# View all logs from your app
journalctl APP=myservice

# Follow logs in real-time
journalctl -f APP=myservice

# View JSON format
journalctl -o json-pretty APP=myservice

# Filter by priority (error level)
journalctl PRIORITY=3 APP=myservice

# View logs since 1 hour ago
journalctl --since "1 hour ago" APP=myservice

Field-Based Filtering

# Filter by specific field values
journalctl SERVICE=api STATUS=500

# Multiple field filters
journalctl APP=myservice USER=alice ENDPOINT=/api/users

# View specific fields only
journalctl -o cat APP=myservice

Advanced Queries

# Export to JSON
journalctl -o json APP=myservice > logs.json

# Count log entries
journalctl APP=myservice | wc -l

# View only messages
journalctl -o cat APP=myservice

# View with timestamps
journalctl -o short-iso APP=myservice

Systemd Service Configuration

Example systemd service file:

[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp
Restart=always
RestartSec=5

# Journald configuration
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

Best Practices

1. Use Structured Fields

Take advantage of journald's field support:

// Good - structured fields
logger.Info().
    Str("user", username).
    Int("count", count).
    Msg("user action")

// Avoid - interpolated message
logger.Info().Msgf("user %s action count %d", username, count)

2. Consistent Field Names

Use consistent field names across your application:

const (
    FieldService  = "service"
    FieldEndpoint = "endpoint"
    FieldStatus   = "status"
)

logger.Info().
    Str(FieldService, "api").
    Str(FieldEndpoint, "/users").
    Int(FieldStatus, 200).
    Msg("request")

3. Add Service Context

Include service information in context:

logger := zerolog.New(journald.NewJournalDWriter()).With().
    Str("service", "myapp").
    Str("version", version).
    Str("environment", env).
    Logger()

4. Detection of Systemd

Only use journald when running under systemd:

func newLogger() zerolog.Logger {
    var writer io.Writer

    // Check if running under systemd
    if _, err := os.Stat("/run/systemd/system"); err == nil {
        writer = journald.NewJournalDWriter()
    } else {
        writer = os.Stderr
    }

    return zerolog.New(writer).With().Timestamp().Logger()
}

5. Avoid Reserved Fields

Don't use journald reserved field names:

// Avoid these field names (journald reserved)
// _PID, _UID, _GID, _COMM, _EXE, _CMDLINE, _CAP_EFFECTIVE
// _SYSTEMD_*, _BOOT_ID, _MACHINE_ID, _HOSTNAME

// Good - custom field names
logger.Info().
    Str("app_pid", fmt.Sprint(os.Getpid())).
    Msg("process info")

6. Use for System Services Only

Journald is best for system services, not for all applications:

// Good - system service
if isSystemService() {
    logger = zerolog.New(journald.NewJournalDWriter())
}

// Avoid - desktop application
// Use file or console output instead

Performance Considerations

  • Journald writes are synchronous (blocking)
  • Writing to journald involves socket communication
  • For high-throughput apps, consider using diode.Writer:
import "github.com/rs/zerolog/diode"

journaldWriter := journald.NewJournalDWriter()
nonBlocking := diode.NewWriter(journaldWriter, 1000, 10*time.Millisecond, nil)

logger := zerolog.New(nonBlocking)

Limitations

  • Linux only - Not available on Windows or other platforms
  • Uppercase keys - All field keys converted to uppercase by journald
  • String values - Complex types are JSON-encoded to strings
  • No nested structures - Journald doesn't support nested fields directly
  • Reserved fields - Some field names are reserved by journald

Troubleshooting

Logs Not Appearing

Check journald is running:

systemctl status systemd-journald

Check journal size limits:

journalctl --disk-usage

View all fields:

journalctl -o json-pretty -n 1

Field Names Not Visible

Remember field names are uppercase:

logger.Info().Str("user_id", "123").Msg("test")

Query with uppercase:

journalctl USER_ID=123

Missing Logs

Check journald rate limiting:

# In /etc/systemd/journald.conf
RateLimitIntervalSec=30s
RateLimitBurst=10000

Comparison with File Logging

FeatureJournaldFile Logging
QueryingBuilt-in (journalctl)Need external tools
RotationAutomaticManual/logrotate
FilteringField-basedText-based
StructuredNativeParsing needed
PlatformLinux onlyCross-platform
PerformanceModerateFast

See Also

  • Writers and Output - Other writer implementations
  • Global Logger - Using with global logger
  • Contextual Logging - Adding persistent fields
  • Diode Writer - Non-blocking wrapper for journald