Skip to content

Commit 294dfff

Browse files
logging: add DirMode options and propagate FileMode to rotations (#7335)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
1 parent 76b198f commit 294dfff

File tree

4 files changed

+450
-25
lines changed

4 files changed

+450
-25
lines changed

caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
11
:80
22

3-
log {
3+
log one {
44
output file /var/log/access.log {
5+
mode 0644
6+
dir_mode 0755
57
roll_size 1gb
68
roll_uncompressed
79
roll_local_time
810
roll_keep 5
911
roll_keep_for 90d
1012
}
1113
}
14+
log two {
15+
output file /var/log/access-2.log {
16+
mode 0777
17+
dir_mode from_file
18+
roll_size 1gib
19+
roll_interval 12h
20+
roll_at 00:00 06:00 12:00,18:00
21+
roll_minutes 10 40 45,46
22+
roll_keep 10
23+
roll_keep_for 90d
24+
}
25+
}
1226
----------
1327
{
1428
"logging": {
1529
"logs": {
1630
"default": {
1731
"exclude": [
18-
"http.log.access.log0"
32+
"http.log.access.one",
33+
"http.log.access.two"
1934
]
2035
},
21-
"log0": {
36+
"one": {
2237
"writer": {
38+
"dir_mode": "0755",
2339
"filename": "/var/log/access.log",
40+
"mode": "0644",
2441
"output": "file",
2542
"roll_gzip": false,
2643
"roll_keep": 5,
@@ -29,7 +46,34 @@ log {
2946
"roll_size_mb": 954
3047
},
3148
"include": [
32-
"http.log.access.log0"
49+
"http.log.access.one"
50+
]
51+
},
52+
"two": {
53+
"writer": {
54+
"dir_mode": "from_file",
55+
"filename": "/var/log/access-2.log",
56+
"mode": "0777",
57+
"output": "file",
58+
"roll_at": [
59+
"00:00",
60+
"06:00",
61+
"12:00",
62+
"18:00"
63+
],
64+
"roll_interval": 43200000000000,
65+
"roll_keep": 10,
66+
"roll_keep_days": 90,
67+
"roll_minutes": [
68+
10,
69+
40,
70+
45,
71+
46
72+
],
73+
"roll_size_mb": 1024
74+
},
75+
"include": [
76+
"http.log.access.two"
3377
]
3478
}
3579
}
@@ -42,7 +86,7 @@ log {
4286
":80"
4387
],
4488
"logs": {
45-
"default_logger_name": "log0"
89+
"default_logger_name": "two"
4690
}
4791
}
4892
}

modules/logging/filewriter.go

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ type FileWriter struct {
9090
// 0600 by default.
9191
Mode fileMode `json:"mode,omitempty"`
9292

93+
// DirMode controls permissions for any directories created to reach Filename.
94+
// Default: 0700 (current behavior).
95+
//
96+
// Special values:
97+
// - "inherit" → copy the nearest existing parent directory's perms (with r→x normalization)
98+
// - "from_file" → derive from the file Mode (with r→x), e.g. 0644 → 0755, 0600 → 0700
99+
// Numeric octal strings (e.g. "0755") are also accepted. Subject to process umask.
100+
DirMode string `json:"dir_mode,omitempty"`
101+
93102
// Roll toggles log rolling or rotation, which is
94103
// enabled by default.
95104
Roll *bool `json:"roll,omitempty"`
@@ -177,11 +186,33 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
177186
// roll log files as a sensible default to avoid disk space exhaustion
178187
roll := fw.Roll == nil || *fw.Roll
179188

180-
// create the file if it does not exist; create with the configured mode, or default
181-
// to restrictive if not set. (timberjack will reuse the file mode across log rotation)
182-
if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil {
183-
return nil, err
189+
// Ensure directory exists before opening the file.
190+
dirPath := filepath.Dir(fw.Filename)
191+
switch strings.ToLower(strings.TrimSpace(fw.DirMode)) {
192+
case "", "0":
193+
// Preserve current behavior: locked-down directories by default.
194+
if err := os.MkdirAll(dirPath, 0o700); err != nil {
195+
return nil, err
196+
}
197+
case "inherit":
198+
if err := mkdirAllInherit(dirPath); err != nil {
199+
return nil, err
200+
}
201+
case "from_file":
202+
if err := mkdirAllFromFile(dirPath, os.FileMode(fw.Mode)); err != nil {
203+
return nil, err
204+
}
205+
default:
206+
dm, err := parseFileMode(fw.DirMode)
207+
if err != nil {
208+
return nil, fmt.Errorf("dir_mode: %w", err)
209+
}
210+
if err := os.MkdirAll(dirPath, dm); err != nil {
211+
return nil, err
212+
}
184213
}
214+
215+
// create/open the file
185216
file, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, modeIfCreating)
186217
if err != nil {
187218
return nil, err
@@ -234,13 +265,70 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
234265
RotateAtMinutes: fw.RollAtMinutes,
235266
RotateAt: fw.RollAt,
236267
BackupTimeFormat: fw.BackupTimeFormat,
268+
FileMode: os.FileMode(fw.Mode),
237269
}, nil
238270
}
239271

272+
// normalizeDirPerm ensures that read bits also have execute bits set.
273+
func normalizeDirPerm(p os.FileMode) os.FileMode {
274+
if p&0o400 != 0 {
275+
p |= 0o100
276+
}
277+
if p&0o040 != 0 {
278+
p |= 0o010
279+
}
280+
if p&0o004 != 0 {
281+
p |= 0o001
282+
}
283+
return p
284+
}
285+
286+
// mkdirAllInherit creates missing dirs using the nearest existing parent's
287+
// permissions, normalized with r→x.
288+
func mkdirAllInherit(dir string) error {
289+
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
290+
return nil
291+
}
292+
cur := dir
293+
var parent string
294+
for {
295+
next := filepath.Dir(cur)
296+
if next == cur {
297+
parent = next
298+
break
299+
}
300+
if fi, err := os.Stat(next); err == nil {
301+
if !fi.IsDir() {
302+
return fmt.Errorf("path component %s exists and is not a directory", next)
303+
}
304+
parent = next
305+
break
306+
}
307+
cur = next
308+
}
309+
perm := os.FileMode(0o700)
310+
if fi, err := os.Stat(parent); err == nil && fi.IsDir() {
311+
perm = fi.Mode().Perm()
312+
}
313+
perm = normalizeDirPerm(perm)
314+
return os.MkdirAll(dir, perm)
315+
}
316+
317+
// mkdirAllFromFile creates missing dirs using the file's mode (with r→x) so
318+
// 0644 → 0755, 0600 → 0700, etc.
319+
func mkdirAllFromFile(dir string, fileMode os.FileMode) error {
320+
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
321+
return nil
322+
}
323+
perm := normalizeDirPerm(fileMode.Perm()) | 0o200 // ensure owner write on dir so files can be created
324+
return os.MkdirAll(dir, perm)
325+
}
326+
240327
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
241328
//
242329
// file <filename> {
243330
// mode <mode>
331+
// dir_mode <mode|inherit|from_file>
244332
// roll_disabled
245333
// roll_size <size>
246334
// roll_uncompressed
@@ -284,6 +372,22 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
284372
}
285373
fw.Mode = fileMode(mode)
286374

375+
case "dir_mode":
376+
var val string
377+
if !d.AllArgs(&val) {
378+
return d.ArgErr()
379+
}
380+
val = strings.TrimSpace(val)
381+
switch strings.ToLower(val) {
382+
case "inherit", "from_file":
383+
fw.DirMode = val
384+
default:
385+
if _, err := parseFileMode(val); err != nil {
386+
return d.Errf("parsing dir_mode: %v", err)
387+
}
388+
fw.DirMode = val
389+
}
390+
287391
case "roll_disabled":
288392
var f bool
289393
fw.Roll = &f
@@ -352,31 +456,48 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
352456
fw.RollInterval = duration
353457

354458
case "roll_minutes":
355-
var minutesArrayStr string
356-
if !d.AllArgs(&minutesArrayStr) {
459+
// Accept either a single comma-separated argument or
460+
// multiple space-separated arguments. Collect all
461+
// remaining args on the line and split on commas.
462+
args := d.RemainingArgs()
463+
if len(args) == 0 {
357464
return d.ArgErr()
358465
}
359-
minutesStr := strings.Split(minutesArrayStr, ",")
360-
minutes := make([]int, len(minutesStr))
361-
for i := range minutesStr {
362-
ms := strings.Trim(minutesStr[i], " ")
363-
m, err := strconv.Atoi(ms)
364-
if err != nil {
365-
return d.Errf("parsing roll_minutes number: %v", err)
466+
var minutes []int
467+
for _, arg := range args {
468+
parts := strings.SplitSeq(arg, ",")
469+
for p := range parts {
470+
ms := strings.TrimSpace(p)
471+
if ms == "" {
472+
return d.Errf("parsing roll_minutes: empty value")
473+
}
474+
m, err := strconv.Atoi(ms)
475+
if err != nil {
476+
return d.Errf("parsing roll_minutes number: %v", err)
477+
}
478+
minutes = append(minutes, m)
366479
}
367-
minutes[i] = m
368480
}
369481
fw.RollAtMinutes = minutes
370482

371483
case "roll_at":
372-
var timeArrayStr string
373-
if !d.AllArgs(&timeArrayStr) {
484+
// Accept either a single comma-separated argument or
485+
// multiple space-separated arguments. Collect all
486+
// remaining args on the line and split on commas.
487+
args := d.RemainingArgs()
488+
if len(args) == 0 {
374489
return d.ArgErr()
375490
}
376-
timeStr := strings.Split(timeArrayStr, ",")
377-
times := make([]string, len(timeStr))
378-
for i := range timeStr {
379-
times[i] = strings.Trim(timeStr[i], " ")
491+
var times []string
492+
for _, arg := range args {
493+
parts := strings.SplitSeq(arg, ",")
494+
for p := range parts {
495+
ts := strings.TrimSpace(p)
496+
if ts == "" {
497+
return d.Errf("parsing roll_at: empty value")
498+
}
499+
times = append(times, ts)
500+
}
380501
}
381502
fw.RollAt = times
382503

0 commit comments

Comments
 (0)