mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
fn: introducing docker-syslog driver as default logger (#1189)
* fn: introducing docker-syslog driver as default logger With this change, fn-agent prefers RFC2454 docker-syslog driver for logging stdout/stderr from containers. The advantage of this is to offload it to docker itself instead of streaming stderr along with stdout, which gets multiplexed through single connection via docker-API. The change will need support from FDKs in order to log correct call-id and supress '\n' that splits syslog lines.
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log/syslog"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -696,7 +695,7 @@ type hotSlot struct {
|
|||||||
done chan struct{} // signal we are done with slot
|
done chan struct{} // signal we are done with slot
|
||||||
errC <-chan error // container error
|
errC <-chan error // container error
|
||||||
container *container // TODO mask this
|
container *container // TODO mask this
|
||||||
maxRespSize uint64 // TODO boo.
|
cfg *Config
|
||||||
fatalErr error
|
fatalErr error
|
||||||
containerSpan trace.SpanContext
|
containerSpan trace.SpanContext
|
||||||
}
|
}
|
||||||
@@ -741,23 +740,10 @@ func (s *hotSlot) exec(ctx context.Context, call *call) error {
|
|||||||
// NOTE: stderr is limited separately (though line writer is vulnerable to attack?)
|
// NOTE: stderr is limited separately (though line writer is vulnerable to attack?)
|
||||||
// limit the bytes allowed to be written to the stdout pipe, which handles any
|
// limit the bytes allowed to be written to the stdout pipe, which handles any
|
||||||
// buffering overflows (json to a string, http to a buffer, etc)
|
// buffering overflows (json to a string, http to a buffer, etc)
|
||||||
stdoutWrite := common.NewClampWriter(stdoutWritePipe, s.maxRespSize, models.ErrFunctionResponseTooBig)
|
stdoutWrite := common.NewClampWriter(stdoutWritePipe, s.cfg.MaxResponseSize, models.ErrFunctionResponseTooBig)
|
||||||
|
|
||||||
// get our own syslogger with THIS call id (cheap), using the container's already open syslog conns (expensive)
|
|
||||||
// TODO? we can basically just do this whether there are conns or not, this is relatively cheap (despite appearances)
|
|
||||||
buf1 := bufPool.Get().(*bytes.Buffer)
|
|
||||||
buf2 := bufPool.Get().(*bytes.Buffer)
|
|
||||||
defer bufPool.Put(buf1)
|
|
||||||
defer bufPool.Put(buf2)
|
|
||||||
|
|
||||||
sw := newSyslogWriter(call.ID, call.Path, call.AppName, syslog.LOG_ERR, s.container.syslogConns, buf1)
|
|
||||||
var syslog io.WriteCloser = &nopCloser{sw}
|
|
||||||
syslog = newLineWriterWithBuffer(buf2, syslog)
|
|
||||||
defer syslog.Close() // close syslogger from here, but NOT the call log stderr OR conns
|
|
||||||
stderr := multiWriteCloser{call.stderr, syslog} // use multiWriteCloser for its error ignoring properties
|
|
||||||
|
|
||||||
proto := protocol.New(protocol.Protocol(call.Format), stdinWrite, stdoutRead)
|
proto := protocol.New(protocol.Protocol(call.Format), stdinWrite, stdoutRead)
|
||||||
swapBack := s.container.swap(stdinRead, stdoutWrite, stderr, &call.Stats)
|
swapBack := s.container.swap(stdinRead, stdoutWrite, call.stderr, &call.Stats)
|
||||||
defer swapBack() // NOTE: it's important this runs before the pipes are closed.
|
defer swapBack() // NOTE: it's important this runs before the pipes are closed.
|
||||||
|
|
||||||
errApp := make(chan error, 1)
|
errApp := make(chan error, 1)
|
||||||
@@ -815,10 +801,17 @@ func (a *agent) prepCold(ctx context.Context, call *call, tok ResourceToken, ch
|
|||||||
cpus: uint64(call.CPUs),
|
cpus: uint64(call.CPUs),
|
||||||
fsSize: a.cfg.MaxFsSize,
|
fsSize: a.cfg.MaxFsSize,
|
||||||
timeout: time.Duration(call.Timeout) * time.Second, // this is unnecessary, but in case removal fails...
|
timeout: time.Duration(call.Timeout) * time.Second, // this is unnecessary, but in case removal fails...
|
||||||
stdin: call.req.Body,
|
logCfg: drivers.LoggerConfig{
|
||||||
stdout: common.NewClampWriter(call.w, a.cfg.MaxResponseSize, models.ErrFunctionResponseTooBig),
|
URL: strings.TrimSpace(call.SyslogURL),
|
||||||
stderr: call.stderr,
|
Tags: []drivers.LoggerTag{
|
||||||
stats: &call.Stats,
|
{Name: "app_name", Value: call.AppName},
|
||||||
|
{Name: "func_name", Value: call.Path},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stdin: call.req.Body,
|
||||||
|
stdout: common.NewClampWriter(call.w, a.cfg.MaxResponseSize, models.ErrFunctionResponseTooBig),
|
||||||
|
stderr: call.stderr,
|
||||||
|
stats: &call.Stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := a.driver.CreateCookie(ctx, container)
|
cookie, err := a.driver.CreateCookie(ctx, container)
|
||||||
@@ -899,7 +892,7 @@ func (a *agent) runHot(ctx context.Context, call *call, tok ResourceToken, state
|
|||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
errC: errC,
|
errC: errC,
|
||||||
container: container,
|
container: container,
|
||||||
maxRespSize: a.cfg.MaxResponseSize,
|
cfg: &a.cfg,
|
||||||
containerSpan: trace.FromContext(ctx).SpanContext(),
|
containerSpan: trace.FromContext(ctx).SpanContext(),
|
||||||
}
|
}
|
||||||
if !a.runHotReq(ctx, call, state, logger, cookie, slot) {
|
if !a.runHotReq(ctx, call, state, logger, cookie, slot) {
|
||||||
@@ -1028,11 +1021,11 @@ type container struct {
|
|||||||
fsSize uint64
|
fsSize uint64
|
||||||
tmpFsSize uint64
|
tmpFsSize uint64
|
||||||
timeout time.Duration // cold only (superfluous, but in case)
|
timeout time.Duration // cold only (superfluous, but in case)
|
||||||
|
logCfg drivers.LoggerConfig
|
||||||
|
|
||||||
stdin io.Reader
|
stdin io.Reader
|
||||||
stdout io.Writer
|
stdout io.Writer
|
||||||
stderr io.Writer
|
stderr io.Writer
|
||||||
syslogConns io.WriteCloser
|
|
||||||
|
|
||||||
// swapMu protects the stats swapping
|
// swapMu protects the stats swapping
|
||||||
swapMu sync.Mutex
|
swapMu sync.Mutex
|
||||||
@@ -1051,13 +1044,6 @@ func newHotContainer(ctx context.Context, call *call, cfg *Config) (*container,
|
|||||||
stderr := common.NewGhostWriter()
|
stderr := common.NewGhostWriter()
|
||||||
stdout := common.NewGhostWriter()
|
stdout := common.NewGhostWriter()
|
||||||
|
|
||||||
// these are only the conns, this doesn't write the syslog format (since it will change between calls)
|
|
||||||
syslogConns, err := syslogConns(ctx, call.SyslogURL)
|
|
||||||
if err != nil {
|
|
||||||
// TODO we could write this to between stderr but between stderr doesn't go to user either. kill me.
|
|
||||||
common.Logger(ctx).WithError(err).WithFields(logrus.Fields{"app_id": call.AppID, "path": call.Path, "image": call.Image, "container_id": id}).Error("error dialing syslog urls")
|
|
||||||
}
|
|
||||||
|
|
||||||
// for use if no freezer (or we ever make up our minds)
|
// for use if no freezer (or we ever make up our minds)
|
||||||
var bufs []*bytes.Buffer
|
var bufs []*bytes.Buffer
|
||||||
|
|
||||||
@@ -1070,7 +1056,7 @@ func newHotContainer(ctx context.Context, call *call, cfg *Config) (*container,
|
|||||||
|
|
||||||
// wrap the syslog and debug loggers in the same (respective) line writer
|
// wrap the syslog and debug loggers in the same (respective) line writer
|
||||||
// syslog complete chain for this (from top):
|
// syslog complete chain for this (from top):
|
||||||
// stderr -> line writer -> syslog -> []conns
|
// stderr -> line writer
|
||||||
|
|
||||||
// TODO(reed): I guess this is worth it
|
// TODO(reed): I guess this is worth it
|
||||||
// TODO(reed): there's a bug here where the between writers could have
|
// TODO(reed): there's a bug here where the between writers could have
|
||||||
@@ -1079,39 +1065,38 @@ func newHotContainer(ctx context.Context, call *call, cfg *Config) (*container,
|
|||||||
// and mostly came to the conclusion that life is meaningless.
|
// and mostly came to the conclusion that life is meaningless.
|
||||||
buf1 := bufPool.Get().(*bytes.Buffer)
|
buf1 := bufPool.Get().(*bytes.Buffer)
|
||||||
buf2 := bufPool.Get().(*bytes.Buffer)
|
buf2 := bufPool.Get().(*bytes.Buffer)
|
||||||
buf3 := bufPool.Get().(*bytes.Buffer)
|
bufs = []*bytes.Buffer{buf1, buf2}
|
||||||
buf4 := bufPool.Get().(*bytes.Buffer)
|
|
||||||
bufs = []*bytes.Buffer{buf1, buf2, buf3, buf4}
|
|
||||||
|
|
||||||
// stdout = LOG_INFO, stderr = LOG_ERR -- ONLY for the between writers, normal stdout is a response
|
soc := &nopCloser{&logWriter{
|
||||||
so := newSyslogWriter(call.ID, call.Path, call.AppName, syslog.LOG_INFO, syslogConns, buf1)
|
|
||||||
se := newSyslogWriter(call.ID, call.Path, call.AppName, syslog.LOG_ERR, syslogConns, buf2)
|
|
||||||
|
|
||||||
// use multiWriteCloser since it ignores errors (io.MultiWriter does not)
|
|
||||||
soc := multiWriteCloser{&nopCloser{so}, &nopCloser{&logWriter{
|
|
||||||
logrus.WithFields(logrus.Fields{"tag": "stdout", "app_id": call.AppID, "path": call.Path, "image": call.Image, "container_id": id}),
|
logrus.WithFields(logrus.Fields{"tag": "stdout", "app_id": call.AppID, "path": call.Path, "image": call.Image, "container_id": id}),
|
||||||
}}}
|
}}
|
||||||
sec := multiWriteCloser{&nopCloser{se}, &nopCloser{&logWriter{
|
sec := &nopCloser{&logWriter{
|
||||||
logrus.WithFields(logrus.Fields{"tag": "stderr", "app_id": call.AppID, "path": call.Path, "image": call.Image, "container_id": id}),
|
logrus.WithFields(logrus.Fields{"tag": "stderr", "app_id": call.AppID, "path": call.Path, "image": call.Image, "container_id": id}),
|
||||||
}}}
|
}}
|
||||||
|
|
||||||
stdout.Swap(newLineWriterWithBuffer(buf4, soc))
|
stdout.Swap(newLineWriterWithBuffer(buf1, soc))
|
||||||
stderr.Swap(newLineWriterWithBuffer(buf3, sec))
|
stderr.Swap(newLineWriterWithBuffer(buf2, sec))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &container{
|
return &container{
|
||||||
id: id, // XXX we could just let docker generate ids...
|
id: id, // XXX we could just let docker generate ids...
|
||||||
image: call.Image,
|
image: call.Image,
|
||||||
env: map[string]string(call.Config),
|
env: map[string]string(call.Config),
|
||||||
extensions: call.extensions,
|
extensions: call.extensions,
|
||||||
memory: call.Memory,
|
memory: call.Memory,
|
||||||
cpus: uint64(call.CPUs),
|
cpus: uint64(call.CPUs),
|
||||||
fsSize: cfg.MaxFsSize,
|
fsSize: cfg.MaxFsSize,
|
||||||
tmpFsSize: uint64(call.TmpFsSize),
|
tmpFsSize: uint64(call.TmpFsSize),
|
||||||
stdin: stdin,
|
logCfg: drivers.LoggerConfig{
|
||||||
stdout: stdout,
|
URL: strings.TrimSpace(call.SyslogURL),
|
||||||
stderr: stderr,
|
Tags: []drivers.LoggerTag{
|
||||||
syslogConns: syslogConns,
|
{Name: "app_name", Value: call.AppName},
|
||||||
|
{Name: "func_name", Value: call.Path},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stdin: stdin,
|
||||||
|
stdout: stdout,
|
||||||
|
stderr: stderr,
|
||||||
}, func() {
|
}, func() {
|
||||||
stdin.Close()
|
stdin.Close()
|
||||||
stderr.Close()
|
stderr.Close()
|
||||||
@@ -1119,7 +1104,6 @@ func newHotContainer(ctx context.Context, call *call, cfg *Config) (*container,
|
|||||||
for _, b := range bufs {
|
for _, b := range bufs {
|
||||||
bufPool.Put(b)
|
bufPool.Put(b)
|
||||||
}
|
}
|
||||||
syslogConns.Close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,21 +1128,22 @@ func (c *container) swap(stdin io.Reader, stdout, stderr io.Writer, cs *drivers.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *container) Id() string { return c.id }
|
func (c *container) Id() string { return c.id }
|
||||||
func (c *container) Command() string { return "" }
|
func (c *container) Command() string { return "" }
|
||||||
func (c *container) Input() io.Reader { return c.stdin }
|
func (c *container) Input() io.Reader { return c.stdin }
|
||||||
func (c *container) Logger() (io.Writer, io.Writer) { return c.stdout, c.stderr }
|
func (c *container) Logger() (io.Writer, io.Writer) { return c.stdout, c.stderr }
|
||||||
func (c *container) Volumes() [][2]string { return nil }
|
func (c *container) Volumes() [][2]string { return nil }
|
||||||
func (c *container) WorkDir() string { return "" }
|
func (c *container) WorkDir() string { return "" }
|
||||||
func (c *container) Close() {}
|
func (c *container) Close() {}
|
||||||
func (c *container) Image() string { return c.image }
|
func (c *container) Image() string { return c.image }
|
||||||
func (c *container) Timeout() time.Duration { return c.timeout }
|
func (c *container) Timeout() time.Duration { return c.timeout }
|
||||||
func (c *container) EnvVars() map[string]string { return c.env }
|
func (c *container) EnvVars() map[string]string { return c.env }
|
||||||
func (c *container) Memory() uint64 { return c.memory * 1024 * 1024 } // convert MB
|
func (c *container) Memory() uint64 { return c.memory * 1024 * 1024 } // convert MB
|
||||||
func (c *container) CPUs() uint64 { return c.cpus }
|
func (c *container) CPUs() uint64 { return c.cpus }
|
||||||
func (c *container) FsSize() uint64 { return c.fsSize }
|
func (c *container) FsSize() uint64 { return c.fsSize }
|
||||||
func (c *container) TmpFsSize() uint64 { return c.tmpFsSize }
|
func (c *container) TmpFsSize() uint64 { return c.tmpFsSize }
|
||||||
func (c *container) Extensions() map[string]string { return c.extensions }
|
func (c *container) Extensions() map[string]string { return c.extensions }
|
||||||
|
func (c *container) LoggerConfig() drivers.LoggerConfig { return c.logCfg }
|
||||||
|
|
||||||
// WriteStat publishes each metric in the specified Stats structure as a histogram metric
|
// WriteStat publishes each metric in the specified Stats structure as a histogram metric
|
||||||
func (c *container) WriteStat(ctx context.Context, stat drivers.Stat) {
|
func (c *container) WriteStat(ctx context.Context, stat drivers.Stat) {
|
||||||
|
|||||||
@@ -25,6 +25,34 @@ type cookie struct {
|
|||||||
drv *DockerDriver
|
drv *DockerDriver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *cookie) configureLogger(log logrus.FieldLogger) {
|
||||||
|
|
||||||
|
conf := c.task.LoggerConfig()
|
||||||
|
if conf.URL == "" {
|
||||||
|
c.opts.HostConfig.LogConfig = docker.LogConfig{
|
||||||
|
Type: "none",
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.opts.HostConfig.LogConfig = docker.LogConfig{
|
||||||
|
Type: "syslog",
|
||||||
|
Config: map[string]string{
|
||||||
|
"syslog-address": conf.URL,
|
||||||
|
"syslog-facility": "user",
|
||||||
|
"syslog-format": "rfc5424",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make([]string, 0, len(conf.Tags))
|
||||||
|
for _, pair := range conf.Tags {
|
||||||
|
tags = append(tags, fmt.Sprintf("%s=%s", pair.Name, pair.Value))
|
||||||
|
}
|
||||||
|
if len(tags) > 0 {
|
||||||
|
c.opts.HostConfig.LogConfig.Config["tag"] = strings.Join(tags, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *cookie) configureMem(log logrus.FieldLogger) {
|
func (c *cookie) configureMem(log logrus.FieldLogger) {
|
||||||
if c.task.Memory() == 0 {
|
if c.task.Memory() == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -217,15 +217,11 @@ func (drv *DockerDriver) CreateCookie(ctx context.Context, task drivers.Containe
|
|||||||
Image: task.Image(),
|
Image: task.Image(),
|
||||||
OpenStdin: true,
|
OpenStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
AttachStderr: true,
|
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
|
AttachStderr: true,
|
||||||
StdinOnce: true,
|
StdinOnce: true,
|
||||||
},
|
},
|
||||||
// turn off logs since we're collecting them from attach
|
|
||||||
HostConfig: &docker.HostConfig{
|
HostConfig: &docker.HostConfig{
|
||||||
LogConfig: docker.LogConfig{
|
|
||||||
Type: "none",
|
|
||||||
},
|
|
||||||
ReadonlyRootfs: drv.conf.EnableReadOnlyRootFs,
|
ReadonlyRootfs: drv.conf.EnableReadOnlyRootFs,
|
||||||
},
|
},
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
@@ -237,6 +233,7 @@ func (drv *DockerDriver) CreateCookie(ctx context.Context, task drivers.Containe
|
|||||||
drv: drv,
|
drv: drv,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cookie.configureLogger(log)
|
||||||
cookie.configureMem(log)
|
cookie.configureMem(log)
|
||||||
cookie.configureCmd(log)
|
cookie.configureCmd(log)
|
||||||
cookie.configureEnv(log)
|
cookie.configureEnv(log)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func (c *poolTask) CPUs() uint64 { return 0
|
|||||||
func (c *poolTask) FsSize() uint64 { return 0 }
|
func (c *poolTask) FsSize() uint64 { return 0 }
|
||||||
func (c *poolTask) TmpFsSize() uint64 { return 0 }
|
func (c *poolTask) TmpFsSize() uint64 { return 0 }
|
||||||
func (c *poolTask) Extensions() map[string]string { return nil }
|
func (c *poolTask) Extensions() map[string]string { return nil }
|
||||||
|
func (c *poolTask) LoggerConfig() drivers.LoggerConfig { return drivers.LoggerConfig{} }
|
||||||
func (c *poolTask) WriteStat(ctx context.Context, stat drivers.Stat) {}
|
func (c *poolTask) WriteStat(ctx context.Context, stat drivers.Stat) {}
|
||||||
|
|
||||||
type dockerPoolItem struct {
|
type dockerPoolItem struct {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func (f *taskDockerTest) WorkDir() string { return "" }
|
|||||||
func (f *taskDockerTest) Close() {}
|
func (f *taskDockerTest) Close() {}
|
||||||
func (f *taskDockerTest) Input() io.Reader { return f.input }
|
func (f *taskDockerTest) Input() io.Reader { return f.input }
|
||||||
func (f *taskDockerTest) Extensions() map[string]string { return nil }
|
func (f *taskDockerTest) Extensions() map[string]string { return nil }
|
||||||
|
func (f *taskDockerTest) LoggerConfig() drivers.LoggerConfig { return drivers.LoggerConfig{} }
|
||||||
|
|
||||||
func TestRunnerDocker(t *testing.T) {
|
func TestRunnerDocker(t *testing.T) {
|
||||||
dkr := NewDocker(drivers.Config{})
|
dkr := NewDocker(drivers.Config{})
|
||||||
|
|||||||
@@ -86,6 +86,21 @@ type RunResult interface {
|
|||||||
Status() string
|
Status() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logger Tags for container
|
||||||
|
type LoggerTag struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger Configuration for container
|
||||||
|
type LoggerConfig struct {
|
||||||
|
// Log Sink URL
|
||||||
|
URL string
|
||||||
|
|
||||||
|
// Log Tag Pairs
|
||||||
|
Tags []LoggerTag
|
||||||
|
}
|
||||||
|
|
||||||
// The ContainerTask interface guides container execution across a wide variety of
|
// The ContainerTask interface guides container execution across a wide variety of
|
||||||
// container oriented runtimes.
|
// container oriented runtimes.
|
||||||
type ContainerTask interface {
|
type ContainerTask interface {
|
||||||
@@ -136,6 +151,9 @@ type ContainerTask interface {
|
|||||||
// leaves it unset.
|
// leaves it unset.
|
||||||
WorkDir() string
|
WorkDir() string
|
||||||
|
|
||||||
|
// Logger Config to use in driver
|
||||||
|
LoggerConfig() LoggerConfig
|
||||||
|
|
||||||
// Close is used to perform cleanup after task execution.
|
// Close is used to perform cleanup after task execution.
|
||||||
// Close should be safe to call multiple times.
|
// Close should be safe to call multiple times.
|
||||||
Close()
|
Close()
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log/syslog"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fnproject/fn/api/common"
|
|
||||||
"go.opencensus.io/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// syslogConns may return a non-nil io.WriteCloser and an error simultaneously,
|
|
||||||
// the error containing any errors from connecting to any of the syslog URLs, and the
|
|
||||||
// io.WriteCloser writing to any syslogURLs that were successfully connected to.
|
|
||||||
// the returned io.WriteCloser is a Writer to each conn, it should be wrapped in another
|
|
||||||
// writer that writes syslog formatted messages (by line).
|
|
||||||
func syslogConns(ctx context.Context, syslogURLs string) (io.WriteCloser, error) {
|
|
||||||
// TODO(reed): we should likely add a trace per conn, need to plumb tagging better
|
|
||||||
ctx, span := trace.StartSpan(ctx, "syslog_conns")
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
if len(syslogURLs) == 0 {
|
|
||||||
return nullReadWriter{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// gather all the conns, re-use the line we make in the syslogWriter
|
|
||||||
// to write the same bytes to each of the conns.
|
|
||||||
var conns []io.WriteCloser
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
sinks := strings.Split(syslogURLs, ",")
|
|
||||||
for _, s := range sinks {
|
|
||||||
conn, err := dialSyslog(ctx, strings.TrimSpace(s))
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("failed to setup remote syslog connection to %v: %v", s, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
conns = append(conns, conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// do this before checking length of conns
|
|
||||||
var err error
|
|
||||||
if len(errs) > 0 {
|
|
||||||
for _, e := range errs {
|
|
||||||
err = fmt.Errorf("%v%v, ", err, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conns) == 0 {
|
|
||||||
return nullReadWriter{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return multiWriteCloser(conns), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialSyslog(ctx context.Context, syslogURL string) (io.WriteCloser, error) {
|
|
||||||
url, err := url.Parse(syslogURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
common.Logger(ctx).WithField("syslog_url", url).Debug("dialing syslog url")
|
|
||||||
|
|
||||||
var dialer net.Dialer
|
|
||||||
deadline, ok := ctx.Deadline()
|
|
||||||
if ok {
|
|
||||||
dialer.Deadline = deadline
|
|
||||||
}
|
|
||||||
|
|
||||||
// slice off 'xxx://' and dial it
|
|
||||||
switch url.Scheme {
|
|
||||||
case "udp", "tcp":
|
|
||||||
return dialer.Dial(url.Scheme, syslogURL[6:])
|
|
||||||
case "tls":
|
|
||||||
return tls.DialWithDialer(&dialer, "tcp", syslogURL[6:], nil)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Unsupported scheme, please use {tcp|udp|tls}: %s: ", url.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syslogWriter prepends a syslog format with call-specific details
|
|
||||||
// for each data segment provided in Write(). This doesn't use
|
|
||||||
// log/syslog pkg because we do not need pid for every line (expensive),
|
|
||||||
// and we have a format that is easier to read than hiding in preamble.
|
|
||||||
// this writes logfmt formatted syslog with values for call, function, and
|
|
||||||
// app, it is up to the user to use logfmt from their functions to get a
|
|
||||||
// fully formatted line out.
|
|
||||||
// TODO not pressing, but we could support json & other formats, too, upon request.
|
|
||||||
type syslogWriter struct {
|
|
||||||
pres []byte
|
|
||||||
post []byte
|
|
||||||
b *bytes.Buffer
|
|
||||||
clock func() time.Time
|
|
||||||
|
|
||||||
// the syslog conns (presumably)
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
const severityMask = 0x07
|
|
||||||
const facilityMask = 0xf8
|
|
||||||
|
|
||||||
func newSyslogWriter(call, function, appName string, severity syslog.Priority, wc io.Writer, buf *bytes.Buffer) *syslogWriter {
|
|
||||||
// Facility = LOG_USER
|
|
||||||
pr := (syslog.LOG_USER & facilityMask) | (severity & severityMask)
|
|
||||||
|
|
||||||
// <priority>VERSION ISOTIMESTAMP HOSTNAME APPLICATION PID MESSAGEID STRUCTURED-DATA MSG
|
|
||||||
//
|
|
||||||
// and for us:
|
|
||||||
// <22>2 ISOTIMESTAMP fn appName funcName callID - MSG
|
|
||||||
// ex:
|
|
||||||
//<11>2 2018-02-31T07:42:21Z Fn - - - - call_id=123 func_name=rdallman/yodawg app_id=123 loggo hereo
|
|
||||||
|
|
||||||
// TODO we could use json for structured data and do that whole thing. up to whoever.
|
|
||||||
return &syslogWriter{
|
|
||||||
pres: []byte(fmt.Sprintf(`<%d>2`, pr)),
|
|
||||||
post: []byte(fmt.Sprintf(`fn - - - - call_id=%s func_name=%s app_name=%s `, call, function, appName)),
|
|
||||||
b: buf,
|
|
||||||
Writer: wc,
|
|
||||||
clock: time.Now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sw *syslogWriter) Write(p []byte) (int, error) {
|
|
||||||
// re-use buffer to write in timestamp hodge podge and reduce writes to
|
|
||||||
// the conn by buffering a whole line here before writing to conn.
|
|
||||||
|
|
||||||
buf := sw.b
|
|
||||||
buf.Reset()
|
|
||||||
buf.Write(sw.pres)
|
|
||||||
buf.WriteString(" ")
|
|
||||||
buf.WriteString(sw.clock().UTC().Format(time.RFC3339))
|
|
||||||
buf.WriteString(" ")
|
|
||||||
buf.Write(sw.post)
|
|
||||||
buf.Write(p)
|
|
||||||
n, err := io.Copy(sw.Writer, buf)
|
|
||||||
return int(n), err
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log/syslog"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyslogFormat(t *testing.T) {
|
|
||||||
var b1 bytes.Buffer
|
|
||||||
var b2 bytes.Buffer
|
|
||||||
|
|
||||||
call := "12345"
|
|
||||||
fn := "yo/dawg"
|
|
||||||
appName := "myapp"
|
|
||||||
now := time.Date(1982, 6, 25, 12, 0, 0, 0, time.UTC)
|
|
||||||
clock := func() time.Time { return now }
|
|
||||||
|
|
||||||
writer := newSyslogWriter(call, fn, appName, syslog.LOG_ERR, &nopCloser{&b1}, &b2)
|
|
||||||
writer.clock = clock
|
|
||||||
writer.Write([]byte("yo"))
|
|
||||||
|
|
||||||
gold := `<11>2 1982-06-25T12:00:00Z fn - - - - call_id=12345 func_name=yo/dawg app_name=myapp yo`
|
|
||||||
|
|
||||||
if b1.String() != gold {
|
|
||||||
t.Fatal("syslog was not what we expected: ", b1.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,21 +85,19 @@ func (a *App) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if a.SyslogURL != nil && *a.SyslogURL != "" {
|
if a.SyslogURL != nil && *a.SyslogURL != "" {
|
||||||
sinks := strings.Split(*a.SyslogURL, ",")
|
url, err := url.Parse(strings.TrimSpace(*a.SyslogURL))
|
||||||
for _, s := range sinks {
|
fail := err != nil
|
||||||
url, err := url.Parse(strings.TrimSpace(s))
|
if !fail {
|
||||||
fail := err != nil
|
// See: https://docs.docker.com/config/containers/logging/syslog/#options
|
||||||
if !fail {
|
switch url.Scheme {
|
||||||
switch url.Scheme {
|
case "udp", "tcp", "unix", "unixgram", "tcp+tls":
|
||||||
case "udp", "tcp", "tls":
|
default:
|
||||||
default:
|
fail = true
|
||||||
fail = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fail {
|
|
||||||
return ErrInvalidSyslog(fmt.Sprintf(`invalid syslog url: "%v"`, s))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fail {
|
||||||
|
return ErrInvalidSyslog(fmt.Sprintf(`invalid syslog url: "%v"`, *a.SyslogURL))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,41 +13,26 @@ All logs are emitted in [logfmt](https://godoc.org/github.com/kr/logfmt) format
|
|||||||
## Call ID
|
## Call ID
|
||||||
|
|
||||||
Every function call/request is assigned a `call_id`. If you search your logs, you can track all the activity
|
Every function call/request is assigned a `call_id`. If you search your logs, you can track all the activity
|
||||||
for each function call and find errors on a call by call basis. For example, these are the log lines for an aynschronous
|
for each function call and find errors on a call by call basis.
|
||||||
function call:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Note the easily searchable `call_id=x` format.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
call_id=477949e2-922c-5da9-8633-0b2887b79f6e
|
|
||||||
```
|
|
||||||
|
|
||||||
## Remote syslog for functions
|
## Remote syslog for functions
|
||||||
|
|
||||||
You may add a syslog url to any function application and all functions that
|
Fn uses [docker syslog driver](https://docs.docker.com/config/containers/logging/syslog/) for remote logging if syslog URL is defined for the application. An example syslog URL is:
|
||||||
exist under that application will ship all of their logs to it. You may
|
|
||||||
provide a comma separated list, if desired. Currently, we support `tcp`,
|
|
||||||
`udp`, and `tls`, and this will not work if behind a proxy [yet?] (this is my
|
|
||||||
life now). This feature only works for 'hot' functions.
|
|
||||||
|
|
||||||
An example syslog url is:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
tls://logs.papertrailapp.com:1
|
tcp+tls://logs.papertrailapp.com:1
|
||||||
```
|
```
|
||||||
|
|
||||||
We log in a syslog format, with some variables added in logfmt format. If you
|
See docker syslog driver [syslog-address](https://docs.docker.com/config/containers/logging/syslog/#options) for supported formats.
|
||||||
find logfmt format offensive, please open an issue and we will consider adding
|
|
||||||
more formats (or open a PR that does it, with tests, and you will receive 1
|
|
||||||
free cookie along with the feature you want). The logs from the functions
|
|
||||||
themselves are not formatted, only our pre-amble, thus, if you'd like a fully
|
|
||||||
logfmt line, you must use a logfmt logger to log from your function.
|
|
||||||
|
|
||||||
* All log lines are sent as level error w/ the current time and `fn` as hostname.
|
Fn adds `func_name` and `app_name` tags to the container logs using [docker syslog driver tags](https://docs.docker.com/config/containers/logging/syslog/#options):
|
||||||
* call_id, func_name, and app_id will prefix every log line.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<11>2 1982-06-25T12:00:00Z fn - - - - call_id=12345 func_name=yo/yo app_id=54321 this is your log line
|
Aug 29 19:41:31 fnserver 178 <11>1 2018-08-29T19:41:31Z 59ff8628a941 app_name=fn-http-func,func_name=/fn-http-func 17 app_name=fn-http-func,func_name=/fn-http-func - 2018/08/29 19:41:31 Example log output
|
||||||
|
Aug 29 19:41:31 fnserver 154 <14>1 2018-08-29T19:41:31Z 59ff8628a941 app_name=fn-http-func,func_name=/fn-http-func 17 app_name=fn-http-func,func_name=/fn-http-func - HTTP/1.1 200 OK
|
||||||
|
Aug 29 19:41:31 fnserver 158 <14>1 2018-08-29T19:41:31Z 59ff8628a941 app_name=fn-http-func,func_name=/fn-http-func 17 app_name=fn-http-func,func_name=/fn-http-func - Content-Length: 11
|
||||||
|
Aug 29 19:41:31 fnserver 139 <14>1 2018-08-29T19:41:31Z 59ff8628a941 app_name=fn-http-func,func_name=/fn-http-func 17 app_name=fn-http-func,func_name=/fn-http-func -
|
||||||
|
Aug 29 19:41:31 fnserver 761 <14>1 2018-08-29T19:41:31Z 59ff8628a941 app_name=fn-http-func,func_name=/fn-http-func 17 app_name=fn-http-func,func_name=/fn-http-func - Hello World
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Currently, Fn does not add `call_id` to the log tags and it's user function responsibility to include this. FDKs will add support for this.
|
||||||
|
|||||||
Reference in New Issue
Block a user