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.
Note: This package is only available on Linux systems. It uses build tags to exclude Windows:
//go:build !windows
// +build !windowsimport (
"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/journalThe journald writer sends zerolog output directly to systemd journal daemon. This provides:
journalctl// Create writer that sends logs to journald
func NewJournalDWriter() io.WriterExample:
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")
}Zerolog levels are mapped to journald priorities:
| Zerolog Level | Journald Priority |
|---|---|
| TraceLevel | PriDebug (7) |
| DebugLevel | PriDebug (7) |
| InfoLevel | PriInfo (6) |
| WarnLevel | PriWarning (4) |
| ErrorLevel | PriErr (3) |
| FatalLevel | PriCrit (2) |
| PanicLevel | PriEmerg (0) |
| NoLevel | PriNotice (5) |
Key transformations:
JSON fieldSpecial fields:
level and time fields are handled specially (not passed as args)message field becomes the journald message textExample:
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"}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-prettypackage 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")
}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'// 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()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")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# 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# 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# 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=myserviceExample 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.targetTake 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)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")Include service information in context:
logger := zerolog.New(journald.NewJournalDWriter()).With().
Str("service", "myapp").
Str("version", version).
Str("environment", env).
Logger()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()
}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")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 insteaddiode.Writer:import "github.com/rs/zerolog/diode"
journaldWriter := journald.NewJournalDWriter()
nonBlocking := diode.NewWriter(journaldWriter, 1000, 10*time.Millisecond, nil)
logger := zerolog.New(nonBlocking)Check journald is running:
systemctl status systemd-journaldCheck journal size limits:
journalctl --disk-usageView all fields:
journalctl -o json-pretty -n 1Remember field names are uppercase:
logger.Info().Str("user_id", "123").Msg("test")Query with uppercase:
journalctl USER_ID=123Check journald rate limiting:
# In /etc/systemd/journald.conf
RateLimitIntervalSec=30s
RateLimitBurst=10000| Feature | Journald | File Logging |
|---|---|---|
| Querying | Built-in (journalctl) | Need external tools |
| Rotation | Automatic | Manual/logrotate |
| Filtering | Field-based | Text-based |
| Structured | Native | Parsing needed |
| Platform | Linux only | Cross-platform |
| Performance | Moderate | Fast |