--- /dev/null
+// Package log implements a standard convention for structured logging.
+// Log entries are formatted as K=V pairs.
+// By default, output is written to stdout; this can be changed with SetOutput.
+package log
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "chain/errors"
+)
+
+const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+
+// context key type
+type key int
+
+var (
+ logWriterMu sync.Mutex // protects the following
+ logWriter io.Writer = os.Stdout
+ procPrefix []byte // process-global prefix; see SetPrefix vs AddPrefixkv
+
+ // pairDelims contains a list of characters that may be used as delimeters
+ // between key-value pairs in a log entry. Keys and values will be quoted or
+ // otherwise formatted to ensure that key-value extraction is unambiguous.
+ //
+ // The list of pair delimiters follows Splunk conventions, described here:
+ // http://answers.splunk.com/answers/143368/default-delimiters-for-key-value-extraction.html
+ pairDelims = " ,;|&\t\n\r"
+ illegalKeyChars = pairDelims + `="`
+
+ // context key for log line prefixes
+ prefixKey key = 0
+)
+
+// Conventional key names for log entries
+const (
+ KeyCaller = "at" // location of caller
+ KeyTime = "t" // time of call
+
+ KeyMessage = "message" // produced by Message
+ KeyError = "error" // produced by Error
+ KeyStack = "stack" // used by Printkv to print stack on subsequent lines
+
+ keyLogError = "log-error" // for errors produced by the log package itself
+)
+
+// SetOutput sets the log output to w.
+// If SetOutput hasn't been called,
+// the default behavior is to write to stdout.
+func SetOutput(w io.Writer) {
+ logWriterMu.Lock()
+ logWriter = w
+ logWriterMu.Unlock()
+}
+
+func appendPrefix(b []byte, keyval ...interface{}) []byte {
+ // Invariant: len(keyval) is always even.
+ if len(keyval)%2 != 0 {
+ panic(fmt.Sprintf("odd-length prefix args: %v", keyval))
+ }
+ for i := 0; i < len(keyval); i += 2 {
+ k := formatKey(keyval[i])
+ v := formatValue(keyval[i+1])
+ b = append(b, k...)
+ b = append(b, '=')
+ b = append(b, v...)
+ b = append(b, ' ')
+ }
+ return b
+}
+
+// SetPrefix sets the global output prefix.
+func SetPrefix(keyval ...interface{}) {
+ b := appendPrefix(nil, keyval...)
+ logWriterMu.Lock()
+ procPrefix = b
+ logWriterMu.Unlock()
+}
+
+// AddPrefixkv appends keyval to any prefix stored in ctx,
+// and returns a new context with the longer prefix.
+func AddPrefixkv(ctx context.Context, keyval ...interface{}) context.Context {
+ p := appendPrefix(prefix(ctx), keyval...)
+ // Note: subsequent calls will append to p, so set cap(p) here.
+ // See TestAddPrefixkvAppendTwice.
+ p = p[0:len(p):len(p)]
+ return context.WithValue(ctx, prefixKey, p)
+}
+
+func prefix(ctx context.Context) []byte {
+ b, _ := ctx.Value(prefixKey).([]byte)
+ return b
+}
+
+// Printkv prints a structured log entry to stdout. Log fields are
+// specified as a variadic sequence of alternating keys and values.
+//
+// Duplicate keys will be preserved.
+//
+// Two fields are automatically added to the log entry: t=[time]
+// and at=[file:line] indicating the location of the caller.
+// Use SkipFunc to prevent helper functions from showing up in the
+// at=[file:line] field.
+//
+// Printkv will also print the stack trace, if any, on separate lines
+// following the message. The stack is obtained from the following,
+// in order of preference:
+// - a KeyStack value with type []byte or []errors.StackFrame
+// - a KeyError value with type error, using the result of errors.Stack
+func Printkv(ctx context.Context, keyvals ...interface{}) {
+ // Invariant: len(keyvals) is always even.
+ if len(keyvals)%2 != 0 {
+ keyvals = append(keyvals, "", keyLogError, "odd number of log params")
+ }
+
+ t := time.Now().UTC()
+
+ // Prepend the log entry with auto-generated fields.
+ out := fmt.Sprintf(
+ "%s=%s %s=%s",
+ KeyCaller, caller(),
+ KeyTime, formatValue(t.Format(rfc3339NanoFixed)),
+ )
+
+ var stack interface{}
+ for i := 0; i < len(keyvals); i += 2 {
+ k := keyvals[i]
+ v := keyvals[i+1]
+ if k == KeyStack && isStackVal(v) {
+ stack = v
+ continue
+ }
+ if k == KeyError {
+ if e, ok := v.(error); ok && stack == nil {
+ stack = errors.Stack(errors.Wrap(e)) // wrap to ensure callstack
+ }
+ }
+ out += " " + formatKey(k) + "=" + formatValue(v)
+ }
+
+ logWriterMu.Lock()
+ logWriter.Write(procPrefix)
+ logWriter.Write(prefix(ctx))
+ logWriter.Write([]byte(out)) // ignore errors
+ logWriter.Write([]byte{'\n'})
+ writeRawStack(logWriter, stack)
+ logWriterMu.Unlock()
+}
+
+// Fatalkv is equivalent to Printkv() followed by a call to os.Exit(1).
+func Fatalkv(ctx context.Context, keyvals ...interface{}) {
+ Printkv(ctx, keyvals...)
+ os.Exit(1)
+}
+
+func writeRawStack(w io.Writer, v interface{}) {
+ switch v := v.(type) {
+ case []byte:
+ if len(v) > 0 {
+ w.Write(v)
+ w.Write([]byte{'\n'})
+ }
+ case []errors.StackFrame:
+ for _, s := range v {
+ io.WriteString(w, s.String())
+ w.Write([]byte{'\n'})
+ }
+ }
+}
+
+func isStackVal(v interface{}) bool {
+ switch v.(type) {
+ case []byte:
+ return true
+ case []errors.StackFrame:
+ return true
+ }
+ return false
+}
+
+// Printf prints a log entry containing a message assigned to the
+// "message" key. Arguments are handled as in fmt.Printf.
+func Printf(ctx context.Context, format string, a ...interface{}) {
+ Printkv(ctx, KeyMessage, fmt.Sprintf(format, a...))
+}
+
+// Error prints a log entry containing an error message assigned to the
+// "error" key.
+// Optionally, an error message prefix can be included. Prefix arguments are
+// handled as in fmt.Print.
+func Error(ctx context.Context, err error, a ...interface{}) {
+ if len(a) > 0 && len(errors.Stack(err)) > 0 {
+ err = errors.Wrap(err, a...) // keep err's stack
+ } else if len(a) > 0 {
+ err = fmt.Errorf("%s: %s", fmt.Sprint(a...), err) // don't add a stack here
+ }
+ Printkv(ctx, KeyError, err)
+}
+
+// formatKey ensures that the stringified key is valid for use in a
+// Splunk-style K=V format. It stubs out delimeter and quoter characters in
+// the key string with hyphens.
+func formatKey(k interface{}) string {
+ s := fmt.Sprint(k)
+ if s == "" {
+ return "?"
+ }
+
+ for _, c := range illegalKeyChars {
+ s = strings.Replace(s, string(c), "-", -1)
+ }
+
+ return s
+}
+
+// formatValue ensures that the stringified value is valid for use in a
+// Splunk-style K=V format. It quotes the string value if delimeter or quoter
+// characters are present in the value string.
+func formatValue(v interface{}) string {
+ s := fmt.Sprint(v)
+ if strings.ContainsAny(s, pairDelims) {
+ return strconv.Quote(s)
+ }
+ return s
+}
+
+// RecoverAndLogError must be used inside a defer.
+func RecoverAndLogError(ctx context.Context) {
+ if err := recover(); err != nil {
+ const size = 64 << 10
+ buf := make([]byte, size)
+ buf = buf[:runtime.Stack(buf, false)]
+ Printkv(ctx,
+ KeyMessage, "panic",
+ KeyError, err,
+ KeyStack, buf,
+ )
+ }
+}
--- /dev/null
+package log
+
+import (
+ "bytes"
+ "context"
+ "io/ioutil"
+ "os"
+ "reflect"
+ "strings"
+ "testing"
+
+ "chain/errors"
+)
+
+func TestSetOutput(t *testing.T) {
+ var buf bytes.Buffer
+ want := "foobar"
+ SetOutput(&buf)
+ Printf(context.Background(), want)
+ SetOutput(os.Stdout)
+ got := buf.String()
+ if !strings.Contains(got, want) {
+ t.Errorf("log = %q; should contain %q", got, want)
+ }
+}
+
+func TestNoExtraFormatDirectives(t *testing.T) {
+ buf := new(bytes.Buffer)
+ SetOutput(buf)
+ SetPrefix("foo", "bar")
+ Printkv(context.Background(), "baz", 1)
+ SetOutput(os.Stdout)
+ got := buf.String()
+ if strings.Contains(got, "%") {
+ t.Errorf("log line appears to contain format directive: %q", got)
+ }
+}
+
+func TestPrefix(t *testing.T) {
+ buf := new(bytes.Buffer)
+ SetOutput(buf)
+ SetPrefix("foo", "bar")
+ Printkv(context.Background(), "baz", 1)
+ SetOutput(os.Stdout)
+
+ got := buf.String()
+ wantPrefix := "foo=bar "
+ if !strings.HasPrefix(got, wantPrefix) {
+ t.Errorf("output = %q want prefix %q", got, wantPrefix)
+ }
+
+ SetPrefix()
+ if procPrefix != nil {
+ t.Errorf("procPrefix = %q want nil", procPrefix)
+ }
+}
+
+func TestAddPrefixkv0(t *testing.T) {
+ got := prefix(context.Background())
+ var want []byte = nil
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf(`prefix(context.Background()) = %#v want %#v`, got, want)
+ }
+}
+
+func TestAddPrefixkv1(t *testing.T) {
+ ctx := context.Background()
+ ctx1 := AddPrefixkv(ctx, "a", "b")
+ got := prefix(ctx1)
+ want := []byte("a=b ")
+ if !bytes.Equal(got, want) {
+ t.Errorf(`prefix(AddPrefixkv(bg, "a", "b")) = %q want %q`, got, want)
+ }
+}
+
+func TestAddPrefixkv2(t *testing.T) {
+ ctx := context.Background()
+ ctx1 := AddPrefixkv(ctx, "a", "b")
+ ctx2 := AddPrefixkv(ctx1, "c", "d")
+ got := prefix(ctx2)
+ want := []byte("a=b c=d ")
+ if !bytes.Equal(got, want) {
+ t.Errorf(`prefix(AddPrefixkv(AddPrefixkv(bg, "a", "b"), "c", "d")) = %q want %q`, got, want)
+ }
+}
+
+func TestAddPrefixkvAppendTwice(t *testing.T) {
+ ctx := context.Background()
+ ctx1 := AddPrefixkv(ctx, "a", "b")
+ ctx2a := AddPrefixkv(ctx1, "c", "d")
+ ctx2b := AddPrefixkv(ctx1, "e", "f")
+ gota := prefix(ctx2a)
+ wanta := []byte("a=b c=d ")
+ if !bytes.Equal(gota, wanta) {
+ t.Errorf(`prefix(AddPrefixkv(AddPrefixkv(bg, "a", "b"), "c", "d")) = %q want %q`, gota, wanta)
+ }
+ gotb := prefix(ctx2b)
+ wantb := []byte("a=b e=f ")
+ if !bytes.Equal(gotb, wantb) {
+ t.Errorf(`prefix(AddPrefixkv(AddPrefixkv(bg, "a", "b"), "e", "f")) = %q want %q`, gotb, wantb)
+ }
+}
+
+func TestPrintkv(t *testing.T) {
+ examples := []struct {
+ keyvals []interface{}
+ want []string
+ }{
+ // Basic example
+ {
+ keyvals: []interface{}{"msg", "hello world"},
+ want: []string{
+ "at=log_test.go:",
+ "t=",
+ `msg="hello world"`,
+ },
+ },
+
+ // Duplicate keys
+ {
+ keyvals: []interface{}{"msg", "hello world", "msg", "goodbye world"},
+ want: []string{
+ "at=log_test.go:",
+ "t=",
+ `msg="hello world"`,
+ `msg="goodbye world"`,
+ },
+ },
+
+ // Zero log params
+ {
+ keyvals: nil,
+ want: []string{
+ "at=log_test.go:",
+ "t=",
+ },
+ },
+
+ // Odd number of log params
+ {
+ keyvals: []interface{}{"k1", "v1", "k2"},
+ want: []string{
+ "at=log_test.go:",
+ "t=",
+ "k1=v1",
+ "k2=",
+ `log-error="odd number of log params"`,
+ },
+ },
+ }
+
+ for i, ex := range examples {
+ t.Log("Example", i)
+
+ buf := new(bytes.Buffer)
+ SetOutput(buf)
+
+ Printkv(context.Background(), ex.keyvals...)
+
+ read, err := ioutil.ReadAll(buf)
+ if err != nil {
+ SetOutput(os.Stdout)
+ t.Fatal("read buffer error:", err)
+ }
+
+ got := string(read)
+
+ for _, w := range ex.want {
+ if !strings.Contains(got, w) {
+ t.Errorf("Result did not contain string:\ngot: %s\nwant: %s", got, w)
+ }
+ }
+ if !strings.HasSuffix(got, "\n") {
+ t.Errorf("log output should end with a newline")
+ }
+
+ SetOutput(os.Stdout)
+ }
+}
+
+func TestMessagef(t *testing.T) {
+ buf := new(bytes.Buffer)
+ SetOutput(buf)
+ defer SetOutput(os.Stdout)
+
+ Printf(context.Background(), "test round %d", 0)
+
+ read, err := ioutil.ReadAll(buf)
+ if err != nil {
+ t.Fatal("read buffer error:", err)
+ }
+
+ got := string(read)
+ want := []string{
+ "at=log_test.go:",
+ `message="test round 0"`,
+ }
+
+ for _, w := range want {
+ if !strings.Contains(got, w) {
+ t.Errorf("Result did not contain string:\ngot: %s\nwant: %s", got, w)
+ }
+ }
+}
+
+func TestPrintkvStack(t *testing.T) {
+ buf := new(bytes.Buffer)
+ SetOutput(buf)
+ defer SetOutput(os.Stdout)
+
+ root := errors.New("boo")
+ wrapped := errors.Wrap(root)
+ Printkv(context.Background(), KeyError, wrapped)
+
+ read, err := ioutil.ReadAll(buf)
+ if err != nil {
+ t.Fatal("read buffer error:", err)
+ }
+
+ got := string(read)
+ want := []string{
+ "at=log_test.go:",
+ "error=boo",
+
+ // stack trace
+ "TestPrintkvStack\n",
+ "/go/",
+ }
+
+ t.Logf("output:\n%s", got)
+ for _, w := range want {
+ if !strings.Contains(got, w) {
+ t.Errorf("output %q did not contain %q", got, w)
+ }
+ }
+}
+
+func TestError(t *testing.T) {
+ buf := new(bytes.Buffer)
+ SetOutput(buf)
+ defer SetOutput(os.Stdout)
+
+ root := errors.New("boo")
+ wrapped := errors.Wrap(root)
+ Error(context.Background(), wrapped, "failure x ", 0)
+
+ read, err := ioutil.ReadAll(buf)
+ if err != nil {
+ t.Fatal("read buffer error:", err)
+ }
+
+ got := string(read)
+ want := []string{
+ "at=log_test.go:",
+ `error="failure x 0: boo"`,
+
+ // stack trace
+ "TestError\n",
+ "/go/",
+ }
+
+ t.Logf("output:\n%s", got)
+ for _, w := range want {
+ if !strings.Contains(got, w) {
+ t.Errorf("output %q did not contain %q", got, w)
+ }
+ }
+}
+
+func TestRawStack(t *testing.T) {
+ var buf bytes.Buffer
+ SetOutput(&buf)
+ defer SetOutput(os.Stdout)
+
+ stack := []byte("this\nis\na\nraw\nstack")
+ Printkv(context.Background(), "message", "foo", "stack", stack)
+
+ got := buf.String()
+ if !strings.HasSuffix(got, "\n"+string(stack)+"\n") {
+ t.Logf("output:\n%s", got)
+ t.Errorf("output did not contain %q", stack)
+ }
+}
+
+func TestIsStackVal(t *testing.T) {
+ cases := []struct {
+ v interface{}
+ w bool
+ }{
+ {[]byte("foo"), true},
+ {[]errors.StackFrame{}, true},
+ {"line1", false},
+ {[...]byte{'x'}, false},
+ {[]string{}, false},
+ }
+ for _, test := range cases {
+ if g := isStackVal(test.v); g != test.w {
+ t.Errorf("isStackVal(%#v) = %v want %v", test.v, g, test.w)
+ }
+ }
+}
+
+func TestWriteRawStack(t *testing.T) {
+ cases := []struct {
+ v interface{}
+ w string
+ }{
+ {[]byte("foo\nbar"), "foo\nbar\n"},
+ {[]errors.StackFrame{{Func: "foo", File: "f.go", Line: 1}}, "f.go:1 - foo\n"},
+ {1, ""}, // int is not a valid stack val
+ }
+
+ for _, test := range cases {
+ var buf bytes.Buffer
+ writeRawStack(&buf, test.v)
+ if g := buf.String(); g != test.w {
+ t.Errorf("writeRawStack(%#v) = %q want %q", test.v, g, test.w)
+ }
+ }
+}
+
+func TestFormatKey(t *testing.T) {
+ examples := []struct {
+ key interface{}
+ want string
+ }{
+ {"hello", "hello"},
+ {"hello world", "hello-world"},
+ {"", "?"},
+ {true, "true"},
+ {"a b\"c\nd;e\tf龜g", "a-b-c-d-e-f龜g"},
+ }
+
+ for i, ex := range examples {
+ t.Log("Example", i)
+ got := formatKey(ex.key)
+ if got != ex.want {
+ t.Errorf("formatKey(%#v) = %q want %q", ex.key, got, ex.want)
+ }
+ }
+}
+
+func TestFormatValue(t *testing.T) {
+ examples := []struct {
+ value interface{}
+ want string
+ }{
+ {"hello", "hello"},
+ {"hello world", `"hello world"`},
+ {1.5, "1.5"},
+ {true, "true"},
+ {errors.New("this is an error"), `"this is an error"`},
+ {[]byte{'a', 'b', 'c'}, `"[97 98 99]"`},
+ {bytes.NewBuffer([]byte{'a', 'b', 'c'}), "abc"},
+ {"a b\"c\nd;e\tf龜g", `"a b\"c\nd;e\tf龜g"`},
+ }
+
+ for i, ex := range examples {
+ t.Log("Example", i)
+ got := formatValue(ex.value)
+ if got != ex.want {
+ t.Errorf("formatKey(%#v) = %q want %q", ex.value, got, ex.want)
+ }
+ }
+}
--- /dev/null
+// Package rotation writes and rotates log files.
+package rotation
+
+import (
+ "bytes"
+ "os"
+ "strconv"
+)
+
+// A File is a log file with associated rotation files.
+// The rotation files are named after the base file
+// with a numeric suffix: base.1, base.2, and so on.
+// Calls to Write write data to the base file.
+// When the base file reaches the given size,
+// it is renamed to base.1
+// (and base.1 is renamed to base.2, and so on)
+// and a new base file is opened for subsequent writes.
+//
+// Any errors encountered while rotating files are ignored.
+// Only errors opening and writing the base file are reported.
+type File struct {
+ base string // file name
+ size int64 // max size of f (limit on w)
+ n int // number of rotated files
+ buf []byte // partial line from last write
+ f *os.File // current base file
+ w int64 // bytes written to f
+}
+
+// Create creates a log writing to the named file
+// with mode 0644 (before umask),
+// appending to it if it already exists.
+// It will rotate to files name.1, name.2,
+// up to name.n.
+// The minimum value for n is 1;
+// lesser values will be taken as 1.
+func Create(name string, size, n int) *File {
+ return &File{
+ base: name,
+ size: int64(size),
+ n: n,
+ }
+}
+
+var dropmsg = []byte("\nlog write error; some data dropped\n")
+
+// Write writes p to the log file f.
+// It writes only complete lines to the underlying file.
+// Incomplete lines are buffered in memory
+// and written once a NL is encountered.
+func (f *File) Write(p []byte) (n int, err error) {
+ f.buf = append(f.buf, p...)
+ n = len(p)
+ if i := bytes.LastIndexByte(f.buf, '\n'); i >= 0 {
+ _, err = f.write(f.buf[:i+1])
+ // Even if the write failed, discard the entire
+ // requested write payload. If we kept it around,
+ // then a failure to open the log file would
+ // cause us to accumulate data in memory
+ // without bound.
+ f.buf = f.buf[i+1:]
+ if err != nil {
+ // If we recover and resume logging,
+ // leave a message to indicate we dropped
+ // some lines.
+ f.buf = append(dropmsg, f.buf...)
+ }
+ }
+ return
+}
+
+// write writes the given data to f,
+// rotating files if necessary.
+func (f *File) write(p []byte) (int, error) {
+ // If p would increase the file over the
+ // max size, it is time to rotate.
+ if f.w+int64(len(p)) > f.size {
+ // best-effort; ignore errors
+ f.rotate()
+ f.f.Close()
+ f.f = nil
+ f.w = 0
+ }
+ if f.f == nil {
+ var err error
+ f.f, err = os.OpenFile(f.base, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) // #nosec
+ if err != nil {
+ return 0, err
+ }
+ f.w, err = f.f.Seek(0, os.SEEK_END)
+ if err != nil {
+ return 0, err
+ }
+ }
+ n, err := f.f.Write(p)
+ f.w += int64(n)
+ return n, err
+}
+
+func (f *File) rotate() {
+ for i := f.n - 1; i > 0; i-- {
+ os.Rename(f.name(i), f.name(i+1))
+ }
+ os.Rename(f.base, f.name(1))
+}
+
+func (f *File) name(i int) string {
+ return f.base + "." + strconv.Itoa(i)
+}
--- /dev/null
+package rotation
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func TestRotate(t *testing.T) {
+ defer os.Remove("x")
+ defer os.Remove("x.1")
+ defer os.Remove("x.2")
+ defer os.Remove("x.3") // just in case
+ os.Remove("x")
+ os.Remove("x.1")
+ os.Remove("x.2")
+ os.Remove("x.3") // just in case
+
+ f := Create("x", 1e6, 2)
+
+ touch("x")
+ f.rotate()
+ if !isRegular("x.1") {
+ t.Fatal("want rotated file x.1")
+ }
+ if isRegular("x.2") {
+ t.Fatal("want no rotated file x.2")
+ }
+ if isRegular("x.3") {
+ t.Fatal("want no rotated file x.3")
+ }
+
+ touch("x")
+ f.rotate()
+ if !isRegular("x.1") {
+ t.Fatal("want rotated file x.1")
+ }
+ if !isRegular("x.2") {
+ t.Fatal("want rotated file x.2")
+ }
+ if isRegular("x.3") {
+ t.Fatal("want no rotated file x.3")
+ }
+
+ touch("x")
+ f.rotate()
+ if !isRegular("x.1") {
+ t.Fatal("want rotated file x.1")
+ }
+ if !isRegular("x.2") {
+ t.Fatal("want rotated file x.2")
+ }
+ if isRegular("x.3") {
+ t.Fatal("want no rotated file x.3")
+ }
+}
+
+func TestRotate0(t *testing.T) {
+ defer os.Remove("x")
+ defer os.Remove("x.1")
+ defer os.Remove("x.2") // just in case
+ os.Remove("x")
+ os.Remove("x.1")
+ os.Remove("x.2") // just in case
+
+ f := Create("x", 1e6, 0)
+
+ touch("x")
+ f.rotate()
+ if !isRegular("x.1") {
+ t.Fatal("want rotated file x.1")
+ }
+ if isRegular("x.2") {
+ t.Fatal("want no rotated file x.2")
+ }
+
+ touch("x")
+ f.rotate()
+ if !isRegular("x.1") {
+ t.Fatal("want rotated file x.1")
+ }
+ if isRegular("x.2") {
+ t.Fatal("want no rotated file x.2")
+ }
+}
+
+func TestInternalWriteNoRotate(t *testing.T) {
+ defer os.Remove("x")
+ defer os.Remove("x.1") // just in case
+ os.Remove("x")
+ os.Remove("x.1") // just in case
+
+ f := &File{
+ base: "x",
+ n: 1,
+ size: 10,
+ w: 0,
+ }
+ b := []byte("abc\n")
+ n, err := f.write(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != len(b) {
+ t.Fatalf("write(%q) = %d want %d", b, n, len(b))
+ }
+ if isRegular("x.1") {
+ t.Fatal("want no rotated file x.1")
+ }
+ if f.w != int64(len(b)) {
+ t.Fatalf("f.w = %d want %d", f.w, len(b))
+ }
+}
+
+func TestInternalWriteRotate(t *testing.T) {
+ defer os.Remove("x")
+ defer os.Remove("x.1")
+ os.Remove("x")
+ os.Remove("x.1")
+
+ f0, err := os.Create("x")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ f := &File{
+ base: "x",
+ n: 1,
+ size: 10,
+ w: 9,
+ f: f0,
+ }
+ b := []byte("abc\n")
+ n, err := f.write(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != len(b) {
+ t.Fatalf("write(%q) = %d want %d", b, n, 4)
+ }
+ if !isRegular("x.1") {
+ t.Fatal("want rotated file x.1")
+ }
+ if f.f == f0 {
+ t.Fatal("want new file object")
+ }
+ if f.w != int64(len(b)) {
+ t.Fatalf("f.w = %d want %d", f.w, len(b))
+ }
+}
+
+func TestWrite(t *testing.T) {
+ defer os.Remove("x")
+ defer os.Remove("x.1")
+ os.Remove("x")
+ os.Remove("x.1")
+ f := Create("x", 1e6, 1)
+
+ b0 := []byte("ab")
+ n, err := f.Write(b0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != len(b0) {
+ t.Fatalf("n = %d want %d", n, len(b0))
+ }
+ if !bytes.Equal(f.buf, b0) {
+ t.Fatalf("buf = %q want %q", f.buf, b0)
+ }
+ if f.w != 0 {
+ t.Fatalf("w = %d want 0", f.w)
+ }
+
+ b1 := []byte("c\n")
+ n, err = f.Write(b1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != len(b1) {
+ t.Fatalf("n = %d want %d", n, len(b1))
+ }
+ if !bytes.Equal(f.buf, nil) {
+ t.Fatalf("buf = %q want %q", f.buf, "")
+ }
+ if f.w != int64(len(b0)+len(b1)) {
+ t.Fatalf("w = %d want %d", f.w, len(b0)+len(b1))
+ }
+}
+
+func TestOpenFail(t *testing.T) {
+ f := Create("/tmp/improbable-test-path/second-level/x", 1e6, 1)
+ b := []byte("abc\nd")
+ want := append(dropmsg, 'd')
+ n, err := f.Write(b)
+ if err == nil {
+ t.Error("want error")
+ }
+ if n != len(b) {
+ t.Errorf("n = %d want %d", n, len(b))
+ }
+ if !bytes.Equal(f.buf, want) {
+ t.Fatalf("buf = %q want %q", f.buf, want)
+ }
+
+ n, err = f.Write(b)
+ if err == nil {
+ t.Error("want error")
+ }
+ if n != len(b) {
+ t.Errorf("n = %d want %d", n, len(b))
+ }
+ // don't accumulate multiple dropped-log messages
+ if !bytes.Equal(f.buf, want) {
+ t.Fatalf("buf = %q want %q", f.buf, want)
+ }
+}
+
+func TestAppend(t *testing.T) {
+ defer os.Remove("x")
+ b0 := []byte("abc\n")
+ b1 := []byte("def\n")
+ err := ioutil.WriteFile("x", b0, 0666)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f := Create("x", 100, 1)
+ f.Write(b1)
+ if want := int64(len(b0) + len(b1)); f.w != want {
+ t.Fatalf("w = %d want %d", f.w, want)
+ }
+}
+
+func isRegular(name string) bool {
+ fi, err := os.Stat(name)
+ return err == nil && fi.Mode().IsRegular()
+}
+
+func touch(name string) {
+ f, _ := os.Create(name)
+ if f != nil {
+ f.Close()
+ }
+}
--- /dev/null
+// Package splunk sends log data to a splunk server.
+package splunk
+
+import (
+ "io"
+ "net"
+ "time"
+)
+
+const (
+ // DialTimeout limits how long a write will block
+ // while dialing the splunk server. Assuming the
+ // connection stays open for a long time, this will
+ // happen only rarely.
+ DialTimeout = 50 * time.Millisecond
+
+ // WriteTimeout limits how long a write will block
+ // sending data on the network. It is deliberately
+ // very small, so that writes can only be satisfied
+ // by the local network buffers. It should never
+ // block waiting for a TCP ACK for an appreciable
+ // amount of time.
+ WriteTimeout = 100 * time.Microsecond
+)
+
+type splunk struct {
+ addr string
+ conn net.Conn
+ dropmsg []byte
+ err error // last write error
+}
+
+// New creates a new writer that sends data
+// to the given TCP address.
+// It connects on the first call to Write,
+// and attempts to reconnect when necessary,
+// with a timeout of DialTimeout.
+//
+// Every write has a timeout of WriteTimeout.
+// If the write doesn't complete in that time,
+// the writer drops the unwritten data.
+// For every contiguous run of dropped data,
+// it writes dropmsg before resuming ordinary writes.
+// As long as the remote endpoint can keep up
+// with the averate data rate and the local
+// network buffers in the kernel and NIC are
+// big enough to hold traffic bursts, no data
+// will be lost.
+func New(addr string, dropmsg []byte) io.Writer {
+ return &splunk{
+ addr: addr,
+ dropmsg: dropmsg,
+ }
+}
+
+func (s *splunk) Write(p []byte) (n int, err error) {
+ if s.conn == nil {
+ s.conn, err = net.DialTimeout("tcp", s.addr, DialTimeout)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ if s.err != nil {
+ s.conn.SetDeadline(time.Now().Add(WriteTimeout))
+ _, s.err = s.conn.Write(s.dropmsg)
+ }
+ if s.err == nil {
+ s.conn.SetDeadline(time.Now().Add(WriteTimeout))
+ n, s.err = s.conn.Write(p)
+ }
+
+ if t, ok := s.err.(net.Error); s.err != nil && (!ok || !t.Temporary()) {
+ s.conn.Close()
+ s.conn = nil
+ }
+ return n, s.err
+}
--- /dev/null
+package log
+
+import (
+ "path/filepath"
+ "runtime"
+ "strconv"
+)
+
+var skipFunc = map[string]bool{
+ "chain/log.Printkv": true,
+ "chain/log.Printf": true,
+ "chain/log.Error": true,
+ "chain/log.Fatalkv": true,
+ "chain/log.RecoverAndLogError": true,
+}
+
+// SkipFunc removes the named function from stack traces
+// and at=[file:line] entries printed to the log output.
+// The provided name should be a fully-qualified function name
+// comprising the import path and identifier separated by a dot.
+// For example, chain/log.Printkv.
+// SkipFunc must not be called concurrently with any function
+// in this package (including itself).
+func SkipFunc(name string) {
+ skipFunc[name] = true
+}
+
+// caller returns a string containing filename and line number of
+// the deepest function invocation on the calling goroutine's stack,
+// after skipping functions in skipFunc.
+// If no stack information is available, it returns "?:?".
+func caller() string {
+ for i := 1; ; i++ {
+ // NOTE(kr): This is quadratic in the number of frames we
+ // ultimately have to skip. Consider using Callers instead.
+ pc, file, line, ok := runtime.Caller(i)
+ if !ok {
+ return "?:?"
+ }
+ if !skipFunc[runtime.FuncForPC(pc).Name()] {
+ return filepath.Base(file) + ":" + strconv.Itoa(line)
+ }
+ }
+}
--- /dev/null
+/*
+Package checked implements basic arithmetic operations
+with underflow and overflow checks.
+*/
+package checked
+
+import (
+ "errors"
+ "math"
+)
+
+var ErrOverflow = errors.New("arithmetic overflow")
+
+// AddInt64 returns a + b
+// with an integer overflow check.
+func AddInt64(a, b int64) (sum int64, ok bool) {
+ if (b > 0 && a > math.MaxInt64-b) ||
+ (b < 0 && a < math.MinInt64-b) {
+ return 0, false
+ }
+ return a + b, true
+}
+
+// SubInt64 returns a - b
+// with an integer overflow check.
+func SubInt64(a, b int64) (diff int64, ok bool) {
+ if (b > 0 && a < math.MinInt64+b) ||
+ (b < 0 && a > math.MaxInt64+b) {
+ return 0, false
+ }
+ return a - b, true
+}
+
+// MulInt64 returns a * b
+// with an integer overflow check.
+func MulInt64(a, b int64) (product int64, ok bool) {
+ if (a > 0 && b > 0 && a > math.MaxInt64/b) ||
+ (a > 0 && b <= 0 && b < math.MinInt64/a) ||
+ (a <= 0 && b > 0 && a < math.MinInt64/b) ||
+ (a < 0 && b <= 0 && b < math.MaxInt64/a) {
+ return 0, false
+ }
+ return a * b, true
+}
+
+// DivInt64 returns a / b
+// with an integer overflow check.
+func DivInt64(a, b int64) (quotient int64, ok bool) {
+ if b == 0 || (a == math.MinInt64 && b == -1) {
+ return 0, false
+ }
+ return a / b, true
+}
+
+// ModInt64 returns a % b
+// with an integer overflow check.
+func ModInt64(a, b int64) (remainder int64, ok bool) {
+ if b == 0 || (a == math.MinInt64 && b == -1) {
+ return 0, false
+ }
+ return a % b, true
+}
+
+// NegateInt64 returns -a
+// with an integer overflow check.
+func NegateInt64(a int64) (negated int64, ok bool) {
+ if a == math.MinInt64 {
+ return 0, false
+ }
+ return -a, true
+}
+
+// LshiftInt64 returns a << b
+// with an integer overflow check.
+func LshiftInt64(a, b int64) (result int64, ok bool) {
+ if b < 0 || b >= 64 {
+ return 0, false
+ }
+ if (a >= 0 && a > math.MaxInt64>>uint(b)) || (a < 0 && a < math.MinInt64>>uint(b)) {
+ return 0, false
+ }
+ return a << uint(b), true
+}
+
+// AddInt32 returns a + b
+// with an integer overflow check.
+func AddInt32(a, b int32) (sum int32, ok bool) {
+ if (b > 0 && a > math.MaxInt32-b) ||
+ (b < 0 && a < math.MinInt32-b) {
+ return 0, false
+ }
+ return a + b, true
+}
+
+// SubInt32 returns a - b
+// with an integer overflow check.
+func SubInt32(a, b int32) (diff int32, ok bool) {
+ if (b > 0 && a < math.MinInt32+b) ||
+ (b < 0 && a > math.MaxInt32+b) {
+ return 0, false
+ }
+ return a - b, true
+}
+
+// MulInt32 returns a * b
+// with an integer overflow check.
+func MulInt32(a, b int32) (product int32, ok bool) {
+ if (a > 0 && b > 0 && a > math.MaxInt32/b) ||
+ (a > 0 && b <= 0 && b < math.MinInt32/a) ||
+ (a <= 0 && b > 0 && a < math.MinInt32/b) ||
+ (a < 0 && b <= 0 && b < math.MaxInt32/a) {
+ return 0, false
+ }
+ return a * b, true
+}
+
+// DivInt32 returns a / b
+// with an integer overflow check.
+func DivInt32(a, b int32) (quotient int32, ok bool) {
+ if b == 0 || (a == math.MinInt32 && b == -1) {
+ return 0, false
+ }
+ return a / b, true
+}
+
+// ModInt32 returns a % b
+// with an integer overflow check.
+func ModInt32(a, b int32) (remainder int32, ok bool) {
+ if b == 0 || (a == math.MinInt32 && b == -1) {
+ return 0, false
+ }
+ return a % b, true
+}
+
+// NegateInt32 returns -a
+// with an integer overflow check.
+func NegateInt32(a int32) (negated int32, ok bool) {
+ if a == math.MinInt32 {
+ return 0, false
+ }
+ return -a, true
+}
+
+// LshiftInt32 returns a << b
+// with an integer overflow check.
+func LshiftInt32(a, b int32) (result int32, ok bool) {
+ if b < 0 || b >= 32 {
+ return 0, false
+ }
+ if (a >= 0 && a > math.MaxInt32>>uint(b)) || (a < 0 && a < math.MinInt32>>uint(b)) {
+ return 0, false
+ }
+ return a << uint(b), true
+}
+
+// AddUint64 returns a + b
+// with an integer overflow check.
+func AddUint64(a, b uint64) (sum uint64, ok bool) {
+ if math.MaxUint64-a < b {
+ return 0, false
+ }
+ return a + b, true
+}
+
+// SubUint64 returns a - b
+// with an integer overflow check.
+func SubUint64(a, b uint64) (diff uint64, ok bool) {
+ if a < b {
+ return 0, false
+ }
+ return a - b, true
+}
+
+// MulUint64 returns a * b
+// with an integer overflow check.
+func MulUint64(a, b uint64) (product uint64, ok bool) {
+ if b > 0 && a > math.MaxUint64/b {
+ return 0, false
+ }
+ return a * b, true
+}
+
+// DivUint64 returns a / b
+// with an integer overflow check.
+func DivUint64(a, b uint64) (quotient uint64, ok bool) {
+ if b == 0 {
+ return 0, false
+ }
+ return a / b, true
+}
+
+// ModUint64 returns a % b
+// with an integer overflow check.
+func ModUint64(a, b uint64) (remainder uint64, ok bool) {
+ if b == 0 {
+ return 0, false
+ }
+ return a % b, true
+}
+
+// LshiftUint64 returns a << b
+// with an integer overflow check.
+func LshiftUint64(a, b uint64) (result uint64, ok bool) {
+ if b >= 64 {
+ return 0, false
+ }
+ if a > math.MaxUint64>>uint(b) {
+ return 0, false
+ }
+ return a << uint(b), true
+}
+
+// AddUint32 returns a + b
+// with an integer overflow check.
+func AddUint32(a, b uint32) (sum uint32, ok bool) {
+ if math.MaxUint32-a < b {
+ return 0, false
+ }
+ return a + b, true
+}
+
+// SubUint32 returns a - b
+// with an integer overflow check.
+func SubUint32(a, b uint32) (diff uint32, ok bool) {
+ if a < b {
+ return 0, false
+ }
+ return a - b, true
+}
+
+// MulUint32 returns a * b
+// with an integer overflow check.
+func MulUint32(a, b uint32) (product uint32, ok bool) {
+ if b > 0 && a > math.MaxUint32/b {
+ return 0, false
+ }
+ return a * b, true
+}
+
+// DivUint32 returns a / b
+// with an integer overflow check.
+func DivUint32(a, b uint32) (quotient uint32, ok bool) {
+ if b == 0 {
+ return 0, false
+ }
+ return a / b, true
+}
+
+// ModUint32 returns a % b
+// with an integer overflow check.
+func ModUint32(a, b uint32) (remainder uint32, ok bool) {
+ if b == 0 {
+ return 0, false
+ }
+ return a % b, true
+}
+
+// LshiftUint32 returns a << b
+// with an integer overflow check.
+func LshiftUint32(a, b uint32) (result uint32, ok bool) {
+ if b >= 32 {
+ return 0, false
+ }
+ if a > math.MaxUint32>>uint(b) {
+ return 0, false
+ }
+ return a << uint(b), true
+}
--- /dev/null
+package checked
+
+import (
+ "math"
+ "reflect"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+func TestInt64(t *testing.T) {
+ cases := []struct {
+ f func(a, b int64) (int64, bool)
+ a, b, want int64
+ wantOk bool
+ }{
+ {AddInt64, 2, 3, 5, true},
+ {AddInt64, 2, -3, -1, true},
+ {AddInt64, -2, -3, -5, true},
+ {AddInt64, math.MaxInt64, 1, 0, false},
+ {AddInt64, math.MinInt64, math.MinInt64, 0, false},
+ {AddInt64, math.MinInt64, -1, 0, false},
+ {SubInt64, 3, 2, 1, true},
+ {SubInt64, 2, 3, -1, true},
+ {SubInt64, -2, -3, 1, true},
+ {SubInt64, math.MinInt64, 1, 0, false},
+ {SubInt64, -2, math.MaxInt64, 0, false},
+ {MulInt64, 2, 3, 6, true},
+ {MulInt64, -2, -3, 6, true},
+ {MulInt64, -2, 3, -6, true},
+ {MulInt64, math.MaxInt64, -1, math.MinInt64 + 1, true},
+ {MulInt64, math.MinInt64, 2, 0, false},
+ {MulInt64, math.MaxInt64, 2, 0, false},
+ {MulInt64, 2, math.MinInt64, 0, false},
+ {MulInt64, -2, math.MinInt64, 0, false},
+ {DivInt64, 2, 2, 1, true},
+ {DivInt64, -2, -2, 1, true},
+ {DivInt64, -2, 2, -1, true},
+ {DivInt64, 1, 0, 0, false},
+ {DivInt64, math.MinInt64, -1, 0, false},
+ {ModInt64, 3, 2, 1, true},
+ {ModInt64, -3, -2, -1, true},
+ {ModInt64, -3, 2, -1, true},
+ {ModInt64, 1, 0, 0, false},
+ {ModInt64, math.MinInt64, -1, 0, false},
+ {LshiftInt64, 1, 2, 4, true},
+ {LshiftInt64, -1, 2, -4, true},
+ {LshiftInt64, 1, 64, 0, false},
+ {LshiftInt64, 2, 63, 0, false},
+ }
+
+ for _, c := range cases {
+ got, gotOk := c.f(c.a, c.b)
+
+ if got != c.want {
+ t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+ }
+
+ if gotOk != c.wantOk {
+ t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+ }
+ }
+
+ negateCases := []struct {
+ a, want int64
+ wantOk bool
+ }{
+ {1, -1, true},
+ {-1, 1, true},
+ {0, 0, true},
+ {math.MinInt64, 0, false},
+ }
+ for _, c := range negateCases {
+ got, gotOk := NegateInt64(c.a)
+
+ if got != c.want {
+ t.Errorf("NegateInt64(%d) = %d want %d", c.a, got, c.want)
+ }
+
+ if gotOk != c.wantOk {
+ t.Errorf("NegateInt64(%d) ok = %v want %v", c.a, gotOk, c.wantOk)
+ }
+ }
+}
+
+func TestUint64(t *testing.T) {
+ cases := []struct {
+ f func(a, b uint64) (uint64, bool)
+ a, b, want uint64
+ wantOk bool
+ }{
+ {AddUint64, 2, 3, 5, true},
+ {AddUint64, math.MaxUint64, 1, 0, false},
+ {SubUint64, 3, 2, 1, true},
+ {SubUint64, 2, 3, 0, false},
+ {MulUint64, 2, 3, 6, true},
+ {MulUint64, math.MaxUint64, 2, 0, false},
+ {DivUint64, 2, 2, 1, true},
+ {DivUint64, 1, 0, 0, false},
+ {ModUint64, 3, 2, 1, true},
+ {ModUint64, 1, 0, 0, false},
+ {LshiftUint64, 1, 2, 4, true},
+ {LshiftUint64, 1, 64, 0, false},
+ {LshiftUint64, 2, 63, 0, false},
+ }
+
+ for _, c := range cases {
+ got, gotOk := c.f(c.a, c.b)
+
+ if got != c.want {
+ t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+ }
+
+ if gotOk != c.wantOk {
+ t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+ }
+ }
+}
+
+func TestInt32(t *testing.T) {
+ cases := []struct {
+ f func(a, b int32) (int32, bool)
+ a, b, want int32
+ wantOk bool
+ }{
+ {AddInt32, 2, 3, 5, true},
+ {AddInt32, 2, -3, -1, true},
+ {AddInt32, -2, -3, -5, true},
+ {AddInt32, math.MaxInt32, 1, 0, false},
+ {AddInt32, math.MinInt32, math.MinInt32, 0, false},
+ {AddInt32, math.MinInt32, -1, 0, false},
+ {SubInt32, 3, 2, 1, true},
+ {SubInt32, 2, 3, -1, true},
+ {SubInt32, -2, -3, 1, true},
+ {SubInt32, math.MinInt32, 1, 0, false},
+ {SubInt32, -2, math.MaxInt32, 0, false},
+ {MulInt32, 2, 3, 6, true},
+ {MulInt32, -2, -3, 6, true},
+ {MulInt32, -2, 3, -6, true},
+ {MulInt32, math.MaxInt32, -1, math.MinInt32 + 1, true},
+ {MulInt32, math.MinInt32, 2, 0, false},
+ {MulInt32, math.MaxInt32, 2, 0, false},
+ {MulInt32, 2, math.MinInt32, 0, false},
+ {MulInt32, -2, math.MinInt32, 0, false},
+ {DivInt32, 2, 2, 1, true},
+ {DivInt32, -2, -2, 1, true},
+ {DivInt32, -2, 2, -1, true},
+ {DivInt32, 1, 0, 0, false},
+ {DivInt32, math.MinInt32, -1, 0, false},
+ {ModInt32, 3, 2, 1, true},
+ {ModInt32, -3, -2, -1, true},
+ {ModInt32, -3, 2, -1, true},
+ {ModInt32, 1, 0, 0, false},
+ {ModInt32, math.MinInt32, -1, 0, false},
+ {LshiftInt32, 1, 2, 4, true},
+ {LshiftInt32, -1, 2, -4, true},
+ {LshiftInt32, 1, 32, 0, false},
+ {LshiftInt32, 2, 31, 0, false},
+ }
+
+ for _, c := range cases {
+ got, gotOk := c.f(c.a, c.b)
+
+ if got != c.want {
+ t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+ }
+
+ if gotOk != c.wantOk {
+ t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+ }
+ }
+
+ negateCases := []struct {
+ a, want int32
+ wantOk bool
+ }{
+ {1, -1, true},
+ {-1, 1, true},
+ {0, 0, true},
+ {math.MinInt32, 0, false},
+ }
+ for _, c := range negateCases {
+ got, gotOk := NegateInt32(c.a)
+
+ if got != c.want {
+ t.Errorf("NegateInt32(%d) = %d want %d", c.a, got, c.want)
+ }
+
+ if gotOk != c.wantOk {
+ t.Errorf("NegateInt32(%d) ok = %v want %v", c.a, gotOk, c.wantOk)
+ }
+ }
+}
+
+func TestUint32(t *testing.T) {
+ cases := []struct {
+ f func(a, b uint32) (uint32, bool)
+ a, b, want uint32
+ wantOk bool
+ }{
+ {AddUint32, 2, 3, 5, true},
+ {AddUint32, math.MaxUint32, 1, 0, false},
+ {SubUint32, 3, 2, 1, true},
+ {SubUint32, 2, 3, 0, false},
+ {MulUint32, 2, 3, 6, true},
+ {MulUint32, math.MaxUint32, 2, 0, false},
+ {DivUint32, 2, 2, 1, true},
+ {DivUint32, 1, 0, 0, false},
+ {ModUint32, 3, 2, 1, true},
+ {ModUint32, 1, 0, 0, false},
+ {LshiftUint32, 1, 2, 4, true},
+ {LshiftUint32, 1, 32, 0, false},
+ {LshiftUint32, 2, 31, 0, false},
+ }
+
+ for _, c := range cases {
+ got, gotOk := c.f(c.a, c.b)
+
+ if got != c.want {
+ t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+ }
+
+ if gotOk != c.wantOk {
+ t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+ }
+ }
+}
+
+func fname(f interface{}) string {
+ name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
+ return name[strings.IndexRune(name, '.')+1:]
+}