A Haraka plugin that forwards log messages to a Graylog server via GELF UDP.
- Forwards all Haraka log messages to Graylog in GELF format
- UDP transport with automatic IPv4/IPv6 detection
- Chunked UDP support for large messages (GELF chunking spec)
- Optional gzip compression
- Per-plugin configuration with independent URL, fields, and routing
- Configurable custom GELF fields with variable interpolation
- Exposes a
loggelfAPI onserver.notesfor other plugins to send structured GELF messages directly
This project makes limited use of generative AI tools during development:
- Generative AI was used for brainstorming, test generation, documentation and code review assistance
- All production code was written, reviewed, and validated by a human
- Final design decisions and implementations are human-driven
- Any defects or limitations in the code are the responsibility of the human authors
npm install haraka-plugin-gelf
Add to config/plugins:
gelf
Create config/gelf.yaml. A minimal configuration:
url: 'udp://graylog.example.com:12201'Full configuration with all options:
# Enable/disable GELF logging
enabled: true
# Enable/disable Haraka log hook
# true - all Haraka log messages are forwarded to Graylog
# false - only messages sent via the server.notes.loggelf API are forwarded
log_hook_enabled: false
# Minimum level of log messages to forward to graylog from log hook
# 0 - emerg, 1 - alert, 2 - crit, 3 - error, 4 - warn, 5 - notice, 6 - info, 7 - debug
log_hook_level: 4
# GELF UDP endpoint. Supports udp://, udp4://, udp6:// schemes
url: 'udp://graylog.example.com:12201'
# Compress messages with gzip
compress: true
# Maximum UDP packet/chunk size in bytes (64–65475)
# 1420 suits standard Ethernet MTU; 8192 suits jumbo frames
max_chunk_size: 1420
# Override the hostname reported in GELF messages (defaults to os.hostname())
# hostname: mail.example.com
# If true, no further log plugins will be called after this one
last: false
# Custom fields added to every GELF message (see Custom Fields below)
fields:
logger: '${logger}'
connection: '${connection_uuid}'
transaction: '${transaction_uuid}'
# Per-plugin overrides (see Per-plugin Configuration below)
# plugins:
# rspamd:
# url: 'udp4://rspamd-graylog.example.com:12201'
# fields:
# component: 'rspamd'The url setting controls both the transport endpoint and the preferred address family:
| Scheme | Behaviour |
|---|---|
udp:// |
Dual-stack; uses system address family preference (IPv4 or IPv6) |
udp4:// |
Forces IPv4 |
udp6:// |
Forces IPv6 only |
When a hostname resolves to multiple addresses, DNS round-robin provides basic load distribution.
The fields section lets you add fixed or dynamic fields to every GELF message. Field values are strings and support variable interpolation using ${variable} syntax.
The following variables are available:
| Variable | Value |
|---|---|
${logger} |
Name of the plugin that emitted the log entry |
${connection_uuid} |
Haraka connection UUID |
${transaction_uuid} |
Haraka transaction UUID (includes .N suffix) |
If a variable is not available in context (e.g. ${transaction_uuid} outside of a transaction), the field is omitted from the message entirely.
Example:
fields:
logger: '${logger}'
connection: '${connection_uuid}'
transaction: '${transaction_uuid}'
environment: 'production'
facility: 'smtp'To explicitly suppress a field that would be inherited from the main config in a per-plugin override, set it to null:
plugins:
rspamd:
fields:
transaction: null # omit transaction field for rspamd messages
component: 'rspamd'Any top-level setting can be overridden per Haraka plugin under the plugins key. Plugin names match plugin.name in Haraka (e.g. karma, dkim, rcpt_to.in_host_list).
Per-plugin fields are merged with (not replaced by) the top-level fields.
plugins:
karma:
url: 'udp://karma-graylog.example.com:12201'
last: true
fields:
component: 'karma'
rspamd:
enabled: false
wildduck:
log_hook_enabled: true
url: 'udp4://wildduck-graylog.example.com:12201'
fields:
component: 'mx'
transaction: null
queue_id: '${transaction_uuid}'When loaded, the plugin exposes server.notes.loggelf for structured GELF logging from other plugins.
Send a raw GELF message object. Returns cfg.last (boolean) — when true, no further log plugins will be called.
exports.hook_queue = function (next, connection) {
connection.server.notes.loggelf?.message(this, {
short_message: 'Mail queued',
level: 6, // INFO
_recipient: connection.transaction.rcpt_to.toString(),
_sender: connection.transaction.mail_from.toString(),
});
next(OK);
};Send a structured log message. Automatically formats short_message as [transaction_uuid] [plugin_name] message to match Haraka's log format, and populates connection_uuid and transaction_uuid template variables from the connection object.
gelf?.log(this, connection, 6, 'Mail queued', { _queue: 'outbound' });All accept (callerPlugin, connection, shortMessage, additionalFields). connection may be null.
const gelf = connection.server.notes.loggelf;
gelf?.emergency(this, connection, 'System failure');
gelf?.alert(this, connection, 'Disk almost full');
gelf?.critical(this, connection, 'Database unreachable');
gelf?.error(this, connection, 'Delivery failed', { _recipient: 'user@example.com' });
gelf?.warning(this, connection, 'Rate limit approached');
gelf?.notice(this, connection, 'New connection');
gelf?.info(this, connection, 'Message accepted');
gelf?.debug(this, connection, 'Processing step', { _detail: 'some value' });Fields in msg beyond the standard GELF fields are included as additional fields, automatically prefixed with _ if not already. Reserved GELF field names (host, version, short_message, full_message, timestamp, level, facility, file, line, id) are never duplicated as additional fields.
| Type | Behaviour |
|---|---|
string |
Passed as-is |
number |
Passed as-is |
boolean |
Converted to "true"/"false" |
Date |
Converted to ISO 8601 string |
| Other | JSON round-tripped |
Messages are sent as GELF 1.1 JSON over UDP. Supported standard fields:
| Field | Source |
|---|---|
version |
Always "1.1" |
host |
msg.host, hostname config, or os.hostname() |
short_message |
Required |
full_message |
Optional |
timestamp |
msg.timestamp (Date or number) or Date.now() |
level |
Syslog severity (0–7), defaults to INFO (6) |
facility |
Optional |
file |
Optional |
line |
Optional integer |
| Level | Value |
|---|---|
| emergency | 0 |
| alert | 1 |
| critical | 2 |
| error | 3 |
| warning | 4 |
| notice | 5 |
| info | 6 |
| debug | 7 |
Haraka log levels DATA and PROTOCOL are mapped to DEBUG (7).
EUPL-1.1+