Lightweight Python service that exports system logs to multiple backends in real-time.
✅ Real-time log monitoring - Watches log files as they grow (like tail -f)
✅ Systemd journal support - Monitors journald logs via journalctl
✅ Multiple backends - Exports to Loki, ElasticSearch, Kafka, etc.
✅ High Availability - Send same logs to multiple backend instances (list support)
✅ Smart log parsing - Auto-detects timestamps and log levels
✅ Custom labels - Add metadata to logs (environment, team, etc.)
✅ Multiple formats - JSON, YAML, YML configuration files
✅ Resilient - Continues on errors, reconnects automatically
✅ Glob patterns - Wildcard support for log file paths (/var/log/*.log, /app/*/logs/*.log)
✅ Rate limiting - Control log throughput per source (logs/second)
✅ Batching - Group logs for efficient sending (configurable buffer size)
✅ Auto-reload - Automatically detect new files matching glob patterns (no restart needed)
✅ Disk buffering (WAL) - At-least-once delivery with Write-Ahead Log for critical logs
❌ No log rotation - Use logrotate for that
❌ No log parsing/filtering - Sends raw logs (except level extraction)
❌ No encryption - Use TLS-enabled backends or reverse proxy
❌ No authentication - Relies on backend authentication
❌ No log archival - Use backend retention policies
- Real-time log export (tail -f style)
- Systemd journal (journald) monitoring
- Multiple backend support
- Glob pattern support with auto-reload
- Smart timestamp and log level detection
- Simple configuration (JSON/YAML)
- Custom labels support
- Rate limiting and batching
- Disk buffering (WAL) for at-least-once delivery
- Resilient error handling
- Systemd integration
# Install
pip3 install -r requirements.txt
sudo mkdir -p /opt/sle /etc/sle.d /var/lib/sle/buffer
sudo cp sle.py config_loader.py file_watcher.py journald_watcher.py disk_buffer.py /opt/sle/
sudo cp -r exporters /opt/sle/
sudo cp sle.service /etc/systemd/system/
sudo systemctl daemon-reload
# Configure (see examples below)
# For journald monitoring:
sudo cp examples/default.json /etc/sle.d/
# For file-based logs:
sudo nano /etc/sle.d/myapp.json
# Start
sudo systemctl start sle
sudo systemctl enable sleConfiguration files in /etc/sle.d/ with .json, .yaml or .yml extension.
Send logs to multiple backend instances:
SLE supports sending the same logs to multiple backend instances for redundancy and high availability. Simply provide a list of URLs instead of a single URL:
{
"LOKI_IP": [
"http://loki-primary:3100",
"http://loki-secondary:3100",
"http://loki-backup:3100"
],
"myapp": {
"LOGS": {
"path_file": "/var/log/myapp.log"
}
}
}Benefits:
- ✅ Logs sent to ALL specified backends simultaneously
- ✅ If one backend is down, others still receive logs
- ✅ Load balancing across multiple instances
- ✅ Data redundancy for critical logs
Supported formats:
- Single URL:
"LOKI_IP": "http://loki:3100" - Multiple URLs:
"LOKI_IP": ["http://loki-1:3100", "http://loki-2:3100"]
Special file: default.json or default.yml only
To enable systemd journal monitoring, create /etc/sle.d/default.json:
{
"LOKI_IP": "http://localhost:3100",
"JOURNALCTL": "on"
}Behavior:
- If
default.jsonexists WITHJOURNALCTL: "on"→ Journald is enabled - If
default.jsonexists WITHJOURNALCTL: "off"→ Journald is disabled - If
default.jsonexists WITHOUTJOURNALCTLkey → Journald is disabled - If
default.jsondoes NOT exist → Journald is disabled
JOURNALCTL: "on" to enable journald monitoring
Other settings:
- Only works in
default.jsonordefault.yml- This setting is ignored in other files - Monitors all systemd services - Logs tagged by unit name (nginx.service → NGINX)
- Optional labels: Add
"JOURNALCTL_LABELS": {"datacenter": "eu"}for custom labels
Example with labels:
{
"LOKI_IP": "http://localhost:3100",
"JOURNALCTL": "on",
"JOURNALCTL_LABELS": {
"source": "systemd",
"host": "prod-server-01"
}
}BACKEND_IP (optional) → Backend endpoint(s), determines type
Can be a single URL or list of URLs
AUTO_RELOAD (optional) → Auto-reload interval in seconds (default.json only)
QUEUE_SIZE (optional) → Queue size limit (default.json only)
↓
SERVICE_NAME (mandatory) → Service identifier
↓
CATEGORY (mandatory) → Log category
↓
path_file (mandatory) → Absolute path to log file
Supports glob patterns: *, ?, []
delimiter (optional) → Line delimiter (default: \n)
labels (optional) → Custom labels dict
rate_limit (optional) → Max logs/sec (enforced per file)
buffer_size (optional) → Batch size (logs grouped before sending)
disk_buffer (optional) → DROP (default) or DISK (at-least-once)
SLE supports wildcards in file paths to monitor multiple files matching a pattern:
*- Matches any characters (except path separator)?- Matches a single character[]- Matches character ranges**- Matches any files and zero or more directories (recursive)
Examples:
{
"LOKI_IP": "http://loki:3100",
"docker": {
"CONTAINERS": {
"path_file": "/var/lib/docker/containers/*/*.log",
"disk_buffer": "DISK"
}
},
"apps": {
"ALL_LOGS": {
"path_file": "/var/log/apps/**/*.log",
"rate_limit": 500,
"buffer_size": 100
}
},
"nginx": {
"ACCESS": {
"path_file": "/var/log/nginx/access*.log",
"disk_buffer": "DROP"
}
}
}Benefits:
- ✅ Monitor all files matching pattern automatically
- ✅ Works with auto-reload to detect new files dynamically
- ✅ Perfect for container logs, rotated logs, multi-instance apps
- ✅ Each matched file is monitored independently
Notes:
- Pattern is resolved at startup and during auto-reload cycles
- If pattern matches no files, a warning is logged and the entry is skipped
- Each matched file gets its own watcher and rate limit/buffer (if specified)
- Enable
AUTO_RELOADin default.json to automatically detect new files
Automatically detect new files matching glob patterns without restart!
Enable auto-reload in default.json or default.yml to periodically check for new files:
{
"LOKI_IP": "http://localhost:3100",
"AUTO_RELOAD": 300,
"JOURNALCTL": "off"
}Configuration:
AUTO_RELOAD- Interval in seconds (e.g., 300 = check every 5 minutes)- Default: 0 (disabled)
- Only works in
default.jsonordefault.yml
Benefits:
- ✅ New files matching glob patterns are automatically monitored
- ✅ No service restart needed when containers/apps are deployed
- ✅ Perfect for dynamic environments (Docker, Kubernetes, auto-scaling)
- ✅ Each new file gets its own watcher with configured rate limits/buffers
Example Use Cases:
- Docker:
/var/lib/docker/containers/*/*.log- auto-detect new containers - Rotated logs:
/var/log/nginx/access*.log- catch rotated files - Multi-instance apps:
/var/log/apps/**/*.log- monitor new app instances
Single backend:
{
"LOKI_IP": "http://loki:3100",
"nginx": {
"ACCESS": {
"path_file": "/var/log/nginx/access.log"
}
}
}Multiple backends (High Availability):
{
"LOKI_IP": [
"http://loki-1:3100",
"http://loki-2:3100",
"http://loki-3:3100"
],
"nginx": {
"ACCESS": {
"path_file": "/var/log/nginx/access.log",
"delimiter": "\n",
"labels": {
"environment": "production",
"datacenter": "eu-west-1",
"team": "ops"
},
"rate_limit": 1000,
"buffer_size": 100
},
"ERROR": {
"path_file": "/var/log/nginx/error.log",
"labels": {
"severity": "high"
}
}
},
"apache2": {
"ACCESS": {
"path_file": "/var/log/apache2/access.log"
}
}
}Single backend:
ELASTIC_IP: "http://elasticsearch:9200"
syslog:
SYSTEM:
path_file: "/var/log/syslog"Multiple backends (High Availability):
# Send logs to multiple ElasticSearch nodes
ELASTIC_IP:
- "http://es-node-1:9200"
- "http://es-node-2:9200"
- "http://es-node-3:9200"
syslog:
SYSTEM:
path_file: "/var/log/syslog"
delimiter: "\n"
labels:
environment: "staging"
component: "system"
rate_limit: 500
buffer_size: 50
auth:
LOGIN:
path_file: "/var/log/auth.log"
labels:
team: "security"
priority: "high"| Field | Type | Description | Example |
|---|---|---|---|
*_IP |
Optional | Backend endpoint - Type extracted from name | LOKI_IP, ELASTIC_IP, KAFKA_IP |
AUTO_RELOAD |
Optional | Auto-reload interval in seconds (default.json only) | 300 (5 minutes) |
QUEUE_SIZE |
Optional | Queue size limit (default.json only, works with disk_buffer) |
10000 (default: 5000 hard limit) |
JOURNALCTL |
Optional | Enable journald monitoring (default.json only) | "on" or "off" |
service_name |
Mandatory | Service identifier | nginx, apache2, myapp |
category |
Mandatory | Log category | ACCESS, ERROR, SYSTEM |
path_file |
Mandatory | Absolute path to log file (supports wildcards: *, ?, [], **) |
/var/log/nginx/*.log, /app/**/logs/*.log |
delimiter |
Optional | Line delimiter (default: \n) |
\n, \r\n, || |
labels |
Optional | Custom labels - any key/value pairs you want | {"environment": "prod"}, {"team": "ops"}, {"priority": "high"} |
rate_limit |
Optional | Max logs per second (int/float) - enforced per file | 1000, 500.5 |
buffer_size |
Optional | Batch size (int) - logs grouped before sending | 100, 50 |
disk_buffer |
Optional | Buffering strategy: DROP or DISK (default: DROP) |
"DISK" for at-least-once delivery |
Important Notes:
labelscan contain any custom key/value pairs - they become searchable labels in your backend (e.g., Loki)rate_limituses token bucket algorithm - excess logs are dropped (DROP) or saved to disk (DISK)buffer_sizegroups logs for efficient batch sending - buffers are flushed when full or periodicallydisk_bufferwithDISKenables Write-Ahead Log for at-least-once delivery guarantee- Both
rate_limitandbuffer_sizeare applied per matched file when using glob patterns AUTO_RELOAD,QUEUE_SIZE, andJOURNALCTLare only read fromdefault.jsonordefault.yml- Without
QUEUE_SIZE: built-in hard limit of 5000 logs, then queue is cleared (logs dropped) - With
QUEUE_SIZE: custom limit, logs can be saved to disk ifdisk_buffer: "DISK"is configured
SLE monitors the internal queue to prevent memory overflow.
QUEUE_SIZE MUST be configured in default.json or default.yml only. It will be ignored in other configuration files.
When QUEUE_SIZE is not set in default.json, SLE uses a built-in hard limit of 5000 logs:
{
"LOKI_IP": "http://localhost:3100",
"JOURNALCTL": "off"
}Automatic queue monitoring:
- ✅ WARNING at 20%: 1000 logs
- ✅ WARNING at 40%: 2000 logs
- ✅ WARNING at 60%: 3000 logs
- ✅ WARNING at 80%: 4000 logs
- 🔴 CRITICAL at 100%: 5000 logs → QUEUE CLEARED
⚠️ All 5000 logs in queue are IMMEDIATELY DROPPED
This protects SLE from memory exhaustion (OOM kill) but logs are lost.
To enable disk buffering when queue is full, configure QUEUE_SIZE in default.json:
{
"LOKI_IP": "http://localhost:3100",
"QUEUE_SIZE": 10000,
"JOURNALCTL": "off"
}Then configure disk_buffer: "DISK" for critical log sources:
{
"LOKI_IP": "http://localhost:3100",
"QUEUE_SIZE": 10000,
"JOURNALCTL": "off",
"nginx": {
"ACCESS": {
"path_file": "/var/log/nginx/access.log",
"disk_buffer": "DROP"
},
"ERROR": {
"path_file": "/var/log/nginx/error.log",
"disk_buffer": "DISK"
}
}
}Queue management with QUEUE_SIZE:
- ✅ WARNING at 20%: 2000 logs
- ✅ WARNING at 40%: 4000 logs
- ✅ WARNING at 60%: 6000 logs
- ✅ WARNING at 80%: 8000 logs
- 🔴 CRITICAL at 100%: 10000 logs
- 💾 Logs with
disk_buffer: "DISK"→ saved to disk for replay ⚠️ Logs withdisk_buffer: "DROP"→ dropped
Key Points:
QUEUE_SIZEMUST be indefault.jsonordefault.yml- ignored elsewhere- Without
QUEUE_SIZE: hard limit of 5000 logs, then clear queue (all dropped) - With
QUEUE_SIZE: custom limit, logs saved to disk ifdisk_buffer: "DISK" - Warnings at 20% intervals are automatic and non-configurable
- Protects SLE from memory exhaustion (OOM)
- See
examples/default-with-queue.jsonfor complete example
Use Cases:
- No QUEUE_SIZE: Simple deployments, logs are not critical
- QUEUE_SIZE + DISK: Production, compliance logs, temporary backend outages
Problem: By default, SLE drops logs when:
- Backend is down or unreachable
- Rate limit is exceeded
- Network issues occur
Solution: Enable disk buffering for critical logs that must not be lost.
{
"LOKI_IP": "http://localhost:3100",
"nginx": {
"ACCESS": {
"path_file": "/var/log/nginx/access.log",
"disk_buffer": "DROP"
},
"ERROR": {
"path_file": "/var/log/nginx/error.log",
"disk_buffer": "DISK"
}
}
}How it works:
- When sending fails or rate limit is hit, logs are written to disk (
/var/lib/sle/buffer/) - Logs are persisted with
fsync()for durability - On startup, buffered logs are replayed automatically
- Only deleted after successful delivery
Options:
disk_buffer: "DROP"(default) - Drop logs on failure/rate limitdisk_buffer: "DISK"- Save to disk, replay later (at-least-once)
Benefits:
- ✅ No log loss during backend downtime
- ✅ At-least-once delivery guarantee
- ✅ Survives SLE restarts
- ✅ Suitable for compliance/regulatory logs
- ✅ Per-file granularity (mix DROP and DISK)
Storage Location:
- Buffers stored in
/var/lib/sle/buffer/<service>/<category>/ - Each log is a separate file with sequence number
- Automatically cleaned up after successful delivery
| Field | Backend | Status | Additional Deps |
|---|---|---|---|
LOKI_IP |
Grafana Loki | ✅ Supported | None |
ELASTIC_IP, ELASTICSEARCH_IP |
ElasticSearch | None | |
OPENSEARCH_IP |
OpenSearch | None | |
GRAYLOG_IP |
GrayLog | None | |
VICTORIALOGS_IP |
VictoriaLogs | None | |
CLICKHOUSE_IP |
ClickHouse | None | |
FLUENTBIT_IP |
FluentBit | None | |
KAFKA_IP |
Kafka | kafka-python |
|
CLOUDWATCH_IP |
AWS CloudWatch | boto3 |
|
GCP_IP |
GCP Cloud Logging | google-cloud-logging |
|
AZURE_IP |
Azure Monitor | None |
Note: Only Grafana Loki is thoroughly tested. Other backends are implemented but untested.
sudo systemctl start sle # Start
sudo systemctl stop sle # Stop
sudo systemctl restart sle # Restart
sudo systemctl status sle # Status
sudo journalctl -u sle -f # Logssudo python3 /opt/sle/sle.py # Normal mode
sudo python3 /opt/sle/sle.py --debug # Debug mode with verbose loggingSLE automatically enhances logs sent to backends:
Timestamp Detection
- Detects existing timestamps in logs (ISO 8601, syslog format, etc.)
- Only adds timestamp if not present in log line
- Prevents timestamp duplication
Log Level Extraction
- Auto-detects log levels: DEBUG, INFO, WARN, ERROR, CRITICAL, etc.
- Removes level from log text to avoid duplication
- Adds as
levellabel in Loki for easy filtering
Example transformation:
Before: "2025-10-17T02:26:16+0200 INFO Complete!"
After:
- Text: "Complete!"
- Label: level="INFO"
- Timestamp: 2025-10-17 02:26:16 (extracted from log)
{job="sle"} # All SLE logs
{job="sle", name="nginx"} # Specific service (file-based)
{job="sle", name="journald"} # All journald logs
{job="sle", name="journald", subname="NGINX"} # Specific systemd unit
{job="sle", name="nginx", subname="ACCESS"} # Specific category
{job="sle", level="ERROR"} # Only errors
{job="sle", level=~"ERROR|CRITICAL"} # Errors + critical
{job="sle"} |= "error" # Filter by content
Labels automatically added by SLE:
job="sle"- Identifies SLE logsname="<service>"- Service name (or "journald")subname="<category>"- Log category (or systemd unit name in uppercase)filepath="<path>"- Source file path (or "journald:")level="<LEVEL>"- Log level if detected (INFO, ERROR, etc.)- Custom labels from config
labelsfield (if specified)
# Check service logs
sudo journalctl -u sle -n 100 --no-pager
# Test backend connectivity
curl http://loki:3100/ready
# Check file permissions
sudo ls -la /var/log/nginx/access.log
# Check disk buffer usage
sudo du -sh /var/lib/sle/buffer/
# Monitor queue warnings
sudo journalctl -u sle -f | grep -i "queue"SLE requires appropriate permissions:
- Read access: to log files (e.g.,
/var/log/nginx/*.log) - Write access: to disk buffer directory (
/var/lib/sle/buffer/)
Recommended setup:
# Create dedicated user
sudo useradd -r -s /bin/false sle
# Set ownership
sudo chown -R sle:sle /var/lib/sle /opt/sle
# Restrict buffer directory
sudo chmod 700 /var/lib/sle/buffer- No TLS: SLE doesn't encrypt data in transit
- Solution: Use TLS-enabled backends or reverse proxy
- Backend auth: SLE relies on backend authentication
- Path traversal protection:
..and/stripped from service/category names - Type validation: All config values validated
- No shell execution: Glob patterns use Python stdlib only
- No size limit: Disk buffer can grow indefinitely
- Monitoring: Check
/var/lib/sle/buffer/regularly - Cleanup: Old files (>24h) automatically removed
Config (JSON/YAML) → ConfigLoader → ExporterFactory
↓
LogFileWatcher (tail -f) → Queue → Exporter → Backend
sudo systemctl stop sle
sudo cp sle.py config_loader.py file_watcher.py journald_watcher.py disk_buffer.py /opt/sle/
sudo cp -r exporters /opt/sle/
sudo systemctl start slesudo systemctl stop sle
sudo systemctl disable sle
sudo rm /etc/systemd/system/sle.service
sudo rm -rf /opt/sle /etc/sle.d /var/lib/sle
sudo systemctl daemon-reloadVersion: 1.0.2