Skip to content

fix(logrotate): sharedscripts, install hex_trim_syslog, and drop redundant delaycompress - #163

Merged
github-actions[bot] merged 4 commits into
developfrom
jim.lin/fix/logrotate-hourly-and-trim-syslog
Sep 8, 2026
Merged

github-actions[bot] merged 4 commits into
developfrom
jim.lin/fix/logrotate-hourly-and-trim-syslog

Conversation

@Eandalf-Bigstack

@Eandalf-Bigstack Eandalf-Bigstack commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

  • bug

What this PR does / why we need it

Three defects in log rotation, all in the generator rather than in any one generated file — which is why they reproduced identically across httpd, syslog and prometheus.

  • sharedscripts was never emitted. WriteLogRotateConf wrote prerotate/postrotate blocks without it, so logrotate ran them once per matched file — its documented behaviour. Every script CubeCOS generates is a "signal the service once" operation, so a glob matching N files fired N signals within the same second. For /var/log/httpd/*.log that is up to eleven overlapping systemctl reload httpd.service calls racing in the master's worker lifecycle, which can crash httpd outright and 503 Horizon and Keystone on whichever control node holds the VIP
  • hex_trim_syslog was referenced but never installed. config_syslogd writes /usr/sbin/hex_trim_syslog into the syslog logrotate postrotate, but no install rule ever shipped it. The script has been in data/hex_syslogd since the initial commit, and its sibling hex_log_event.sh is installed a few lines above — this one was simply missed. Every nightly rotation logged No such file or directory, and the /var/log/messages size budget it enforces never applied
  • delaycompress was emitted unconditionally and is redundant here. It exists for the case where the writing process keeps its old file descriptor and must be signalled to reopen, so the newest rotated generation has to stay uncompressed. All 30 LogRotateConf declarations across hex and cubecos set copytruncate, so the rotated file is already a finished copy that nothing is still writing to. Delaying only kept the single largest generation uncompressed

Which issue(s) this PR fixes

Special notes for your reviewer

Paired with bigstack-oss/cubecos PR for the same work — that PR carries the submodule bump plus the hourly logrotate.timer drop-in, and the two need to land together. The hourly timer is what makes the per-config maxsize 128M cap actually enforceable (maxsize is only evaluated when logrotate runs, so a daily timer let a logstash.log.1 reach 1.4G under a 128M cap). It is also why the sharedscripts fix could not wait: hourly rotation would have turned eleven httpd reloads a day into two hundred and sixty four.

Why sharedscripts is conditional. It is emitted only when a config actually carries prerotate or postrotate commands. On a config with no scripts the directive is meaningless, and three configs is the whole affected set: httpd (systemctl reload httpd.service), syslog (hex_trim_syslog + rsyslog HUP) and prometheus (killall -HUP prometheus).

Why delaycompress is removed globally rather than per config. extraArgs is written before the common block, so a nodelaycompress override from cubecos would be cancelled again by the very directive it is trying to override. And since every declaration in both repos sets copytruncate, there is no config that wants the old behaviour.

One caveat on hex_trim_syslog. It has never run in production, so its behaviour on a real oversized /var/log/messages set is unexercised. It is inert below its limit — traced under sh -x with the limit it is actually passed (4592725 KB) against a 4 KB total, the loop does not execute — but the trimming path itself only starts being reachable with this PR.

Additional documentation

Verified on accept-3cc (3-node) and jim-1cc.

sharedscripts -- forced rotation of the httpd config, counting postrotate runs:

  [cc1 ~]# logrotate -d -f /etc/logrotate.d/httpd | grep -c 'running postrotate script'
  11                      # accept-3cc, 13 files matched, 11 non-empty
  9                       # jim-1cc, same config
  1                       # both, after sharedscripts

Measured against the live service rather than only the dry run:

  [cc1 ~]# logrotate -f /etc/logrotate.d/httpd ; journalctl -u httpd --since ... | grep -c Reloading
  4                       # before
  1                       # after
  [cc1 ~]# systemctl is-active httpd ; curl -sk -o /dev/null -w '%{http_code}' https://localhost/horizon/
  active
  302

The same defect in the other two script-bearing configs, before the fix:

  syslog: postrotate=3  sharedscripts=0
  prometheus: postrotate=0 sharedscripts=0

syslog's count of 3 is the direct cause of a symptom seen independently: exactly
three "hex_trim_syslog: No such file or directory" errors in the journal every
night on every node -- three of syslog's five files rotate, and the script ran
once per file.

hex_trim_syslog -- before, every nightly rotation logged the missing file; after
installing, a forced rotation of the syslog config exits 0 with no error. Traced
under sh -x with the real limit against a 4 KB total: the loop does not execute
and no retention is removed.

delaycompress -- isolated single-file comparison, rotate 14, copytruncate:

  without delaycompress:  a.log.1.gz            then  a.log.1.gz  a.log.2.gz
  with    delaycompress:  a.log.1 (uncompressed) then a.log.1  a.log.2.gz

No errors either way; the newest generation is simply compressed instead of held.
Applied across a live cluster this reclaimed 3.0G on cc1, 1.1G on cc2 and 0.8G on
cc3 in a single pass, and cleared ceph's "mon cc2 is low on available space".

Eandalf-Bigstack and others added 3 commits September 6, 2026 15:00
config_syslogd writes /usr/sbin/hex_trim_syslog into the syslog logrotate
postrotate, but nothing ever installed it. The script has been in
data/hex_syslogd since the initial commit; its sibling hex_log_event.sh is
installed a few lines above and this one was simply missed.

Effect on a running node: every nightly rotation logged three
'No such file or directory' errors, and the /var/log/messages size budget the
script enforces was never applied. Because it is postrotate guarded by
'|| true' the rotation itself still happened, which is why this stayed
invisible.

Verified on jim-1cc: before, 'logrotate -f /etc/logrotate.d/syslog' logged the
missing-file error; after installing, it exits 0 with no error. Traced the
script under sh -x with the real limit it is passed (4592725 KB) against a 4 KB
total -- the loop does not execute and no retention is removed, so it is inert
until /var/log/messages* actually exceeds the budget.

Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
WriteLogRotateConf emitted delaycompress for every generated config. That
directive exists for the case where the writing process keeps its old file
descriptor and must be signalled to reopen, so the newest rotated generation
has to stay uncompressed until the next cycle. Every LogRotateConf in hex and
cubecos -- all 30 of them -- sets copytruncate, so the rotated file is already
a finished copy that nothing is still writing to. Delaying bought nothing and
kept the single largest generation uncompressed.

Measured on accept-3cc: logstash.log.1 was 1.4G uncompressed against
logstash.log.2.gz at 25M, a ~56x difference on the generation that dominates
the footprint. Removing it and re-running logrotate reclaimed 3.0G on cc1,
1.1G on cc2 and 0.8G on cc3 in a single pass, and cleared ceph's
'mon cc2 is low on available space'.

Done here rather than per-config in cubecos because extraArgs is written
before the common block, so a nodelaycompress override would be overridden
again by the directive it is trying to cancel.

Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
WriteLogRotateConf emitted prerotate/postrotate without sharedscripts, so
logrotate ran them once per matched file -- its documented behaviour. Every
script we generate is a 'signal the service once' operation, so a glob matching
N files fired N signals in the same second.

For httpd that is up to eleven overlapping 'systemctl reload httpd.service'
calls racing each other in the master's worker lifecycle, which can take httpd
down outright and 503 Horizon and Keystone on whichever control node holds the
VIP (cubecos#1192). The same shape applied to syslog, where three of five files
rotate and hex_trim_syslog therefore ran three times per night, and to
prometheus's killall -HUP.

It also had to be fixed before rotation moves to hourly: eleven reloads a day
becomes two hundred and sixty four.

Verified on accept-3cc and jim-1cc: forced rotation of the httpd config ran the
postrotate script 11 and 9 times respectively before, exactly 1 after. Measured
on the live service, a forced rotation produced 4 real httpd reloads before and
1 after, with httpd staying active and Horizon answering 302.

Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

@SekiXu SekiXu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran a scoped experiment on a 1cc (logrotate 3.18.0) to check one interaction, and it came out in your favour — recording it here so it's not re-litigated later.

postrotate runs before compression, so what hex_trim_syslog observes is byte-for-byte identical with and without delaycompressmessages + plain messages.1 + .2.gz.4.gz in both. Dropping delaycompress does not shrink the set the script can match.

One cosmetic side effect when the trim actually fires: it removes messages.1 during postrotate, and logrotate then tries to compress the file that just went away —

trim: removing messages.1
error: unable to open /var/log/messages.1 for compression: No such file or directory

Deterministic, 6/6 forced cycles, and absent with delaycompress. No data loss and the trim still does its job, so not a blocker — but it does mean the "No such file or directory" line the install rule removes comes back in a different form on over-budget nights. Cheap to avoid if you want the journal fully clean.

… trim

hex_trim_syslog runs from the syslog postrotate, and logrotate runs postrotate
*before* compression. Dropping delaycompress in 56fdc47 made messages.1 the
generation logrotate compresses in the same cycle, so a trim that reaches .1
removes the file logrotate is about to open:

    trim: removing messages.1
    error: unable to open /var/log/messages.1 for compression: No such file or
           directory

That is the same class of error fea23e1 installed the script to stop -- it
would have come back in a different form, on exactly the over-budget nights the
trim exists for. Stopping the loop at .2 avoids it, and costs the budget
nothing: the next rotation renames .1 to .2, so the same bytes become eligible
one cycle later.

Reported by SekiXu in review on #163, who measured the interaction and
confirmed the set the script can match is byte-for-byte identical with and
without delaycompress -- so this is the only thing that needed changing.

Verified on jim-1cc (logrotate 3.18.0), in an isolated tree mirroring the real
syslog config -- compress, no delaycompress, copytruncate, the trim in
postrotate -- forced with a 1KB limit so the loop reaches the newest
generation. With the old bound the "removing messages.1" line is followed by
the compression error; with the new one gzip runs normally and no error is
logged. The installed script stays inert at the real limit: total 179712 KB
against LIMIT 4592725.

Refs #163

Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Eandalf-Bigstack

Copy link
Copy Markdown
Collaborator Author

Thank you for catching that. Fixed in cb13101.

@Eandalf-Bigstack Eandalf-Bigstack added the done Merge the pull request label Sep 8, 2026
@github-actions
github-actions Bot merged commit cb13101 into develop Sep 8, 2026
5 checks passed
@github-actions
github-actions Bot deleted the jim.lin/fix/logrotate-hourly-and-trim-syslog branch September 8, 2026 15:23
Eandalf-Bigstack added a commit that referenced this pull request Sep 12, 2026
…ove something

hex_trim_syslog has never been able to remove a compressed generation, and since
cb13101 it has not been able to remove anything at all.

Two mismatches with the directory it runs against, one of them mine:

- It tested `-e /var/log/messages.$I` only. WriteLogRotateConf always emits
  `compress` and the global conf sets no `dateext`, so at postrotate time the
  directory holds `messages`, a still-uncompressed `messages.1`, and
  `messages.2.gz` onward. The only unsuffixed generation that ever exists is .1 --
  which cb13101 correctly stopped it from touching, leaving a loop with nothing it
  could match.
- `I=14` mirrored the old global `rotate 14`. b5ba5d8 raises that to
  `retention * 24`, rendering as 336, so even with .gz matching the scan would have
  covered generations 2..14 of up to 336 -- and a log that reaches 336 generations
  is exactly the busy log the budget exists for.

Take the generation list from the filesystem instead of counting down from a
literal. That removes the coupling that has now drifted twice, and it is immune to
whatever `rotate` becomes next. messages.1 stays excluded for cb13101's reason:
postrotate runs before compression, so .1 is the file logrotate is about to open.

Reported by SekiXu in review on #165, who also traced it back to his own #163
measurement -- "the matchable set is byte-for-byte identical with and without
delaycompress" was already this fact, one step short of the conclusion.

Verified on jim-1cc, both revisions run out of git with only /var/log rewritten to
a scratch dir, seeded to the real postrotate shape observed on that node
(messages, plain messages.1, messages.2.gz..messages.14.gz) and LIMIT set to a
sixth of the total:

  shipped   total 3236 -> 3236   removed nothing
  fixed     total 3236 ->  688   messages.1 survived

Inert at the real budget: with the messages set at 168816K against a 5% budget of
4592725K on accept-3cc, a run changes nothing. And seeded to 336 generations with
a large one at .300, it trims oldest-first down to .299 -- generations the old
bound could never reach.

Refs bigstack-oss/cubecos#1433

Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

done Merge the pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants