pax_global_header00006660000000000000000000000064132525707300014516gustar00rootroot0000000000000052 comment=1530c81a31162bebce8a37bca43d56fb2ff6d8b1 reopen-1.0.0/000077500000000000000000000000001325257073000130045ustar00rootroot00000000000000reopen-1.0.0/.gitignore000066400000000000000000000004341325257073000147750ustar00rootroot00000000000000# 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 reopen-1.0.0/.travis.yml000066400000000000000000000002371325257073000151170ustar00rootroot00000000000000sudo: false dist: trusty language: go install: - go get -u github.com/alecthomas/gometalinter - gometalinter --install script: make build test reopen-1.0.0/LICENSE000066400000000000000000000020721325257073000140120ustar00rootroot00000000000000The 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. reopen-1.0.0/Makefile000066400000000000000000000020051325257073000144410ustar00rootroot00000000000000 build: ## build and lint go build ./... gometalinter \ --vendor \ --vendored-linters \ --deadline=60s \ --disable-all \ --enable=goimports \ --enable=vetshadow \ --enable=varcheck \ --enable=structcheck \ --enable=deadcode \ --enable=ineffassign \ --enable=unconvert \ --enable=goconst \ --enable=golint \ --enable=gosimple \ --enable=gofmt \ --enable=misspell \ --enable=staticcheck \ . test: ## just test go test -cover . clean: ## cleanup rm -f ./example1/example1 rm -f ./example2/example2 go clean ./... git gc # https://www.client9.com/self-documenting-makefiles/ help: @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .DEFAULT_GOAL=help .PHONY=help reopen-1.0.0/README.md000066400000000000000000000051421325257073000142650ustar00rootroot00000000000000[![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) [![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)) } ``` reopen-1.0.0/example1/000077500000000000000000000000001325257073000145205ustar00rootroot00000000000000reopen-1.0.0/example1/curl.sh000077500000000000000000000002101325257073000160150ustar00rootroot00000000000000#!/bin/sh server=$1 count=$2 a=1 while [ $a -le $count ] do echo $a curl --silent "$server/$a" > /dev/null a=`expr $a + 1` donereopen-1.0.0/example1/example.go000066400000000000000000000014011325257073000164760ustar00rootroot00000000000000package main /* Simple logrotate logger */ import ( "fmt" "log" "net/http" "os" "os/signal" "syscall" "github.com/client9/reopen" ) func main() { f, err := reopen.NewFileWriter("/tmp/example.log") if err != nil { log.Fatalf("Unable to set output log: %s", err) } log.SetOutput(f) // channel is number of signals needed to catch (more or less) // we only are working with one here, SIGUP sighup := make(chan os.Signal, 1) signal.Notify(sighup, syscall.SIGHUP) go func() { for { <-sighup fmt.Printf("Got a sighup\n") f.Reopen() } }() 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)) } reopen-1.0.0/example1/run.sh000077500000000000000000000006331325257073000156650ustar00rootroot00000000000000#!/bin/sh #set -e SERVER=127.0.0.1:8123 COUNT=300 killall example1 rm -f ./example1 /tmp/example.log* echo "Starting server..." go build . ./example1 & echo "Starting requests...." ./curl.sh $SERVER $COUNT & sleep 1 echo "Rotating..." mv /tmp/example.log /tmp/example.log-old killall -1 example1 # wait for curl to finish wait %2 killall -TERM example1 # count results wc -l /tmp/example.log* # exitreopen-1.0.0/example2/000077500000000000000000000000001325257073000145215ustar00rootroot00000000000000reopen-1.0.0/example2/curl.sh000077500000000000000000000002101325257073000160160ustar00rootroot00000000000000#!/bin/sh server=$1 count=$2 a=1 while [ $a -le $count ] do echo $a curl --silent "$server/$a" > /dev/null a=`expr $a + 1` donereopen-1.0.0/example2/example.go000066400000000000000000000022261325257073000165050ustar00rootroot00000000000000package main /* Similar to previous example but uses a BufferedFileWriter * When buf is full OR every 30 seconds, the buffer is flushed to disk * * care is done to make sure transient or partial log messages are not written. * * Note the signal handler catches SIGTERM to flush out and existing buffers */ import ( "fmt" "log" "net/http" "os" "os/signal" "syscall" "github.com/client9/reopen" ) func main() { f, err := reopen.NewFileWriter("/tmp/example.log") if err != nil { log.Fatalf("Unable to set output log: %s", err) } bf := reopen.NewBufferedFileWriter(f) log.SetOutput(bf) sighup := make(chan os.Signal, 2) signal.Notify(sighup, syscall.SIGHUP, syscall.SIGTERM) go func() { for { s := <-sighup switch s { case syscall.SIGHUP: fmt.Printf("Got a sighup\n") bf.Reopen() case syscall.SIGTERM: fmt.Printf("Got SIGTERM\n") // make sure any remaining logs are flushed out bf.Close() os.Exit(0) } } }() 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)) } reopen-1.0.0/example2/run.sh000077500000000000000000000006331325257073000156660ustar00rootroot00000000000000#!/bin/sh #set -e SERVER=127.0.0.1:8123 COUNT=300 killall example2 rm -f ./example2 /tmp/example.log* echo "Starting server..." go build . ./example2 & echo "Starting requests...." ./curl.sh $SERVER $COUNT & sleep 1 echo "Rotating..." mv /tmp/example.log /tmp/example.log-old killall -1 example2 # wait for curl to finish wait %2 killall -TERM example2 # count results wc -l /tmp/example.log* # exitreopen-1.0.0/reopen.go000066400000000000000000000126221325257073000146260ustar00rootroot00000000000000package 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 quitChan chan bool done bool 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.quitChan <- true bw.mu.Lock() bw.done = true 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 } // Flush flushes the buffer. func (bw *BufferedFileWriter) Flush() { bw.mu.Lock() // could add check if bw.done already // should never happen bw.bufWriter.Flush() bw.origWriter.f.Sync() bw.mu.Unlock() } // flushDaemon periodically flushes the log file buffers. func (bw *BufferedFileWriter) flushDaemon(interval time.Duration) { ticker := time.NewTicker(interval) for { select { case <-bw.quitChan: ticker.Stop() return case <-ticker.C: bw.Flush() } } } const bufferSize = 256 * 1024 const flushInterval = 30 * time.Second // NewBufferedFileWriter opens a buffered file that is periodically // flushed. func NewBufferedFileWriter(w *FileWriter) *BufferedFileWriter { return NewBufferedFileWriterSize(w, bufferSize, flushInterval) } // NewBufferedFileWriterSize opens a buffered file with the given size that is periodically // flushed on the given interval. func NewBufferedFileWriterSize(w *FileWriter, size int, flush time.Duration) *BufferedFileWriter { bw := BufferedFileWriter{ quitChan: make(chan bool, 1), origWriter: w, bufWriter: bufio.NewWriterSize(w, size), } go bw.flushDaemon(flush) 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. See https://en.wikipedia.org/wiki/NOP 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) ) reopen-1.0.0/reopen_test.go000066400000000000000000000067351325257073000156750ustar00rootroot00000000000000package reopen import ( "io/ioutil" "os" "testing" "time" ) // TestReopenAppend -- make sure we always append to an existing file // // 1. Create a sample file using normal means // 2. Open a ioreopen.File // write line 1 // 3. call Reopen // write line 2 // 4. close file // 5. read file, make sure it contains line0,line1,line2 // func TestReopenAppend(t *testing.T) { // TODO fix var fname = "/tmp/foo" // Step 1 -- Create a sample file using normal means forig, err := os.Create(fname) if err != nil { t.Fatalf("Unable to create initial file %s: %s", fname, err) } _, err = forig.Write([]byte("line0\n")) if err != nil { t.Fatalf("Unable to write initial line %s: %s", fname, err) } err = forig.Close() if err != nil { t.Fatalf("Unable to close initial file: %s", err) } // Test that making a new File appends f, err := NewFileWriter(fname) if err != nil { t.Fatalf("Unable to create %s", fname) } _, err = f.Write([]byte("line1\n")) if err != nil { t.Errorf("Got write error1: %s", err) } // Test that reopen always appends err = f.Reopen() if err != nil { t.Errorf("Got reopen error %s: %s", fname, err) } _, err = f.Write([]byte("line2\n")) if err != nil { t.Errorf("Got write error2 on %s: %s", fname, err) } err = f.Close() if err != nil { t.Errorf("Got closing error for %s: %s", fname, err) } out, err := ioutil.ReadFile(fname) if err != nil { t.Fatalf("Unable read in final file %s: %s", fname, err) } outstr := string(out) if outstr != "line0\nline1\nline2\n" { t.Errorf("Result was %s", outstr) } } // Test that reopen works when Inode is swapped out // 1. Create a sample file using normal means // 2. Open a ioreopen.File // write line 1 // 3. call Reopen // write line 2 // 4. close file // 5. read file, make sure it contains line0,line1,line2 // func TestChangeInode(t *testing.T) { // TODO fix var fname = "/tmp/foo" // Step 1 -- Create a empty sample file forig, err := os.Create(fname) if err != nil { t.Fatalf("Unable to create initial file %s: %s", fname, err) } err = forig.Close() if err != nil { t.Fatalf("Unable to close initial file: %s", err) } // Test that making a new File appends f, err := NewFileWriter(fname) if err != nil { t.Fatalf("Unable to create %s", fname) } _, err = f.Write([]byte("line1\n")) if err != nil { t.Errorf("Got write error1: %s", err) } // Now move file err = os.Rename(fname, fname+".orig") if err != nil { t.Errorf("Renaming error: %s", err) } f.Write([]byte("after1\n")) // Test that reopen always appends err = f.Reopen() if err != nil { t.Errorf("Got reopen error %s: %s", fname, err) } _, err = f.Write([]byte("line2\n")) if err != nil { t.Errorf("Got write error2 on %s: %s", fname, err) } err = f.Close() if err != nil { t.Errorf("Got closing error for %s: %s", fname, err) } out, err := ioutil.ReadFile(fname) if err != nil { t.Fatalf("Unable read in final file %s: %s", fname, err) } outstr := string(out) if outstr != "line2\n" { t.Errorf("Result was %s", outstr) } } func TestBufferedWriter(t *testing.T) { const fname = "/tmp/foo" fw, err := NewFileWriter(fname) if err != nil { t.Fatalf("Unable to create test file %q: %s", fname, err) } logger := NewBufferedFileWriterSize(fw, 0, time.Millisecond*100) // make sure 3-4 flush events happen time.Sleep(time.Millisecond * 4) // close should shutdown the flush logger.Close() // if flush is still happening we should get one or two here time.Sleep(time.Millisecond * 200) }