Skip to content

Commit

Permalink
init 1
Browse files Browse the repository at this point in the history
  • Loading branch information
dwisiswant0 committed Jan 28, 2023
0 parents commit 908e762
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 0 deletions.
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2023 Dwi Siswanto

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.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[![GoDoc](https://godoc.org/github.com/kitabisa/tailpipe?status.svg)](https://godoc.org/github.com/kitabisa/tailpipe)

> This version of https://github.com/droyo/tailpipe has been modified to include additional improvements.
The tailpipe package provides an implementation of `tail -F` for Go. In
effect, it produces files that are infinite length, and re-opens the
file when it is changed, such as during log rotation. It can be used
for programs that need to watch log files indefinitely.

There are several `tail` packages for Go available. However, most
packages do not preserve the io.Reader interface, instead opting for
a line-oriented approach involving `chan string`. By preserving the
`io.Reader` interface, this package composes better with the standard
library.
33 changes: 33 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tailpipe_test

import (
"bufio"
"bytes"
"log"
"os"

"github.com/kitabisa/tailpipe"
)

func ExampleOpen() {
tail, err := tailpipe.Open("/var/log/messages")
if err != nil {
log.Fatal(err)
}
defer tail.Close()

go func() {
for range tail.Rotated {
log.Printf("file %s rotated; following new file", tail.Name())
}
}()

scanner := bufio.NewScanner(tail)
for scanner.Scan() {
if bytes.Contains(scanner.Bytes(), []byte("ntpd")) {
if _, err := os.Stdout.Write(scanner.Bytes()); err != nil {
break
}
}
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/kitabisa/tailpipe

go 1.13
64 changes: 64 additions & 0 deletions rotate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tailpipe

import (
"os"
"testing"
"time"
)

func TestRotation(t *testing.T) {
f, teardown := tmpfile(t, "tailpipe-go-test")
defer teardown()

follow, err := Open(f.Name())
if err != nil {
t.Fatal(err)
}

write(t, f, "hello, ")
compare(t, follow, "hello, ")

done := make(chan struct{})
go func() {
select {
case <-follow.Rotated:
t.Logf("got rotate for %s", f.Name())
case <-time.After(time.Second):
t.Error("did not receive rotation notification")
}
close(done)
}()
teardown()
f, err = os.Create(f.Name())
if err != nil {
t.Fatal(err)
}

write(t, f, "world!")
compare(t, follow, "world!")
<-done
}

func TestDelayedRotation(t *testing.T) {
f, teardown := tmpfile(t, "tailpipe-go-test")
defer teardown()

follow, err := Open(f.Name())
if err != nil {
t.Fatal(err)
}

write(t, f, "hello, ")
compare(t, follow, "hello, ")

teardown()
go compare(t, follow, "world!")

time.Sleep(time.Millisecond * 100)
f, err = os.Create(f.Name())
if err != nil {
t.Fatal(err)
}

write(t, f, "world!")
}
157 changes: 157 additions & 0 deletions tailpipe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Package tailpipe allows for reading normal files indefinitely. With the
// tailpipe package, code that uses the standard library's io.Reader
// interface can be transparently adapted to receive future updates to
// normal files.
package tailpipe

import (
"errors"
"io"
"os"
"sync"
"time"
)

// The Follow function allows for the creation of a File with an underlying
// stream that may not implement all interfaces which a File implements. Such
// Files will return ErrNotSupported when this is the case.
var ErrNotSupported = errors.New("Operation not supported by underlying stream")

// A File represents an open normal file. A File is effectively of infinite
// length; all reads to the file will block until data are available,
// even if EOF on the underlying file is reached.
//
// The tailpipe package will attempt to detect when a file has been
// rotated. Programs that wish to be notified when such a rotation occurs
// should receive from the Rotated channel.
type File struct {
r io.Reader
Rotated <-chan struct{}
mu sync.Mutex
rc chan struct{}
}

// Read reads up to len(p) bytes into p. If end-of-file is reached, Read
// will block until new data are available. Read returns the number of
// bytes read and any errors other than io.EOF.
//
// If the underlying stream is an *os.File, Read will attempt to detect
// if it has been replaced, such as during log rotation. If so, Read will
// re-open the file at the original path provided to Open. Re-opening
// does not occur until the old file is exhausted.
func (f *File) Read(p []byte) (n int, err error) {
for {
n, err = f.r.Read(p)
if n == 0 && err == io.EOF {
time.Sleep(time.Millisecond * 100)
if file, ok := f.r.(*os.File); ok {
if file, ok = newFile(file); ok {
select {
case f.rc <- struct{}{}:
default:
}
f.r = file
}
}
} else {
break
}
}
if err == io.EOF {
return n, nil
}
return n, err
}

func newFile(oldfile *os.File) (*os.File, bool) {
// NOTE(droyo) We could save some code by using os.Stat()
// here. However, doing so is racy, as there is no guarantee
// that the file won't be rotated *again* between the time
// we stat() it and the time we open() it. os.File.Stat pulls
// the stat from the opened file descriptor.
newfile, err := os.Open(oldfile.Name())
if err != nil {
// NOTE(droyo) time will tell whether this is the right
// thing to do. The file could be gone for good, or we
// could just be in-between rotations. A (long) timeout
// could be better, but would be more complex.
return nil, false
}

if oldstat, err := oldfile.Stat(); err != nil {
oldfile.Close()
return newfile, true
} else if newstat, err := newfile.Stat(); err != nil {
newfile.Close()
return oldfile, false
} else if !os.SameFile(oldstat, newstat) {
oldfile.Close()
return newfile, true
}
newfile.Close()
return oldfile, false
}

// Open opens the given file for reading.
func Open(path string) (*File, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
return Follow(f), err
}

// Follow converts an existing io.Reader to a tailpipe.File. This can
// be useful when opening files with special permissions. In general,
// the behavior of a tailpipe.File is only suitable for normal files;
// using Follow on an io.Pipe, net.Conn, or other non-file stream will
// yield undesirable results.
func Follow(r io.Reader) *File {
// BUG(droyo): a File's Rotated channel should not be relied upon to
// provide an accurate count of file rotations; because the tailpipe
// will only perform a non-blocking send on the Rotated channel,
// a goroutine may miss a new notification while it is responding
// to a previous notification. This is addressed by buffering the
// channel, but can still be a problem with files that are rotated
// very frequently.
f := &File{r: r, rc: make(chan struct{}, 1)}
f.Rotated = f.rc
return f
}

// Name returns the name of the underlying file, if available. If the
// underlying stream does not have a name, Name returns an empty string.
func (f *File) Name() string {
type named interface {
Name() string
}
if v, ok := f.r.(named); ok {
return v.Name()
}
return ""
}

// Seek calls Seek on the underlying stream. If the underlying stream does
// not provide a Seek method, ErrNotSupported is returned.
func (f *File) Seek(offset int64, whence int) (int64, error) {
if v, ok := f.r.(io.Seeker); ok {
return v.Seek(offset, whence)
}
return 0, ErrNotSupported
}

// Close closes the underlying file or stream. It also has the
// side effect of closing the File's Rotated channel. Close
// is safe to call multiple times.
func (f *File) Close() error {
f.mu.Lock()
if f.rc != nil {
close(f.rc)
f.rc = nil
}
f.mu.Unlock()
if v, ok := f.r.(io.ReadCloser); ok {
return v.Close()
}
return ErrNotSupported
}
Loading

0 comments on commit 908e762

Please sign in to comment.