// Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package logging import ( "encoding/json" "fmt" "io" "math" "os" "path/filepath" "strconv" "github.com/dustin/go-humanize" "gopkg.in/natefinch/lumberjack.v2" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { caddy.RegisterModule(FileWriter{}) } // fileMode is a string made of 1 to 4 octal digits representing // a numeric mode as specified with the `chmod` unix command. // `"0777"` and `"777"` are thus equivalent values. type fileMode os.FileMode // UnmarshalJSON satisfies json.Unmarshaler. func (m *fileMode) UnmarshalJSON(b []byte) error { if len(b) == 0 { return io.EOF } var s string if err := json.Unmarshal(b, &s); err != nil { return err } mode, err := parseFileMode(s) if err != nil { return err } *m = fileMode(mode) return err } // MarshalJSON satisfies json.Marshaler. func (m *fileMode) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("\"%04o\"", *m)), nil } // parseFileMode parses a file mode string, // adding support for `chmod` unix command like // 1 to 4 digital octal values. func parseFileMode(s string) (os.FileMode, error) { modeStr := fmt.Sprintf("%04s", s) mode, err := strconv.ParseUint(modeStr, 8, 32) if err != nil { return 0, err } return os.FileMode(mode), nil } // FileWriter can write logs to files. By default, log files // are rotated ("rolled") when they get large, and old log // files get deleted, to ensure that the process does not // exhaust disk space. type FileWriter struct { // Filename is the name of the file to write. Filename string `json:"filename,omitempty"` // The file permissions mode. // 0600 by default. Mode fileMode `json:"mode,omitempty"` // Roll toggles log rolling or rotation, which is // enabled by default. Roll *bool `json:"roll,omitempty"` // When a log file reaches approximately this size, // it will be rotated. RollSizeMB int `json:"roll_size_mb,omitempty"` // Whether to compress rolled files. Default: true RollCompress *bool `json:"roll_gzip,omitempty"` // Whether to use local timestamps in rolled filenames. // Default: false RollLocalTime bool `json:"roll_local_time,omitempty"` // The maximum number of rolled log files to keep. // Default: 10 RollKeep int `json:"roll_keep,omitempty"` // How many days to keep rolled log files. Default: 90 RollKeepDays int `json:"roll_keep_days,omitempty"` } // CaddyModule returns the Caddy module information. func (FileWriter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ ID: "caddy.logging.writers.file", New: func() caddy.Module { return new(FileWriter) }, } } // Provision sets up the module func (fw *FileWriter) Provision(ctx caddy.Context) error { // Replace placeholder in filename repl := caddy.NewReplacer() filename, err := repl.ReplaceOrErr(fw.Filename, true, true) if err != nil { return fmt.Errorf("invalid filename for log file: %v", err) } fw.Filename = filename return nil } func (fw FileWriter) String() string { fpath, err := filepath.Abs(fw.Filename) if err == nil { return fpath } return fw.Filename } // WriterKey returns a unique key representing this fw. func (fw FileWriter) WriterKey() string { return "file:" + fw.Filename } // OpenWriter opens a new file writer. func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { if fw.Mode == 0 { fw.Mode = 0o600 } // roll log files by default if fw.Roll == nil || *fw.Roll { if fw.RollSizeMB == 0 { fw.RollSizeMB = 100 } if fw.RollCompress == nil { compress := true fw.RollCompress = &compress } if fw.RollKeep == 0 { fw.RollKeep = 10 } if fw.RollKeepDays == 0 { fw.RollKeepDays = 90 } // create the file if it does not exist with the right mode. // lumberjack will reuse the file mode across log rotation. f_tmp, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode)) if err != nil { return nil, err } f_tmp.Close() // ensure already existing files have the right mode, // since OpenFile will not set the mode in such case. if err = os.Chmod(fw.Filename, os.FileMode(fw.Mode)); err != nil { return nil, err } return &lumberjack.Logger{ Filename: fw.Filename, MaxSize: fw.RollSizeMB, MaxAge: fw.RollKeepDays, MaxBackups: fw.RollKeep, LocalTime: fw.RollLocalTime, Compress: *fw.RollCompress, }, nil } // otherwise just open a regular file return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode)) } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // file { // mode // roll_disabled // roll_size // roll_uncompressed // roll_local_time // roll_keep // roll_keep_for // } // // The roll_size value has megabyte resolution. // Fractional values are rounded up to the next whole megabyte (MiB). // // By default, compression is enabled, but can be turned off by setting // the roll_uncompressed option. // // The roll_keep_for duration has day resolution. // Fractional values are rounded up to the next whole number of days. // // If any of the mode, roll_size, roll_keep, or roll_keep_for subdirectives are // omitted or set to a zero value, then Caddy's default value for that // subdirective is used. func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume writer name if !d.NextArg() { return d.ArgErr() } fw.Filename = d.Val() if d.NextArg() { return d.ArgErr() } for d.NextBlock(0) { switch d.Val() { case "mode": var modeStr string if !d.AllArgs(&modeStr) { return d.ArgErr() } mode, err := parseFileMode(modeStr) if err != nil { return d.Errf("parsing mode: %v", err) } fw.Mode = fileMode(mode) case "roll_disabled": var f bool fw.Roll = &f if d.NextArg() { return d.ArgErr() } case "roll_size": var sizeStr string if !d.AllArgs(&sizeStr) { return d.ArgErr() } size, err := humanize.ParseBytes(sizeStr) if err != nil { return d.Errf("parsing size: %v", err) } fw.RollSizeMB = int(math.Ceil(float64(size) / humanize.MiByte)) case "roll_uncompressed": var f bool fw.RollCompress = &f if d.NextArg() { return d.ArgErr() } case "roll_local_time": fw.RollLocalTime = true if d.NextArg() { return d.ArgErr() } case "roll_keep": var keepStr string if !d.AllArgs(&keepStr) { return d.ArgErr() } keep, err := strconv.Atoi(keepStr) if err != nil { return d.Errf("parsing roll_keep number: %v", err) } fw.RollKeep = keep case "roll_keep_for": var keepForStr string if !d.AllArgs(&keepForStr) { return d.ArgErr() } keepFor, err := caddy.ParseDuration(keepForStr) if err != nil { return d.Errf("parsing roll_keep_for duration: %v", err) } if keepFor < 0 { return d.Errf("negative roll_keep_for duration: %v", keepFor) } fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) } } return nil } // Interface guards var ( _ caddy.Provisioner = (*FileWriter)(nil) _ caddy.WriterOpener = (*FileWriter)(nil) _ caddyfile.Unmarshaler = (*FileWriter)(nil) )