Commit 00816099 authored by Maxime's avatar Maxime

Added LogRoller parser and entity.

The errors and logs can now have log rolling if provided by the user.
The current customisable parameter of it are:
The maximal size of the file before rolling.
The maximal age/time of the file before rolling.
The number of backups to keep.
parent bb5a322c
...@@ -119,6 +119,11 @@ func (d *Dispenser) NextBlock() bool { ...@@ -119,6 +119,11 @@ func (d *Dispenser) NextBlock() bool {
return true return true
} }
func (d *Dispenser) IncrNest() {
d.nesting++
return
}
// Val gets the text of the current token. If there is no token // Val gets the text of the current token. If there is no token
// loaded, it returns empty string. // loaded, it returns empty string.
func (d *Dispenser) Val() string { func (d *Dispenser) Val() string {
......
...@@ -11,7 +11,6 @@ import ( ...@@ -11,7 +11,6 @@ import (
"github.com/hashicorp/go-syslog" "github.com/hashicorp/go-syslog"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors" "github.com/mholt/caddy/middleware/errors"
"gopkg.in/natefinch/lumberjack.v2"
) )
// Errors configures a new gzip middleware instance. // Errors configures a new gzip middleware instance.
...@@ -24,30 +23,35 @@ func Errors(c *Controller) (middleware.Middleware, error) { ...@@ -24,30 +23,35 @@ func Errors(c *Controller) (middleware.Middleware, error) {
// Open the log file for writing when the server starts // Open the log file for writing when the server starts
c.Startup = append(c.Startup, func() error { c.Startup = append(c.Startup, func() error {
var err error var err error
var file io.Writer var writer io.Writer
if handler.LogFile == "stdout" { if handler.LogFile == "stdout" {
file = os.Stdout writer = os.Stdout
} else if handler.LogFile == "stderr" { } else if handler.LogFile == "stderr" {
file = os.Stderr writer = os.Stderr
} else if handler.LogFile == "syslog" { } else if handler.LogFile == "syslog" {
file, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy") writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
if err != nil { if err != nil {
return err return err
} }
} else if handler.LogFile != "" { } else if handler.LogFile != "" {
_, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) var file *os.File
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil { if err != nil {
return err return err
} }
file = &lumberjack.Logger{ if handler.LogRoller != nil {
Filename: handler.LogFile, file.Close()
MaxSize: 20,
MaxBackups: 10, handler.LogRoller.Filename = handler.LogFile
writer = handler.LogRoller.GetLogWriter()
} else {
writer = file
} }
} }
handler.Log = log.New(file, "", 0) handler.Log = log.New(writer, "", 0)
return nil return nil
}) })
...@@ -77,6 +81,16 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { ...@@ -77,6 +81,16 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
if what == "log" { if what == "log" {
handler.LogFile = where handler.LogFile = where
if c.NextArg() {
if c.Val() == "{" {
c.IncrNest()
logRoller, err := parseRoller(c)
if err != nil {
return hadBlock, err
}
handler.LogRoller = logRoller
}
}
} else { } else {
// Error page; ensure it exists // Error page; ensure it exists
where = path.Join(c.Root, where) where = path.Join(c.Root, where)
...@@ -97,6 +111,10 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { ...@@ -97,6 +111,10 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
} }
for c.Next() { for c.Next() {
// weird hack to avoid having the handler values overwritten.
if c.Val() == "}" {
continue
}
// Configuration may be in a block // Configuration may be in a block
hadBlock, err := optionalBlock() hadBlock, err := optionalBlock()
if err != nil { if err != nil {
......
package setup
import (
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
)
func TestErrors(t *testing.T) {
c := NewTestController(`errors`)
mid, err := Errors(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(*errors.ErrorHandler)
if !ok {
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
}
if myHandler.LogFile != errors.DefaultLogFilename {
t.Errorf("Expected %s as the default LogFile", errors.DefaultLogFilename)
}
if myHandler.LogRoller != nil {
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestErrorsParse(t *testing.T) {
tests := []struct {
inputErrorsRules string
shouldErr bool
expectedErrorHandler errors.ErrorHandler
}{
{`errors`, false, errors.ErrorHandler{
LogFile: errors.DefaultLogFilename,
}},
{`errors errors.txt`, false, errors.ErrorHandler{
LogFile: "errors.txt",
}},
{`errors { log errors.txt
404 404.html
500 500.html
}`, false, errors.ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
500: "500.html",
},
}},
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
LogFile: "errors.txt",
LogRoller: &middleware.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
},
}},
{`errors { log errors.txt {
size 3
age 11
keep 5
}
404 404.html
503 503.html
}`, false, errors.ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
LogRoller: &middleware.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
},
}},
}
for i, test := range tests {
c := NewTestController(test.inputErrorsRules)
actualErrorsRule, err := errorsParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
t.Errorf("Test %d expected LogFile to be %s , but got %s",
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
}
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil {
t.Fatalf("Test %d expected LogRoller to be %v, but got %v",
i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller)
}
if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) {
t.Fatalf("Test %d expected %d no of Error pages, but got %d ",
i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages))
}
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil {
if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename {
t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s",
i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename)
}
if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge {
t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge)
}
if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups {
t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups)
}
if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize {
t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize)
}
if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime {
t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t",
i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime)
}
}
}
}
...@@ -9,7 +9,6 @@ import ( ...@@ -9,7 +9,6 @@ import (
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log" caddylog "github.com/mholt/caddy/middleware/log"
"github.com/mholt/caddy/server" "github.com/mholt/caddy/server"
"gopkg.in/natefinch/lumberjack.v2"
) )
// Log sets up the logging middleware. // Log sets up the logging middleware.
...@@ -23,30 +22,33 @@ func Log(c *Controller) (middleware.Middleware, error) { ...@@ -23,30 +22,33 @@ func Log(c *Controller) (middleware.Middleware, error) {
c.Startup = append(c.Startup, func() error { c.Startup = append(c.Startup, func() error {
for i := 0; i < len(rules); i++ { for i := 0; i < len(rules); i++ {
var err error var err error
var file io.Writer var writer io.Writer
if rules[i].OutputFile == "stdout" { if rules[i].OutputFile == "stdout" {
file = os.Stdout writer = os.Stdout
} else if rules[i].OutputFile == "stderr" { } else if rules[i].OutputFile == "stderr" {
file = os.Stderr writer = os.Stderr
} else if rules[i].OutputFile == "syslog" { } else if rules[i].OutputFile == "syslog" {
file, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy") writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy")
if err != nil { if err != nil {
return err return err
} }
} else { } else {
_, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) var file *os.File
file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil { if err != nil {
return err return err
} }
file = &lumberjack.Logger{ if rules[i].Roller != nil {
Filename: rules[i].OutputFile, file.Close()
MaxSize: 20, rules[i].Roller.Filename = rules[i].OutputFile
MaxBackups: 10, writer = rules[i].Roller.GetLogWriter()
} else {
writer = file
} }
} }
rules[i].Log = log.New(file, "", 0) rules[i].Log = log.New(writer, "", 0)
} }
return nil return nil
...@@ -63,12 +65,33 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { ...@@ -63,12 +65,33 @@ func logParse(c *Controller) ([]caddylog.Rule, error) {
for c.Next() { for c.Next() {
args := c.RemainingArgs() args := c.RemainingArgs()
var logRoller *middleware.LogRoller
if c.NextBlock() {
if c.Val() == "rotate" {
if c.NextArg() {
if c.Val() == "{" {
var err error
logRoller, err = parseRoller(c)
if err != nil {
return nil, err
}
// This part doesn't allow having something after the rotate block
if c.Next() {
if c.Val() != "}" {
return nil, c.ArgErr()
}
}
}
}
}
}
if len(args) == 0 { if len(args) == 0 {
// Nothing specified; use defaults // Nothing specified; use defaults
rules = append(rules, caddylog.Rule{ rules = append(rules, caddylog.Rule{
PathScope: "/", PathScope: "/",
OutputFile: caddylog.DefaultLogFilename, OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat, Format: caddylog.DefaultLogFormat,
Roller: logRoller,
}) })
} else if len(args) == 1 { } else if len(args) == 1 {
// Only an output file specified // Only an output file specified
...@@ -76,6 +99,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { ...@@ -76,6 +99,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) {
PathScope: "/", PathScope: "/",
OutputFile: args[0], OutputFile: args[0],
Format: caddylog.DefaultLogFormat, Format: caddylog.DefaultLogFormat,
Roller: logRoller,
}) })
} else { } else {
// Path scope, output file, and maybe a format specified // Path scope, output file, and maybe a format specified
...@@ -97,6 +121,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { ...@@ -97,6 +121,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) {
PathScope: args[0], PathScope: args[0],
OutputFile: args[1], OutputFile: args[1],
Format: format, Format: format,
Roller: logRoller,
}) })
} }
} }
......
...@@ -3,6 +3,7 @@ package setup ...@@ -3,6 +3,7 @@ package setup
import ( import (
"testing" "testing"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log" caddylog "github.com/mholt/caddy/middleware/log"
) )
...@@ -36,6 +37,9 @@ func TestLog(t *testing.T) { ...@@ -36,6 +37,9 @@ func TestLog(t *testing.T) {
if myHandler.Rules[0].Format != caddylog.DefaultLogFormat { if myHandler.Rules[0].Format != caddylog.DefaultLogFormat {
t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat) t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat)
} }
if myHandler.Rules[0].Roller != nil {
t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller)
}
if !SameNext(myHandler.Next, EmptyNext) { if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly") t.Error("'Next' field of handler was not set properly")
} }
...@@ -98,6 +102,16 @@ func TestLogParse(t *testing.T) { ...@@ -98,6 +102,16 @@ func TestLogParse(t *testing.T) {
OutputFile: "log.txt", OutputFile: "log.txt",
Format: "{when}", Format: "{when}",
}}}, }}},
{`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: "access.log",
Format: caddylog.DefaultLogFormat,
Roller: &middleware.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
},
}}},
} }
for i, test := range tests { for i, test := range tests {
c := NewTestController(test.inputLogRules) c := NewTestController(test.inputLogRules)
...@@ -128,6 +142,32 @@ func TestLogParse(t *testing.T) { ...@@ -128,6 +142,32 @@ func TestLogParse(t *testing.T) {
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s", t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
i, j, test.expectedLogRules[j].Format, actualLogRule.Format) i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
} }
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil {
t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v",
i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller)
}
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil {
if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename {
t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s",
i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename)
}
if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge {
t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge)
}
if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups {
t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups)
}
if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize {
t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize)
}
if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime {
t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t",
i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime)
}
}
} }
} }
......
package setup
import (
"strconv"
"github.com/mholt/caddy/middleware"
)
func parseRoller(c *Controller) (*middleware.LogRoller, error) {
var size, age, keep int
// This is kind of a hack to support nested blocks:
// As we are already in a block: either log or errors,
// c.nesting > 0 but, as soon as c meets a }, it thinks
// the block is over and return false for c.NextBlock.
for c.NextBlock() {
what := c.Val()
if !c.NextArg() {
return nil, c.ArgErr()
}
value := c.Val()
var err error
switch what {
case "size":
size, err = strconv.Atoi(value)
case "age":
age, err = strconv.Atoi(value)
case "keep":
keep, err = strconv.Atoi(value)
}
if err != nil {
return nil, err
}
}
return &middleware.LogRoller{
MaxSize: size,
MaxAge: age,
MaxBackups: keep,
}, nil
}
...@@ -20,6 +20,7 @@ type ErrorHandler struct { ...@@ -20,6 +20,7 @@ type ErrorHandler struct {
ErrorPages map[int]string // map of status code to filename ErrorPages map[int]string // map of status code to filename
LogFile string LogFile string
Log *log.Logger Log *log.Logger
LogRoller *middleware.LogRoller
} }
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
......
...@@ -47,6 +47,7 @@ type Rule struct { ...@@ -47,6 +47,7 @@ type Rule struct {
OutputFile string OutputFile string
Format string Format string
Log *log.Logger Log *log.Logger
Roller *middleware.LogRoller
} }
const ( const (
......
package middleware
import (
"io"
"gopkg.in/natefinch/lumberjack.v2"
)
type LogRoller struct {
Filename string
MaxSize int
MaxAge int
MaxBackups int
LocalTime bool
}
func (l LogRoller) GetLogWriter() io.Writer {
return &lumberjack.Logger{
Filename: l.Filename,
MaxSize: l.MaxSize,
MaxAge: l.MaxAge,
MaxBackups: l.MaxBackups,
LocalTime: l.LocalTime,
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment