logger.Outlet: WriteEntry must not block

- make TCPOutlet fully asynchronous, dropping messages if connection is
  not fast enough
- syslog is just fine for now, local anyways
- stdout same thing

refs #26
This commit is contained in:
Christian Schwarz
2017-12-29 17:19:07 +01:00
parent 9a19615fd4
commit 839eccf513
6 changed files with 125 additions and 80 deletions
+78 -41
View File
@@ -1,6 +1,7 @@
package cmd
import (
"bytes"
"context"
"crypto/tls"
"github.com/pkg/errors"
@@ -16,7 +17,7 @@ type WriterOutlet struct {
Writer io.Writer
}
func (h WriterOutlet) WriteEntry(ctx context.Context, entry logger.Entry) error {
func (h WriterOutlet) WriteEntry(entry logger.Entry) error {
bytes, err := h.Formatter.Format(&entry)
if err != nil {
return err
@@ -27,56 +28,92 @@ func (h WriterOutlet) WriteEntry(ctx context.Context, entry logger.Entry) error
}
type TCPOutlet struct {
Formatter EntryFormatter
Net, Address string
Dialer net.Dialer
TLS *tls.Config
formatter EntryFormatter
// Specifies how much time must pass between a connection error and a reconnection attempt
// Log entries written to the outlet during this time interval are silently dropped.
RetryInterval time.Duration
// nil if there was an error sending / connecting to remote server
conn net.Conn
// Last time an error occurred when sending / connecting to remote server
retry time.Time
connect func(ctx context.Context) (net.Conn, error)
entryChan chan *bytes.Buffer
}
func (h *TCPOutlet) WriteEntry(ctx context.Context, e logger.Entry) error {
func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *tls.Config, retryInterval time.Duration) *TCPOutlet {
b, err := h.Formatter.Format(&e)
connect := func(ctx context.Context) (conn net.Conn, err error) {
deadl, ok := ctx.Deadline()
if !ok {
deadl = time.Time{}
}
dialer := net.Dialer{
Deadline: deadl,
}
if tlsConfig != nil {
conn, err = tls.DialWithDialer(&dialer, network, address, tlsConfig)
} else {
conn, err = dialer.DialContext(ctx, network, address)
}
return
}
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previos is in io.Copy()
o := &TCPOutlet{
formatter: formatter,
connect: connect,
entryChan: entryChan,
}
go o.outLoop(retryInterval)
return o
}
// FIXME: use this method
func (h *TCPOutlet) Close() {
close(h.entryChan)
}
func (h *TCPOutlet) outLoop(retryInterval time.Duration) {
var retry time.Time
var conn net.Conn
for msg := range h.entryChan {
var err error
for conn == nil {
time.Sleep(time.Until(retry))
ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(retryInterval))
conn, err = h.connect(ctx)
cancel()
if err != nil {
retry = time.Now().Add(retryInterval)
conn = nil
}
}
conn.SetWriteDeadline(time.Now().Add(retryInterval))
_, err = io.Copy(conn, msg)
if err != nil {
retry = time.Now().Add(retryInterval)
conn.Close()
conn = nil
}
}
}
func (h *TCPOutlet) WriteEntry(e logger.Entry) error {
ebytes, err := h.formatter.Format(&e)
if err != nil {
return err
}
if h.conn == nil {
if time.Now().Sub(h.retry) < h.RetryInterval {
// cool-down phase, drop the log entry
return nil
}
buf := new(bytes.Buffer)
buf.Write(ebytes)
buf.WriteString("\n")
if h.TLS != nil {
h.conn, err = tls.DialWithDialer(&h.Dialer, h.Net, h.Address, h.TLS)
} else {
h.conn, err = h.Dialer.DialContext(ctx, h.Net, h.Address)
}
if err != nil {
h.conn = nil
h.retry = time.Now()
return errors.Wrap(err, "cannot dial")
}
select {
case h.entryChan <- buf:
return nil
default:
return errors.New("connection broken or not fast enough")
}
_, err = h.conn.Write(b)
if err == nil {
_, err = h.conn.Write([]byte("\n"))
}
if err != nil {
h.conn.Close()
h.conn = nil
h.retry = time.Now()
return errors.Wrap(err, "cannot write")
}
return nil
}
type SyslogOutlet struct {
@@ -86,7 +123,7 @@ type SyslogOutlet struct {
lastConnectAttempt time.Time
}
func (o *SyslogOutlet) WriteEntry(ctx context.Context, entry logger.Entry) error {
func (o *SyslogOutlet) WriteEntry(entry logger.Entry) error {
bytes, err := o.Formatter.Format(&entry)
if err != nil {