refactor everything to use context's annotation system, making some significant changes to annotations themselves along the way

This commit is contained in:
Brian Picciano 2019-02-09 14:08:30 -05:00
parent 000a57689d
commit e1e52db208
50 changed files with 1080 additions and 1383 deletions

View File

@ -27,8 +27,15 @@ Everything here are guidelines, not actual rules.
* https://github.com/golang/go/wiki/CodeReviewComments
* Package names may be abbreviations of a concept, but types, functions, and
methods should all be full words.
* When deciding if a package should initialize a struct as a value or pointer, a
good rule is: If it's used as an immutable value it should be a value,
otherwise it's a pointer. Even if the immutable value implementation has
internal caching, locks, etc..., that can be hidden as pointers inside the
struct, but the struct itself can remain a value type.
* A function which takes in a `context.Context` and returns a modified copy of
that same `context.Context` should have a name prefixed with `With`, e.g.
`WithValue` or `WithLogger`. Exceptions like `Annotate` do exist.

2
TODO Normal file
View File

@ -0,0 +1,2 @@
- read through all docs, especially package docs, make sure they make sense
- write examples

View File

@ -20,6 +20,7 @@ import (
"github.com/mediocregopher/mediocre-go-lib/m"
"github.com/mediocregopher/mediocre-go-lib/mcfg"
"github.com/mediocregopher/mediocre-go-lib/mcrypto"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mhttp"
"github.com/mediocregopher/mediocre-go-lib/mlog"
"github.com/mediocregopher/mediocre-go-lib/mrand"
@ -29,27 +30,27 @@ import (
)
func main() {
ctx := m.NewServiceCtx()
ctx, cookieName := mcfg.String(ctx, "cookie-name", "_totp_proxy", "String to use as the name for cookies")
ctx, cookieTimeout := mcfg.Duration(ctx, "cookie-timeout", mtime.Duration{1 * time.Hour}, "Timeout for cookies")
ctx := m.ServiceContext()
ctx, cookieName := mcfg.WithString(ctx, "cookie-name", "_totp_proxy", "String to use as the name for cookies")
ctx, cookieTimeout := mcfg.WithDuration(ctx, "cookie-timeout", mtime.Duration{1 * time.Hour}, "Timeout for cookies")
var userSecrets map[string]string
ctx = mcfg.RequiredJSON(ctx, "users", &userSecrets, "JSON object which maps usernames to their TOTP secret strings")
ctx = mcfg.WithRequiredJSON(ctx, "users", &userSecrets, "JSON object which maps usernames to their TOTP secret strings")
var secret mcrypto.Secret
ctx, secretStr := mcfg.String(ctx, "secret", "", "String used to sign authentication tokens. If one isn't given a new one will be generated on each startup, invalidating all previous tokens.")
ctx = mrun.OnStart(ctx, func(context.Context) error {
ctx, secretStr := mcfg.WithString(ctx, "secret", "", "String used to sign authentication tokens. If one isn't given a new one will be generated on each startup, invalidating all previous tokens.")
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
if *secretStr == "" {
*secretStr = mrand.Hex(32)
}
mlog.Info(ctx, "generating secret")
mlog.Info("generating secret", ctx)
secret = mcrypto.NewSecret([]byte(*secretStr))
return nil
})
proxyHandler := new(struct{ http.Handler })
ctx, proxyURL := mcfg.RequiredString(ctx, "dst-url", "URL to proxy requests to. Only the scheme and host should be set.")
ctx = mrun.OnStart(ctx, func(context.Context) error {
ctx, proxyURL := mcfg.WithRequiredString(ctx, "dst-url", "URL to proxy requests to. Only the scheme and host should be set.")
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
u, err := url.Parse(*proxyURL)
if err != nil {
return err
@ -60,7 +61,6 @@ func main() {
authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO mlog.FromHTTP?
// TODO annotate this ctx
ctx := r.Context()
unauthorized := func() {
@ -79,7 +79,8 @@ func main() {
}
if cookie, _ := r.Cookie(*cookieName); cookie != nil {
mlog.Debug(ctx, "authenticating with cookie", mlog.KV{"cookie": cookie.String()})
mlog.Debug("authenticating with cookie",
mctx.Annotate(ctx, "cookie", cookie.String()))
var sig mcrypto.Signature
if err := sig.UnmarshalText([]byte(cookie.Value)); err == nil {
err := mcrypto.VerifyString(secret, sig, "")
@ -91,10 +92,8 @@ func main() {
}
if user, pass, ok := r.BasicAuth(); ok && pass != "" {
mlog.Debug(ctx, "authenticating with user/pass", mlog.KV{
"user": user,
"pass": pass,
})
mlog.Debug("authenticating with user",
mctx.Annotate(ctx, "user", user))
if userSecret, ok := userSecrets[user]; ok {
if totp.Validate(pass, userSecret) {
authorized()
@ -106,6 +105,6 @@ func main() {
unauthorized()
})
ctx, _ = mhttp.MListenAndServe(ctx, authHandler)
ctx, _ = mhttp.WithListeningServer(ctx, authHandler)
m.Run(ctx)
}

27
m/m.go
View File

@ -6,10 +6,12 @@ package m
import (
"context"
"log"
"os"
"os/signal"
"github.com/mediocregopher/mediocre-go-lib/mcfg"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr"
"github.com/mediocregopher/mediocre-go-lib/mlog"
"github.com/mediocregopher/mediocre-go-lib/mrun"
@ -24,25 +26,26 @@ func CfgSource() mcfg.Source {
}
}
// NewServiceCtx returns a Context which should be used as the root Context when
// creating long running services, such as an RPC service or database.
// ServiceContext returns a Context which should be used as the root Context
// when creating long running services, such as an RPC service or database.
//
// The returned Context will automatically handle setting up global
// configuration parameters like "log-level", as well as an http endpoint where
// debug information about the running process can be accessed.
//
// TODO set up the debug endpoint.
func NewServiceCtx() context.Context {
func ServiceContext() context.Context {
ctx := context.Background()
// set up log level handling
logger := mlog.NewLogger()
ctx = mlog.Set(ctx, logger)
ctx, logLevelStr := mcfg.String(ctx, "log-level", "info", "Maximum log level which will be printed.")
ctx = mrun.OnStart(ctx, func(context.Context) error {
ctx = mlog.WithLogger(ctx, logger)
ctx, logLevelStr := mcfg.WithString(ctx, "log-level", "info", "Maximum log level which will be printed.")
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
logLevel := mlog.LevelFromString(*logLevelStr)
log.Printf("setting log level to %v", logLevel)
if logLevel == nil {
return merr.New("invalid log level", "log-level", *logLevelStr)
return merr.New(ctx, "invalid log level", "log-level", *logLevelStr)
}
logger.SetMaxLevel(logLevel)
return nil
@ -58,20 +61,20 @@ func NewServiceCtx() context.Context {
func Run(ctx context.Context) {
log := mlog.From(ctx)
if err := mcfg.Populate(ctx, CfgSource()); err != nil {
log.Fatal(ctx, "error populating configuration", merr.KV(err))
log.Fatal("error populating configuration", ctx, merr.Context(err))
} else if err := mrun.Start(ctx); err != nil {
log.Fatal(ctx, "error triggering start event", merr.KV(err))
log.Fatal("error triggering start event", ctx, merr.Context(err))
}
{
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
s := <-ch
log.Info(ctx, "signal received, stopping", mlog.KV{"signal": s})
log.Info("signal received, stopping", mctx.Annotate(ctx, "signal", s))
}
if err := mrun.Stop(ctx); err != nil {
log.Fatal(ctx, "error triggering stop event", merr.KV(err))
log.Fatal("error triggering stop event", ctx, merr.Context(err))
}
log.Info(ctx, "exiting process")
log.Info("exiting process", ctx)
}

View File

@ -1,6 +1,7 @@
package m
import (
"context"
"encoding/json"
. "testing"
@ -13,7 +14,7 @@ import (
func TestServiceCtx(t *T) {
t.Run("log-level", func(t *T) {
ctx := NewServiceCtx()
ctx := ServiceContext()
// pull the Logger out of the ctx and set the Handler on it, so we can check
// the log level
@ -36,16 +37,16 @@ func TestServiceCtx(t *T) {
t.Fatal(err)
}
mlog.From(ctxA).Info(ctxA, "foo")
mlog.From(ctxA).Debug(ctxA, "bar")
mlog.From(ctxA).Info("foo", ctxA)
mlog.From(ctxA).Debug("bar", ctxA)
massert.Fatal(t, massert.All(
massert.Len(msgs, 2),
massert.Equal(msgs[0].Level.String(), "INFO"),
massert.Equal(msgs[0].Description, "foo"),
massert.Equal(msgs[0].Context, ctxA),
massert.Equal(msgs[0].Contexts, []context.Context{ctxA}),
massert.Equal(msgs[1].Level.String(), "DEBUG"),
massert.Equal(msgs[1].Description, "bar"),
massert.Equal(msgs[1].Context, ctxA),
massert.Equal(msgs[1].Contexts, []context.Context{ctxA}),
))
})
}

View File

@ -1,6 +1,7 @@
package mcfg
import (
"context"
"fmt"
"io"
"os"
@ -89,7 +90,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
break
}
if !pvOk {
return nil, merr.New("unexpected config parameter", "param", arg)
return nil, merr.New(context.Background(), "unexpected config parameter", "param", arg)
}
}
@ -120,7 +121,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
pvStrValOk = false
}
if pvOk && !pvStrValOk {
return nil, merr.New("param expected a value", "param", key)
return nil, merr.New(p.Context, "param expected a value", "param", key)
}
return pvs, nil
}

View File

@ -15,11 +15,11 @@ import (
func TestSourceCLIHelp(t *T) {
ctx := context.Background()
ctx, _ = Int(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed
ctx, _ = Bool(ctx, "bar", "Test bool param.")
ctx, _ = String(ctx, "baz", "baz", "Test string param")
ctx, _ = RequiredString(ctx, "baz2", "")
ctx, _ = RequiredString(ctx, "baz3", "")
ctx, _ = WithInt(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed
ctx, _ = WithBool(ctx, "bar", "Test bool param.")
ctx, _ = WithString(ctx, "baz", "baz", "Test string param")
ctx, _ = WithRequiredString(ctx, "baz2", "")
ctx, _ = WithRequiredString(ctx, "baz3", "")
src := SourceCLI{}
buf := new(bytes.Buffer)

View File

@ -1,6 +1,7 @@
package mcfg
import (
"context"
"os"
"strings"
@ -58,7 +59,7 @@ func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
for _, kv := range kvs {
split := strings.SplitN(kv, "=", 2)
if len(split) != 2 {
return nil, merr.New("malformed environment key/value pair", "kv", kv)
return nil, merr.New(context.Background(), "malformed environment key/value pair", "kv", kv)
}
k, v := split[0], split[1]
if p, ok := pM[k]; ok {

View File

@ -110,7 +110,7 @@ func populate(params []Param, src Source) error {
if !p.Required {
continue
} else if _, ok := pvM[hash]; !ok {
return merr.New("required parameter is not set",
return merr.New(p.Context, "required parameter is not set",
"param", paramFullName(mctx.Path(p.Context), p.Name))
}
}

View File

@ -11,10 +11,10 @@ import (
func TestPopulate(t *T) {
{
ctx := context.Background()
ctx, a := Int(ctx, "a", 0, "")
ctx, a := WithInt(ctx, "a", 0, "")
ctxChild := mctx.NewChild(ctx, "foo")
ctxChild, b := Int(ctxChild, "b", 0, "")
ctxChild, c := Int(ctxChild, "c", 0, "")
ctxChild, b := WithInt(ctxChild, "b", 0, "")
ctxChild, c := WithInt(ctxChild, "c", 0, "")
ctx = mctx.WithChild(ctx, ctxChild)
err := Populate(ctx, SourceCLI{
@ -28,10 +28,10 @@ func TestPopulate(t *T) {
{ // test that required params are enforced
ctx := context.Background()
ctx, a := Int(ctx, "a", 0, "")
ctx, a := WithInt(ctx, "a", 0, "")
ctxChild := mctx.NewChild(ctx, "foo")
ctxChild, b := Int(ctxChild, "b", 0, "")
ctxChild, c := RequiredInt(ctxChild, "c", "")
ctxChild, b := WithInt(ctxChild, "b", 0, "")
ctxChild, c := WithRequiredInt(ctxChild, "c", "")
ctx = mctx.WithChild(ctx, ctxChild)
err := Populate(ctx, SourceCLI{

View File

@ -45,7 +45,7 @@ type Param struct {
Into interface{}
// The Context this Param was added to. NOTE that this will be automatically
// filled in by MustAdd when the Param is added to the Context.
// filled in by WithParam when the Param is added to the Context.
Context context.Context
}
@ -74,9 +74,9 @@ func getParam(ctx context.Context, name string) (Param, bool) {
return param, ok
}
// MustAdd returns a Context with the given Param added to it. It will panic if
// a Param with the same Name already exists in the Context.
func MustAdd(ctx context.Context, param Param) context.Context {
// WithParam returns a Context with the given Param added to it. It will panic
// if a Param with the same Name already exists in the Context.
func WithParam(ctx context.Context, param Param) context.Context {
param.Name = strings.ToLower(param.Name)
param.Context = ctx
@ -99,113 +99,114 @@ func getLocalParams(ctx context.Context) []Param {
return params
}
// Int64 returns an *int64 which will be populated once Populate is run on the
// returned Context.
func Int64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) {
// WithInt64 returns an *int64 which will be populated once Populate is run on
// the returned Context.
func WithInt64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) {
i := defaultVal
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &i})
return ctx, &i
}
// RequiredInt64 returns an *int64 which will be populated once Populate is run
// on the returned Context, and which must be supplied by a configuration
// Source.
func RequiredInt64(ctx context.Context, name string, usage string) (context.Context, *int64) {
var i int64
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
return ctx, &i
}
// Int returns an *int which will be populated once Populate is run on the
// returned Context.
func Int(ctx context.Context, name string, defaultVal int, usage string) (context.Context, *int) {
i := defaultVal
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, Into: &i})
return ctx, &i
}
// RequiredInt returns an *int which will be populated once Populate is run on
// the returned Context, and which must be supplied by a configuration Source.
func RequiredInt(ctx context.Context, name string, usage string) (context.Context, *int) {
var i int
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
return ctx, &i
}
// String returns a *string which will be populated once Populate is run on the
// returned Context.
func String(ctx context.Context, name, defaultVal, usage string) (context.Context, *string) {
s := defaultVal
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s})
return ctx, &s
}
// RequiredString returns a *string which will be populated once Populate is
// WithRequiredInt64 returns an *int64 which will be populated once Populate is
// run on the returned Context, and which must be supplied by a configuration
// Source.
func RequiredString(ctx context.Context, name, usage string) (context.Context, *string) {
var s string
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s})
func WithRequiredInt64(ctx context.Context, name string, usage string) (context.Context, *int64) {
var i int64
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
return ctx, &i
}
// WithInt returns an *int which will be populated once Populate is run on the
// returned Context.
func WithInt(ctx context.Context, name string, defaultVal int, usage string) (context.Context, *int) {
i := defaultVal
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &i})
return ctx, &i
}
// WithRequiredInt returns an *int which will be populated once Populate is run
// on the returned Context, and which must be supplied by a configuration
// Source.
func WithRequiredInt(ctx context.Context, name string, usage string) (context.Context, *int) {
var i int
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i})
return ctx, &i
}
// WithString returns a *string which will be populated once Populate is run on
// the returned Context.
func WithString(ctx context.Context, name, defaultVal, usage string) (context.Context, *string) {
s := defaultVal
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s})
return ctx, &s
}
// Bool returns a *bool which will be populated once Populate is run on the
// WithRequiredString returns a *string which will be populated once Populate is
// run on the returned Context, and which must be supplied by a configuration
// Source.
func WithRequiredString(ctx context.Context, name, usage string) (context.Context, *string) {
var s string
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s})
return ctx, &s
}
// WithBool returns a *bool which will be populated once Populate is run on the
// returned Context, and which defaults to false if unconfigured.
//
// The default behavior of all Sources is that a boolean parameter will be set
// to true unless the value is "", 0, or false. In the case of the CLI Source
// the value will also be true when the parameter is used with no value at all,
// as would be expected.
func Bool(ctx context.Context, name, usage string) (context.Context, *bool) {
func WithBool(ctx context.Context, name, usage string) (context.Context, *bool) {
var b bool
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b})
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b})
return ctx, &b
}
// TS returns an *mtime.TS which will be populated once Populate is run on the
// returned Context.
func TS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) {
// WithTS returns an *mtime.TS which will be populated once Populate is run on
// the returned Context.
func WithTS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) {
t := defaultVal
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, Into: &t})
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &t})
return ctx, &t
}
// RequiredTS returns an *mtime.TS which will be populated once Populate is run
// on the returned Context, and which must be supplied by a configuration
// WithRequiredTS returns an *mtime.TS which will be populated once Populate is
// run on the returned Context, and which must be supplied by a configuration
// Source.
func RequiredTS(ctx context.Context, name, usage string) (context.Context, *mtime.TS) {
func WithRequiredTS(ctx context.Context, name, usage string) (context.Context, *mtime.TS) {
var t mtime.TS
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t})
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t})
return ctx, &t
}
// Duration returns an *mtime.Duration which will be populated once Populate is
// run on the returned Context.
func Duration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
// WithDuration returns an *mtime.Duration which will be populated once Populate
// is run on the returned Context.
func WithDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
d := defaultVal
ctx = MustAdd(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d})
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d})
return ctx, &d
}
// RequiredDuration returns an *mtime.Duration which will be populated once
// WithRequiredDuration returns an *mtime.Duration which will be populated once
// Populate is run on the returned Context, and which must be supplied by a
// configuration Source.
func RequiredDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
func WithRequiredDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
var d mtime.Duration
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d})
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d})
return ctx, &d
}
// JSON reads the parameter value as a JSON value and unmarshals it into the
// WithJSON reads the parameter value as a JSON value and unmarshals it into the
// given interface{} (which should be a pointer). The receiver (into) is also
// used to determine the default value.
func JSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
return MustAdd(ctx, Param{Name: name, Usage: usage, Into: into})
func WithJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
return WithParam(ctx, Param{Name: name, Usage: usage, Into: into})
}
// RequiredJSON reads the parameter value as a JSON value and unmarshals it into
// the given interface{} (which should be a pointer). The value must be supplied
// by a configuration Source.
func RequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
return MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
// WithRequiredJSON reads the parameter value as a JSON value and unmarshals it
// into the given interface{} (which should be a pointer). The value must be
// supplied by a configuration Source.
func WithRequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
return WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
}

