Commit 820dc660 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'improve-log-rotation' into 'master'

Adds Unix signal to reopen log file

This MR adds a command line flag to provide a log file and creates a handler to reopen the log file upon receiving a SIGHUP signal.

A new dependency is also added: [github.com/client9/reopen](https://github.com/client9/reopen)

Relevant Issue: [#52](https://gitlab.com/gitlab-org/gitlab-workhorse/issues/52)

cc: @jacobvosmaer-gitlab, @smcgivern 

See merge request !77
parents 377df647 22527bb6
...@@ -8,6 +8,10 @@ ...@@ -8,6 +8,10 @@
"Comment": "2016.08.31", "Comment": "2016.08.31",
"Rev": "ec89d50f00d39494f5b3ec5cf2fe75c53467a937" "Rev": "ec89d50f00d39494f5b3ec5cf2fe75c53467a937"
}, },
{
"ImportPath": "github.com/client9/reopen",
"Rev": "4b86f9c0ead51cc410d05655596e30f281ed9071"
},
{ {
"ImportPath": "github.com/dgrijalva/jwt-go", "ImportPath": "github.com/dgrijalva/jwt-go",
"Comment": "v3.0.0", "Comment": "v3.0.0",
......
...@@ -2,10 +2,23 @@ package helper ...@@ -2,10 +2,23 @@ package helper
import ( import (
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"os"
"time" "time"
) )
var responseLogger *log.Logger
func init() {
SetCustomResponseLogger(os.Stderr)
}
func SetCustomResponseLogger(writer io.Writer) {
responseLogger = log.New(writer, "", 0)
}
type LoggingResponseWriter struct { type LoggingResponseWriter struct {
rw http.ResponseWriter rw http.ResponseWriter
status int status int
...@@ -44,7 +57,7 @@ func (l *LoggingResponseWriter) WriteHeader(status int) { ...@@ -44,7 +57,7 @@ func (l *LoggingResponseWriter) WriteHeader(status int) {
func (l *LoggingResponseWriter) Log(r *http.Request) { func (l *LoggingResponseWriter) Log(r *http.Request) {
duration := time.Since(l.started) duration := time.Since(l.started)
fmt.Printf("%s %s - - [%s] %q %d %d %q %q %f\n", responseLogger.Printf("%s %s - - [%s] %q %d %d %q %q %f\n",
r.Host, r.RemoteAddr, l.started, r.Host, r.RemoteAddr, l.started,
fmt.Sprintf("%s %s %s", r.Method, r.RequestURI, r.Proto), fmt.Sprintf("%s %s %s", r.Method, r.RequestURI, r.Proto),
l.status, l.written, r.Referer(), r.UserAgent(), duration.Seconds(), l.status, l.written, r.Referer(), r.UserAgent(), duration.Seconds(),
......
package main
import (
"log"
"os"
"os/signal"
"syscall"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
"github.com/client9/reopen"
)
func reopenLogWriter(l reopen.WriteCloser, sighup chan os.Signal) {
for _ = range sighup {
log.Printf("Reopening log file")
l.Reopen()
}
}
func startLogging(logFile string) {
var logWriter = reopen.Stderr
if logFile != "" {
file, err := reopen.NewFileWriter(logFile)
if err != nil {
log.Fatalf("Unable to set output log: %s", err)
}
logWriter = file
}
log.SetOutput(logWriter)
helper.SetCustomResponseLogger(logWriter)
sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
go reopenLogWriter(logWriter, sighup)
}
...@@ -45,6 +45,7 @@ var secretPath = flag.String("secretPath", "./.gitlab_workhorse_secret", "File w ...@@ -45,6 +45,7 @@ var secretPath = flag.String("secretPath", "./.gitlab_workhorse_secret", "File w
var apiLimit = flag.Uint("apiLimit", 0, "Number of API requests allowed at single time") var apiLimit = flag.Uint("apiLimit", 0, "Number of API requests allowed at single time")
var apiQueueLimit = flag.Uint("apiQueueLimit", 0, "Number of API requests allowed to be queued") var apiQueueLimit = flag.Uint("apiQueueLimit", 0, "Number of API requests allowed to be queued")
var apiQueueTimeout = flag.Duration("apiQueueDuration", queueing.DefaultTimeout, "Maximum queueing duration of requests") var apiQueueTimeout = flag.Duration("apiQueueDuration", queueing.DefaultTimeout, "Maximum queueing duration of requests")
var logFile = flag.String("logFile", "", "Log file to be used")
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
...@@ -60,10 +61,11 @@ func main() { ...@@ -60,10 +61,11 @@ func main() {
os.Exit(0) os.Exit(0)
} }
startLogging(*logFile)
backendURL, err := parseAuthBackend(*authBackend) backendURL, err := parseAuthBackend(*authBackend)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "invalid authBackend: %v\n", err) log.Fatalf("invalid authBackend: %v", err)
os.Exit(1)
} }
log.Printf("Starting %s", version) log.Printf("Starting %s", version)
......
# emacs turds
*~
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
sudo: required
dist: trusty
language: generic
script:
- make -e docker-ci
The MIT License (MIT)
Copyright (c) 2015 Nick Galbreath
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
all: build test lint
build:
go build ./...
test:
go test ./...
lint:
golint ./...
gofmt -w -s . ./example*
goimports -w . ./example*
clean:
rm -f *~ ./example*/*~
rm -f ./example1/example1
rm -f ./example2/example2
go clean ./...
git gc
ci: build test lint
docker-ci:
docker run --rm \
-e COVERALLS_REPO_TOKEN=$(COVERALLS_REPO_TOKEN) \
-v $(PWD):/go/src/github.com/client9/reopen \
-w /go/src/github.com/client9/reopen \
nickg/golang-dev-docker \
make ci
.PHONY: ci docker-ci
[![Build Status](https://travis-ci.org/client9/reopen.svg)](https://travis-ci.org/client9/reopen) [![Go Report Card](http://goreportcard.com/badge/client9/reopen)](http://goreportcard.com/report/client9/reopen) [![GoDoc](https://godoc.org/github.com/client9/reopen?status.svg)](https://godoc.org/github.com/client9/reopen) [![Coverage](http://gocover.io/_badge/github.com/client9/reopen)](http://gocover.io/github.com/client9/reopen) [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/client9/reopen/master/LICENSE)
Makes a standard os.File a "reopenable writer" and allows SIGHUP signals
to reopen log files, as needed by
[logrotated](https://fedorahosted.org/logrotate/). This is inspired
by the C/Posix
[freopen](http://pubs.opengroup.org/onlinepubs/009695399/functions/freopen.html)
The simple version `reopen.NewFileWriter` does unbuffered writing. A
call to `.Reopen` closes the existing file handle, and then re-opens
it using the original filename.
The more advanced version `reopen.NewBufferedFileWriter` buffers input
and flushes when the internal buffer is full (with care) or if 30 seconds has
elapsed.
There is also `reopen.Stderr` and `reopen.Stdout` which implements the `reopen.Reopener` interface (and does nothing on a reopen call).
`reopen.Discard` wraps `ioutil.Discard`
Samples are in `example1` and `example2`. The `run.sh` scripts are a
dumb test where the file is rotated underneath the server, and nothing
is lost. This is not the most robust test but gives you an idea of how it works.
Here's some sample code.
```go
package main
/* Simple logrotate logger
*/
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/client9/reopen"
)
func main() {
// setup logger to write to our new *reopenable* log file
f, err := reopen.NewFileWriter("/tmp/example.log")
if err != nil {
log.Fatalf("Unable to set output log: %s", err)
}
log.SetOutput(f)
// Handle SIGHUP
//
// channel is number of signals needed to catch (more or less)
// we only are working with one here, SIGHUP
sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
go func() {
for {
<-sighup
fmt.Println("Got a sighup")
f.Reopen()
}
}()
// dumb http server that just prints and logs the path
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s", r.URL.Path)
fmt.Fprintf(w, "%s\n", r.URL.Path)
})
log.Fatal(http.ListenAndServe("127.0.0.1:8123", nil))
}
```
package reopen
import (
"bufio"
"io"
"io/ioutil"
"os"
"sync"
"time"
)
// Reopener interface defines something that can be reopened
type Reopener interface {
Reopen() error
}
// Writer is a writer that also can be reopened
type Writer interface {
Reopener
io.Writer
}
// WriteCloser is a io.WriteCloser that can also be reopened
type WriteCloser interface {
Reopener
io.WriteCloser
}
// FileWriter that can also be reopened
type FileWriter struct {
mu sync.Mutex // ensures close / reopen / write are not called at the same time, protects f
f *os.File
mode os.FileMode
name string
}
// Close calls the underlyding File.Close()
func (f *FileWriter) Close() error {
f.mu.Lock()
err := f.f.Close()
f.mu.Unlock()
return err
}
// mutex free version
func (f *FileWriter) reopen() error {
if f.f != nil {
f.f.Close()
f.f = nil
}
newf, err := os.OpenFile(f.name, os.O_WRONLY|os.O_APPEND|os.O_CREATE, f.mode)
if err != nil {
f.f = nil
return err
}
f.f = newf
return nil
}
// Reopen the file
func (f *FileWriter) Reopen() error {
f.mu.Lock()
err := f.reopen()
f.mu.Unlock()
return err
}
// Write implements the stander io.Writer interface
func (f *FileWriter) Write(p []byte) (int, error) {
f.mu.Lock()
n, err := f.f.Write(p)
f.mu.Unlock()
return n, err
}
// NewFileWriter opens a file for appending and writing and can be reopened.
// it is a ReopenWriteCloser...
func NewFileWriter(name string) (*FileWriter, error) {
// Standard default mode
return NewFileWriterMode(name, 0666)
}
// NewFileWriterMode opens a Reopener file with a specific permission
func NewFileWriterMode(name string, mode os.FileMode) (*FileWriter, error) {
writer := FileWriter{
f: nil,
name: name,
mode: mode,
}
err := writer.reopen()
if err != nil {
return nil, err
}
return &writer, nil
}
// BufferedFileWriter is buffer writer than can be reopned
type BufferedFileWriter struct {
mu sync.Mutex
OrigWriter *FileWriter
BufWriter *bufio.Writer
}
// Reopen implement Reopener
func (bw *BufferedFileWriter) Reopen() error {
bw.mu.Lock()
bw.BufWriter.Flush()
// use non-mutex version since we are using this one
err := bw.OrigWriter.reopen()
bw.BufWriter.Reset(io.Writer(bw.OrigWriter))
bw.mu.Unlock()
return err
}
// Close flushes the internal buffer and closes the destination file
func (bw *BufferedFileWriter) Close() error {
bw.mu.Lock()
bw.BufWriter.Flush()
bw.OrigWriter.f.Close()
bw.mu.Unlock()
return nil
}
// Write implements io.Writer (and reopen.Writer)
func (bw *BufferedFileWriter) Write(p []byte) (int, error) {
bw.mu.Lock()
n, err := bw.BufWriter.Write(p)
// Special Case... if the used space in the buffer is LESS than
// the input, then we did a flush in the middle of the line
// and the full log line was not sent on its way.
if bw.BufWriter.Buffered() < len(p) {
bw.BufWriter.Flush()
}
bw.mu.Unlock()
return n, err
}
// flushDaemon periodically flushes the log file buffers.
// props to glog
func (bw *BufferedFileWriter) flushDaemon() {
for range time.NewTicker(flushInterval).C {
bw.mu.Lock()
bw.BufWriter.Flush()
bw.OrigWriter.f.Sync()
bw.mu.Unlock()
}
}
const bufferSize = 256 * 1024
const flushInterval = 30 * time.Second
// NewBufferedFileWriter opens a buffered file that is periodically
// flushed.
// TODO: allow size and interval to be passed in.
func NewBufferedFileWriter(w *FileWriter) *BufferedFileWriter {
bw := BufferedFileWriter{
OrigWriter: w,
BufWriter: bufio.NewWriterSize(w, bufferSize),
}
go bw.flushDaemon()
return &bw
}
type multiReopenWriter struct {
writers []Writer
}
// Reopen reopens all child Reopeners
func (t *multiReopenWriter) Reopen() error {
for _, w := range t.writers {
err := w.Reopen()
if err != nil {
return err
}
}
return nil
}
// Write implements standard io.Write and reopen.Write
func (t *multiReopenWriter) Write(p []byte) (int, error) {
for _, w := range t.writers {
n, err := w.Write(p)
if err != nil {
return n, err
}
if n != len(p) {
return n, io.ErrShortWrite
}
}
return len(p), nil
}
// MultiWriter creates a writer that duplicates its writes to all the
// provided writers, similar to the Unix tee(1) command.
// Also allow reopen
func MultiWriter(writers ...Writer) Writer {
w := make([]Writer, len(writers))
copy(w, writers)
return &multiReopenWriter{w}
}
type nopReopenWriteCloser struct {
io.Writer
}
func (nopReopenWriteCloser) Reopen() error {
return nil
}
func (nopReopenWriteCloser) Close() error {
return nil
}
// NopWriter turns a normal writer into a ReopenWriter
// by doing a NOP on Reopen
// TODO: better name
func NopWriter(w io.Writer) WriteCloser {
return nopReopenWriteCloser{w}
}
// Reopenable versions of os.Stdout, os.Stderr, /dev/null (reopen does nothing)
var (
Stdout = NopWriter(os.Stdout)
Stderr = NopWriter(os.Stderr)
Discard = NopWriter(ioutil.Discard)
)
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