Files
fn-serverless/api/agent/syslog.go
Reed Allman cbe0d5e9ac add user syslog writers to app (#970)
* add user syslog writers to app

users may specify a syslog url[s] on apps now and all functions under that app
will spew their logs out to it. the docs have more information around details
there, please review those (swagger and operating/logging.md), tried to
implement to spec in some parts and improve others, open to feedback on
format though, lots of liberty there.

design decision wise, I am looking to the future and ignoring cold containers.
the overhead of the connections there will not be worth it, so this feature
only works for hot functions, since we're killing cold anyway (even if a user
can just straight up exit a hot container).

syslog connections will be opened against a container when it starts up, and
then the call id that is logged gets swapped out for each call that goes
through the container, this cuts down on the cost of opening/closing
connections significantly. there are buffers to accumulate logs until we get a
`\n` to actually write a syslog line, and a buffer to save some bytes when
we're writing the syslog formatting as well. underneath writers re-use the
line writer in certain scenarios (swapper). we could likely improve the ease
of setting this up, but opening the syslog conns against a container seems
worth it, and is a different path than the other func loggers that we create
when we make a call object. the Close() stuff is a little tricky, not sure how
to make it easier and have the ^ benefits, open to idears.

this does add another vector of 'limits' to consider for more strict service
operators. one being how many syslog urls can a user add to an app (infinite,
atm) and the other being on the order of number of containers per host we
could run out of connections in certain scenarios. there may be some utility
in having multiple syslog sinks to send to, it could help with debugging at
times to send to another destination or if a user is a client w/ someone and
both want the function logs, e.g. (have used this for that in the past,
specifically).

this also doesn't work behind a proxy, which is something i'm open to fixing,
but afaict will require a 3rd party dependency (we can pretty much steal what
docker does). this is mostly of utility for those of us that work behind a
proxy all the time, not really for end users.

there are some unit tests. integration tests for this don't sound very fun to
maintain. I did test against papertrail with each protocol and it works (and
even times out if you're behind a proxy!).

closes #337

* add trace to syslog dial
2018-05-15 11:00:26 -07:00

146 lines
4.1 KiB
Go

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, app 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 appID 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_id=%s `, call, function, app)),
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
}