CRITICAL FIX: Problem: Previous version used multiple stat operations and loops which created too many file descriptors and fsnotify watchers, causing 'too many open files' errors. Solution: Use only: - slurp: Direct file content reading (no watchers) - find: Single operation to list directory files (no loops) New logic is clean and simple: 1. Read Git rsyslog.conf + server rsyslog.conf (slurp) 2. Compare content directly (byte comparison) 3. List Git rsyslog.d files + server rsyslog.d files (find) 4. Compare file names (no permission checks, no loops) 5. Output DRIFTED_FILES and SYNC_STATUS markers This eliminates file descriptor exhaustion while maintaining correct drift detection. After deploy, when content matches, playbook exits 0 (SYNCED).
rsyslog GitOps
Manage rsyslog configuration on Linux servers using Git as the single source of truth.
If it's not in Git, it doesn't belong on the server.
How it works in one sentence
Every change goes through Git. The pipeline makes sure the server always matches what's in Git — and if someone changes the server directly, the system detects it automatically.
The three pipelines
1. Pull Request — "Is this config safe?"
Triggered when you open or update a pull request.
Does not touch the live server beyond a basic reachability check.
Does not compare the PR content to the server (they're expected to differ before merge).
Open PR
│
├─► syntax-check Check the YAML/Ansible syntax is valid
│
└─► validate Connect to the server and verify rsyslog is running
and the current config is loadable
Pass = safe to review and merge.
Fail = syntax error or server is unreachable / config is broken.
2. Push to master — "Deploy and verify"
Triggered when a PR is merged into master.
Merge to master
│
├─► syntax-check Same lint check as PR
│
├─► validate Same server check as PR
│
├─► deploy Copy the new config files from Git to the server
│ and restart rsyslog
│
└─► update-gitops-status Run a diff between Git and the live server
│
├─ Matches? → send JSON (SYNCED, drift_count: 0)
└─ Differs? → send JSON (OUT_OF_SYNC, drift_count: N, files: [...])
│
└─ Update gitops-status-server for Grafana visualization
Pass = new config is live and the server matches Git.
The sync status JSON is always sent to gitops-status-server regardless of outcome.
3. Cron — "Is the server still synced?"
Runs automatically every 2 minutes, even with no new push.
This is the ArgoCD-style continuous check.
It only reads — never deploys anything.
Every 2 minutes (cron)
│
└─► gitops_sync_check SSH to the server, compare every managed config
file against the latest Git commit
│
├─ Matches? → send JSON (SYNCED, drift_count: 0, files: [])
└─ Differs? → send JSON (OUT_OF_SYNC, drift_count: N, files: [...])
│
└─ Update gitops-status-server for Grafana visualization
Why this matters: if someone edits /etc/rsyslog.conf directly on the server
(bypassing Git), the next cron run catches it within 2 minutes and marks OUT_OF_SYNC
with detailed information about which specific files have drifted.
Full flow diagramgitops-status-server
│ │ │ │
│── open PR ───────────────►│ │ │
│ │── syntax-check │ │
│ │── validate ─────────────►│ │
│◄── PR ok / failed ────────│ │ │
│ │ │ │
│── merge to master ───────►│ │ │
│ │── syntax-check │ │
│ │── validate ─────────────►│ │
│ │── deploy ───────────────►│ write config │
│ │ │ restart rsyslog │
│ │── drift-check ──────────►│ compare files │
│ │ │◄────────────────────│
│ │── JSON status ───────────────────────────────►│
│ │ │ │
│ │ [every 2 min, no push] │ │
│ │── drift-check ──────────►│ compare files │
│ │── JSON status ───────────────────────────────►│
│ │ │ │
Someone edits the server directly (bad):
rogue admin Woodpecker CI Linux Server gitops-status-server
│ │ │ │
│── ssh rsyslog-lab │ │ │
│── vim /etc/rsyslog.conf ──────────────────────────► │ file changed │
│ │ │ │
│ [2 min later, cron runs] │ │
│ │── drift-check ──────────►│ diff detected │
│ │── JSON status (OUT_OF_SYNC)─────────────────►│
│ │ drift_count: 1 │ Grafana shows
│ │ files: [rsyslog.conf] OUT_OF_SYNC
│ │── drift-check ──────────►│ diff detected │
│ │── metric 0 (OUT_OF_SYNC)────────────────────►│
│ │ │ alert fires
---
GitOps status JSON format
Instead of simple numeric metrics, this repo now sends rich JSON status data to gitops-status-server:
```json
{
"repo": "rsyslog",
"server": "rsyslog-lab",
"sync_status": "SYNCED",
"drift_count": 0,
"files": [],
"last_check": "2026-04-21T10:30:00Z"
}
When drift is detected:
{
"repo": "rsyslog",
"server": "rsyslog-lab",
"sync_status": "OUT_OF_SYNC",
"drift_count": 2,
"files": [
{ "name": "rsyslog.conf" },
{ "name": "rsyslog.d/30-lab.conf" }
],
"last_check": "2026-04-21T10:32:15Z"
}
update-gitops-status.sh Script to generate and send JSON status to gitops-status-server
This JSON is sent to gitops-status-server at:
http://gitops-status-server.observability-stack.svc.cluster.local:80/api/status
The gitops-status-server app serves this data via /status.json for Grafana Infinity datasource,
providing rich visualization with file-level drift details instead of just a numeric metric
Alert on gitops_sync_status == 0 in Grafana/Alertmanager.
What drift-check actually compares
The drift-check playbook compares files from the Woodpecker CI container (always the latest Git commit) against the live server. It checks:
/etc/rsyslog.conf— must matchfiles/rsyslog.confin Git/etc/rsyslog.d/30-lab.conf— must matchfiles/rsyslog.d/30-lab.confin Git- Any file managed by Git must not be missing from the server
Files on the server that are not in Git (e.g. 50-default.conf, 20-ufw.conf) are ignored — they are owned by the OS and are not our concern.
Repository structure
.woodpecker.yml CI pipeline definition
ansible/
inventory/
hosts.yml Server list
group_vars/all.yml Variables (paths, user, etc.)
playbooks/
validate.yml Check rsyslog is running and config loads
apply.yml Deploy config files from Git to server
drift-check.yml Compare Git files to live server (read-only)
files/
rsyslog.conf Main rsyslog config (source of truth)
rsyslog.d/
30-lab.conf Drop-in config for lab logging
Woodpecker cron setup
Go to Repository Settings → Crons → Add cron:
| Field | Value |
|---|---|
| Name | gitops_sync_check |
| Branch | master |
| Schedule | */2 * * * * |
Required secrets
Go to Repository Settings → Secrets:
| Name | Description |
|---|---|
SSH_PRIVATE_KEY |
Private key to SSH into the server |
Optional environment variables
These can be overridden in the Woodpecker pipeline or .woodpecker.yml:
| Variable | Default | Description |
|---|---|---|
GITOPS_STATUS_SERVER_URL |
http://gitops-status-server.observability-stack.svc.cluster.local:80 |
URL of gitops-status-server API |
REPO_NAME |
rsyslog |
Repository name for JSON status |
SERVER_NAME |
rsyslog-lab |
Server name for JSON status |