View File

@ -116,7 +116,7 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
// the Sources don't actually care about the other fields of Param,
// those are only used by Populate once it has all ParamValues together
}
*thisCtx = MustAdd(*thisCtx, ctxP)
*thisCtx = WithParam(*thisCtx, ctxP)
ctxP, _ = getParam(*thisCtx, ctxP.Name) // get it back out to get any added fields
if !p.unset {
@ -155,9 +155,9 @@ func (scs srcCommonState) assert(s Source) error {
func TestSources(t *T) {
ctx := context.Background()
ctx, a := RequiredInt(ctx, "a", "")
ctx, b := RequiredInt(ctx, "b", "")
ctx, c := RequiredInt(ctx, "c", "")
ctx, a := WithRequiredInt(ctx, "a", "")
ctx, b := WithRequiredInt(ctx, "b", "")
ctx, c := WithRequiredInt(ctx, "c", "")
err := Populate(ctx, Sources{
SourceCLI{Args: []string{"--a=1", "--b=666"}},
@ -173,10 +173,10 @@ func TestSources(t *T) {
func TestSourceParamValues(t *T) {
ctx := context.Background()
ctx, a := RequiredInt(ctx, "a", "")
ctx, a := WithRequiredInt(ctx, "a", "")
foo := mctx.NewChild(ctx, "foo")
foo, b := RequiredString(foo, "b", "")
foo, c := Bool(foo, "c", "")
foo, b := WithRequiredString(foo, "b", "")
foo, c := WithBool(foo, "c", "")
ctx = mctx.WithChild(ctx, foo)
err := Populate(ctx, ParamValues{

View File

@ -1,6 +1,7 @@
package mcrypto
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
@ -58,7 +59,7 @@ func (pk PublicKey) verify(s Signature, r io.Reader) error {
return err
}
if err := rsa.VerifyPSS(&pk.PublicKey, crypto.SHA256, h.Sum(nil), s.sig, nil); err != nil {
return merr.WithValue(ErrInvalidSig, "sig", s, true)
return merr.Wrap(context.Background(), ErrInvalidSig, "sig", s)
}
return nil
}
@ -88,12 +89,12 @@ func (pk *PublicKey) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, pubKeyV0)
if !ok || len(strEnc) <= hex.EncodedLen(8) {
return merr.WithValue(errMalformedPublicKey, "pubKeyStr", str, true)
return merr.Wrap(context.Background(), errMalformedPublicKey, "pubKeyStr", str)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.WithValue(err, "pubKeyStr", str, true)
return merr.Wrap(context.Background(), err, "pubKeyStr", str)
}
pk.E = int(binary.BigEndian.Uint64(b))
@ -184,17 +185,17 @@ func (pk *PrivateKey) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, privKeyV0)
if !ok {
return merr.Wrap(errMalformedPrivateKey)
return merr.Wrap(context.Background(), errMalformedPrivateKey)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.Wrap(errMalformedPrivateKey)
return merr.Wrap(context.Background(), errMalformedPrivateKey)
}
e, n := binary.Uvarint(b)
if n <= 0 {
return merr.Wrap(errMalformedPrivateKey)
return merr.Wrap(context.Background(), errMalformedPrivateKey)
}
pk.PublicKey.E = int(e)
b = b[n:]
@ -205,7 +206,7 @@ func (pk *PrivateKey) UnmarshalText(b []byte) error {
}
l, n := binary.Uvarint(b)
if n <= 0 {
err = merr.Wrap(errMalformedPrivateKey)
err = merr.Wrap(context.Background(), errMalformedPrivateKey)
}
b = b[n:]
i := new(big.Int)

View File

@ -1,6 +1,7 @@
package mcrypto
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
@ -76,9 +77,9 @@ func (s Secret) sign(r io.Reader) (Signature, error) {
func (s Secret) verify(sig Signature, r io.Reader) error {
sigB, err := s.signRaw(r, uint8(len(sig.sig)), sig.salt, sig.t)
if err != nil {
return merr.WithValue(err, "sig", sig, true)
return merr.Wrap(context.Background(), err, "sig", sig)
} else if !hmac.Equal(sigB, sig.sig) {
return merr.WithValue(ErrInvalidSig, "sig", sig, true)
return merr.Wrap(context.Background(), ErrInvalidSig, "sig", sig)
}
return nil
}

View File

@ -2,6 +2,7 @@ package mcrypto
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
@ -66,12 +67,12 @@ func (s *Signature) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, sigV0)
if !ok || len(strEnc) < hex.EncodedLen(10) {
return merr.WithValue(errMalformedSig, "sigStr", str, true)
return merr.Wrap(context.Background(), errMalformedSig, "sigStr", str)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.WithValue(err, "sigStr", str, true)
return merr.Wrap(context.Background(), err, "sigStr", str)
}
unixNano, b := int64(binary.BigEndian.Uint64(b[:8])), b[8:]
@ -81,7 +82,7 @@ func (s *Signature) UnmarshalText(b []byte) error {
if err != nil {
return nil
} else if len(b) < 1+int(b[0]) {
err = merr.WithValue(errMalformedSig, "sigStr", str, true)
err = merr.Wrap(context.Background(), errMalformedSig, "sigStr", str)
return nil
}
out := b[1 : 1+b[0]]
@ -161,7 +162,7 @@ func SignString(s Signer, in string) Signature {
// Verify reads all data from the io.Reader and uses the Verifier to verify that
// the Signature is for that data.
//
// Returns any errors from io.Reader, or ErrInvalidSig (use merry.Is(err,
// Returns any errors from io.Reader, or ErrInvalidSig (use merr.Equal(err,
// mcrypto.ErrInvalidSig) to check).
func Verify(v Verifier, s Signature, r io.Reader) error {
return v.verify(s, r)
@ -170,7 +171,7 @@ func Verify(v Verifier, s Signature, r io.Reader) error {
// VerifyBytes uses the Verifier to verify that the Signature is for the given
// []bytes.
//
// Returns ErrInvalidSig (use merry.Is(err, mcrypto.ErrInvalidSig) to check).
// Returns ErrInvalidSig (use merr.Equal(err, mcrypto.ErrInvalidSig) to check).
func VerifyBytes(v Verifier, s Signature, b []byte) error {
return v.verify(s, bytes.NewBuffer(b))
}
@ -178,7 +179,7 @@ func VerifyBytes(v Verifier, s Signature, b []byte) error {
// VerifyString uses the Verifier to verify that the Signature is for the given
// string.
//
// Returns ErrInvalidSig (use merry.Is(err, mcrypto.ErrInvalidSig) to check).
// Returns ErrInvalidSig (use merr.Equal(err, mcrypto.ErrInvalidSig) to check).
func VerifyString(v Verifier, s Signature, in string) error {
return VerifyBytes(v, s, []byte(in))
}

View File

@ -2,6 +2,7 @@ package mcrypto
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"encoding/hex"
@ -68,11 +69,11 @@ func (u *UUID) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, uuidV0)
if !ok || len(strEnc) != hex.EncodedLen(16) {
return merr.WithValue(errMalformedUUID, "uuidStr", str, true)
return merr.Wrap(context.Background(), errMalformedUUID, "uuidStr", str)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.WithValue(err, "uuidStr", str, true)
return merr.Wrap(context.Background(), err, "uuidStr", str)
}
u.b = b
return nil

View File

@ -3,69 +3,284 @@ package mctx
import (
"context"
"fmt"
"sort"
"strings"
)
type annotateKey struct {
userKey interface{}
// Annotation describes the annotation of a key/value pair made on a Context via
// the Annotate call. The Path field is the Path of the Context on which the
// call was made.
type Annotation struct {
Key, Value interface{}
Path []string
}
type annotation struct {
Annotation
root, prev *annotation
}
type annotationKey int
// Annotate takes in one or more key/value pairs (kvs' length must be even) and
// returns a Context carrying them. Annotations only exist on the local level,
// i.e. a child and parent share different annotation namespaces.
//
// NOTE that annotations are preserved across NewChild calls, but are keyed
// based on the passed in key _and_ the Context's Path.
func Annotate(ctx context.Context, kvs ...interface{}) context.Context {
for i := 0; i < len(kvs); i += 2 {
ctx = WithLocalValue(ctx, annotateKey{kvs[i]}, kvs[i+1])
if len(kvs)%2 > 0 {
panic("kvs being passed to mctx.Annotate must have an even number of elements")
} else if len(kvs) == 0 {
return ctx
}
// if multiple annotations are passed in here it's not actually necessary to
// create an intermediate Context for each one, so keep curr outside and
// only use it later
var curr, root *annotation
prev, _ := ctx.Value(annotationKey(0)).(*annotation)
if prev != nil {
root = prev.root
}
path := Path(ctx)
for i := 0; i < len(kvs); i += 2 {
curr = &annotation{
Annotation: Annotation{
Key: kvs[i], Value: kvs[i+1],
Path: path,
},
prev: prev,
}
if root == nil {
root = curr
}
curr.root = curr
prev = curr
}
ctx = context.WithValue(ctx, annotationKey(0), curr)
return ctx
}
// Annotations describes a set of keys/values which were set on a Context (but
// not its parents or children) using Annotate.
type Annotations [][2]interface{}
// AnnotationSet describes a set of unique Annotation values which were
// retrieved off a Context via the Annotations function. An AnnotationSet has a
// couple methods on it to aid in post-processing.
type AnnotationSet []Annotation
// LocalAnnotations returns all key/value pairs which have been set via Annotate
// on this Context (but not its parent or children). If a key was set twice then
// only the most recent value is included. The returned order is
// non-deterministic.
func LocalAnnotations(ctx context.Context) Annotations {
var annotations Annotations
localValuesIter(ctx, func(key, val interface{}) {
aKey, ok := key.(annotateKey)
if !ok {
return
// Annotations returns all Annotation values which have been set via Annotate on
// this Context. If a key was set twice then only the most recent value is
// included.
func Annotations(ctx context.Context) AnnotationSet {
a, _ := ctx.Value(annotationKey(0)).(*annotation)
if a == nil {
return nil
}
annotations = append(annotations, [2]interface{}{aKey.userKey, val})
type mKey struct {
pathHash string
key interface{}
}
m := map[mKey]bool{}
var aa AnnotationSet
for {
if a == nil {
break
}
k := mKey{pathHash: pathHash(a.Path), key: a.Key}
if m[k] {
a = a.prev
continue
}
aa = append(aa, a.Annotation)
m[k] = true
a = a.prev
}
return aa
}
// StringMapByPath is similar to StringMap, but it first maps each annotation
// datum by its path.
func (aa AnnotationSet) StringMapByPath() map[string]map[string]string {
type mKey struct {
str string
path string
typ string
}
m := map[mKey][]Annotation{}
for _, a := range aa {
k := mKey{
str: fmt.Sprint(a.Key),
path: "/" + strings.Join(a.Path, "/"),
}
m[k] = append(m[k], a)
}
nextK := func(k mKey, a Annotation) mKey {
if k.typ == "" {
k.typ = fmt.Sprintf("%T", a.Key)
} else {
panic(fmt.Sprintf("mKey %#v is somehow conflicting with another", k))
}
return k
}
for {
var any bool
for k, annotations := range m {
if len(annotations) == 1 {
continue
}
any = true
for _, a := range annotations {
k2 := nextK(k, a)
m[k2] = append(m[k2], a)
}
delete(m, k)
}
if !any {
break
}
}
outM := map[string]map[string]string{}
for k, annotations := range m {
a := annotations[0]
if outM[k.path] == nil {
outM[k.path] = map[string]string{}
}
kStr := k.str
if k.typ != "" {
kStr += "(" + k.typ + ")"
}
outM[k.path][kStr] = fmt.Sprint(a.Value)
}
return outM
}
// StringMap formats each of the Annotations into strings using fmt.Sprint. If
// any two keys format to the same string, then path information will be
// prefaced to each one. If they still format to the same string, then type
// information will be prefaced to each one.
func (aa AnnotationSet) StringMap() map[string]string {
type mKey struct {
str string
path string
typ string
}
m := map[mKey][]Annotation{}
for _, a := range aa {
k := mKey{str: fmt.Sprint(a.Key)}
m[k] = append(m[k], a)
}
nextK := func(k mKey, a Annotation) mKey {
if k.path == "" {
k.path = "/" + strings.Join(a.Path, "/")
} else if k.typ == "" {
k.typ = fmt.Sprintf("%T", a.Key)
} else {
panic(fmt.Sprintf("mKey %#v is somehow conflicting with another", k))
}
return k
}
for {
var any bool
for k, annotations := range m {
if len(annotations) == 1 {
continue
}
any = true
for _, a := range annotations {
k2 := nextK(k, a)
m[k2] = append(m[k2], a)
}
delete(m, k)
}
if !any {
break
}
}
outM := map[string]string{}
for k, annotations := range m {
a := annotations[0]
kStr := k.str
if k.path != "" {
kStr += "(" + k.path + ")"
}
if k.typ != "" {
kStr += "(" + k.typ + ")"
}
outM[kStr] = fmt.Sprint(a.Value)
}
return outM
}
// StringSlice is like StringMap but it returns a slice of key/value tuples
// rather than a map. If sorted is true then the slice will be sorted by key in
// ascending order.
func (aa AnnotationSet) StringSlice(sorted bool) [][2]string {
m := aa.StringMap()
slice := make([][2]string, 0, len(m))
for k, v := range m {
slice = append(slice, [2]string{k, v})
}
if sorted {
sort.Slice(slice, func(i, j int) bool {
return slice[i][0] < slice[j][0]
})
return annotations
}
return slice
}
// StringMap formats each of the key/value pairs into strings using fmt.Sprint.
// If any two keys format to the same string, then type information will be
// prefaced to each one.
func (aa Annotations) StringMap() map[string]string {
keyTypes := make(map[string]interface{}, len(aa))
keyVals := map[string]string{}
setKey := func(k, v interface{}) {
kStr := fmt.Sprint(k)
oldType := keyTypes[kStr]
if oldType == nil {
keyTypes[kStr] = k
keyVals[kStr] = fmt.Sprint(v)
return
func mergeAnnotations(ctxA, ctxB context.Context) context.Context {
annotationA, _ := ctxA.Value(annotationKey(0)).(*annotation)
annotationB, _ := ctxB.Value(annotationKey(0)).(*annotation)
if annotationB == nil {
return ctxA
} else if annotationA == nil {
return context.WithValue(ctxA, annotationKey(0), annotationB)
}
// check if oldKey is in kvm, if so it needs to be moved to account for
// its type info
if oldV, ok := keyVals[kStr]; ok {
delete(keyVals, kStr)
keyVals[fmt.Sprintf("%T(%s)", oldType, kStr)] = oldV
var headA, currA *annotation
currB := annotationB
for {
if currB == nil {
break
}
keyVals[fmt.Sprintf("%T(%s)", k, kStr)] = fmt.Sprint(v)
prevA := &annotation{
Annotation: currB.Annotation,
root: annotationA.root,
}
if currA != nil {
currA.prev = prevA
}
currA, currB = prevA, currB.prev
if headA == nil {
headA = currA
}
}
for _, kv := range aa {
setKey(kv[0], kv[1])
}
return keyVals
currA.prev = annotationA
return context.WithValue(ctxA, annotationKey(0), headA)
}
// MergeAnnotations sequentially merges the annotation data of the passed in
// Contexts into the first passed in one. Data from a Context overwrites
// overlapping data on all passed in Contexts to the left of it. All other
// aspects of the first Context remain the same, and that Context is returned
// with the new set of Annotation data.
//
// NOTE this will panic if no Contexts are passed in.
func MergeAnnotations(ctxs ...context.Context) context.Context {
ctxA := ctxs[0]
for _, ctxB := range ctxs[1:] {
ctxA = mergeAnnotations(ctxA, ctxB)
}
return ctxA
}

View File

@ -13,42 +13,73 @@ func TestAnnotate(t *T) {
parent = Annotate(parent, "b", "bar")
child := NewChild(parent, "child")
child = Annotate(child, "a", "Foo")
child = Annotate(child, "a", "FOO")
child = Annotate(child, "c", "BAZ")
parent = WithChild(parent, child)
parentAnnotations := LocalAnnotations(parent)
childAnnotations := LocalAnnotations(child)
parentAnnotations := Annotations(parent)
childAnnotations := Annotations(child)
massert.Fatal(t, massert.All(
massert.Len(parentAnnotations, 2),
massert.Has(parentAnnotations, [2]interface{}{"a", "foo"}),
massert.Has(parentAnnotations, [2]interface{}{"b", "bar"}),
massert.Len(childAnnotations, 2),
massert.Has(childAnnotations, [2]interface{}{"a", "FOO"}),
massert.Has(childAnnotations, [2]interface{}{"c", "BAZ"}),
massert.Has(parentAnnotations, Annotation{Key: "a", Value: "foo"}),
massert.Has(parentAnnotations, Annotation{Key: "b", Value: "bar"}),
massert.Len(childAnnotations, 4),
massert.Has(childAnnotations, Annotation{Key: "a", Value: "foo"}),
massert.Has(childAnnotations, Annotation{Key: "b", Value: "bar"}),
massert.Has(childAnnotations,
Annotation{Key: "a", Path: []string{"child"}, Value: "FOO"}),
massert.Has(childAnnotations,
Annotation{Key: "c", Path: []string{"child"}, Value: "BAZ"}),
))
}
func TestAnnotationsStingMap(t *T) {
func TestAnnotationsStringMap(t *T) {
type A int
type B int
aa := Annotations{
{"foo", "bar"},
{"1", "one"},
{1, 1},
{0, 0},
{A(0), 0},
{B(0), 0},
aa := AnnotationSet{
{Key: 0, Path: nil, Value: "zero"},
{Key: 1, Path: nil, Value: "one"},
{Key: 1, Path: []string{"foo"}, Value: "ONE"},
{Key: A(2), Path: []string{"foo"}, Value: "two"},
{Key: B(2), Path: []string{"foo"}, Value: "TWO"},
}
massert.Fatal(t, massert.All(
massert.Equal(map[string]string{
"0": "zero",
"1(/)": "one",
"1(/foo)": "ONE",
"2(/foo)(mctx.A)": "two",
"2(/foo)(mctx.B)": "TWO",
}, aa.StringMap()),
massert.Equal(map[string]map[string]string{
"/": {
"0": "zero",
"1": "one",
},
"/foo": {
"1": "ONE",
"2(mctx.A)": "two",
"2(mctx.B)": "TWO",
},
}, aa.StringMapByPath()),
))
}
func TestMergeAnnotations(t *T) {
ctxA := Annotate(context.Background(), 0, "zero", 1, "one")
ctxA = Annotate(ctxA, 0, "ZERO")
ctxB := Annotate(context.Background(), 2, "two")
ctxB = Annotate(ctxB, 1, "ONE", 2, "TWO")
ctx := MergeAnnotations(ctxA, ctxB)
err := massert.Equal(map[string]string{
"foo": "bar",
"string(1)": "one",
"int(1)": "1",
"int(0)": "0",
"mctx.A(0)": "0",
"mctx.B(0)": "0",
}, aa.StringMap()).Assert()
"0": "ZERO",
"1": "ONE",
"2": "TWO",
}, Annotations(ctx).StringMap()).Assert()
if err != nil {
t.Fatal(err)
}

View File

@ -11,17 +11,13 @@ package mctx
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
)
////////////////////////////////////////////////////////////////////////////////
// New returns a new context which can be used as the root context for all
// purposes in this framework.
//func New() Context {
// return &context{Context: goctx.Background()}
//}
type ancestryKey int // 0 -> children, 1 -> parent, 2 -> path
const (
@ -97,6 +93,14 @@ func pathCP(ctx context.Context) []string {
return outPath
}
func pathHash(path []string) string {
pathHash := sha256.New()
for _, pathEl := range path {
fmt.Fprintf(pathHash, "%q.", pathEl)
}
return hex.EncodeToString(pathHash.Sum(nil))
}
// Name returns the name this Context was generated with via NewChild, or false
// if this Context was not generated via NewChild.
func Name(ctx context.Context) (string, bool) {
@ -119,6 +123,7 @@ func NewChild(parent context.Context, name string) context.Context {
}
childPath := append(pathCP(parent), name)
child := withoutLocalValues(parent)
child = context.WithValue(child, ancestryKeyChildren, nil) // unset children
child = context.WithValue(child, ancestryKeyChildrenMap, nil) // unset children

94
mctx/stack.go Normal file
View File

@ -0,0 +1,94 @@
package mctx
import (
"context"
"fmt"
"path/filepath"
"runtime"
"strings"
"text/tabwriter"
)
// MaxStackSize indicates the maximum number of stack frames which will be
// stored when embedding stack traces in errors.
var MaxStackSize = 50
type ctxStackKey int
// Stacktrace represents a stack trace at a particular point in execution.
type Stacktrace struct {
frames []uintptr
}
// Frame returns the first frame in the stack.
func (s Stacktrace) Frame() runtime.Frame {
if len(s.frames) == 0 {
panic("cannot call Frame on empty stack")
}
frame, _ := runtime.CallersFrames([]uintptr(s.frames)).Next()
return frame
}
// Frames returns all runtime.Frame instances for this stack.
func (s Stacktrace) Frames() []runtime.Frame {
if len(s.frames) == 0 {
return nil
}
out := make([]runtime.Frame, 0, len(s.frames))
frames := runtime.CallersFrames([]uintptr(s.frames))
for {
frame, more := frames.Next()
out = append(out, frame)
if !more {
break
}
}
return out
}
// String returns a string representing the top-most frame of the stack.
func (s Stacktrace) String() string {
if len(s.frames) == 0 {
return ""
}
frame := s.Frame()
file, dir := filepath.Base(frame.File), filepath.Dir(frame.File)
dir = filepath.Base(dir) // only want the first dirname, ie the pkg name
return fmt.Sprintf("%s/%s:%d", dir, file, frame.Line)
}
// FullString returns the full stack trace.
func (s Stacktrace) FullString() string {
sb := new(strings.Builder)
tw := tabwriter.NewWriter(sb, 0, 4, 4, ' ', 0)
for _, frame := range s.Frames() {
file := fmt.Sprintf("%s:%d", frame.File, frame.Line)
fmt.Fprintf(tw, "%s\t%s\n", file, frame.Function)
}
if err := tw.Flush(); err != nil {
panic(err)
}
return sb.String()
}
// WithStack returns a Context with the current stacktrace embedded in it (as a
// Stacktrace type). If skip is non-zero it will skip that many frames from the
// top of the stack. The frame containing the WithStack call itself is always
// excluded.
func WithStack(ctx context.Context, skip int) context.Context {
stackSlice := make([]uintptr, MaxStackSize)
// incr skip once for WithStack, and once for runtime.Callers
l := runtime.Callers(skip+2, stackSlice)
stack := Stacktrace{frames: stackSlice[:l]}
return context.WithValue(ctx, ctxStackKey(0), stack)
}
// Stack returns the Stacktrace instance which was embedded by WithStack, or false if
// none ever was.
func Stack(ctx context.Context) (Stacktrace, bool) {
stack, ok := ctx.Value(ctxStackKey(0)).(Stacktrace)
return stack, ok
}

View File

@ -1,6 +1,7 @@
package merr
package mctx
import (
"context"
"strings"
. "testing"
@ -8,8 +9,9 @@ import (
)
func TestStack(t *T) {
foo := New("foo")
fooStack := GetStack(foo)
foo := WithStack(context.Background(), 0)
fooStack, ok := Stack(foo)
massert.Fatal(t, massert.Equal(true, ok))
// test Frame
frame := fooStack.Frame()
@ -25,13 +27,13 @@ func TestStack(t *T) {
massert.Equal(true, strings.Contains(frames[0].File, "stack_test.go")),
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
),
"fooStack.String():\n%s", fooStack.String(),
"fooStack.FullString():\n%s", fooStack.FullString(),
))
// test that WithStack works and can be used to skip frames
inner := func() {
bar := WithStack(foo, 1)
barStack := GetStack(bar)
barStack, _ := Stack(bar)
frames := barStack.Frames()
massert.Fatal(t, massert.Comment(
massert.All(
@ -39,7 +41,7 @@ func TestStack(t *T) {
massert.Equal(true, strings.Contains(frames[0].File, "stack_test.go")),
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
),
"barStack.String():\n%s", barStack.String(),
"barStack.FullString():\n%s", barStack.FullString(),
))
}
inner()

View File

@ -42,43 +42,37 @@ type BigQuery struct {
tableUploaders map[[2]string]*bigquery.Uploader
}
// MNew returns a BigQuery instance which will be initialized and configured
// when the start event is triggered on the returned (see mrun.Start). The
// BigQuery instance will have Close called on it when the stop event is
// triggered on the returned Context (see mrun.Stop).
// WithBigQuery returns a BigQuery instance which will be initialized and
// configured when the start event is triggered on the returned (see
// mrun.Start). The BigQuery instance will have Close called on it when the stop
// event is triggered on the returned Context (see mrun.Stop).
//
// gce is optional and can be passed in if there's an existing gce object which
// should be used, otherwise a new one will be created with mdb.MGCE.
func MNew(ctx context.Context, gce *mdb.GCE) (context.Context, *BigQuery) {
func WithBigQuery(parent context.Context, gce *mdb.GCE) (context.Context, *BigQuery) {
ctx := mctx.NewChild(parent, "mbigquery")
if gce == nil {
ctx, gce = mdb.MGCE(ctx, "")
ctx, gce = mdb.WithGCE(ctx, "")
}
bq := &BigQuery{
gce: gce,
tables: map[[2]string]*bigquery.Table{},
tableUploaders: map[[2]string]*bigquery.Uploader{},
ctx: mctx.NewChild(ctx, "bigquery"),
}
// TODO the equivalent functionality as here will be added with annotations
// bq.log.SetKV(bq)
bq.ctx = mrun.OnStart(bq.ctx, func(innerCtx context.Context) error {
mlog.Info(bq.ctx, "connecting to bigquery")
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
bq.ctx = mctx.MergeAnnotations(bq.ctx, bq.gce.Context())
mlog.Info("connecting to bigquery", bq.ctx)
var err error
bq.Client, err = bigquery.NewClient(innerCtx, bq.gce.Project, bq.gce.ClientOptions()...)
return merr.WithKV(err, bq.KV())
return merr.Wrap(bq.ctx, err)
})
bq.ctx = mrun.OnStop(bq.ctx, func(context.Context) error {
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return bq.Client.Close()
})
return mctx.WithChild(ctx, bq.ctx), bq
}
// KV implements the mlog.KVer interface.
func (bq *BigQuery) KV() map[string]interface{} {
return bq.gce.KV()
bq.ctx = ctx
return mctx.WithChild(parent, ctx), bq
}
// Table initializes and returns the table instance with the given dataset and
@ -100,17 +94,18 @@ func (bq *BigQuery) Table(
return table, bq.tableUploaders[key], nil
}
kv := mlog.KV{"bigQueryDataset": dataset, "bigQueryTable": tableName}
mlog.Debug(bq.ctx, "creating/grabbing table", kv)
ctx = mctx.MergeAnnotations(ctx, bq.ctx)
ctx = mctx.Annotate(ctx, "dataset", dataset, "table", tableName)
mlog.Debug("creating/grabbing table", bq.ctx)
schema, err := bigquery.InferSchema(schemaObj)
if err != nil {
return nil, nil, merr.WithKV(err, bq.KV(), kv.KV())
return nil, nil, merr.Wrap(ctx, err)
}
ds := bq.Dataset(dataset)
if err := ds.Create(ctx, nil); err != nil && !isErrAlreadyExists(err) {
return nil, nil, merr.WithKV(err, bq.KV(), kv.KV())
return nil, nil, merr.Wrap(ctx, err)
}
table := ds.Table(tableName)
@ -119,7 +114,7 @@ func (bq *BigQuery) Table(
Schema: schema,
}
if err := table.Create(ctx, meta); err != nil && !isErrAlreadyExists(err) {
return nil, nil, merr.WithKV(err, bq.KV(), kv.KV())
return nil, nil, merr.Wrap(ctx, err)
}
uploader := table.Uploader()

View File

@ -31,61 +31,56 @@ type Bigtable struct {
ctx context.Context
}
// MNew returns a Bigtable instance which will be initialized and configured
// when the start event is triggered on the returned Context (see mrun.Start).
// The Bigtable instance will have Close called on it when the stop event is
// triggered on the returned Context (see mrun.Stop).
// WithBigTable returns a Bigtable instance which will be initialized and
// configured when the start event is triggered on the returned Context (see
// mrun.Start). The Bigtable instance will have Close called on it when the
// stop event is triggered on the returned Context (see mrun.Stop).
//
// gce is optional and can be passed in if there's an existing gce object which
// should be used, otherwise a new one will be created with mdb.MGCE.
//
// defaultInstance can be given as the instance name to use as the default
// parameter value. If empty the parameter will be required to be set.
func MNew(ctx context.Context, gce *mdb.GCE, defaultInstance string) (context.Context, *Bigtable) {
func WithBigTable(parent context.Context, gce *mdb.GCE, defaultInstance string) (context.Context, *Bigtable) {
ctx := mctx.NewChild(parent, "bigtable")
if gce == nil {
ctx, gce = mdb.MGCE(ctx, "")
ctx, gce = mdb.WithGCE(ctx, "")
}
bt := &Bigtable{
gce: gce,
ctx: mctx.NewChild(ctx, "bigtable"),
}
// TODO the equivalent functionality as here will be added with annotations
// bt.log.SetKV(bt)
var inst *string
{
const name, descr = "instance", "name of the bigtable instance in the project to connect to"
if defaultInstance != "" {
bt.ctx, inst = mcfg.String(bt.ctx, name, defaultInstance, descr)
ctx, inst = mcfg.WithString(ctx, name, defaultInstance, descr)
} else {
bt.ctx, inst = mcfg.RequiredString(bt.ctx, name, descr)
ctx, inst = mcfg.WithRequiredString(ctx, name, descr)
}
}
bt.ctx = mrun.OnStart(bt.ctx, func(innerCtx context.Context) error {
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
bt.Instance = *inst
mlog.Info(bt.ctx, "connecting to bigtable", bt)
bt.ctx = mctx.MergeAnnotations(bt.ctx, bt.gce.Context())
bt.ctx = mctx.Annotate(bt.ctx, "instance", bt.Instance)
mlog.Info("connecting to bigtable", bt.ctx)
var err error
bt.Client, err = bigtable.NewClient(
innerCtx,
bt.gce.Project, bt.Instance,
bt.gce.ClientOptions()...,
)
return merr.WithKV(err, bt.KV())
return merr.Wrap(bt.ctx, err)
})
bt.ctx = mrun.OnStop(bt.ctx, func(context.Context) error {
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return bt.Client.Close()
})
return mctx.WithChild(ctx, bt.ctx), bt
}
// KV implements the mlog.KVer interface.
func (bt *Bigtable) KV() map[string]interface{} {
kv := bt.gce.KV()
kv["bigtableInstance"] = bt.Instance
return kv
bt.ctx = ctx
return mctx.WithChild(parent, ctx), bt
}
// EnsureTable ensures that the given table exists and has (at least) the given
@ -93,28 +88,29 @@ func (bt *Bigtable) KV() map[string]interface{} {
//
// This method requires admin privileges on the bigtable instance.
func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...string) error {
kv := mlog.KV{"bigtableTable": name}
mlog.Info(bt.ctx, "ensuring table", kv)
ctx = mctx.MergeAnnotations(ctx, bt.ctx)
ctx = mctx.Annotate(ctx, "table", name)
mlog.Info("ensuring table", ctx)
mlog.Debug(bt.ctx, "creating admin client", kv)
mlog.Debug("creating admin client", ctx)
adminClient, err := bigtable.NewAdminClient(ctx, bt.gce.Project, bt.Instance)
if err != nil {
return merr.WithKV(err, bt.KV(), kv.KV())
return merr.Wrap(ctx, err)
}
defer adminClient.Close()
mlog.Debug(bt.ctx, "creating bigtable table (if needed)", kv)
mlog.Debug("creating bigtable table (if needed)", ctx)
err = adminClient.CreateTable(ctx, name)
if err != nil && !isErrAlreadyExists(err) {
return merr.WithKV(err, bt.KV(), kv.KV())
return merr.Wrap(ctx, err)
}
for _, colFam := range colFams {
kv := kv.Set("family", colFam)
mlog.Debug(bt.ctx, "creating bigtable column family (if needed)", kv)
ctx := mctx.Annotate(ctx, "family", colFam)
mlog.Debug("creating bigtable column family (if needed)", ctx)
err := adminClient.CreateColumnFamily(ctx, name, colFam)
if err != nil && !isErrAlreadyExists(err) {
return merr.WithKV(err, bt.KV(), kv.KV())
return merr.Wrap(ctx, err)
}
}

View File

@ -11,9 +11,9 @@ import (
)
func TestBasic(t *T) {
ctx := mtest.NewCtx()
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "testProject")
ctx, bt := MNew(ctx, nil, "testInstance")
ctx := mtest.Context()
ctx = mtest.WithEnv(ctx, "BIGTABLE_GCE_PROJECT", "testProject")
ctx, bt := WithBigTable(ctx, nil, "testInstance")
mtest.Run(ctx, t, func() {
tableName := "test-" + mrand.Hex(8)

View File

@ -22,39 +22,33 @@ type Datastore struct {
ctx context.Context
}
// MNew returns a Datastore instance which will be initialized and configured
// when the start event is triggered on the returned Context (see mrun.Start).
// The Datastore instance will have Close called on it when the stop event is
// triggered on the returned Context (see mrun.Stop).
// WithDatastore returns a Datastore instance which will be initialized and
// configured when the start event is triggered on the returned Context (see
// mrun.Start). The Datastore instance will have Close called on it when the
// stop event is triggered on the returned Context (see mrun.Stop).
//
// gce is optional and can be passed in if there's an existing gce object which
// should be used, otherwise a new one will be created with mdb.MGCE.
func MNew(ctx context.Context, gce *mdb.GCE) (context.Context, *Datastore) {
func WithDatastore(parent context.Context, gce *mdb.GCE) (context.Context, *Datastore) {
ctx := mctx.NewChild(parent, "datastore")
if gce == nil {
ctx, gce = mdb.MGCE(ctx, "")
ctx, gce = mdb.WithGCE(ctx, "")
}
ds := &Datastore{
gce: gce,
ctx: mctx.NewChild(ctx, "datastore"),
}
// TODO the equivalent functionality as here will be added with annotations
// ds.log.SetKV(ds)
ds.ctx = mrun.OnStart(ds.ctx, func(innerCtx context.Context) error {
mlog.Info(ds.ctx, "connecting to datastore")
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
ds.ctx = mctx.MergeAnnotations(ds.ctx, ds.gce.Context())
mlog.Info("connecting to datastore", ds.ctx)
var err error
ds.Client, err = datastore.NewClient(innerCtx, ds.gce.Project, ds.gce.ClientOptions()...)
return merr.WithKV(err, ds.KV())
return merr.Wrap(ds.ctx, err)
})
ds.ctx = mrun.OnStop(ds.ctx, func(context.Context) error {
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return ds.Client.Close()
})
return mctx.WithChild(ctx, ds.ctx), ds
}
// KV implements the mlog.KVer interface.
func (ds *Datastore) KV() map[string]interface{} {
return ds.gce.KV()
ds.ctx = ctx
return mctx.WithChild(parent, ctx), ds
}

View File

@ -11,9 +11,9 @@ import (
// Requires datastore emulator to be running
func TestBasic(t *T) {
ctx := mtest.NewCtx()
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
ctx, ds := MNew(ctx, nil)
ctx := mtest.Context()
ctx = mtest.WithEnv(ctx, "DATASTORE_GCE_PROJECT", "test")
ctx, ds := WithDatastore(ctx, nil)
mtest.Run(ctx, t, func() {
name := mrand.Hex(8)
key := datastore.NameKey("testKind", name, nil)

View File

@ -7,7 +7,6 @@ import (
"github.com/mediocregopher/mediocre-go-lib/mcfg"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mlog"
"github.com/mediocregopher/mediocre-go-lib/mrun"
"google.golang.org/api/option"
)
@ -15,32 +14,36 @@ import (
// GCE wraps configuration parameters commonly used for interacting with GCE
// services.
type GCE struct {
ctx context.Context
Project string
CredFile string
}
// MGCE returns a GCE instance which will be initialized and configured when the
// start event is triggered on the returned Context (see mrun.Start).
// WithGCE returns a GCE instance which will be initialized and configured when
// the start event is triggered on the returned Context (see mrun.Start).
// defaultProject is used as the default value for the mcfg parameter this
// function creates.
func MGCE(parent context.Context, defaultProject string) (context.Context, *GCE) {
func WithGCE(parent context.Context, defaultProject string) (context.Context, *GCE) {
ctx := mctx.NewChild(parent, "gce")
ctx, credFile := mcfg.String(ctx, "cred-file", "", "Path to GCE credientials JSON file, if any")
ctx, credFile := mcfg.WithString(ctx, "cred-file", "", "Path to GCE credientials JSON file, if any")
var project *string
const projectUsage = "Name of GCE project to use"
if defaultProject == "" {
ctx, project = mcfg.RequiredString(ctx, "project", projectUsage)
ctx, project = mcfg.WithRequiredString(ctx, "project", projectUsage)
} else {
ctx, project = mcfg.String(ctx, "project", defaultProject, projectUsage)
ctx, project = mcfg.WithString(ctx, "project", defaultProject, projectUsage)
}
var gce GCE
ctx = mrun.OnStart(ctx, func(context.Context) error {
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
gce.Project = *project
gce.CredFile = *credFile
gce.ctx = mctx.Annotate(ctx, "project", gce.Project)
return nil
})
gce.ctx = ctx
return mctx.WithChild(parent, ctx), &gce
}
@ -54,9 +57,7 @@ func (gce *GCE) ClientOptions() []option.ClientOption {
return opts
}
// KV implements the mlog.KVer interface.
func (gce *GCE) KV() map[string]interface{} {
return mlog.KV{
"gceProject": gce.Project,
}
// Context returns the annotated Context from this instance's initialization.
func (gce *GCE) Context() context.Context {
return gce.ctx
}

View File

@ -4,7 +4,6 @@ package mpubsub
import (
"context"
"errors"
"sync"
"time"
@ -41,45 +40,40 @@ type PubSub struct {
ctx context.Context
}
// MNew returns a PubSub instance which will be initialized and configured when
// the start event is triggered on the returned Context (see mrun.Start). The
// PubSub instance will have Close called on it when the stop event is triggered
// on the returned Context(see mrun.Stop).
// WithPubSub returns a PubSub instance which will be initialized and configured
// when the start event is triggered on the returned Context (see mrun.Start).
// The PubSub instance will have Close called on it when the stop event is
// triggered on the returned Context(see mrun.Stop).
//
// gce is optional and can be passed in if there's an existing gce object which
// should be used, otherwise a new one will be created with mdb.MGCE.
func MNew(ctx context.Context, gce *mdb.GCE) (context.Context, *PubSub) {
func WithPubSub(parent context.Context, gce *mdb.GCE) (context.Context, *PubSub) {
ctx := mctx.NewChild(parent, "pubsub")
if gce == nil {
ctx, gce = mdb.MGCE(ctx, "")
ctx, gce = mdb.WithGCE(ctx, "")
}
ps := &PubSub{
gce: gce,
ctx: mctx.NewChild(ctx, "pubsub"),
}
// TODO the equivalent functionality as here will be added with annotations
// ps.log.SetKV(ps)
ps.ctx = mrun.OnStart(ps.ctx, func(innerCtx context.Context) error {
mlog.Info(ps.ctx, "connecting to pubsub")
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
ps.ctx = mctx.MergeAnnotations(ps.ctx, ps.gce.Context())
mlog.Info("connecting to pubsub", ps.ctx)
var err error
ps.Client, err = pubsub.NewClient(innerCtx, ps.gce.Project, ps.gce.ClientOptions()...)
return merr.WithKV(err, ps.KV())
return merr.Wrap(ps.ctx, err)
})
ps.ctx = mrun.OnStop(ps.ctx, func(context.Context) error {
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return ps.Client.Close()
})
return mctx.WithChild(ctx, ps.ctx), ps
}
// KV implements the mlog.KVer interface
func (ps *PubSub) KV() map[string]interface{} {
return ps.gce.KV()
ps.ctx = ctx
return mctx.WithChild(parent, ctx), ps
}
// Topic provides methods around a particular topic in PubSub
type Topic struct {
ctx context.Context
ps *PubSub
topic *pubsub.Topic
name string
@ -88,6 +82,7 @@ type Topic struct {
// Topic returns, after potentially creating, a topic of the given name
func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic, error) {
t := &Topic{
ctx: mctx.Annotate(ps.ctx, "topicName", name),
ps: ps,
name: name,
}
@ -98,38 +93,31 @@ func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic,
if isErrAlreadyExists(err) {
t.topic = ps.Client.Topic(name)
} else if err != nil {
return nil, merr.WithKV(err, t.KV())
return nil, merr.Wrap(ctx, merr.Wrap(t.ctx, err))
}
} else {
t.topic = ps.Client.Topic(name)
if exists, err := t.topic.Exists(ctx); err != nil {
return nil, merr.WithKV(err, t.KV())
if exists, err := t.topic.Exists(t.ctx); err != nil {
return nil, merr.Wrap(ctx, merr.Wrap(t.ctx, err))
} else if !exists {
err := merr.New("topic dne")
return nil, merr.WithKV(err, t.KV())
return nil, merr.Wrap(ctx, merr.New(t.ctx, "topic dne"))
}
}
return t, nil
}
// KV implements the mlog.KVer interface
func (t *Topic) KV() map[string]interface{} {
kv := t.ps.KV()
kv["topicName"] = t.name
return kv
}
// Publish publishes a message with the given data as its body to the Topic
func (t *Topic) Publish(ctx context.Context, data []byte) error {
_, err := t.topic.Publish(ctx, &Message{Data: data}).Get(ctx)
if err != nil {
return merr.WithKV(err, t.KV())
return merr.Wrap(ctx, merr.Wrap(t.ctx, err))
}
return nil
}
// Subscription provides methods around a subscription to a topic in PubSub
type Subscription struct {
ctx context.Context
topic *Topic
sub *pubsub.Subscription
name string
@ -143,6 +131,7 @@ type Subscription struct {
func (t *Topic) Subscription(ctx context.Context, name string, create bool) (*Subscription, error) {
name = t.name + "_" + name
s := &Subscription{
ctx: mctx.Annotate(t.ctx, "subName", name),
topic: t,
name: name,
}
@ -155,27 +144,19 @@ func (t *Topic) Subscription(ctx context.Context, name string, create bool) (*Su
if isErrAlreadyExists(err) {
s.sub = t.ps.Subscription(name)
} else if err != nil {
return nil, merr.WithKV(err, s.KV())
return nil, merr.Wrap(ctx, merr.Wrap(s.ctx, err))
}
} else {
s.sub = t.ps.Subscription(name)
if exists, err := s.sub.Exists(ctx); err != nil {
return nil, merr.WithKV(err, s.KV())
return nil, merr.Wrap(ctx, merr.Wrap(s.ctx, err))
} else if !exists {
err := merr.New("sub dne")
return nil, merr.WithKV(err, s.KV())
return nil, merr.Wrap(ctx, merr.New(s.ctx, "sub dne"))
}
}
return s, nil
}
// KV implements the mlog.KVer interface
func (s *Subscription) KV() map[string]interface{} {
kv := s.topic.KV()
kv["subName"] = s.name
return kv
}
// ConsumerFunc is a function which messages being consumed will be passed. The
// returned boolean and returned error are independent. If the bool is false the
// message will be returned to the queue for retrying later. If an error is
@ -221,12 +202,14 @@ func (s *Subscription) Consume(ctx context.Context, fn ConsumerFunc, opts Consum
octx := oldctx.Context(ctx)
for {
err := s.sub.Receive(octx, func(octx oldctx.Context, msg *Message) {
innerCtx, cancel := oldctx.WithTimeout(octx, opts.Timeout)
innerOCtx, cancel := oldctx.WithTimeout(octx, opts.Timeout)
defer cancel()
innerCtx := context.Context(innerOCtx)
ok, err := fn(context.Context(innerCtx), msg)
ok, err := fn(innerCtx, msg)
if err != nil {
mlog.Warn(s.topic.ps.ctx, "error consuming pubsub message", s, merr.KV(err))
mlog.Warn("error consuming pubsub message",
s.ctx, ctx, innerCtx, merr.Context(err))
}
if ok {
@ -238,7 +221,8 @@ func (s *Subscription) Consume(ctx context.Context, fn ConsumerFunc, opts Consum
if octx.Err() == context.Canceled || err == nil {
return
} else if err != nil {
mlog.Warn(s.topic.ps.ctx, "error consuming from pubsub", s, merr.KV(err))
mlog.Warn("error consuming from pubsub",
s.ctx, ctx, merr.Context(err))
time.Sleep(1 * time.Second)
}
}
@ -331,7 +315,8 @@ func (s *Subscription) BatchConsume(
}
ret, err := fn(thisCtx, msgs)
if err != nil {
mlog.Warn(s.topic.ps.ctx, "error consuming pubsub batch messages", s, merr.KV(err))
mlog.Warn("error consuming pubsub batch messages",
s.ctx, merr.Context(err))
}
for i := range thisGroup {
thisGroup[i].retCh <- ret // retCh is buffered
@ -365,7 +350,7 @@ func (s *Subscription) BatchConsume(
case ret := <-retCh:
return ret, nil
case <-ctx.Done():
return false, errors.New("reading from batch grouping process timed out")
return false, merr.Wrap(ctx, merr.New(s.ctx, "reading from batch grouping process timed out"))
}
}, opts)

View File

@ -13,9 +13,9 @@ import (
// this requires the pubsub emulator to be running
func TestPubSub(t *T) {
ctx := mtest.NewCtx()
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
ctx, ps := MNew(ctx, nil)
ctx := mtest.Context()
ctx = mtest.WithEnv(ctx, "PUBSUB_GCE_PROJECT", "test")
ctx, ps := WithPubSub(ctx, nil)
mtest.Run(ctx, t, func() {
topicName := "testTopic_" + mrand.Hex(8)
ctx := context.Background()
@ -47,9 +47,9 @@ func TestPubSub(t *T) {
}
func TestBatchPubSub(t *T) {
ctx := mtest.NewCtx()
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
ctx, ps := MNew(ctx, nil)
ctx := mtest.Context()
ctx = mtest.WithEnv(ctx, "PUBSUB_GCE_PROJECT", "test")
ctx, ps := WithPubSub(ctx, nil)
mtest.Run(ctx, t, func() {
topicName := "testBatchTopic_" + mrand.Hex(8)

View File

@ -1,120 +0,0 @@
package merr
import (
"fmt"
"path/filepath"
)
// WithValue returns a copy of the original error, automatically wrapping it if
// the error is not from merr (see Wrap). The returned error has a value set on
// with for the given key.
//
// visible determines whether or not the value is visible in the output of
// Error.
func WithValue(e error, k, v interface{}, visible bool) error {
if e == nil {
return nil
}
er := wrap(e, true, 1)
er.attr[k] = val{val: v, visible: visible}
return er
}
// GetValue returns the value embedded in the error for the given key, or nil if
// the error isn't from this package or doesn't have that key embedded.
func GetValue(e error, k interface{}) interface{} {
if e == nil {
return nil
}
return wrap(e, false, -1).attr[k].val
}
////////////////////////////////////////////////////////////////////////////////
// not really used for attributes, but w/e
const attrKeyErr attrKey = "err"
const attrKeyErrSrc attrKey = "errSrc"
// KVer implements the mlog.KVer interface. This is defined here to avoid this
// package needing to actually import mlog.
type KVer struct {
kv map[string]interface{}
}
// KV implements the mlog.KVer interface.
func (kv KVer) KV() map[string]interface{} {
return kv.kv
}
// KV returns a KVer which contains all visible values embedded in the error, as
// well as the original error string itself. Keys will be turned into strings
// using the fmt.Sprint function.
//
// If any keys conflict then their type information will be included as part of
// the key.
func KV(e error) KVer {
if e == nil {
return KVer{}
}
er := wrap(e, false, 1)
kvm := make(map[string]interface{}, len(er.attr)+1)
keys := map[string]interface{}{} // in this case the value is the raw key
setKey := func(k, v interface{}) {
kStr := fmt.Sprint(k)
oldKey := keys[kStr]
if oldKey == nil {
keys[kStr] = k
kvm[kStr] = v
return
}
// check if oldKey is in kvm, if so it needs to be moved to account for
// its type info
if oldV, ok := kvm[kStr]; ok {
delete(kvm, kStr)
kvm[fmt.Sprintf("%T(%s)", oldKey, kStr)] = oldV
}
kvm[fmt.Sprintf("%T(%s)", k, kStr)] = v
}
setKey(attrKeyErr, er.err.Error())
for k, v := range er.attr {
if !v.visible {
continue
}
stack, ok := v.val.(Stack)
if !ok {
setKey(k, v.val)
continue
}
// compress the stack trace to just be the top-most frame
frame := stack.Frame()
file, dir := filepath.Base(frame.File), filepath.Dir(frame.File)
dir = filepath.Base(dir) // only want the first dirname, ie the pkg name
setKey(attrKeyErrSrc, fmt.Sprintf("%s/%s:%d", dir, file, frame.Line))
}
return KVer{kvm}
}
type kvKey string
// WithKV embeds key/value pairs into an error, just like WithValue, but it does
// so via one or more passed in maps
func WithKV(e error, kvMaps ...map[string]interface{}) error {
if e == nil {
return nil
}
er := wrap(e, true, 1)
for _, kvMap := range kvMaps {
for k, v := range kvMap {
er.attr[kvKey(k)] = val{val: v, visible: true}
}
}
return er
}

View File

@ -1,98 +0,0 @@
package merr
import (
"strings"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestKV(t *T) {
massert.Fatal(t, massert.All(
massert.Nil(WithValue(nil, "foo", "bar", true)),
massert.Nil(WithValue(nil, "foo", "bar", false)),
massert.Nil(GetValue(nil, "foo")),
massert.Len(KV(nil).KV(), 0),
))
er := New("foo", "bar", "baz")
kv := KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 3),
massert.Equal("foo", kv["err"]),
massert.Equal("baz", kv["bar"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
),
"kv: %#v", kv,
))
type A string
type B string
type C string
er = WithValue(er, "invisible", "you can't see me", false)
er = WithValue(er, A("k"), "1", true)
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 4),
massert.Equal("foo", kv["err"]),
massert.Equal("baz", kv["bar"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["k"]),
),
"kv: %#v", kv,
))
er = WithValue(er, B("k"), "2", true)
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 5),
massert.Equal("foo", kv["err"]),
massert.Equal("baz", kv["bar"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["merr.A(k)"]),
massert.Equal("2", kv["merr.B(k)"]),
),
"kv: %#v", kv,
))
er = WithValue(er, C("k"), "3", true)
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 6),
massert.Equal("foo", kv["err"]),
massert.Equal("baz", kv["bar"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["merr.A(k)"]),
massert.Equal("2", kv["merr.B(k)"]),
massert.Equal("3", kv["merr.C(k)"]),
),
"kv: %#v", kv,
))
er = WithKV(er, map[string]interface{}{"D": 4, "k": 5})
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 8),
massert.Equal("foo", kv["err"]),
massert.Equal("baz", kv["bar"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["merr.A(k)"]),
massert.Equal("2", kv["merr.B(k)"]),
massert.Equal("3", kv["merr.C(k)"]),
massert.Equal(4, kv["D"]),
massert.Equal(5, kv["merr.kvKey(k)"]),
),
"kv: %#v", kv,
))
}

View File

@ -9,11 +9,12 @@
package merr
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"github.com/mediocregopher/mediocre-go-lib/mctx"
)
var strBuilderPool = sync.Pool{
@ -27,114 +28,158 @@ func putStrBuilder(sb *strings.Builder) {
////////////////////////////////////////////////////////////////////////////////
type val struct {
visible bool
val interface{}
}
func (v val) String() string {
return fmt.Sprint(v.val)
}
type err struct {
err error
attr map[interface{}]val
attr map[interface{}]interface{}
}
// attr keys internal to this package
type attrKey string
type attrKey int
func wrap(e error, cp bool, skip int) *err {
const (
attrKeyCtx attrKey = iota
)
func wrap(e error, cp bool) *err {
if e == nil {
return nil
}
er, ok := e.(*err)
if !ok {
er := &err{err: e, attr: map[interface{}]val{}}
if skip >= 0 {
setStack(er, skip+1)
}
return er
return &err{err: e}
} else if !cp {
return er
}
er2 := &err{
err: er.err,
attr: make(map[interface{}]val, len(er.attr)),
attr: make(map[interface{}]interface{}, len(er.attr)),
}
for k, v := range er.attr {
er2.attr[k] = v
}
if _, ok := er2.attr[attrKeyStack]; !ok && skip >= 0 {
setStack(er, skip+1)
}
return er2
}
// Wrap takes in an error and returns one wrapping it in merr's inner type,
// which embeds information like the stack trace.
func Wrap(e error) error {
return wrap(e, false, 1)
// Base takes in an error and checks if it is merr's internal error type. If it
// is then the underlying error which is being wrapped is returned. If it's not
// then the passed in error is returned as-is.
func Base(e error) error {
if er, ok := e.(*err); ok {
return er.err
}
return e
}
// New returns a new error with the given string as its error string. New
// automatically wraps the error in merr's inner type, which embeds information
// like the stack trace.
//
// For convenience, visible key/values may be passed into New at this point. For
// example, the following two are equivalent:
//
// merr.WithValue(merr.New("foo"), "bar", "baz", true)
// merr.New("foo", "bar", "baz")
//
func New(str string, kvs ...interface{}) error {
if len(kvs)%2 != 0 {
panic("key passed in without corresponding value")
// WithValue returns a copy of the original error, automatically wrapping it if
// the error is not from merr (see Wrap). The returned error has an attribute
// value set on it for the given key.
func WithValue(e error, k, v interface{}) error {
if e == nil {
return nil
}
err := wrap(errors.New(str), false, 1)
for i := 0; i < len(kvs); i += 2 {
err.attr[kvs[i]] = val{
visible: true,
val: kvs[i+1],
er := wrap(e, true)
if er.attr == nil {
er.attr = map[interface{}]interface{}{}
}
}
return err
er.attr[k] = v
return er
}
func (er *err) visibleAttrs() [][2]string {
out := make([][2]string, 0, len(er.attr))
for k, v := range er.attr {
if !v.visible {
continue
// Value returns the value embedded in the error for the given key, or nil if
// the error isn't from this package or doesn't have that key embedded.
func Value(e error, k interface{}) interface{} {
if e == nil {
return nil
}
out = append(out, [2]string{
strings.Trim(fmt.Sprintf("%q", k), `"`),
fmt.Sprint(v.val),
})
return wrap(e, false).attr[k]
}
// WrapSkip is like Wrap but also allows for skipping extra stack frames when
// embedding the stack into the error.
func WrapSkip(ctx context.Context, e error, skip int, kvs ...interface{}) error {
prevCtx, _ := Value(e, attrKeyCtx).(context.Context)
if prevCtx != nil {
ctx = mctx.MergeAnnotations(prevCtx, ctx)
}
sort.Slice(out, func(i, j int) bool {
return out[i][0] < out[j][0]
})
if _, ok := mctx.Stack(ctx); !ok {
ctx = mctx.WithStack(ctx, skip+1)
}
if len(kvs) > 0 {
ctx = mctx.Annotate(ctx, kvs...)
}
return out
return WithValue(e, attrKeyCtx, ctx)
}
// Wrap takes in an error and returns one which wraps it in merr's inner type,
// embedding the given Context (which can later be retrieved by Ctx) at the same
// time.
//
// For convenience, extra annotation information can be passed in here as well
// via the kvs argument. See mctx.Annotate for more information.
//
// This function automatically embeds stack information into the Context as it's
// being stored, using mctx.WithStack, unless the error already has stack
// information in it.
func Wrap(ctx context.Context, e error, kvs ...interface{}) error {
return WrapSkip(ctx, e, 1, kvs...)
}
// New is a shortcut for:
// merr.Wrap(ctx, errors.New(str), kvs...)
func New(ctx context.Context, str string, kvs ...interface{}) error {
return WrapSkip(ctx, errors.New(str), 1, kvs...)
}
// TODO it would be more convenient in a lot of cases if New and Wrap took in a
// list of Contexts.
type annotateKey string
func ctx(e error) context.Context {
ctx, _ := Value(e, attrKeyCtx).(context.Context)
if ctx == nil {
ctx = context.Background()
}
if stack, ok := mctx.Stack(ctx); ok {
ctx = mctx.Annotate(ctx, annotateKey("errLoc"), stack.String())
}
return ctx
}
// Context returns the Context embedded in this error from the last call to Wrap
// or New. If none is embedded this uses context.Background().
//
// The returned Context will have annotated on it (see mctx.Annotate) the
// underlying error's string (as returned by Error()) and the stack location in
// the Context. Stack locations are automatically added by New and Wrap via
// mctx.WithStack.
//
// If this error is nil this returns context.Background().
func Context(e error) context.Context {
if e == nil {
return context.Background()
}
ctx := ctx(e)
ctx = mctx.Annotate(ctx, annotateKey("err"), Base(e).Error())
return ctx
}
func (er *err) Error() string {
visAttrs := er.visibleAttrs()
if len(visAttrs) == 0 {
return er.err.Error()
}
ctx := ctx(er)
sb := strBuilderPool.Get().(*strings.Builder)
defer putStrBuilder(sb)
sb.WriteString(strings.TrimSpace(er.err.Error()))
for _, attr := range visAttrs {
k, v := strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1])
annotations := mctx.Annotations(ctx).StringSlice(true)
for _, kve := range annotations {
k, v := strings.TrimSpace(kve[0]), strings.TrimSpace(kve[1])
sb.WriteString("\n\t* ")
sb.WriteString(k)
sb.WriteString(": ")
@ -154,16 +199,6 @@ func (er *err) Error() string {
return sb.String()
}
// Base takes in an error and checks if it is merr's internal error type. If it
// is then the underlying error which is being wrapped is returned. If it's not
// then the passed in error is returned as-is.
func Base(e error) error {
if er, ok := e.(*err); ok {
return er.err
}
return e
}
// Equal is a shortcut for Base(e1) == Base(e2).
func Equal(e1, e2 error) bool {
return Base(e1) == Base(e2)

View File

@ -1,35 +1,33 @@
package merr
import (
"context"
"errors"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestError(t *T) {
er := &err{
err: errors.New("foo"),
attr: map[interface{}]val{
"a": val{val: "aaa aaa\n", visible: true},
"b": val{val: "invisible"},
"c": val{val: "ccc\nccc\n", visible: true},
"d\t": val{val: "weird key but ok", visible: true},
},
}
str := er.Error()
e := New(context.Background(), "foo",
"a", "aaa aaa\n",
"c", "ccc\nccc\n",
"d\t", "weird key but ok",
)
exp := `foo
* a: aaa aaa
* c:
ccc
ccc
* d\t: weird key but ok`
massert.Fatal(t, massert.Equal(exp, str))
* d: weird key but ok
* errLoc: merr/merr_test.go:13`
massert.Fatal(t, massert.Equal(exp, e.Error()))
}
func TestBase(t *T) {
errFoo, errBar := errors.New("foo"), errors.New("bar")
erFoo := Wrap(errFoo)
erFoo := Wrap(context.Background(), errFoo)
massert.Fatal(t, massert.All(
massert.Nil(Base(nil)),
massert.Equal(errFoo, Base(erFoo)),
@ -40,3 +38,50 @@ func TestBase(t *T) {
massert.Equal(false, Equal(errBar, erFoo)),
))
}
func TestValue(t *T) {
massert.Fatal(t, massert.All(
massert.Nil(WithValue(nil, "foo", "bar")),
massert.Nil(Value(nil, "foo")),
))
e1 := New(context.Background(), "foo")
e1 = WithValue(e1, "a", "A")
e2 := WithValue(errors.New("bar"), "a", "A")
massert.Fatal(t, massert.All(
massert.Equal("A", Value(e1, "a")),
massert.Equal("A", Value(e2, "a")),
))
e3 := WithValue(e2, "a", "AAA")
massert.Fatal(t, massert.All(
massert.Equal("A", Value(e1, "a")),
massert.Equal("A", Value(e2, "a")),
massert.Equal("AAA", Value(e3, "a")),
))
}
func mkErr(ctx context.Context, err error) error {
return Wrap(ctx, err) // it's important that this is line 65
}
func TestCtx(t *T) {
ctxA := mctx.Annotate(context.Background(), "0", "ZERO", "1", "one")
ctxB := mctx.Annotate(context.Background(), "1", "ONE", "2", "TWO")
// use mkErr so that it's easy to test that the stack info isn't overwritten
// when Wrap is called with ctxB.
e := mkErr(ctxA, errors.New("hello"))
e = Wrap(ctxB, e)
err := massert.Equal(map[string]string{
"0": "ZERO",
"1": "ONE",
"2": "TWO",
"err": "hello",
"errLoc": "merr/merr_test.go:65",
}, mctx.Annotations(Context(e)).StringMap()).Assert()
if err != nil {
t.Fatal(err)
}
}

View File

@ -1,85 +0,0 @@
package merr
import (
"fmt"
"runtime"
"strings"
"text/tabwriter"
)
// MaxStackSize indicates the maximum number of stack frames which will be
// stored when embedding stack traces in errors.
var MaxStackSize = 50
const attrKeyStack attrKey = "stack"
// Stack represents a stack trace at a particular point in execution.
type Stack []uintptr
// Frame returns the first frame in the stack.
func (s Stack) Frame() runtime.Frame {
if len(s) == 0 {
panic("cannot call Frame on empty stack")
}
frame, _ := runtime.CallersFrames([]uintptr(s)).Next()
return frame
}
// Frames returns all runtime.Frame instances for this stack.
func (s Stack) Frames() []runtime.Frame {
if len(s) == 0 {
return nil
}
out := make([]runtime.Frame, 0, len(s))
frames := runtime.CallersFrames([]uintptr(s))
for {
frame, more := frames.Next()
out = append(out, frame)
if !more {
break
}
}
return out
}
// String returns the full stack trace.
func (s Stack) String() string {
sb := strBuilderPool.Get().(*strings.Builder)
defer putStrBuilder(sb)
tw := tabwriter.NewWriter(sb, 0, 4, 4, ' ', 0)
for _, frame := range s.Frames() {
file := fmt.Sprintf("%s:%d", frame.File, frame.Line)
fmt.Fprintf(tw, "%s\t%s\n", file, frame.Function)
}
if err := tw.Flush(); err != nil {
panic(err)
}
return sb.String()
}
func setStack(er *err, skip int) {
stackSlice := make([]uintptr, MaxStackSize)
// incr skip once for setStack, and once for runtime.Callers
l := runtime.Callers(skip+2, stackSlice)
er.attr[attrKeyStack] = val{val: Stack(stackSlice[:l]), visible: true}
}
// WithStack returns a copy of the original error, automatically wrapping it if
// the error is not from merr (see Wrap). The returned error has the embedded
// stacktrace set to the frame calling this function.
//
// skip can be used to exclude that many frames from the top of the stack.
func WithStack(e error, skip int) error {
er := wrap(e, true, -1)
setStack(er, skip+1)
return er
}
// GetStack returns the Stack which was embedded in the error, if the error is
// from this package. If not then nil is returned.
func GetStack(e error) Stack {
stack, _ := wrap(e, false, -1).attr[attrKeyStack].val.(Stack)
return stack
}

View File

@ -17,13 +17,13 @@ import (
"github.com/mediocregopher/mediocre-go-lib/mrun"
)
// MServer is returned by MListenAndServe and simply wraps an *http.Server.
type MServer struct {
// Server is returned by WithListeningServer and simply wraps an *http.Server.
type Server struct {
*http.Server
ctx context.Context
}
// MListenAndServe returns an http.Server which will be initialized and have
// WithListeningServer returns a *Server which will be initialized and have
// ListenAndServe called on it (asynchronously) when the start event is
// triggered on the returned Context (see mrun.Start). The Server will have
// Shutdown called on it when the stop event is triggered on the returned
@ -31,25 +31,22 @@ type MServer struct {
//
// This function automatically handles setting up configuration parameters via
// mcfg. The default listen address is ":0".
func MListenAndServe(ctx context.Context, h http.Handler) (context.Context, *MServer) {
srv := &MServer{
func WithListeningServer(ctx context.Context, h http.Handler) (context.Context, *Server) {
srv := &Server{
Server: &http.Server{Handler: h},
ctx: mctx.NewChild(ctx, "http"),
}
var listener *mnet.MListener
srv.ctx, listener = mnet.MListen(srv.ctx, "tcp", "")
var listener *mnet.Listener
srv.ctx, listener = mnet.WithListener(srv.ctx, "tcp", "")
listener.NoCloseOnStop = true // http.Server.Shutdown will do this
// TODO the equivalent functionality as here will be added with annotations
//logger := mlog.From(ctx)
//logger.SetKV(listener)
srv.ctx = mrun.OnStart(srv.ctx, func(context.Context) error {
srv.ctx = mrun.WithStartHook(srv.ctx, func(context.Context) error {
srv.Addr = listener.Addr().String()
srv.ctx = mrun.Thread(srv.ctx, func() error {
if err := srv.Serve(listener); err != http.ErrServerClosed {
mlog.Error(srv.ctx, "error serving listener", merr.KV(err))
srv.ctx = mrun.WithThread(srv.ctx, func() error {
mlog.Info("serving requests", srv.ctx)
if err := srv.Serve(listener); !merr.Equal(err, http.ErrServerClosed) {
mlog.Error("error serving listener", srv.ctx, merr.Context(err))
return err
}
return nil
@ -57,8 +54,8 @@ func MListenAndServe(ctx context.Context, h http.Handler) (context.Context, *MSe
return nil
})
srv.ctx = mrun.OnStop(srv.ctx, func(innerCtx context.Context) error {
mlog.Info(srv.ctx, "shutting down server")
srv.ctx = mrun.WithStopHook(srv.ctx, func(innerCtx context.Context) error {
mlog.Info("shutting down server", srv.ctx)
if err := srv.Shutdown(innerCtx); err != nil {
return err
}

View File

@ -13,9 +13,9 @@ import (
)
func TestMListenAndServe(t *T) {
ctx := mtest.NewCtx()
ctx := mtest.Context()
ctx, srv := MListenAndServe(ctx, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx, srv := WithListeningServer(ctx, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
io.Copy(rw, r.Body)
}))

View File

@ -1,53 +1,58 @@
package mlog
import "context"
import (
"context"
)
type ctxKey int
// Set returns the Context with the Logger carried by it.
func Set(ctx context.Context, l *Logger) context.Context {
// WithLogger returns the Context with the Logger carried by it.
func WithLogger(ctx context.Context, l *Logger) context.Context {
return context.WithValue(ctx, ctxKey(0), l)
}
// DefaultLogger is an instance of Logger which is returned by From when a
// Logger hasn't been previously Set on the Context passed in.
// Logger hasn't been previously WithLogger on the Contexts passed in.
var DefaultLogger = NewLogger()
// From returns the Logger carried by this Context, or DefaultLogger if none is
// being carried.
func From(ctx context.Context) *Logger {
// From looks at each context and returns the Logger from the first Context
// which carries one via a WithLogger call. If none carry a Logger than
// DefaultLogger is returned.
func From(ctxs ...context.Context) *Logger {
for _, ctx := range ctxs {
if l, _ := ctx.Value(ctxKey(0)).(*Logger); l != nil {
return l
}
}
return DefaultLogger
}
// Debug is a shortcut for
// mlog.From(ctx).Debug(ctx, descr, kvs...)
func Debug(ctx context.Context, descr string, kvs ...KVer) {
From(ctx).Debug(ctx, descr, kvs...)
// mlog.From(ctxs...).Debug(desc, ctxs...)
func Debug(descr string, ctxs ...context.Context) {
From(ctxs...).Debug(descr, ctxs...)
}
// Info is a shortcut for
// mlog.From(ctx).Info(ctx, descr, kvs...)
func Info(ctx context.Context, descr string, kvs ...KVer) {
From(ctx).Info(ctx, descr, kvs...)
// mlog.From(ctxs...).Info(desc, ctxs...)
func Info(descr string, ctxs ...context.Context) {
From(ctxs...).Info(descr, ctxs...)
}
// Warn is a shortcut for
// mlog.From(ctx).Warn(ctx, descr, kvs...)
func Warn(ctx context.Context, descr string, kvs ...KVer) {
From(ctx).Warn(ctx, descr, kvs...)
// mlog.From(ctxs...).Warn(desc, ctxs...)
func Warn(descr string, ctxs ...context.Context) {
From(ctxs...).Warn(descr, ctxs...)
}
// Error is a shortcut for
// mlog.From(ctx).Error(ctx, descr, kvs...)
func Error(ctx context.Context, descr string, kvs ...KVer) {
From(ctx).Error(ctx, descr, kvs...)
// mlog.From(ctxs...).Error(desc, ctxs...)
func Error(descr string, ctxs ...context.Context) {
From(ctxs...).Error(descr, ctxs...)
}
// Fatal is a shortcut for
// mlog.From(ctx).Fatal(ctx, descr, kvs...)
func Fatal(ctx context.Context, descr string, kvs ...KVer) {
From(ctx).Fatal(ctx, descr, kvs...)
// mlog.From(ctxs...).Fatal(desc, ctxs...)
func Fatal(descr string, ctxs ...context.Context) {
From(ctxs...).Fatal(descr, ctxs...)
}

View File

@ -1,67 +0,0 @@
package mlog
import (
"bytes"
"context"
"strings"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestContextLogging(t *T) {
var lines []string
l := NewLogger()
l.SetHandler(func(msg Message) error {
buf := new(bytes.Buffer)
if err := DefaultFormat(buf, msg); err != nil {
t.Fatal(err)
}
lines = append(lines, strings.TrimSuffix(buf.String(), "\n"))
return nil
})
ctx := Set(context.Background(), l)
ctx1 := mctx.NewChild(ctx, "1")
ctx1a := mctx.NewChild(ctx1, "a")
ctx1b := mctx.NewChild(ctx1, "b")
ctx1 = mctx.WithChild(ctx1, ctx1a)
ctx1 = mctx.WithChild(ctx1, ctx1b)
ctx = mctx.WithChild(ctx, ctx1)
From(ctx).Info(ctx1a, "ctx1a")
From(ctx).Info(ctx1, "ctx1")
From(ctx).Info(ctx, "ctx")
From(ctx).Debug(ctx1b, "ctx1b (shouldn't show up)")
From(ctx).Info(ctx1b, "ctx1b")
ctx2 := mctx.NewChild(ctx, "2")
ctx = mctx.WithChild(ctx, ctx2)
From(ctx2).Info(ctx2, "ctx2")
massert.Fatal(t, massert.All(
massert.Len(lines, 5),
massert.Equal(lines[0], "~ INFO -- (/1/a) ctx1a"),
massert.Equal(lines[1], "~ INFO -- (/1) ctx1"),
massert.Equal(lines[2], "~ INFO -- ctx"),
massert.Equal(lines[3], "~ INFO -- (/1/b) ctx1b"),
massert.Equal(lines[4], "~ INFO -- (/2) ctx2"),
))
// changing MaxLevel on ctx's Logger should change it for all
From(ctx).SetMaxLevel(DebugLevel)
lines = lines[:0]
From(ctx).Info(ctx, "ctx")
From(ctx).Debug(ctx, "ctx debug")
From(ctx2).Debug(ctx2, "ctx2 debug")
massert.Fatal(t, massert.All(
massert.Len(lines, 3),
massert.Equal(lines[0], "~ INFO -- ctx"),
massert.Equal(lines[1], "~ DEBUG -- ctx debug"),
massert.Equal(lines[2], "~ DEBUG -- (/2) ctx2 debug"),
))
}

View File

@ -1,30 +0,0 @@
package mlog
import (
"context"
)
type kvKey int
// CtxWithKV embeds a KV into a Context, returning a new Context instance. If
// the Context already has a KV embedded in it then the returned error will have
// the merging of the two, with the given KVs taking precedence.
func CtxWithKV(ctx context.Context, kvs ...KVer) context.Context {
existingKV := ctx.Value(kvKey(0))
var kv KV
if existingKV != nil {
kv = mergeInto(existingKV.(KV), kvs...)
} else {
kv = Merge(kvs...).KV()
}
return context.WithValue(ctx, kvKey(0), kv)
}
// CtxKV returns a copy of the KV embedded in the Context by CtxWithKV
func CtxKV(ctx context.Context) KVer {
kv := ctx.Value(kvKey(0))
if kv == nil {
return KV{}
}
return kv.(KV)
}

View File

@ -1,44 +0,0 @@
package mlog
import (
"context"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestCtxKV(t *T) {
ctx := context.Background()
massert.Fatal(t, massert.Equal(KV{}, CtxKV(ctx)))
kv := KV{"a": "a"}
ctx2 := CtxWithKV(ctx, kv)
massert.Fatal(t, massert.All(
massert.Equal(KV{}, CtxKV(ctx)),
massert.Equal(KV{"a": "a"}, CtxKV(ctx2)),
))
// changing the kv now shouldn't do anything
kv["a"] = "b"
massert.Fatal(t, massert.All(
massert.Equal(KV{}, CtxKV(ctx)),
massert.Equal(KV{"a": "a"}, CtxKV(ctx2)),
))
// a new CtxWithKV shouldn't affect the previous one
ctx3 := CtxWithKV(ctx2, KV{"b": "b"})
massert.Fatal(t, massert.All(
massert.Equal(KV{}, CtxKV(ctx)),
massert.Equal(KV{"a": "a"}, CtxKV(ctx2)),
massert.Equal(KV{"a": "a", "b": "b"}, CtxKV(ctx3)),
))
// make sure precedence works
ctx4 := CtxWithKV(ctx3, KV{"b": "bb"})
massert.Fatal(t, massert.All(
massert.Equal(KV{}, CtxKV(ctx)),
massert.Equal(KV{"a": "a"}, CtxKV(ctx2)),
massert.Equal(KV{"a": "a", "b": "b"}, CtxKV(ctx3)),
massert.Equal(KV{"a": "a", "b": "bb"}, CtxKV(ctx4)),
))
}

View File

@ -1,28 +1,17 @@
// Package mlog is a generic logging library. The log methods come in different
// severities: Debug, Info, Warn, Error, and Fatal.
//
// The log methods take in a string describing the error, and a set of key/value
// pairs giving the specific context around the error. The string is intended to
// always be the same no matter what, while the key/value pairs give information
// like which userID the error happened to, or any other relevant contextual
// information.
//
// Examples:
//
// log := mlog.NewLogger()
// log.Info("Something important has occurred")
// log.Error("Could not open file", mlog.KV{"filename": filename}, merr.KV(err))
// The log methods take in a message string and a Context. The Context can be
// loaded with additional annotations which will be included in the log entry as
// well (see mctx package).
//
package mlog
import (
"bufio"
"context"
"fmt"
"encoding/json"
"io"
"os"
"sort"
"strconv"
"strings"
"sync"
@ -99,134 +88,12 @@ func LevelFromString(s string) Level {
////////////////////////////////////////////////////////////////////////////////
// KVer is used to provide context to a log entry in the form of a dynamic set
// of key/value pairs which can be different for every entry.
//
// The returned map is read-only, and may be nil.
type KVer interface {
KV() map[string]interface{}
}
// KVerFunc is a function which implements the KVer interface by calling itself.
type KVerFunc func() map[string]interface{}
// KV implements the KVer interface by calling the KVerFunc itself.
func (kvf KVerFunc) KV() map[string]interface{} {
return kvf()
}
// KV is a KVer which returns a copy of itself when KV is called.
type KV map[string]interface{}
// KV implements the KVer method by returning a copy of itself.
func (kv KV) KV() map[string]interface{} {
return map[string]interface{}(kv)
}
// Set returns a copy of the KV being called on with the given key/val set on
// it. The original KV is unaffected
func (kv KV) Set(k string, v interface{}) KV {
kvm := make(map[string]interface{}, len(kv)+1)
copyM(kvm, kv.KV())
kvm[k] = v
return KV(kvm)
}
// returns a key/value map which should not be written to. saves a map-cloning
// if KVer is a KV
func readOnlyKVM(kver KVer) map[string]interface{} {
if kver == nil {
return map[string]interface{}(nil)
} else if kv, ok := kver.(KV); ok {
return map[string]interface{}(kv)
}
return kver.KV()
}
func copyM(dst, src map[string]interface{}) {
for k, v := range src {
dst[k] = v
}
}
// this may take in any amount of nil values, but should never return nil
func mergeInto(kv KVer, kvs ...KVer) map[string]interface{} {
kvm := map[string]interface{}{}
if kv != nil {
copyM(kvm, kv.KV())
}
for _, innerKV := range kvs {
if innerKV == nil {
continue
}
copyM(kvm, innerKV.KV())
}
return kvm
}
type merger struct {
base KVer
rest []KVer
}
// Merge takes in multiple KVers and returns a single KVer which is the union of
// all the passed in ones. Key/Vals on the rightmost of the set take precedence
// over conflicting ones to the left.
//
// The KVer returned will call KV() on each of the passed in KVers every time
// its KV method is called.
func Merge(kvs ...KVer) KVer {
if len(kvs) == 0 {
return merger{}
}
return merger{base: kvs[0], rest: kvs[1:]}
}
// MergeInto is a convenience function which acts similarly to Merge.
func MergeInto(kv KVer, kvs ...KVer) KVer {
return merger{base: kv, rest: kvs}
}
func (m merger) KV() map[string]interface{} {
return mergeInto(m.base, m.rest...)
}
// Prefix prefixes all keys returned from the given KVer with the given prefix
// string.
func Prefix(kv KVer, prefix string) KVer {
return KVerFunc(func() map[string]interface{} {
kvm := readOnlyKVM(kv)
newKVM := make(map[string]interface{}, len(kvm))
for k, v := range kvm {
newKVM[prefix+k] = v
}
return newKVM
})
}
////////////////////////////////////////////////////////////////////////////////
// Message describes a message to be logged, after having already resolved the
// KVer
type Message struct {
context.Context
Level
Description string
KVer
}
func stringSlice(kv KV) [][2]string {
slice := make([][2]string, 0, len(kv))
for k, v := range kv {
slice = append(slice, [2]string{
k,
strconv.QuoteToGraphic(fmt.Sprint(v)),
})
}
sort.Slice(slice, func(i, j int) bool {
return slice[i][0] < slice[j][0]
})
return slice
Contexts []context.Context
}
// Handler is a function which can process Messages in some way.
@ -235,47 +102,39 @@ func stringSlice(kv KV) [][2]string {
// Handler if necessary.
type Handler func(msg Message) error
// DefaultFormat formats and writes the Message to the given Writer using mlog's
// default format.
func DefaultFormat(w io.Writer, msg Message) error {
var err error
write := func(s string, args ...interface{}) {
if err == nil {
_, err = fmt.Fprintf(w, s, args...)
}
}
write("~ %s -- ", msg.Level.String())
if path := mctx.Path(msg.Context); len(path) > 0 {
write("(%s) ", "/"+strings.Join(path, "/"))
}
write("%s", msg.Description)
if msg.KVer != nil {
if kv := msg.KV(); len(kv) > 0 {
write(" --")
for _, kve := range stringSlice(kv) {
write(" %s=%s", kve[0], kve[1])
}
}
}
write("\n")
return err
// MessageJSON is the type used to encode Messages to JSON in DefaultHandler
type MessageJSON struct {
Level string `json:"level"`
Description string `json:"descr"`
// path -> key -> value
Annotations map[string]map[string]string `json:"annotations,omitempty"`
}
// DefaultHandler initializes and returns a Handler which will write all
// messages to os.Stderr in a thread-safe way. This is the Handler which
// NewLogger will use automatically.
func DefaultHandler() Handler {
return defaultHandler(os.Stderr)
}
func defaultHandler(out io.Writer) Handler {
l := new(sync.Mutex)
bw := bufio.NewWriter(os.Stderr)
enc := json.NewEncoder(out)
return func(msg Message) error {
l.Lock()
defer l.Unlock()
err := DefaultFormat(bw, msg)
if err == nil {
err = bw.Flush()
msgJSON := MessageJSON{
Level: msg.Level.String(),
Description: msg.Description,
}
return err
if len(msg.Contexts) > 0 {
ctx := mctx.MergeAnnotations(msg.Contexts...)
msgJSON.Annotations = mctx.Annotations(ctx).StringMapByPath()
}
return enc.Encode(msgJSON)
}
}
@ -286,7 +145,6 @@ type Logger struct {
l *sync.RWMutex
h Handler
maxLevel uint
kv KVer
testMsgWrittenCh chan struct{} // only initialized/used in tests
}
@ -332,15 +190,6 @@ func (l *Logger) Handler() Handler {
return l.h
}
// SetKV sets the Logger to use the merging of the given KVers as a base KVer
// for all Messages. If the Logger already had a base KVer (via a previous SetKV
// call) then this set will be merged onto that one.
func (l *Logger) SetKV(kvs ...KVer) {
l.l.Lock()
defer l.l.Unlock()
l.kv = MergeInto(l.kv, kvs...)
}
// Log can be used to manually log a message of some custom defined Level.
//
// If the Level is a fatal (Uint() == 0) then calling this will never return,
@ -353,12 +202,8 @@ func (l *Logger) Log(msg Message) {
return
}
if l.kv != nil {
msg.KVer = MergeInto(l.kv, msg.KVer)
}
if err := l.h(msg); err != nil {
go l.Error(context.Background(), "Logger.Handler returned error", merr.KV(err))
go l.Error("Logger.Handler returned error", merr.Context(err))
return
}
@ -371,37 +216,36 @@ func (l *Logger) Log(msg Message) {
}
}
func mkMsg(ctx context.Context, lvl Level, descr string, kvs ...KVer) Message {
func mkMsg(lvl Level, descr string, ctxs ...context.Context) Message {
return Message{
Context: ctx,
Level: lvl,
Description: descr,
KVer: Merge(kvs...),
Contexts: ctxs,
}
}
// Debug logs a DebugLevel message, merging the KVers together first
func (l *Logger) Debug(ctx context.Context, descr string, kvs ...KVer) {
l.Log(mkMsg(ctx, DebugLevel, descr, kvs...))
// Debug logs a DebugLevel message.
func (l *Logger) Debug(descr string, ctxs ...context.Context) {
l.Log(mkMsg(DebugLevel, descr, ctxs...))
}
// Info logs a InfoLevel message, merging the KVers together first
func (l *Logger) Info(ctx context.Context, descr string, kvs ...KVer) {
l.Log(mkMsg(ctx, InfoLevel, descr, kvs...))
// Info logs a InfoLevel message.
func (l *Logger) Info(descr string, ctxs ...context.Context) {
l.Log(mkMsg(InfoLevel, descr, ctxs...))
}
// Warn logs a WarnLevel message, merging the KVers together first
func (l *Logger) Warn(ctx context.Context, descr string, kvs ...KVer) {
l.Log(mkMsg(ctx, WarnLevel, descr, kvs...))
// Warn logs a WarnLevel message.
func (l *Logger) Warn(descr string, ctxs ...context.Context) {
l.Log(mkMsg(WarnLevel, descr, ctxs...))
}
// Error logs a ErrorLevel message, merging the KVers together first
func (l *Logger) Error(ctx context.Context, descr string, kvs ...KVer) {
l.Log(mkMsg(ctx, ErrorLevel, descr, kvs...))
// Error logs a ErrorLevel message.
func (l *Logger) Error(descr string, ctxs ...context.Context) {
l.Log(mkMsg(ErrorLevel, descr, ctxs...))
}
// Fatal logs a FatalLevel message, merging the KVers together first. A Fatal
// message automatically stops the process with an os.Exit(1)
func (l *Logger) Fatal(ctx context.Context, descr string, kvs ...KVer) {
l.Log(mkMsg(ctx, FatalLevel, descr, kvs...))
// Fatal logs a FatalLevel message. A Fatal message automatically stops the
// process with an os.Exit(1)
func (l *Logger) Fatal(descr string, ctxs ...context.Context) {
l.Log(mkMsg(FatalLevel, descr, ctxs...))
}

View File

@ -3,11 +3,11 @@ package mlog
import (
"bytes"
"context"
"regexp"
"strings"
. "testing"
"time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
@ -19,29 +19,9 @@ func TestTruncate(t *T) {
))
}
func TestKV(t *T) {
var kv KV
massert.Fatal(t, massert.All(
massert.Nil(kv.KV()),
massert.Len(kv.KV(), 0),
))
// test that the Set method returns a copy
kv = KV{"foo": "a"}
kv2 := kv.Set("bar", "wat")
kv["bur"] = "ok"
massert.Fatal(t, massert.All(
massert.Equal(KV{"foo": "a", "bur": "ok"}, kv),
massert.Equal(KV{"foo": "a", "bar": "wat"}, kv2),
))
}
func TestLogger(t *T) {
ctx := context.Background()
buf := new(bytes.Buffer)
h := func(msg Message) error {
return DefaultFormat(buf, msg)
}
h := defaultHandler(buf)
l := NewLogger()
l.SetHandler(h)
@ -56,29 +36,31 @@ func TestLogger(t *T) {
out, err := buf.ReadString('\n')
return massert.All(
massert.Nil(err),
massert.Equal(expected, out),
massert.Equal(expected, strings.TrimSpace(out)),
)
}
// Default max level should be INFO
l.Debug(ctx, "foo")
l.Info(ctx, "bar")
l.Warn(ctx, "baz")
l.Error(ctx, "buz")
l.Debug("foo")
l.Info("bar")
l.Warn("baz")
l.Error("buz")
massert.Fatal(t, massert.All(
assertOut("~ INFO -- bar\n"),
assertOut("~ WARN -- baz\n"),
assertOut("~ ERROR -- buz\n"),
assertOut(`{"level":"INFO","descr":"bar"}`),
assertOut(`{"level":"WARN","descr":"baz"}`),
assertOut(`{"level":"ERROR","descr":"buz"}`),
))
ctx := context.Background()
l.SetMaxLevel(WarnLevel)
l.Debug(ctx, "foo")
l.Info(ctx, "bar")
l.Warn(ctx, "baz")
l.Error(ctx, "buz", KV{"a": "b"})
l.Debug("foo")
l.Info("bar")
l.Warn("baz")
l.Error("buz", mctx.Annotate(ctx, "a", "b", "c", "d"))
massert.Fatal(t, massert.All(
assertOut("~ WARN -- baz\n"),
assertOut("~ ERROR -- buz -- a=\"b\"\n"),
assertOut(`{"level":"WARN","descr":"baz"}`),
assertOut(`{"level":"ERROR","descr":"buz","annotations":{"/":{"a":"b","c":"d"}}}`),
))
l2 := l.Clone()
@ -87,109 +69,12 @@ func TestLogger(t *T) {
msg.Description = strings.ToUpper(msg.Description)
return h(msg)
})
l2.Info(ctx, "bar")
l2.Warn(ctx, "baz")
l.Error(ctx, "buz")
l2.Info("bar")
l2.Warn("baz")
l.Error("buz")
massert.Fatal(t, massert.All(
assertOut("~ INFO -- BAR\n"),
assertOut("~ WARN -- BAZ\n"),
assertOut("~ ERROR -- buz\n"),
))
l3 := l2.Clone()
l3.SetKV(KV{"a": 1})
l3.Info(ctx, "foo", KV{"b": 2})
l3.Info(ctx, "bar", KV{"a": 2, "b": 3})
massert.Fatal(t, massert.All(
assertOut("~ INFO -- FOO -- a=\"1\" b=\"2\"\n"),
assertOut("~ INFO -- BAR -- a=\"2\" b=\"3\"\n"),
))
}
func TestDefaultFormat(t *T) {
assertFormat := func(postfix string, msg Message) massert.Assertion {
expectedRegex := regexp.MustCompile(`^~ ` + postfix + `\n$`)
buf := bytes.NewBuffer(make([]byte, 0, 128))
writeErr := DefaultFormat(buf, msg)
line, err := buf.ReadString('\n')
return massert.Comment(
massert.All(
massert.Nil(writeErr),
massert.Nil(err),
massert.Equal(true, expectedRegex.MatchString(line)),
),
"line:%q", line,
)
}
msg := Message{
Context: context.Background(),
Level: InfoLevel,
Description: "this is a test",
}
massert.Fatal(t, assertFormat("INFO -- this is a test", msg))
msg.KVer = KV{}
massert.Fatal(t, assertFormat("INFO -- this is a test", msg))
msg.KVer = KV{"foo": "a"}
massert.Fatal(t, assertFormat("INFO -- this is a test -- foo=\"a\"", msg))
msg.KVer = KV{"foo": "a", "bar": "b"}
massert.Fatal(t,
assertFormat("INFO -- this is a test -- bar=\"b\" foo=\"a\"", msg))
}
func TestMerge(t *T) {
assertMerge := func(exp KV, kvs ...KVer) massert.Assertion {
return massert.Equal(exp.KV(), Merge(kvs...).KV())
}
massert.Fatal(t, massert.All(
assertMerge(KV{}),
assertMerge(KV{}, nil),
assertMerge(KV{}, nil, nil),
assertMerge(KV{"a": "a"}, KV{"a": "a"}),
assertMerge(KV{"a": "a"}, nil, KV{"a": "a"}),
assertMerge(KV{"a": "a"}, KV{"a": "a"}, nil),
assertMerge(
KV{"a": "a", "b": "b"},
KV{"a": "a"}, KV{"b": "b"},
),
assertMerge(
KV{"a": "a", "b": "b"},
KV{"a": "a"}, KV{"b": "b"},
),
assertMerge(
KV{"a": "b"},
KV{"a": "a"}, KV{"a": "b"},
),
))
// Merge should _not_ call KV() on the inner KVers until the outer one is
// called.
{
kv := KV{"a": "a"}
mergedKV := Merge(kv)
kv["a"] = "b"
massert.Fatal(t, massert.All(
massert.Equal(KV{"a": "b"}, kv),
massert.Equal(map[string]interface{}{"a": "b"}, kv.KV()),
massert.Equal(map[string]interface{}{"a": "b"}, mergedKV.KV()),
))
}
}
func TestPrefix(t *T) {
kv := KV{"foo": "bar"}
prefixKV := Prefix(kv, "aa")
massert.Fatal(t, massert.All(
massert.Equal(map[string]interface{}{"foo": "bar"}, kv.KV()),
massert.Equal(map[string]interface{}{"aafoo": "bar"}, prefixKV.KV()),
massert.Equal(map[string]interface{}{"foo": "bar"}, kv.KV()),
assertOut(`{"level":"INFO","descr":"BAR"}`),
assertOut(`{"level":"WARN","descr":"BAZ"}`),
assertOut(`{"level":"ERROR","descr":"buz"}`),
))
}

View File

@ -8,13 +8,13 @@ import (
"strings"
"github.com/mediocregopher/mediocre-go-lib/mcfg"
"github.com/mediocregopher/mediocre-go-lib/merr"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/mlog"
"github.com/mediocregopher/mediocre-go-lib/mrun"
)
// MListener is returned by MListen and simply wraps a net.Listener.
type MListener struct {
// Listener is returned by WithListen and simply wraps a net.Listener.
type Listener struct {
net.Listener
ctx context.Context
@ -23,13 +23,13 @@ type MListener struct {
NoCloseOnStop bool
}
// MListen returns an MListener which will be initialized when the start event
// is triggered on the returned Context (see mrun.Start), and closed when the
// stop event is triggered on the returned Context (see mrun.Stop).
// WithListener returns a Listener which will be initialized when the start
// event is triggered on the returned Context (see mrun.Start), and closed when
// the stop event is triggered on the returned Context (see mrun.Stop).
//
// network defaults to "tcp" if empty. defaultAddr defaults to ":0" if empty,
// and will be configurable via mcfg.
func MListen(ctx context.Context, network, defaultAddr string) (context.Context, *MListener) {
func WithListener(ctx context.Context, network, defaultAddr string) (context.Context, *Listener) {
if network == "" {
network = "tcp"
}
@ -37,59 +37,52 @@ func MListen(ctx context.Context, network, defaultAddr string) (context.Context,
defaultAddr = ":0"
}
l := new(MListener)
l := &Listener{
ctx: mctx.NewChild(ctx, "net"),
}
// TODO the equivalent functionality as here will be added with annotations
//l.log = mlog.From(ctx)
//l.log.SetKV(l)
ctx, addr := mcfg.String(ctx, "listen-addr", defaultAddr, strings.ToUpper(network)+" address to listen on in format [host]:port. If port is 0 then a random one will be chosen")
ctx = mrun.OnStart(ctx, func(context.Context) error {
var addr *string
l.ctx, addr = mcfg.WithString(l.ctx, "listen-addr", defaultAddr, strings.ToUpper(network)+" address to listen on in format [host]:port. If port is 0 then a random one will be chosen")
l.ctx = mrun.WithStartHook(l.ctx, func(context.Context) error {
var err error
if l.Listener, err = net.Listen(network, *addr); err != nil {
return err
}
mlog.Info(l.ctx, "listening")
l.ctx = mctx.Annotate(l.ctx, "addr", l.Addr().String())
mlog.Info("listening", l.ctx)
return nil
})
// TODO track connections and wait for them to complete before shutting
// down?
ctx = mrun.OnStop(ctx, func(context.Context) error {
l.ctx = mrun.WithStopHook(l.ctx, func(context.Context) error {
if l.NoCloseOnStop {
return nil
}
mlog.Info(l.ctx, "stopping listener")
mlog.Info("stopping listener", l.ctx)
return l.Close()
})
l.ctx = ctx
return ctx, l
return mctx.WithChild(ctx, l.ctx), l
}
// Accept wraps a call to Accept on the underlying net.Listener, providing debug
// logging.
func (l *MListener) Accept() (net.Conn, error) {
func (l *Listener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return conn, err
}
mlog.Debug(l.ctx, "connection accepted", mlog.KV{"remoteAddr": conn.RemoteAddr()})
mlog.Debug("connection accepted",
mctx.Annotate(l.ctx, "remoteAddr", conn.RemoteAddr().String()))
return conn, nil
}
// Close wraps a call to Close on the underlying net.Listener, providing debug
// logging.
func (l *MListener) Close() error {
mlog.Debug(l.ctx, "listener closing")
err := l.Listener.Close()
mlog.Debug(l.ctx, "listener closed", merr.KV(err))
return err
}
// KV implements the mlog.KVer interface.
func (l *MListener) KV() map[string]interface{} {
return map[string]interface{}{"addr": l.Addr().String()}
func (l *Listener) Close() error {
mlog.Info("listener closing", l.ctx)
return l.Listener.Close()
}
////////////////////////////////////////////////////////////////////////////////

View File

@ -35,9 +35,9 @@ func TestIsReservedIP(t *T) {
))
}
func TestMListen(t *T) {
ctx := mtest.NewCtx()
ctx, l := MListen(ctx, "", "")
func TestWithListener(t *T) {
ctx := mtest.Context()
ctx, l := WithListener(ctx, "", "")
mtest.Run(ctx, t, func() {
go func() {
conn, err := net.Dial("tcp", l.Addr().String())

View File

@ -7,7 +7,7 @@ import (
)
// Hook describes a function which can be registered to trigger on an event via
// the RegisterHook function.
// the WithHook function.
type Hook func(context.Context) error
type ctxKey int
@ -65,12 +65,12 @@ func getHookEls(ctx context.Context, userKey interface{}) ([]hookEl, int, int) {
return hookEls, len(children), lastNumHooks
}
// RegisterHook registers a Hook under a typed key. The Hook will be called when
// WithHook registers a Hook under a typed key. The Hook will be called when
// TriggerHooks is called with that same key. Multiple Hooks can be registered
// for the same key, and will be called sequentially when triggered.
//
// Hooks will be called with whatever Context is passed into TriggerHooks.
func RegisterHook(ctx context.Context, key interface{}, hook Hook) context.Context {
func WithHook(ctx context.Context, key interface{}, hook Hook) context.Context {
hookEls, numChildren, numHooks := getHookEls(ctx, key)
hookEls = append(hookEls, hookEl{hook: hook})
@ -100,9 +100,9 @@ func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl)
return nil
}
// TriggerHooks causes all Hooks registered with RegisterHook on the Context
// (and its predecessors) under the given key to be called in the order they
// were registered.
// TriggerHooks causes all Hooks registered with WithHook on the Context (and
// its predecessors) under the given key to be called in the order they were
// registered.
//
// If any Hook returns an error no further Hooks will be called and that error
// will be returned.
@ -113,15 +113,15 @@ func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl)
//
// // parent context has hookA registered
// ctx := context.Background()
// ctx = RegisterHook(ctx, 0, hookA)
// ctx = WithHook(ctx, 0, hookA)
//
// // child context has hookB registered
// childCtx := mctx.NewChild(ctx, "child")
// childCtx = RegisterHook(childCtx, 0, hookB)
// childCtx = WithHook(childCtx, 0, hookB)
// ctx = mctx.WithChild(ctx, childCtx) // needed to link childCtx to ctx
//
// // parent context has another Hook, hookC, registered
// ctx = RegisterHook(ctx, 0, hookC)
// ctx = WithHook(ctx, 0, hookC)
//
// // The Hooks will be triggered in the order: hookA, hookB, then hookC
// err := TriggerHooks(ctx, 0)
@ -148,33 +148,33 @@ const (
stop
)
// OnStart registers the given Hook to run when Start is called. This is a
// special case of RegisterHook.
// WithStartHook registers the given Hook to run when Start is called. This is a
// special case of WithHook.
//
// As a convention Hooks running on the start event should block only as long as
// it takes to ensure that whatever is running can do so successfully. For
// short-lived tasks this isn't a problem, but long-lived tasks (e.g. a web
// server) will want to use the Hook only to initialize, and spawn off a
// go-routine to do their actual work. Long-lived tasks should set themselves up
// to stop on the stop event (see OnStop).
func OnStart(ctx context.Context, hook Hook) context.Context {
return RegisterHook(ctx, start, hook)
// to stop on the stop event (see WithStopHook).
func WithStartHook(ctx context.Context, hook Hook) context.Context {
return WithHook(ctx, start, hook)
}
// Start runs all Hooks registered using OnStart. This is a special case of
// TriggerHooks.
// Start runs all Hooks registered using WithStartHook. This is a special case
// of TriggerHooks.
func Start(ctx context.Context) error {
return TriggerHooks(ctx, start)
}
// OnStop registers the given Hook to run when Stop is called. This is a special
// case of RegisterHook.
func OnStop(ctx context.Context, hook Hook) context.Context {
return RegisterHook(ctx, stop, hook)
// WithStopHook registers the given Hook to run when Stop is called. This is a
// special case of WithHook.
func WithStopHook(ctx context.Context, hook Hook) context.Context {
return WithHook(ctx, stop, hook)
}
// Stop runs all Hooks registered using OnStop in the reverse order in which
// they were registered. This is a special case of TriggerHooks.
// Stop runs all Hooks registered using WithStopHook in the reverse order in
// which they were registered. This is a special case of TriggerHooks.
func Stop(ctx context.Context) error {
return TriggerHooksReverse(ctx, stop)
}

View File

@ -18,20 +18,20 @@ func TestHooks(t *T) {
}
ctx := context.Background()
ctx = RegisterHook(ctx, 0, mkHook(1))
ctx = RegisterHook(ctx, 0, mkHook(2))
ctx = WithHook(ctx, 0, mkHook(1))
ctx = WithHook(ctx, 0, mkHook(2))
ctxA := mctx.NewChild(ctx, "a")
ctxA = RegisterHook(ctxA, 0, mkHook(3))
ctxA = RegisterHook(ctxA, 999, mkHook(999)) // different key
ctxA = WithHook(ctxA, 0, mkHook(3))
ctxA = WithHook(ctxA, 999, mkHook(999)) // different key
ctx = mctx.WithChild(ctx, ctxA)
ctx = RegisterHook(ctx, 0, mkHook(4))
ctx = WithHook(ctx, 0, mkHook(4))
ctxB := mctx.NewChild(ctx, "b")
ctxB = RegisterHook(ctxB, 0, mkHook(5))
ctxB = WithHook(ctxB, 0, mkHook(5))
ctxB1 := mctx.NewChild(ctxB, "1")
ctxB1 = RegisterHook(ctxB1, 0, mkHook(6))
ctxB1 = WithHook(ctxB1, 0, mkHook(6))
ctxB = mctx.WithChild(ctxB, ctxB1)
ctx = mctx.WithChild(ctx, ctxB)

View File

@ -36,10 +36,10 @@ func (fe *futureErr) set(err error) {
type threadCtxKey int
// Thread spawns a go-routine which executes the given function. The returned
// Context tracks this go-routine, which can then be passed into the Wait
// function to block until the spawned go-routine returns.
func Thread(ctx context.Context, fn func() error) context.Context {
// WithThread spawns a go-routine which executes the given function. The
// returned Context tracks this go-routine, which can then be passed into the
// Wait function to block until the spawned go-routine returns.
func WithThread(ctx context.Context, fn func() error) context.Context {
futErr := newFutureErr()
oldFutErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr)
futErrs := make([]*futureErr, len(oldFutErrs), len(oldFutErrs)+1)

View File

@ -30,7 +30,7 @@ func TestThreadWait(t *T) {
t.Run("noBlock", func(t *T) {
t.Run("noErr", func(t *T) {
ctx := context.Background()
ctx = Thread(ctx, func() error { return nil })
ctx = WithThread(ctx, func() error { return nil })
if err := Wait(ctx, nil); err != nil {
t.Fatal(err)
}
@ -38,7 +38,7 @@ func TestThreadWait(t *T) {
t.Run("err", func(t *T) {
ctx := context.Background()
ctx = Thread(ctx, func() error { return testErr })
ctx = WithThread(ctx, func() error { return testErr })
if err := Wait(ctx, nil); err != testErr {
t.Fatalf("should have got test error, got: %v", err)
}
@ -48,7 +48,7 @@ func TestThreadWait(t *T) {
t.Run("block", func(t *T) {
t.Run("noErr", func(t *T) {
ctx := context.Background()
ctx = Thread(ctx, func() error {
ctx = WithThread(ctx, func() error {
time.Sleep(1 * time.Second)
return nil
})
@ -59,7 +59,7 @@ func TestThreadWait(t *T) {
t.Run("err", func(t *T) {
ctx := context.Background()
ctx = Thread(ctx, func() error {
ctx = WithThread(ctx, func() error {
time.Sleep(1 * time.Second)
return testErr
})
@ -70,7 +70,7 @@ func TestThreadWait(t *T) {
t.Run("canceled", func(t *T) {
ctx := context.Background()
ctx = Thread(ctx, func() error {
ctx = WithThread(ctx, func() error {
time.Sleep(5 * time.Second)
return testErr
})
@ -90,7 +90,7 @@ func TestThreadWait(t *T) {
t.Run("noBlock", func(t *T) {
t.Run("noErr", func(t *T) {
ctx, childCtx := ctxWithChild()
childCtx = Thread(childCtx, func() error { return nil })
childCtx = WithThread(childCtx, func() error { return nil })
ctx = mctx.WithChild(ctx, childCtx)
if err := Wait(ctx, nil); err != nil {
t.Fatal(err)
@ -99,7 +99,7 @@ func TestThreadWait(t *T) {
t.Run("err", func(t *T) {
ctx, childCtx := ctxWithChild()
childCtx = Thread(childCtx, func() error { return testErr })
childCtx = WithThread(childCtx, func() error { return testErr })
ctx = mctx.WithChild(ctx, childCtx)
if err := Wait(ctx, nil); err != testErr {
t.Fatalf("should have got test error, got: %v", err)
@ -110,7 +110,7 @@ func TestThreadWait(t *T) {
t.Run("block", func(t *T) {
t.Run("noErr", func(t *T) {
ctx, childCtx := ctxWithChild()
childCtx = Thread(childCtx, func() error {
childCtx = WithThread(childCtx, func() error {
time.Sleep(1 * time.Second)
return nil
})
@ -122,7 +122,7 @@ func TestThreadWait(t *T) {
t.Run("err", func(t *T) {
ctx, childCtx := ctxWithChild()
childCtx = Thread(childCtx, func() error {
childCtx = WithThread(childCtx, func() error {
time.Sleep(1 * time.Second)
return testErr
})
@ -134,7 +134,7 @@ func TestThreadWait(t *T) {
t.Run("canceled", func(t *T) {
ctx, childCtx := ctxWithChild()
childCtx = Thread(childCtx, func() error {
childCtx = WithThread(childCtx, func() error {
time.Sleep(5 * time.Second)
return testErr
})

View File

@ -12,18 +12,18 @@ import (
type envCtxKey int
// NewCtx creates and returns a root Context suitable for testing.
func NewCtx() context.Context {
// Context creates and returns a root Context suitable for testing.
func Context() context.Context {
ctx := context.Background()
logger := mlog.NewLogger()
logger.SetMaxLevel(mlog.DebugLevel)
return mlog.Set(ctx, logger)
return mlog.WithLogger(ctx, logger)
}
// SetEnv sets the given environment variable on the given Context, such that it
// will be used as if it was a real environment variable when the Run function
// from this package is called.
func SetEnv(ctx context.Context, key, val string) context.Context {
// WithEnv sets the given environment variable on the given Context, such that
// it will be used as if it was a real environment variable when the Run
// function from this package is called.
func WithEnv(ctx context.Context, key, val string) context.Context {
prevEnv, _ := ctx.Value(envCtxKey(0)).([][2]string)
env := make([][2]string, len(prevEnv), len(prevEnv)+1)
copy(env, prevEnv)
@ -33,7 +33,7 @@ func SetEnv(ctx context.Context, key, val string) context.Context {
// Run performs the following using the given Context:
//
// - Calls mcfg.Populate using any variables set by SetEnv.
// - Calls mcfg.Populate using any variables set by WithEnv.
//
// - Calls mrun.Start
//
@ -42,7 +42,7 @@ func SetEnv(ctx context.Context, key, val string) context.Context {
// - Calls mrun.Stop
//
// The intention is that Run is used within a test on a Context created via
// NewCtx, after any setup functions have been called (e.g. mnet.MListen).
// NewCtx, after any setup functions have been called (e.g. mnet.WithListener).
func Run(ctx context.Context, t *testing.T, body func()) {
envTups, _ := ctx.Value(envCtxKey(0)).([][2]string)
env := make([]string, 0, len(envTups))

View File

@ -7,9 +7,9 @@ import (
)
func TestRun(t *T) {
ctx := NewCtx()
ctx, arg := mcfg.RequiredString(ctx, "arg", "Required by this test")
ctx = SetEnv(ctx, "ARG", "foo")
ctx := Context()
ctx, arg := mcfg.WithRequiredString(ctx, "arg", "Required by this test")
ctx = WithEnv(ctx, "ARG", "foo")
Run(ctx, t, func() {
if *arg != "foo" {
t.Fatalf(`arg not set to "foo", is set to %q`, *arg)