refactor everything to use context's annotation system, making some significant changes to annotations themselves along the way
This commit is contained in:
parent
000a57689d
commit
e1e52db208
@ -27,8 +27,15 @@ Everything here are guidelines, not actual rules.
|
|||||||
|
|
||||||
* https://github.com/golang/go/wiki/CodeReviewComments
|
* 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
|
* 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,
|
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
|
otherwise it's a pointer. Even if the immutable value implementation has
|
||||||
internal caching, locks, etc..., that can be hidden as pointers inside the
|
internal caching, locks, etc..., that can be hidden as pointers inside the
|
||||||
struct, but the struct itself can remain a value type.
|
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
2
TODO
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- read through all docs, especially package docs, make sure they make sense
|
||||||
|
- write examples
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/mediocregopher/mediocre-go-lib/m"
|
"github.com/mediocregopher/mediocre-go-lib/m"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcrypto"
|
"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/mhttp"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||||
@ -29,27 +30,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := m.NewServiceCtx()
|
ctx := m.ServiceContext()
|
||||||
ctx, cookieName := mcfg.String(ctx, "cookie-name", "_totp_proxy", "String to use as the name for cookies")
|
ctx, cookieName := mcfg.WithString(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, cookieTimeout := mcfg.WithDuration(ctx, "cookie-timeout", mtime.Duration{1 * time.Hour}, "Timeout for cookies")
|
||||||
|
|
||||||
var userSecrets map[string]string
|
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
|
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, 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.OnStart(ctx, func(context.Context) error {
|
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
|
||||||
if *secretStr == "" {
|
if *secretStr == "" {
|
||||||
*secretStr = mrand.Hex(32)
|
*secretStr = mrand.Hex(32)
|
||||||
}
|
}
|
||||||
mlog.Info(ctx, "generating secret")
|
mlog.Info("generating secret", ctx)
|
||||||
secret = mcrypto.NewSecret([]byte(*secretStr))
|
secret = mcrypto.NewSecret([]byte(*secretStr))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
proxyHandler := new(struct{ http.Handler })
|
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, proxyURL := mcfg.WithRequiredString(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 = mrun.WithStartHook(ctx, func(context.Context) error {
|
||||||
u, err := url.Parse(*proxyURL)
|
u, err := url.Parse(*proxyURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -60,7 +61,6 @@ func main() {
|
|||||||
|
|
||||||
authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO mlog.FromHTTP?
|
// TODO mlog.FromHTTP?
|
||||||
// TODO annotate this ctx
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
unauthorized := func() {
|
unauthorized := func() {
|
||||||
@ -79,7 +79,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cookie, _ := r.Cookie(*cookieName); cookie != nil {
|
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
|
var sig mcrypto.Signature
|
||||||
if err := sig.UnmarshalText([]byte(cookie.Value)); err == nil {
|
if err := sig.UnmarshalText([]byte(cookie.Value)); err == nil {
|
||||||
err := mcrypto.VerifyString(secret, sig, "")
|
err := mcrypto.VerifyString(secret, sig, "")
|
||||||
@ -91,10 +92,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user, pass, ok := r.BasicAuth(); ok && pass != "" {
|
if user, pass, ok := r.BasicAuth(); ok && pass != "" {
|
||||||
mlog.Debug(ctx, "authenticating with user/pass", mlog.KV{
|
mlog.Debug("authenticating with user",
|
||||||
"user": user,
|
mctx.Annotate(ctx, "user", user))
|
||||||
"pass": pass,
|
|
||||||
})
|
|
||||||
if userSecret, ok := userSecrets[user]; ok {
|
if userSecret, ok := userSecrets[user]; ok {
|
||||||
if totp.Validate(pass, userSecret) {
|
if totp.Validate(pass, userSecret) {
|
||||||
authorized()
|
authorized()
|
||||||
@ -106,6 +105,6 @@ func main() {
|
|||||||
unauthorized()
|
unauthorized()
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, _ = mhttp.MListenAndServe(ctx, authHandler)
|
ctx, _ = mhttp.WithListeningServer(ctx, authHandler)
|
||||||
m.Run(ctx)
|
m.Run(ctx)
|
||||||
}
|
}
|
||||||
|
27
m/m.go
27
m/m.go
@ -6,10 +6,12 @@ package m
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"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/merr"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"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
|
// ServiceContext returns a Context which should be used as the root Context
|
||||||
// creating long running services, such as an RPC service or database.
|
// when creating long running services, such as an RPC service or database.
|
||||||
//
|
//
|
||||||
// The returned Context will automatically handle setting up global
|
// The returned Context will automatically handle setting up global
|
||||||
// configuration parameters like "log-level", as well as an http endpoint where
|
// configuration parameters like "log-level", as well as an http endpoint where
|
||||||
// debug information about the running process can be accessed.
|
// debug information about the running process can be accessed.
|
||||||
//
|
//
|
||||||
// TODO set up the debug endpoint.
|
// TODO set up the debug endpoint.
|
||||||
func NewServiceCtx() context.Context {
|
func ServiceContext() context.Context {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// set up log level handling
|
// set up log level handling
|
||||||
logger := mlog.NewLogger()
|
logger := mlog.NewLogger()
|
||||||
ctx = mlog.Set(ctx, logger)
|
ctx = mlog.WithLogger(ctx, logger)
|
||||||
ctx, logLevelStr := mcfg.String(ctx, "log-level", "info", "Maximum log level which will be printed.")
|
ctx, logLevelStr := mcfg.WithString(ctx, "log-level", "info", "Maximum log level which will be printed.")
|
||||||
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
|
||||||
logLevel := mlog.LevelFromString(*logLevelStr)
|
logLevel := mlog.LevelFromString(*logLevelStr)
|
||||||
|
log.Printf("setting log level to %v", logLevel)
|
||||||
if logLevel == nil {
|
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)
|
logger.SetMaxLevel(logLevel)
|
||||||
return nil
|
return nil
|
||||||
@ -58,20 +61,20 @@ func NewServiceCtx() context.Context {
|
|||||||
func Run(ctx context.Context) {
|
func Run(ctx context.Context) {
|
||||||
log := mlog.From(ctx)
|
log := mlog.From(ctx)
|
||||||
if err := mcfg.Populate(ctx, CfgSource()); err != nil {
|
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 {
|
} 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)
|
ch := make(chan os.Signal, 1)
|
||||||
signal.Notify(ch, os.Interrupt)
|
signal.Notify(ch, os.Interrupt)
|
||||||
s := <-ch
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
11
m/m_test.go
11
m/m_test.go
@ -1,6 +1,7 @@
|
|||||||
package m
|
package m
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func TestServiceCtx(t *T) {
|
func TestServiceCtx(t *T) {
|
||||||
t.Run("log-level", func(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
|
// pull the Logger out of the ctx and set the Handler on it, so we can check
|
||||||
// the log level
|
// the log level
|
||||||
@ -36,16 +37,16 @@ func TestServiceCtx(t *T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mlog.From(ctxA).Info(ctxA, "foo")
|
mlog.From(ctxA).Info("foo", ctxA)
|
||||||
mlog.From(ctxA).Debug(ctxA, "bar")
|
mlog.From(ctxA).Debug("bar", ctxA)
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Len(msgs, 2),
|
massert.Len(msgs, 2),
|
||||||
massert.Equal(msgs[0].Level.String(), "INFO"),
|
massert.Equal(msgs[0].Level.String(), "INFO"),
|
||||||
massert.Equal(msgs[0].Description, "foo"),
|
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].Level.String(), "DEBUG"),
|
||||||
massert.Equal(msgs[1].Description, "bar"),
|
massert.Equal(msgs[1].Description, "bar"),
|
||||||
massert.Equal(msgs[1].Context, ctxA),
|
massert.Equal(msgs[1].Contexts, []context.Context{ctxA}),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mcfg
|
package mcfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -89,7 +90,7 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !pvOk {
|
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
|
pvStrValOk = false
|
||||||
}
|
}
|
||||||
if pvOk && !pvStrValOk {
|
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
|
return pvs, nil
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,11 @@ import (
|
|||||||
|
|
||||||
func TestSourceCLIHelp(t *T) {
|
func TestSourceCLIHelp(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx, _ = Int(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed
|
ctx, _ = WithInt(ctx, "foo", 5, "Test int param ") // trailing space should be trimmed
|
||||||
ctx, _ = Bool(ctx, "bar", "Test bool param.")
|
ctx, _ = WithBool(ctx, "bar", "Test bool param.")
|
||||||
ctx, _ = String(ctx, "baz", "baz", "Test string param")
|
ctx, _ = WithString(ctx, "baz", "baz", "Test string param")
|
||||||
ctx, _ = RequiredString(ctx, "baz2", "")
|
ctx, _ = WithRequiredString(ctx, "baz2", "")
|
||||||
ctx, _ = RequiredString(ctx, "baz3", "")
|
ctx, _ = WithRequiredString(ctx, "baz3", "")
|
||||||
src := SourceCLI{}
|
src := SourceCLI{}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mcfg
|
package mcfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
|
|||||||
for _, kv := range kvs {
|
for _, kv := range kvs {
|
||||||
split := strings.SplitN(kv, "=", 2)
|
split := strings.SplitN(kv, "=", 2)
|
||||||
if len(split) != 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]
|
k, v := split[0], split[1]
|
||||||
if p, ok := pM[k]; ok {
|
if p, ok := pM[k]; ok {
|
||||||
|
@ -110,7 +110,7 @@ func populate(params []Param, src Source) error {
|
|||||||
if !p.Required {
|
if !p.Required {
|
||||||
continue
|
continue
|
||||||
} else if _, ok := pvM[hash]; !ok {
|
} 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))
|
"param", paramFullName(mctx.Path(p.Context), p.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ import (
|
|||||||
func TestPopulate(t *T) {
|
func TestPopulate(t *T) {
|
||||||
{
|
{
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx, a := Int(ctx, "a", 0, "")
|
ctx, a := WithInt(ctx, "a", 0, "")
|
||||||
ctxChild := mctx.NewChild(ctx, "foo")
|
ctxChild := mctx.NewChild(ctx, "foo")
|
||||||
ctxChild, b := Int(ctxChild, "b", 0, "")
|
ctxChild, b := WithInt(ctxChild, "b", 0, "")
|
||||||
ctxChild, c := Int(ctxChild, "c", 0, "")
|
ctxChild, c := WithInt(ctxChild, "c", 0, "")
|
||||||
ctx = mctx.WithChild(ctx, ctxChild)
|
ctx = mctx.WithChild(ctx, ctxChild)
|
||||||
|
|
||||||
err := Populate(ctx, SourceCLI{
|
err := Populate(ctx, SourceCLI{
|
||||||
@ -28,10 +28,10 @@ func TestPopulate(t *T) {
|
|||||||
|
|
||||||
{ // test that required params are enforced
|
{ // test that required params are enforced
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx, a := Int(ctx, "a", 0, "")
|
ctx, a := WithInt(ctx, "a", 0, "")
|
||||||
ctxChild := mctx.NewChild(ctx, "foo")
|
ctxChild := mctx.NewChild(ctx, "foo")
|
||||||
ctxChild, b := Int(ctxChild, "b", 0, "")
|
ctxChild, b := WithInt(ctxChild, "b", 0, "")
|
||||||
ctxChild, c := RequiredInt(ctxChild, "c", "")
|
ctxChild, c := WithRequiredInt(ctxChild, "c", "")
|
||||||
ctx = mctx.WithChild(ctx, ctxChild)
|
ctx = mctx.WithChild(ctx, ctxChild)
|
||||||
|
|
||||||
err := Populate(ctx, SourceCLI{
|
err := Populate(ctx, SourceCLI{
|
||||||
|
143
mcfg/param.go
143
mcfg/param.go
@ -45,7 +45,7 @@ type Param struct {
|
|||||||
Into interface{}
|
Into interface{}
|
||||||
|
|
||||||
// The Context this Param was added to. NOTE that this will be automatically
|
// 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
|
Context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,9 +74,9 @@ func getParam(ctx context.Context, name string) (Param, bool) {
|
|||||||
return param, ok
|
return param, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustAdd returns a Context with the given Param added to it. It will panic if
|
// WithParam returns a Context with the given Param added to it. It will panic
|
||||||
// a Param with the same Name already exists in the Context.
|
// if a Param with the same Name already exists in the Context.
|
||||||
func MustAdd(ctx context.Context, param Param) context.Context {
|
func WithParam(ctx context.Context, param Param) context.Context {
|
||||||
param.Name = strings.ToLower(param.Name)
|
param.Name = strings.ToLower(param.Name)
|
||||||
param.Context = ctx
|
param.Context = ctx
|
||||||
|
|
||||||
@ -99,113 +99,114 @@ func getLocalParams(ctx context.Context) []Param {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64 returns an *int64 which will be populated once Populate is run on the
|
// WithInt64 returns an *int64 which will be populated once Populate is run on
|
||||||
// returned Context.
|
// the returned Context.
|
||||||
func Int64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) {
|
func WithInt64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) {
|
||||||
i := defaultVal
|
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
|
return ctx, &i
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredInt64 returns an *int64 which will be populated once Populate is run
|
// WithRequiredInt64 returns an *int64 which will be populated once Populate is
|
||||||
// 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
|
|
||||||
// run on the returned Context, and which must be supplied by a configuration
|
// run on the returned Context, and which must be supplied by a configuration
|
||||||
// Source.
|
// Source.
|
||||||
func RequiredString(ctx context.Context, name, usage string) (context.Context, *string) {
|
func WithRequiredInt64(ctx context.Context, name string, usage string) (context.Context, *int64) {
|
||||||
var s string
|
var i int64
|
||||||
ctx = MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s})
|
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
|
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.
|
// returned Context, and which defaults to false if unconfigured.
|
||||||
//
|
//
|
||||||
// The default behavior of all Sources is that a boolean parameter will be set
|
// 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
|
// 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,
|
// the value will also be true when the parameter is used with no value at all,
|
||||||
// as would be expected.
|
// 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
|
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
|
return ctx, &b
|
||||||
}
|
}
|
||||||
|
|
||||||
// TS returns an *mtime.TS which will be populated once Populate is run on the
|
// WithTS returns an *mtime.TS which will be populated once Populate is run on
|
||||||
// returned Context.
|
// the returned Context.
|
||||||
func TS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) {
|
func WithTS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) {
|
||||||
t := defaultVal
|
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
|
return ctx, &t
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredTS returns an *mtime.TS which will be populated once Populate is run
|
// WithRequiredTS returns an *mtime.TS which will be populated once Populate is
|
||||||
// on the returned Context, and which must be supplied by a configuration
|
// run on the returned Context, and which must be supplied by a configuration
|
||||||
// Source.
|
// 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
|
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
|
return ctx, &t
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration returns an *mtime.Duration which will be populated once Populate is
|
// WithDuration returns an *mtime.Duration which will be populated once Populate
|
||||||
// run on the returned Context.
|
// is run on the returned Context.
|
||||||
func Duration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
|
func WithDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) {
|
||||||
d := defaultVal
|
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
|
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
|
// Populate is run on the returned Context, and which must be supplied by a
|
||||||
// configuration Source.
|
// 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
|
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
|
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
|
// given interface{} (which should be a pointer). The receiver (into) is also
|
||||||
// used to determine the default value.
|
// used to determine the default value.
|
||||||
func JSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
|
func WithJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
|
||||||
return MustAdd(ctx, Param{Name: name, Usage: usage, Into: into})
|
return WithParam(ctx, Param{Name: name, Usage: usage, Into: into})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredJSON reads the parameter value as a JSON value and unmarshals it into
|
// WithRequiredJSON reads the parameter value as a JSON value and unmarshals it
|
||||||
// the given interface{} (which should be a pointer). The value must be supplied
|
// into the given interface{} (which should be a pointer). The value must be
|
||||||
// by a configuration Source.
|
// supplied by a configuration Source.
|
||||||
func RequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
|
func WithRequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context {
|
||||||
return MustAdd(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
|
return WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: into})
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ func (scs srcCommonState) applyCtxAndPV(p srcCommonParams) srcCommonState {
|
|||||||
// the Sources don't actually care about the other fields of Param,
|
// the Sources don't actually care about the other fields of Param,
|
||||||
// those are only used by Populate once it has all ParamValues together
|
// 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
|
ctxP, _ = getParam(*thisCtx, ctxP.Name) // get it back out to get any added fields
|
||||||
|
|
||||||
if !p.unset {
|
if !p.unset {
|
||||||
@ -155,9 +155,9 @@ func (scs srcCommonState) assert(s Source) error {
|
|||||||
|
|
||||||
func TestSources(t *T) {
|
func TestSources(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx, a := RequiredInt(ctx, "a", "")
|
ctx, a := WithRequiredInt(ctx, "a", "")
|
||||||
ctx, b := RequiredInt(ctx, "b", "")
|
ctx, b := WithRequiredInt(ctx, "b", "")
|
||||||
ctx, c := RequiredInt(ctx, "c", "")
|
ctx, c := WithRequiredInt(ctx, "c", "")
|
||||||
|
|
||||||
err := Populate(ctx, Sources{
|
err := Populate(ctx, Sources{
|
||||||
SourceCLI{Args: []string{"--a=1", "--b=666"}},
|
SourceCLI{Args: []string{"--a=1", "--b=666"}},
|
||||||
@ -173,10 +173,10 @@ func TestSources(t *T) {
|
|||||||
|
|
||||||
func TestSourceParamValues(t *T) {
|
func TestSourceParamValues(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx, a := RequiredInt(ctx, "a", "")
|
ctx, a := WithRequiredInt(ctx, "a", "")
|
||||||
foo := mctx.NewChild(ctx, "foo")
|
foo := mctx.NewChild(ctx, "foo")
|
||||||
foo, b := RequiredString(foo, "b", "")
|
foo, b := WithRequiredString(foo, "b", "")
|
||||||
foo, c := Bool(foo, "c", "")
|
foo, c := WithBool(foo, "c", "")
|
||||||
ctx = mctx.WithChild(ctx, foo)
|
ctx = mctx.WithChild(ctx, foo)
|
||||||
|
|
||||||
err := Populate(ctx, ParamValues{
|
err := Populate(ctx, ParamValues{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mcrypto
|
package mcrypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
@ -58,7 +59,7 @@ func (pk PublicKey) verify(s Signature, r io.Reader) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := rsa.VerifyPSS(&pk.PublicKey, crypto.SHA256, h.Sum(nil), s.sig, nil); err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -88,12 +89,12 @@ func (pk *PublicKey) UnmarshalText(b []byte) error {
|
|||||||
str := string(b)
|
str := string(b)
|
||||||
strEnc, ok := stripPrefix(str, pubKeyV0)
|
strEnc, ok := stripPrefix(str, pubKeyV0)
|
||||||
if !ok || len(strEnc) <= hex.EncodedLen(8) {
|
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)
|
b, err := hex.DecodeString(strEnc)
|
||||||
if err != nil {
|
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))
|
pk.E = int(binary.BigEndian.Uint64(b))
|
||||||
@ -184,17 +185,17 @@ func (pk *PrivateKey) UnmarshalText(b []byte) error {
|
|||||||
str := string(b)
|
str := string(b)
|
||||||
strEnc, ok := stripPrefix(str, privKeyV0)
|
strEnc, ok := stripPrefix(str, privKeyV0)
|
||||||
if !ok {
|
if !ok {
|
||||||
return merr.Wrap(errMalformedPrivateKey)
|
return merr.Wrap(context.Background(), errMalformedPrivateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := hex.DecodeString(strEnc)
|
b, err := hex.DecodeString(strEnc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merr.Wrap(errMalformedPrivateKey)
|
return merr.Wrap(context.Background(), errMalformedPrivateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
e, n := binary.Uvarint(b)
|
e, n := binary.Uvarint(b)
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return merr.Wrap(errMalformedPrivateKey)
|
return merr.Wrap(context.Background(), errMalformedPrivateKey)
|
||||||
}
|
}
|
||||||
pk.PublicKey.E = int(e)
|
pk.PublicKey.E = int(e)
|
||||||
b = b[n:]
|
b = b[n:]
|
||||||
@ -205,7 +206,7 @@ func (pk *PrivateKey) UnmarshalText(b []byte) error {
|
|||||||
}
|
}
|
||||||
l, n := binary.Uvarint(b)
|
l, n := binary.Uvarint(b)
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
err = merr.Wrap(errMalformedPrivateKey)
|
err = merr.Wrap(context.Background(), errMalformedPrivateKey)
|
||||||
}
|
}
|
||||||
b = b[n:]
|
b = b[n:]
|
||||||
i := new(big.Int)
|
i := new(big.Int)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package mcrypto
|
package mcrypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"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 {
|
func (s Secret) verify(sig Signature, r io.Reader) error {
|
||||||
sigB, err := s.signRaw(r, uint8(len(sig.sig)), sig.salt, sig.t)
|
sigB, err := s.signRaw(r, uint8(len(sig.sig)), sig.salt, sig.t)
|
||||||
if err != nil {
|
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) {
|
} else if !hmac.Equal(sigB, sig.sig) {
|
||||||
return merr.WithValue(ErrInvalidSig, "sig", sig, true)
|
return merr.Wrap(context.Background(), ErrInvalidSig, "sig", sig)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package mcrypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -66,12 +67,12 @@ func (s *Signature) UnmarshalText(b []byte) error {
|
|||||||
str := string(b)
|
str := string(b)
|
||||||
strEnc, ok := stripPrefix(str, sigV0)
|
strEnc, ok := stripPrefix(str, sigV0)
|
||||||
if !ok || len(strEnc) < hex.EncodedLen(10) {
|
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)
|
b, err := hex.DecodeString(strEnc)
|
||||||
if err != nil {
|
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:]
|
unixNano, b := int64(binary.BigEndian.Uint64(b[:8])), b[8:]
|
||||||
@ -81,7 +82,7 @@ func (s *Signature) UnmarshalText(b []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
} else if len(b) < 1+int(b[0]) {
|
} 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
|
return nil
|
||||||
}
|
}
|
||||||
out := b[1 : 1+b[0]]
|
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
|
// Verify reads all data from the io.Reader and uses the Verifier to verify that
|
||||||
// the Signature is for that data.
|
// 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).
|
// mcrypto.ErrInvalidSig) to check).
|
||||||
func Verify(v Verifier, s Signature, r io.Reader) error {
|
func Verify(v Verifier, s Signature, r io.Reader) error {
|
||||||
return v.verify(s, r)
|
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
|
// VerifyBytes uses the Verifier to verify that the Signature is for the given
|
||||||
// []bytes.
|
// []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 {
|
func VerifyBytes(v Verifier, s Signature, b []byte) error {
|
||||||
return v.verify(s, bytes.NewBuffer(b))
|
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
|
// VerifyString uses the Verifier to verify that the Signature is for the given
|
||||||
// string.
|
// 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 {
|
func VerifyString(v Verifier, s Signature, in string) error {
|
||||||
return VerifyBytes(v, s, []byte(in))
|
return VerifyBytes(v, s, []byte(in))
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package mcrypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -68,11 +69,11 @@ func (u *UUID) UnmarshalText(b []byte) error {
|
|||||||
str := string(b)
|
str := string(b)
|
||||||
strEnc, ok := stripPrefix(str, uuidV0)
|
strEnc, ok := stripPrefix(str, uuidV0)
|
||||||
if !ok || len(strEnc) != hex.EncodedLen(16) {
|
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)
|
b, err := hex.DecodeString(strEnc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merr.WithValue(err, "uuidStr", str, true)
|
return merr.Wrap(context.Background(), err, "uuidStr", str)
|
||||||
}
|
}
|
||||||
u.b = b
|
u.b = b
|
||||||
return nil
|
return nil
|
||||||
|
301
mctx/annotate.go
301
mctx/annotate.go
@ -3,69 +3,284 @@ package mctx
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type annotateKey struct {
|
// Annotation describes the annotation of a key/value pair made on a Context via
|
||||||
userKey interface{}
|
// 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
|
// 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,
|
// returns a Context carrying them. Annotations only exist on the local level,
|
||||||
// i.e. a child and parent share different annotation namespaces.
|
// 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 {
|
func Annotate(ctx context.Context, kvs ...interface{}) context.Context {
|
||||||
for i := 0; i < len(kvs); i += 2 {
|
if len(kvs)%2 > 0 {
|
||||||
ctx = WithLocalValue(ctx, annotateKey{kvs[i]}, kvs[i+1])
|
panic("kvs being passed to mctx.Annotate must have an even number of elements")
|
||||||
}
|
} else if len(kvs) == 0 {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotations describes a set of keys/values which were set on a Context (but
|
// if multiple annotations are passed in here it's not actually necessary to
|
||||||
// not its parents or children) using Annotate.
|
// create an intermediate Context for each one, so keep curr outside and
|
||||||
type Annotations [][2]interface{}
|
// only use it later
|
||||||
|
var curr, root *annotation
|
||||||
// LocalAnnotations returns all key/value pairs which have been set via Annotate
|
prev, _ := ctx.Value(annotationKey(0)).(*annotation)
|
||||||
// on this Context (but not its parent or children). If a key was set twice then
|
if prev != nil {
|
||||||
// only the most recent value is included. The returned order is
|
root = prev.root
|
||||||
// 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 = append(annotations, [2]interface{}{aKey.userKey, val})
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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.
|
func mergeAnnotations(ctxA, ctxB context.Context) context.Context {
|
||||||
// If any two keys format to the same string, then type information will be
|
annotationA, _ := ctxA.Value(annotationKey(0)).(*annotation)
|
||||||
// prefaced to each one.
|
annotationB, _ := ctxB.Value(annotationKey(0)).(*annotation)
|
||||||
func (aa Annotations) StringMap() map[string]string {
|
if annotationB == nil {
|
||||||
keyTypes := make(map[string]interface{}, len(aa))
|
return ctxA
|
||||||
keyVals := map[string]string{}
|
} else if annotationA == nil {
|
||||||
setKey := func(k, v interface{}) {
|
return context.WithValue(ctxA, annotationKey(0), annotationB)
|
||||||
kStr := fmt.Sprint(k)
|
|
||||||
oldType := keyTypes[kStr]
|
|
||||||
if oldType == nil {
|
|
||||||
keyTypes[kStr] = k
|
|
||||||
keyVals[kStr] = fmt.Sprint(v)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if oldKey is in kvm, if so it needs to be moved to account for
|
var headA, currA *annotation
|
||||||
// its type info
|
currB := annotationB
|
||||||
if oldV, ok := keyVals[kStr]; ok {
|
for {
|
||||||
delete(keyVals, kStr)
|
if currB == nil {
|
||||||
keyVals[fmt.Sprintf("%T(%s)", oldType, kStr)] = oldV
|
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 {
|
currA.prev = annotationA
|
||||||
setKey(kv[0], kv[1])
|
return context.WithValue(ctxA, annotationKey(0), headA)
|
||||||
}
|
}
|
||||||
return keyVals
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
@ -13,42 +13,73 @@ func TestAnnotate(t *T) {
|
|||||||
parent = Annotate(parent, "b", "bar")
|
parent = Annotate(parent, "b", "bar")
|
||||||
|
|
||||||
child := NewChild(parent, "child")
|
child := NewChild(parent, "child")
|
||||||
|
child = Annotate(child, "a", "Foo")
|
||||||
child = Annotate(child, "a", "FOO")
|
child = Annotate(child, "a", "FOO")
|
||||||
child = Annotate(child, "c", "BAZ")
|
child = Annotate(child, "c", "BAZ")
|
||||||
parent = WithChild(parent, child)
|
parent = WithChild(parent, child)
|
||||||
|
|
||||||
parentAnnotations := LocalAnnotations(parent)
|
parentAnnotations := Annotations(parent)
|
||||||
childAnnotations := LocalAnnotations(child)
|
childAnnotations := Annotations(child)
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Len(parentAnnotations, 2),
|
massert.Len(parentAnnotations, 2),
|
||||||
massert.Has(parentAnnotations, [2]interface{}{"a", "foo"}),
|
massert.Has(parentAnnotations, Annotation{Key: "a", Value: "foo"}),
|
||||||
massert.Has(parentAnnotations, [2]interface{}{"b", "bar"}),
|
massert.Has(parentAnnotations, Annotation{Key: "b", Value: "bar"}),
|
||||||
massert.Len(childAnnotations, 2),
|
|
||||||
massert.Has(childAnnotations, [2]interface{}{"a", "FOO"}),
|
massert.Len(childAnnotations, 4),
|
||||||
massert.Has(childAnnotations, [2]interface{}{"c", "BAZ"}),
|
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 A int
|
||||||
type B int
|
type B int
|
||||||
aa := Annotations{
|
aa := AnnotationSet{
|
||||||
{"foo", "bar"},
|
{Key: 0, Path: nil, Value: "zero"},
|
||||||
{"1", "one"},
|
{Key: 1, Path: nil, Value: "one"},
|
||||||
{1, 1},
|
{Key: 1, Path: []string{"foo"}, Value: "ONE"},
|
||||||
{0, 0},
|
{Key: A(2), Path: []string{"foo"}, Value: "two"},
|
||||||
{A(0), 0},
|
{Key: B(2), Path: []string{"foo"}, Value: "TWO"},
|
||||||
{B(0), 0},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{
|
err := massert.Equal(map[string]string{
|
||||||
"foo": "bar",
|
"0": "ZERO",
|
||||||
"string(1)": "one",
|
"1": "ONE",
|
||||||
"int(1)": "1",
|
"2": "TWO",
|
||||||
"int(0)": "0",
|
}, Annotations(ctx).StringMap()).Assert()
|
||||||
"mctx.A(0)": "0",
|
|
||||||
"mctx.B(0)": "0",
|
|
||||||
}, aa.StringMap()).Assert()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
17
mctx/ctx.go
17
mctx/ctx.go
@ -11,17 +11,13 @@ package mctx
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"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
|
type ancestryKey int // 0 -> children, 1 -> parent, 2 -> path
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -97,6 +93,14 @@ func pathCP(ctx context.Context) []string {
|
|||||||
return outPath
|
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
|
// Name returns the name this Context was generated with via NewChild, or false
|
||||||
// if this Context was not generated via NewChild.
|
// if this Context was not generated via NewChild.
|
||||||
func Name(ctx context.Context) (string, bool) {
|
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)
|
childPath := append(pathCP(parent), name)
|
||||||
|
|
||||||
child := withoutLocalValues(parent)
|
child := withoutLocalValues(parent)
|
||||||
child = context.WithValue(child, ancestryKeyChildren, nil) // unset children
|
child = context.WithValue(child, ancestryKeyChildren, nil) // unset children
|
||||||
child = context.WithValue(child, ancestryKeyChildrenMap, nil) // unset children
|
child = context.WithValue(child, ancestryKeyChildrenMap, nil) // unset children
|
||||||
|
94
mctx/stack.go
Normal file
94
mctx/stack.go
Normal 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
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package merr
|
package mctx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
@ -8,8 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStack(t *T) {
|
func TestStack(t *T) {
|
||||||
foo := New("foo")
|
foo := WithStack(context.Background(), 0)
|
||||||
fooStack := GetStack(foo)
|
fooStack, ok := Stack(foo)
|
||||||
|
massert.Fatal(t, massert.Equal(true, ok))
|
||||||
|
|
||||||
// test Frame
|
// test Frame
|
||||||
frame := fooStack.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].File, "stack_test.go")),
|
||||||
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
|
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
|
// test that WithStack works and can be used to skip frames
|
||||||
inner := func() {
|
inner := func() {
|
||||||
bar := WithStack(foo, 1)
|
bar := WithStack(foo, 1)
|
||||||
barStack := GetStack(bar)
|
barStack, _ := Stack(bar)
|
||||||
frames := barStack.Frames()
|
frames := barStack.Frames()
|
||||||
massert.Fatal(t, massert.Comment(
|
massert.Fatal(t, massert.Comment(
|
||||||
massert.All(
|
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].File, "stack_test.go")),
|
||||||
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
|
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
|
||||||
),
|
),
|
||||||
"barStack.String():\n%s", barStack.String(),
|
"barStack.FullString():\n%s", barStack.FullString(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
inner()
|
inner()
|
@ -42,43 +42,37 @@ type BigQuery struct {
|
|||||||
tableUploaders map[[2]string]*bigquery.Uploader
|
tableUploaders map[[2]string]*bigquery.Uploader
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a BigQuery instance which will be initialized and configured
|
// WithBigQuery returns a BigQuery instance which will be initialized and
|
||||||
// when the start event is triggered on the returned (see mrun.Start). The
|
// configured when the start event is triggered on the returned (see
|
||||||
// BigQuery instance will have Close called on it when the stop event is
|
// mrun.Start). The BigQuery instance will have Close called on it when the stop
|
||||||
// triggered on the returned Context (see mrun.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
|
// 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.
|
// 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 {
|
if gce == nil {
|
||||||
ctx, gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.WithGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
bq := &BigQuery{
|
bq := &BigQuery{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
tables: map[[2]string]*bigquery.Table{},
|
tables: map[[2]string]*bigquery.Table{},
|
||||||
tableUploaders: map[[2]string]*bigquery.Uploader{},
|
tableUploaders: map[[2]string]*bigquery.Uploader{},
|
||||||
ctx: mctx.NewChild(ctx, "bigquery"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the equivalent functionality as here will be added with annotations
|
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
|
||||||
// bq.log.SetKV(bq)
|
bq.ctx = mctx.MergeAnnotations(bq.ctx, bq.gce.Context())
|
||||||
|
mlog.Info("connecting to bigquery", bq.ctx)
|
||||||
bq.ctx = mrun.OnStart(bq.ctx, func(innerCtx context.Context) error {
|
|
||||||
mlog.Info(bq.ctx, "connecting to bigquery")
|
|
||||||
var err error
|
var err error
|
||||||
bq.Client, err = bigquery.NewClient(innerCtx, bq.gce.Project, bq.gce.ClientOptions()...)
|
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 bq.Client.Close()
|
||||||
})
|
})
|
||||||
return mctx.WithChild(ctx, bq.ctx), bq
|
bq.ctx = ctx
|
||||||
}
|
return mctx.WithChild(parent, ctx), bq
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface.
|
|
||||||
func (bq *BigQuery) KV() map[string]interface{} {
|
|
||||||
return bq.gce.KV()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table initializes and returns the table instance with the given dataset and
|
// 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
|
return table, bq.tableUploaders[key], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
kv := mlog.KV{"bigQueryDataset": dataset, "bigQueryTable": tableName}
|
ctx = mctx.MergeAnnotations(ctx, bq.ctx)
|
||||||
mlog.Debug(bq.ctx, "creating/grabbing table", kv)
|
ctx = mctx.Annotate(ctx, "dataset", dataset, "table", tableName)
|
||||||
|
|
||||||
|
mlog.Debug("creating/grabbing table", bq.ctx)
|
||||||
schema, err := bigquery.InferSchema(schemaObj)
|
schema, err := bigquery.InferSchema(schemaObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, merr.WithKV(err, bq.KV(), kv.KV())
|
return nil, nil, merr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds := bq.Dataset(dataset)
|
ds := bq.Dataset(dataset)
|
||||||
if err := ds.Create(ctx, nil); err != nil && !isErrAlreadyExists(err) {
|
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)
|
table := ds.Table(tableName)
|
||||||
@ -119,7 +114,7 @@ func (bq *BigQuery) Table(
|
|||||||
Schema: schema,
|
Schema: schema,
|
||||||
}
|
}
|
||||||
if err := table.Create(ctx, meta); err != nil && !isErrAlreadyExists(err) {
|
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()
|
uploader := table.Uploader()
|
||||||
|
|
||||||
|
@ -31,61 +31,56 @@ type Bigtable struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a Bigtable instance which will be initialized and configured
|
// WithBigTable returns a Bigtable instance which will be initialized and
|
||||||
// when the start event is triggered on the returned Context (see mrun.Start).
|
// configured when the start event is triggered on the returned Context (see
|
||||||
// The Bigtable instance will have Close called on it when the stop event is
|
// mrun.Start). The Bigtable instance will have Close called on it when the
|
||||||
// triggered on the returned Context (see mrun.Stop).
|
// 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
|
// 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.
|
// 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
|
// 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.
|
// 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 {
|
if gce == nil {
|
||||||
ctx, gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.WithGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
bt := &Bigtable{
|
bt := &Bigtable{
|
||||||
gce: gce,
|
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
|
var inst *string
|
||||||
{
|
{
|
||||||
const name, descr = "instance", "name of the bigtable instance in the project to connect to"
|
const name, descr = "instance", "name of the bigtable instance in the project to connect to"
|
||||||
if defaultInstance != "" {
|
if defaultInstance != "" {
|
||||||
bt.ctx, inst = mcfg.String(bt.ctx, name, defaultInstance, descr)
|
ctx, inst = mcfg.WithString(ctx, name, defaultInstance, descr)
|
||||||
} else {
|
} 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
|
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
|
var err error
|
||||||
bt.Client, err = bigtable.NewClient(
|
bt.Client, err = bigtable.NewClient(
|
||||||
innerCtx,
|
innerCtx,
|
||||||
bt.gce.Project, bt.Instance,
|
bt.gce.Project, bt.Instance,
|
||||||
bt.gce.ClientOptions()...,
|
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 bt.Client.Close()
|
||||||
})
|
})
|
||||||
return mctx.WithChild(ctx, bt.ctx), bt
|
bt.ctx = ctx
|
||||||
}
|
return mctx.WithChild(parent, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureTable ensures that the given table exists and has (at least) the given
|
// 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.
|
// This method requires admin privileges on the bigtable instance.
|
||||||
func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...string) error {
|
func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...string) error {
|
||||||
kv := mlog.KV{"bigtableTable": name}
|
ctx = mctx.MergeAnnotations(ctx, bt.ctx)
|
||||||
mlog.Info(bt.ctx, "ensuring table", kv)
|
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)
|
adminClient, err := bigtable.NewAdminClient(ctx, bt.gce.Project, bt.Instance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merr.WithKV(err, bt.KV(), kv.KV())
|
return merr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
defer adminClient.Close()
|
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)
|
err = adminClient.CreateTable(ctx, name)
|
||||||
if err != nil && !isErrAlreadyExists(err) {
|
if err != nil && !isErrAlreadyExists(err) {
|
||||||
return merr.WithKV(err, bt.KV(), kv.KV())
|
return merr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, colFam := range colFams {
|
for _, colFam := range colFams {
|
||||||
kv := kv.Set("family", colFam)
|
ctx := mctx.Annotate(ctx, "family", colFam)
|
||||||
mlog.Debug(bt.ctx, "creating bigtable column family (if needed)", kv)
|
mlog.Debug("creating bigtable column family (if needed)", ctx)
|
||||||
err := adminClient.CreateColumnFamily(ctx, name, colFam)
|
err := adminClient.CreateColumnFamily(ctx, name, colFam)
|
||||||
if err != nil && !isErrAlreadyExists(err) {
|
if err != nil && !isErrAlreadyExists(err) {
|
||||||
return merr.WithKV(err, bt.KV(), kv.KV())
|
return merr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBasic(t *T) {
|
func TestBasic(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.Context()
|
||||||
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "testProject")
|
ctx = mtest.WithEnv(ctx, "BIGTABLE_GCE_PROJECT", "testProject")
|
||||||
ctx, bt := MNew(ctx, nil, "testInstance")
|
ctx, bt := WithBigTable(ctx, nil, "testInstance")
|
||||||
|
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
tableName := "test-" + mrand.Hex(8)
|
tableName := "test-" + mrand.Hex(8)
|
||||||
|
@ -22,39 +22,33 @@ type Datastore struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a Datastore instance which will be initialized and configured
|
// WithDatastore returns a Datastore instance which will be initialized and
|
||||||
// when the start event is triggered on the returned Context (see mrun.Start).
|
// configured when the start event is triggered on the returned Context (see
|
||||||
// The Datastore instance will have Close called on it when the stop event is
|
// mrun.Start). The Datastore instance will have Close called on it when the
|
||||||
// triggered on the returned Context (see mrun.Stop).
|
// 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
|
// 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.
|
// 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 {
|
if gce == nil {
|
||||||
ctx, gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.WithGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ds := &Datastore{
|
ds := &Datastore{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
ctx: mctx.NewChild(ctx, "datastore"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the equivalent functionality as here will be added with annotations
|
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
|
||||||
// ds.log.SetKV(ds)
|
ds.ctx = mctx.MergeAnnotations(ds.ctx, ds.gce.Context())
|
||||||
|
mlog.Info("connecting to datastore", ds.ctx)
|
||||||
ds.ctx = mrun.OnStart(ds.ctx, func(innerCtx context.Context) error {
|
|
||||||
mlog.Info(ds.ctx, "connecting to datastore")
|
|
||||||
var err error
|
var err error
|
||||||
ds.Client, err = datastore.NewClient(innerCtx, ds.gce.Project, ds.gce.ClientOptions()...)
|
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 ds.Client.Close()
|
||||||
})
|
})
|
||||||
return mctx.WithChild(ctx, ds.ctx), ds
|
ds.ctx = ctx
|
||||||
}
|
return mctx.WithChild(parent, ctx), ds
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface.
|
|
||||||
func (ds *Datastore) KV() map[string]interface{} {
|
|
||||||
return ds.gce.KV()
|
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
|
|
||||||
// Requires datastore emulator to be running
|
// Requires datastore emulator to be running
|
||||||
func TestBasic(t *T) {
|
func TestBasic(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.Context()
|
||||||
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
ctx = mtest.WithEnv(ctx, "DATASTORE_GCE_PROJECT", "test")
|
||||||
ctx, ds := MNew(ctx, nil)
|
ctx, ds := WithDatastore(ctx, nil)
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
name := mrand.Hex(8)
|
name := mrand.Hex(8)
|
||||||
key := datastore.NameKey("testKind", name, nil)
|
key := datastore.NameKey("testKind", name, nil)
|
||||||
|
27
mdb/mdb.go
27
mdb/mdb.go
@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
)
|
)
|
||||||
@ -15,32 +14,36 @@ import (
|
|||||||
// GCE wraps configuration parameters commonly used for interacting with GCE
|
// GCE wraps configuration parameters commonly used for interacting with GCE
|
||||||
// services.
|
// services.
|
||||||
type GCE struct {
|
type GCE struct {
|
||||||
|
ctx context.Context
|
||||||
Project string
|
Project string
|
||||||
CredFile string
|
CredFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MGCE returns a GCE instance which will be initialized and configured when the
|
// WithGCE returns a GCE instance which will be initialized and configured when
|
||||||
// start event is triggered on the returned Context (see mrun.Start).
|
// the start event is triggered on the returned Context (see mrun.Start).
|
||||||
// defaultProject is used as the default value for the mcfg parameter this
|
// defaultProject is used as the default value for the mcfg parameter this
|
||||||
// function creates.
|
// 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 := 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
|
var project *string
|
||||||
const projectUsage = "Name of GCE project to use"
|
const projectUsage = "Name of GCE project to use"
|
||||||
if defaultProject == "" {
|
if defaultProject == "" {
|
||||||
ctx, project = mcfg.RequiredString(ctx, "project", projectUsage)
|
ctx, project = mcfg.WithRequiredString(ctx, "project", projectUsage)
|
||||||
} else {
|
} else {
|
||||||
ctx, project = mcfg.String(ctx, "project", defaultProject, projectUsage)
|
ctx, project = mcfg.WithString(ctx, "project", defaultProject, projectUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
var gce GCE
|
var gce GCE
|
||||||
ctx = mrun.OnStart(ctx, func(context.Context) error {
|
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
|
||||||
gce.Project = *project
|
gce.Project = *project
|
||||||
gce.CredFile = *credFile
|
gce.CredFile = *credFile
|
||||||
|
gce.ctx = mctx.Annotate(ctx, "project", gce.Project)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
gce.ctx = ctx
|
||||||
return mctx.WithChild(parent, ctx), &gce
|
return mctx.WithChild(parent, ctx), &gce
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +57,7 @@ func (gce *GCE) ClientOptions() []option.ClientOption {
|
|||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface.
|
// Context returns the annotated Context from this instance's initialization.
|
||||||
func (gce *GCE) KV() map[string]interface{} {
|
func (gce *GCE) Context() context.Context {
|
||||||
return mlog.KV{
|
return gce.ctx
|
||||||
"gceProject": gce.Project,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ package mpubsub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -41,45 +40,40 @@ type PubSub struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// MNew returns a PubSub instance which will be initialized and configured when
|
// WithPubSub returns a PubSub instance which will be initialized and configured
|
||||||
// the start event is triggered on the returned Context (see mrun.Start). The
|
// when the start event is triggered on the returned Context (see mrun.Start).
|
||||||
// PubSub instance will have Close called on it when the stop event is triggered
|
// The PubSub instance will have Close called on it when the stop event is
|
||||||
// on the returned Context(see mrun.Stop).
|
// triggered on the returned Context(see mrun.Stop).
|
||||||
//
|
//
|
||||||
// gce is optional and can be passed in if there's an existing gce object which
|
// 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.
|
// 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 {
|
if gce == nil {
|
||||||
ctx, gce = mdb.MGCE(ctx, "")
|
ctx, gce = mdb.WithGCE(ctx, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ps := &PubSub{
|
ps := &PubSub{
|
||||||
gce: gce,
|
gce: gce,
|
||||||
ctx: mctx.NewChild(ctx, "pubsub"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the equivalent functionality as here will be added with annotations
|
ctx = mrun.WithStartHook(ctx, func(innerCtx context.Context) error {
|
||||||
// ps.log.SetKV(ps)
|
ps.ctx = mctx.MergeAnnotations(ps.ctx, ps.gce.Context())
|
||||||
|
mlog.Info("connecting to pubsub", ps.ctx)
|
||||||
ps.ctx = mrun.OnStart(ps.ctx, func(innerCtx context.Context) error {
|
|
||||||
mlog.Info(ps.ctx, "connecting to pubsub")
|
|
||||||
var err error
|
var err error
|
||||||
ps.Client, err = pubsub.NewClient(innerCtx, ps.gce.Project, ps.gce.ClientOptions()...)
|
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 ps.Client.Close()
|
||||||
})
|
})
|
||||||
return mctx.WithChild(ctx, ps.ctx), ps
|
ps.ctx = ctx
|
||||||
}
|
return mctx.WithChild(parent, ctx), ps
|
||||||
|
|
||||||
// KV implements the mlog.KVer interface
|
|
||||||
func (ps *PubSub) KV() map[string]interface{} {
|
|
||||||
return ps.gce.KV()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Topic provides methods around a particular topic in PubSub
|
// Topic provides methods around a particular topic in PubSub
|
||||||
type Topic struct {
|
type Topic struct {
|
||||||
|
ctx context.Context
|
||||||
ps *PubSub
|
ps *PubSub
|
||||||
topic *pubsub.Topic
|
topic *pubsub.Topic
|
||||||
name string
|
name string
|
||||||
@ -88,6 +82,7 @@ type Topic struct {
|
|||||||
// Topic returns, after potentially creating, a topic of the given name
|
// Topic returns, after potentially creating, a topic of the given name
|
||||||
func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic, error) {
|
func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic, error) {
|
||||||
t := &Topic{
|
t := &Topic{
|
||||||
|
ctx: mctx.Annotate(ps.ctx, "topicName", name),
|
||||||
ps: ps,
|
ps: ps,
|
||||||
name: name,
|
name: name,
|
||||||
}
|
}
|
||||||
@ -98,38 +93,31 @@ func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic,
|
|||||||
if isErrAlreadyExists(err) {
|
if isErrAlreadyExists(err) {
|
||||||
t.topic = ps.Client.Topic(name)
|
t.topic = ps.Client.Topic(name)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, merr.WithKV(err, t.KV())
|
return nil, merr.Wrap(ctx, merr.Wrap(t.ctx, err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.topic = ps.Client.Topic(name)
|
t.topic = ps.Client.Topic(name)
|
||||||
if exists, err := t.topic.Exists(ctx); err != nil {
|
if exists, err := t.topic.Exists(t.ctx); err != nil {
|
||||||
return nil, merr.WithKV(err, t.KV())
|
return nil, merr.Wrap(ctx, merr.Wrap(t.ctx, err))
|
||||||
} else if !exists {
|
} else if !exists {
|
||||||
err := merr.New("topic dne")
|
return nil, merr.Wrap(ctx, merr.New(t.ctx, "topic dne"))
|
||||||
return nil, merr.WithKV(err, t.KV())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t, nil
|
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
|
// Publish publishes a message with the given data as its body to the Topic
|
||||||
func (t *Topic) Publish(ctx context.Context, data []byte) error {
|
func (t *Topic) Publish(ctx context.Context, data []byte) error {
|
||||||
_, err := t.topic.Publish(ctx, &Message{Data: data}).Get(ctx)
|
_, err := t.topic.Publish(ctx, &Message{Data: data}).Get(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return merr.WithKV(err, t.KV())
|
return merr.Wrap(ctx, merr.Wrap(t.ctx, err))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscription provides methods around a subscription to a topic in PubSub
|
// Subscription provides methods around a subscription to a topic in PubSub
|
||||||
type Subscription struct {
|
type Subscription struct {
|
||||||
|
ctx context.Context
|
||||||
topic *Topic
|
topic *Topic
|
||||||
sub *pubsub.Subscription
|
sub *pubsub.Subscription
|
||||||
name string
|
name string
|
||||||
@ -143,6 +131,7 @@ type Subscription struct {
|
|||||||
func (t *Topic) Subscription(ctx context.Context, name string, create bool) (*Subscription, error) {
|
func (t *Topic) Subscription(ctx context.Context, name string, create bool) (*Subscription, error) {
|
||||||
name = t.name + "_" + name
|
name = t.name + "_" + name
|
||||||
s := &Subscription{
|
s := &Subscription{
|
||||||
|
ctx: mctx.Annotate(t.ctx, "subName", name),
|
||||||
topic: t,
|
topic: t,
|
||||||
name: name,
|
name: name,
|
||||||
}
|
}
|
||||||
@ -155,27 +144,19 @@ func (t *Topic) Subscription(ctx context.Context, name string, create bool) (*Su
|
|||||||
if isErrAlreadyExists(err) {
|
if isErrAlreadyExists(err) {
|
||||||
s.sub = t.ps.Subscription(name)
|
s.sub = t.ps.Subscription(name)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, merr.WithKV(err, s.KV())
|
return nil, merr.Wrap(ctx, merr.Wrap(s.ctx, err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s.sub = t.ps.Subscription(name)
|
s.sub = t.ps.Subscription(name)
|
||||||
if exists, err := s.sub.Exists(ctx); err != nil {
|
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 {
|
} else if !exists {
|
||||||
err := merr.New("sub dne")
|
return nil, merr.Wrap(ctx, merr.New(s.ctx, "sub dne"))
|
||||||
return nil, merr.WithKV(err, s.KV())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s, nil
|
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
|
// 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
|
// 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
|
// 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)
|
octx := oldctx.Context(ctx)
|
||||||
for {
|
for {
|
||||||
err := s.sub.Receive(octx, func(octx oldctx.Context, msg *Message) {
|
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()
|
defer cancel()
|
||||||
|
innerCtx := context.Context(innerOCtx)
|
||||||
|
|
||||||
ok, err := fn(context.Context(innerCtx), msg)
|
ok, err := fn(innerCtx, msg)
|
||||||
if err != nil {
|
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 {
|
if ok {
|
||||||
@ -238,7 +221,8 @@ func (s *Subscription) Consume(ctx context.Context, fn ConsumerFunc, opts Consum
|
|||||||
if octx.Err() == context.Canceled || err == nil {
|
if octx.Err() == context.Canceled || err == nil {
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} 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)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,7 +315,8 @@ func (s *Subscription) BatchConsume(
|
|||||||
}
|
}
|
||||||
ret, err := fn(thisCtx, msgs)
|
ret, err := fn(thisCtx, msgs)
|
||||||
if err != nil {
|
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 {
|
for i := range thisGroup {
|
||||||
thisGroup[i].retCh <- ret // retCh is buffered
|
thisGroup[i].retCh <- ret // retCh is buffered
|
||||||
@ -365,7 +350,7 @@ func (s *Subscription) BatchConsume(
|
|||||||
case ret := <-retCh:
|
case ret := <-retCh:
|
||||||
return ret, nil
|
return ret, nil
|
||||||
case <-ctx.Done():
|
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)
|
}, opts)
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ import (
|
|||||||
|
|
||||||
// this requires the pubsub emulator to be running
|
// this requires the pubsub emulator to be running
|
||||||
func TestPubSub(t *T) {
|
func TestPubSub(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.Context()
|
||||||
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
ctx = mtest.WithEnv(ctx, "PUBSUB_GCE_PROJECT", "test")
|
||||||
ctx, ps := MNew(ctx, nil)
|
ctx, ps := WithPubSub(ctx, nil)
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
topicName := "testTopic_" + mrand.Hex(8)
|
topicName := "testTopic_" + mrand.Hex(8)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@ -47,9 +47,9 @@ func TestPubSub(t *T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBatchPubSub(t *T) {
|
func TestBatchPubSub(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.Context()
|
||||||
ctx = mtest.SetEnv(ctx, "GCE_PROJECT", "test")
|
ctx = mtest.WithEnv(ctx, "PUBSUB_GCE_PROJECT", "test")
|
||||||
ctx, ps := MNew(ctx, nil)
|
ctx, ps := WithPubSub(ctx, nil)
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
|
|
||||||
topicName := "testBatchTopic_" + mrand.Hex(8)
|
topicName := "testBatchTopic_" + mrand.Hex(8)
|
||||||
|
120
merr/kv.go
120
merr/kv.go
@ -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
|
|
||||||
}
|
|
@ -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,
|
|
||||||
))
|
|
||||||
}
|
|
197
merr/merr.go
197
merr/merr.go
@ -9,11 +9,12 @@
|
|||||||
package merr
|
package merr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
var strBuilderPool = sync.Pool{
|
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 {
|
type err struct {
|
||||||
err error
|
err error
|
||||||
attr map[interface{}]val
|
attr map[interface{}]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// attr keys internal to this package
|
// 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 {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
er, ok := e.(*err)
|
er, ok := e.(*err)
|
||||||
if !ok {
|
if !ok {
|
||||||
er := &err{err: e, attr: map[interface{}]val{}}
|
return &err{err: e}
|
||||||
if skip >= 0 {
|
|
||||||
setStack(er, skip+1)
|
|
||||||
}
|
|
||||||
return er
|
|
||||||
} else if !cp {
|
} else if !cp {
|
||||||
return er
|
return er
|
||||||
}
|
}
|
||||||
|
|
||||||
er2 := &err{
|
er2 := &err{
|
||||||
err: er.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 {
|
for k, v := range er.attr {
|
||||||
er2.attr[k] = v
|
er2.attr[k] = v
|
||||||
}
|
}
|
||||||
if _, ok := er2.attr[attrKeyStack]; !ok && skip >= 0 {
|
|
||||||
setStack(er, skip+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return er2
|
return er2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap takes in an error and returns one wrapping it in merr's inner type,
|
// Base takes in an error and checks if it is merr's internal error type. If it
|
||||||
// which embeds information like the stack trace.
|
// is then the underlying error which is being wrapped is returned. If it's not
|
||||||
func Wrap(e error) error {
|
// then the passed in error is returned as-is.
|
||||||
return wrap(e, false, 1)
|
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
|
// WithValue returns a copy of the original error, automatically wrapping it if
|
||||||
// automatically wraps the error in merr's inner type, which embeds information
|
// the error is not from merr (see Wrap). The returned error has an attribute
|
||||||
// like the stack trace.
|
// value set on it for the given key.
|
||||||
|
func WithValue(e error, k, v interface{}) error {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
er := wrap(e, true)
|
||||||
|
if er.attr == nil {
|
||||||
|
er.attr = map[interface{}]interface{}{}
|
||||||
|
}
|
||||||
|
er.attr[k] = v
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := mctx.Stack(ctx); !ok {
|
||||||
|
ctx = mctx.WithStack(ctx, skip+1)
|
||||||
|
}
|
||||||
|
if len(kvs) > 0 {
|
||||||
|
ctx = mctx.Annotate(ctx, kvs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, visible key/values may be passed into New at this point. For
|
// For convenience, extra annotation information can be passed in here as well
|
||||||
// example, the following two are equivalent:
|
// via the kvs argument. See mctx.Annotate for more information.
|
||||||
//
|
//
|
||||||
// merr.WithValue(merr.New("foo"), "bar", "baz", true)
|
// This function automatically embeds stack information into the Context as it's
|
||||||
// merr.New("foo", "bar", "baz")
|
// 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().
|
||||||
//
|
//
|
||||||
func New(str string, kvs ...interface{}) error {
|
// The returned Context will have annotated on it (see mctx.Annotate) the
|
||||||
if len(kvs)%2 != 0 {
|
// underlying error's string (as returned by Error()) and the stack location in
|
||||||
panic("key passed in without corresponding value")
|
// 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()
|
||||||
}
|
}
|
||||||
err := wrap(errors.New(str), false, 1)
|
ctx := ctx(e)
|
||||||
for i := 0; i < len(kvs); i += 2 {
|
ctx = mctx.Annotate(ctx, annotateKey("err"), Base(e).Error())
|
||||||
err.attr[kvs[i]] = val{
|
return ctx
|
||||||
visible: true,
|
|
||||||
val: kvs[i+1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (er *err) visibleAttrs() [][2]string {
|
|
||||||
out := make([][2]string, 0, len(er.attr))
|
|
||||||
for k, v := range er.attr {
|
|
||||||
if !v.visible {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, [2]string{
|
|
||||||
strings.Trim(fmt.Sprintf("%q", k), `"`),
|
|
||||||
fmt.Sprint(v.val),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(out, func(i, j int) bool {
|
|
||||||
return out[i][0] < out[j][0]
|
|
||||||
})
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (er *err) Error() string {
|
func (er *err) Error() string {
|
||||||
visAttrs := er.visibleAttrs()
|
ctx := ctx(er)
|
||||||
if len(visAttrs) == 0 {
|
|
||||||
return er.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
sb := strBuilderPool.Get().(*strings.Builder)
|
sb := strBuilderPool.Get().(*strings.Builder)
|
||||||
defer putStrBuilder(sb)
|
defer putStrBuilder(sb)
|
||||||
|
|
||||||
sb.WriteString(strings.TrimSpace(er.err.Error()))
|
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("\n\t* ")
|
||||||
sb.WriteString(k)
|
sb.WriteString(k)
|
||||||
sb.WriteString(": ")
|
sb.WriteString(": ")
|
||||||
@ -154,16 +199,6 @@ func (er *err) Error() string {
|
|||||||
return sb.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).
|
// Equal is a shortcut for Base(e1) == Base(e2).
|
||||||
func Equal(e1, e2 error) bool {
|
func Equal(e1, e2 error) bool {
|
||||||
return Base(e1) == Base(e2)
|
return Base(e1) == Base(e2)
|
||||||
|
@ -1,35 +1,33 @@
|
|||||||
package merr
|
package merr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
. "testing"
|
. "testing"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestError(t *T) {
|
func TestError(t *T) {
|
||||||
er := &err{
|
e := New(context.Background(), "foo",
|
||||||
err: errors.New("foo"),
|
"a", "aaa aaa\n",
|
||||||
attr: map[interface{}]val{
|
"c", "ccc\nccc\n",
|
||||||
"a": val{val: "aaa aaa\n", visible: true},
|
"d\t", "weird key but ok",
|
||||||
"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()
|
|
||||||
exp := `foo
|
exp := `foo
|
||||||
* a: aaa aaa
|
* a: aaa aaa
|
||||||
* c:
|
* c:
|
||||||
ccc
|
ccc
|
||||||
ccc
|
ccc
|
||||||
* d\t: weird key but ok`
|
* d: weird key but ok
|
||||||
massert.Fatal(t, massert.Equal(exp, str))
|
* errLoc: merr/merr_test.go:13`
|
||||||
|
massert.Fatal(t, massert.Equal(exp, e.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBase(t *T) {
|
func TestBase(t *T) {
|
||||||
errFoo, errBar := errors.New("foo"), errors.New("bar")
|
errFoo, errBar := errors.New("foo"), errors.New("bar")
|
||||||
erFoo := Wrap(errFoo)
|
erFoo := Wrap(context.Background(), errFoo)
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
massert.Nil(Base(nil)),
|
massert.Nil(Base(nil)),
|
||||||
massert.Equal(errFoo, Base(erFoo)),
|
massert.Equal(errFoo, Base(erFoo)),
|
||||||
@ -40,3 +38,50 @@ func TestBase(t *T) {
|
|||||||
massert.Equal(false, Equal(errBar, erFoo)),
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -17,13 +17,13 @@ import (
|
|||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MServer is returned by MListenAndServe and simply wraps an *http.Server.
|
// Server is returned by WithListeningServer and simply wraps an *http.Server.
|
||||||
type MServer struct {
|
type Server struct {
|
||||||
*http.Server
|
*http.Server
|
||||||
ctx context.Context
|
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
|
// ListenAndServe called on it (asynchronously) when the start event is
|
||||||
// triggered on the returned Context (see mrun.Start). The Server will have
|
// 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
|
// 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
|
// This function automatically handles setting up configuration parameters via
|
||||||
// mcfg. The default listen address is ":0".
|
// mcfg. The default listen address is ":0".
|
||||||
func MListenAndServe(ctx context.Context, h http.Handler) (context.Context, *MServer) {
|
func WithListeningServer(ctx context.Context, h http.Handler) (context.Context, *Server) {
|
||||||
srv := &MServer{
|
srv := &Server{
|
||||||
Server: &http.Server{Handler: h},
|
Server: &http.Server{Handler: h},
|
||||||
ctx: mctx.NewChild(ctx, "http"),
|
ctx: mctx.NewChild(ctx, "http"),
|
||||||
}
|
}
|
||||||
|
|
||||||
var listener *mnet.MListener
|
var listener *mnet.Listener
|
||||||
srv.ctx, listener = mnet.MListen(srv.ctx, "tcp", "")
|
srv.ctx, listener = mnet.WithListener(srv.ctx, "tcp", "")
|
||||||
listener.NoCloseOnStop = true // http.Server.Shutdown will do this
|
listener.NoCloseOnStop = true // http.Server.Shutdown will do this
|
||||||
|
|
||||||
// TODO the equivalent functionality as here will be added with annotations
|
srv.ctx = mrun.WithStartHook(srv.ctx, func(context.Context) error {
|
||||||
//logger := mlog.From(ctx)
|
|
||||||
//logger.SetKV(listener)
|
|
||||||
|
|
||||||
srv.ctx = mrun.OnStart(srv.ctx, func(context.Context) error {
|
|
||||||
srv.Addr = listener.Addr().String()
|
srv.Addr = listener.Addr().String()
|
||||||
srv.ctx = mrun.Thread(srv.ctx, func() error {
|
srv.ctx = mrun.WithThread(srv.ctx, func() error {
|
||||||
if err := srv.Serve(listener); err != http.ErrServerClosed {
|
mlog.Info("serving requests", srv.ctx)
|
||||||
mlog.Error(srv.ctx, "error serving listener", merr.KV(err))
|
if err := srv.Serve(listener); !merr.Equal(err, http.ErrServerClosed) {
|
||||||
|
mlog.Error("error serving listener", srv.ctx, merr.Context(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -57,8 +54,8 @@ func MListenAndServe(ctx context.Context, h http.Handler) (context.Context, *MSe
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
srv.ctx = mrun.OnStop(srv.ctx, func(innerCtx context.Context) error {
|
srv.ctx = mrun.WithStopHook(srv.ctx, func(innerCtx context.Context) error {
|
||||||
mlog.Info(srv.ctx, "shutting down server")
|
mlog.Info("shutting down server", srv.ctx)
|
||||||
if err := srv.Shutdown(innerCtx); err != nil {
|
if err := srv.Shutdown(innerCtx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMListenAndServe(t *T) {
|
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)
|
io.Copy(rw, r.Body)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
49
mlog/ctx.go
49
mlog/ctx.go
@ -1,53 +1,58 @@
|
|||||||
package mlog
|
package mlog
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
type ctxKey int
|
type ctxKey int
|
||||||
|
|
||||||
// Set returns the Context with the Logger carried by it.
|
// WithLogger returns the Context with the Logger carried by it.
|
||||||
func Set(ctx context.Context, l *Logger) context.Context {
|
func WithLogger(ctx context.Context, l *Logger) context.Context {
|
||||||
return context.WithValue(ctx, ctxKey(0), l)
|
return context.WithValue(ctx, ctxKey(0), l)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultLogger is an instance of Logger which is returned by From when a
|
// 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()
|
var DefaultLogger = NewLogger()
|
||||||
|
|
||||||
// From returns the Logger carried by this Context, or DefaultLogger if none is
|
// From looks at each context and returns the Logger from the first Context
|
||||||
// being carried.
|
// which carries one via a WithLogger call. If none carry a Logger than
|
||||||
func From(ctx context.Context) *Logger {
|
// DefaultLogger is returned.
|
||||||
|
func From(ctxs ...context.Context) *Logger {
|
||||||
|
for _, ctx := range ctxs {
|
||||||
if l, _ := ctx.Value(ctxKey(0)).(*Logger); l != nil {
|
if l, _ := ctx.Value(ctxKey(0)).(*Logger); l != nil {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return DefaultLogger
|
return DefaultLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug is a shortcut for
|
// Debug is a shortcut for
|
||||||
// mlog.From(ctx).Debug(ctx, descr, kvs...)
|
// mlog.From(ctxs...).Debug(desc, ctxs...)
|
||||||
func Debug(ctx context.Context, descr string, kvs ...KVer) {
|
func Debug(descr string, ctxs ...context.Context) {
|
||||||
From(ctx).Debug(ctx, descr, kvs...)
|
From(ctxs...).Debug(descr, ctxs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info is a shortcut for
|
// Info is a shortcut for
|
||||||
// mlog.From(ctx).Info(ctx, descr, kvs...)
|
// mlog.From(ctxs...).Info(desc, ctxs...)
|
||||||
func Info(ctx context.Context, descr string, kvs ...KVer) {
|
func Info(descr string, ctxs ...context.Context) {
|
||||||
From(ctx).Info(ctx, descr, kvs...)
|
From(ctxs...).Info(descr, ctxs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn is a shortcut for
|
// Warn is a shortcut for
|
||||||
// mlog.From(ctx).Warn(ctx, descr, kvs...)
|
// mlog.From(ctxs...).Warn(desc, ctxs...)
|
||||||
func Warn(ctx context.Context, descr string, kvs ...KVer) {
|
func Warn(descr string, ctxs ...context.Context) {
|
||||||
From(ctx).Warn(ctx, descr, kvs...)
|
From(ctxs...).Warn(descr, ctxs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error is a shortcut for
|
// Error is a shortcut for
|
||||||
// mlog.From(ctx).Error(ctx, descr, kvs...)
|
// mlog.From(ctxs...).Error(desc, ctxs...)
|
||||||
func Error(ctx context.Context, descr string, kvs ...KVer) {
|
func Error(descr string, ctxs ...context.Context) {
|
||||||
From(ctx).Error(ctx, descr, kvs...)
|
From(ctxs...).Error(descr, ctxs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal is a shortcut for
|
// Fatal is a shortcut for
|
||||||
// mlog.From(ctx).Fatal(ctx, descr, kvs...)
|
// mlog.From(ctxs...).Fatal(desc, ctxs...)
|
||||||
func Fatal(ctx context.Context, descr string, kvs ...KVer) {
|
func Fatal(descr string, ctxs ...context.Context) {
|
||||||
From(ctx).Fatal(ctx, descr, kvs...)
|
From(ctxs...).Fatal(descr, ctxs...)
|
||||||
}
|
}
|
||||||
|
@ -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"),
|
|
||||||
))
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)),
|
|
||||||
))
|
|
||||||
}
|
|
246
mlog/mlog.go
246
mlog/mlog.go
@ -1,28 +1,17 @@
|
|||||||
// Package mlog is a generic logging library. The log methods come in different
|
// Package mlog is a generic logging library. The log methods come in different
|
||||||
// severities: Debug, Info, Warn, Error, and Fatal.
|
// severities: Debug, Info, Warn, Error, and Fatal.
|
||||||
//
|
//
|
||||||
// The log methods take in a string describing the error, and a set of key/value
|
// The log methods take in a message string and a Context. The Context can be
|
||||||
// pairs giving the specific context around the error. The string is intended to
|
// loaded with additional annotations which will be included in the log entry as
|
||||||
// always be the same no matter what, while the key/value pairs give information
|
// well (see mctx package).
|
||||||
// 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))
|
|
||||||
//
|
//
|
||||||
package mlog
|
package mlog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"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
|
// Message describes a message to be logged, after having already resolved the
|
||||||
// KVer
|
// KVer
|
||||||
type Message struct {
|
type Message struct {
|
||||||
context.Context
|
|
||||||
Level
|
Level
|
||||||
Description string
|
Description string
|
||||||
KVer
|
Contexts []context.Context
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is a function which can process Messages in some way.
|
// Handler is a function which can process Messages in some way.
|
||||||
@ -235,47 +102,39 @@ func stringSlice(kv KV) [][2]string {
|
|||||||
// Handler if necessary.
|
// Handler if necessary.
|
||||||
type Handler func(msg Message) error
|
type Handler func(msg Message) error
|
||||||
|
|
||||||
// DefaultFormat formats and writes the Message to the given Writer using mlog's
|
// MessageJSON is the type used to encode Messages to JSON in DefaultHandler
|
||||||
// default format.
|
type MessageJSON struct {
|
||||||
func DefaultFormat(w io.Writer, msg Message) error {
|
Level string `json:"level"`
|
||||||
var err error
|
Description string `json:"descr"`
|
||||||
write := func(s string, args ...interface{}) {
|
|
||||||
if err == nil {
|
// path -> key -> value
|
||||||
_, err = fmt.Fprintf(w, s, args...)
|
Annotations map[string]map[string]string `json:"annotations,omitempty"`
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHandler initializes and returns a Handler which will write all
|
// DefaultHandler initializes and returns a Handler which will write all
|
||||||
// messages to os.Stderr in a thread-safe way. This is the Handler which
|
// messages to os.Stderr in a thread-safe way. This is the Handler which
|
||||||
// NewLogger will use automatically.
|
// NewLogger will use automatically.
|
||||||
func DefaultHandler() Handler {
|
func DefaultHandler() Handler {
|
||||||
|
return defaultHandler(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultHandler(out io.Writer) Handler {
|
||||||
l := new(sync.Mutex)
|
l := new(sync.Mutex)
|
||||||
bw := bufio.NewWriter(os.Stderr)
|
enc := json.NewEncoder(out)
|
||||||
return func(msg Message) error {
|
return func(msg Message) error {
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
|
|
||||||
err := DefaultFormat(bw, msg)
|
msgJSON := MessageJSON{
|
||||||
if err == nil {
|
Level: msg.Level.String(),
|
||||||
err = bw.Flush()
|
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
|
l *sync.RWMutex
|
||||||
h Handler
|
h Handler
|
||||||
maxLevel uint
|
maxLevel uint
|
||||||
kv KVer
|
|
||||||
|
|
||||||
testMsgWrittenCh chan struct{} // only initialized/used in tests
|
testMsgWrittenCh chan struct{} // only initialized/used in tests
|
||||||
}
|
}
|
||||||
@ -332,15 +190,6 @@ func (l *Logger) Handler() Handler {
|
|||||||
return l.h
|
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.
|
// 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,
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.kv != nil {
|
|
||||||
msg.KVer = MergeInto(l.kv, msg.KVer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := l.h(msg); err != nil {
|
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
|
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{
|
return Message{
|
||||||
Context: ctx,
|
|
||||||
Level: lvl,
|
Level: lvl,
|
||||||
Description: descr,
|
Description: descr,
|
||||||
KVer: Merge(kvs...),
|
Contexts: ctxs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a DebugLevel message, merging the KVers together first
|
// Debug logs a DebugLevel message.
|
||||||
func (l *Logger) Debug(ctx context.Context, descr string, kvs ...KVer) {
|
func (l *Logger) Debug(descr string, ctxs ...context.Context) {
|
||||||
l.Log(mkMsg(ctx, DebugLevel, descr, kvs...))
|
l.Log(mkMsg(DebugLevel, descr, ctxs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a InfoLevel message, merging the KVers together first
|
// Info logs a InfoLevel message.
|
||||||
func (l *Logger) Info(ctx context.Context, descr string, kvs ...KVer) {
|
func (l *Logger) Info(descr string, ctxs ...context.Context) {
|
||||||
l.Log(mkMsg(ctx, InfoLevel, descr, kvs...))
|
l.Log(mkMsg(InfoLevel, descr, ctxs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a WarnLevel message, merging the KVers together first
|
// Warn logs a WarnLevel message.
|
||||||
func (l *Logger) Warn(ctx context.Context, descr string, kvs ...KVer) {
|
func (l *Logger) Warn(descr string, ctxs ...context.Context) {
|
||||||
l.Log(mkMsg(ctx, WarnLevel, descr, kvs...))
|
l.Log(mkMsg(WarnLevel, descr, ctxs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a ErrorLevel message, merging the KVers together first
|
// Error logs a ErrorLevel message.
|
||||||
func (l *Logger) Error(ctx context.Context, descr string, kvs ...KVer) {
|
func (l *Logger) Error(descr string, ctxs ...context.Context) {
|
||||||
l.Log(mkMsg(ctx, ErrorLevel, descr, kvs...))
|
l.Log(mkMsg(ErrorLevel, descr, ctxs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a FatalLevel message, merging the KVers together first. A Fatal
|
// Fatal logs a FatalLevel message. A Fatal message automatically stops the
|
||||||
// message automatically stops the process with an os.Exit(1)
|
// process with an os.Exit(1)
|
||||||
func (l *Logger) Fatal(ctx context.Context, descr string, kvs ...KVer) {
|
func (l *Logger) Fatal(descr string, ctxs ...context.Context) {
|
||||||
l.Log(mkMsg(ctx, FatalLevel, descr, kvs...))
|
l.Log(mkMsg(FatalLevel, descr, ctxs...))
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,11 @@ package mlog
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
"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) {
|
func TestLogger(t *T) {
|
||||||
ctx := context.Background()
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
h := func(msg Message) error {
|
h := defaultHandler(buf)
|
||||||
return DefaultFormat(buf, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
l := NewLogger()
|
l := NewLogger()
|
||||||
l.SetHandler(h)
|
l.SetHandler(h)
|
||||||
@ -56,29 +36,31 @@ func TestLogger(t *T) {
|
|||||||
out, err := buf.ReadString('\n')
|
out, err := buf.ReadString('\n')
|
||||||
return massert.All(
|
return massert.All(
|
||||||
massert.Nil(err),
|
massert.Nil(err),
|
||||||
massert.Equal(expected, out),
|
massert.Equal(expected, strings.TrimSpace(out)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default max level should be INFO
|
// Default max level should be INFO
|
||||||
l.Debug(ctx, "foo")
|
l.Debug("foo")
|
||||||
l.Info(ctx, "bar")
|
l.Info("bar")
|
||||||
l.Warn(ctx, "baz")
|
l.Warn("baz")
|
||||||
l.Error(ctx, "buz")
|
l.Error("buz")
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ INFO -- bar\n"),
|
assertOut(`{"level":"INFO","descr":"bar"}`),
|
||||||
assertOut("~ WARN -- baz\n"),
|
assertOut(`{"level":"WARN","descr":"baz"}`),
|
||||||
assertOut("~ ERROR -- buz\n"),
|
assertOut(`{"level":"ERROR","descr":"buz"}`),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
l.SetMaxLevel(WarnLevel)
|
l.SetMaxLevel(WarnLevel)
|
||||||
l.Debug(ctx, "foo")
|
l.Debug("foo")
|
||||||
l.Info(ctx, "bar")
|
l.Info("bar")
|
||||||
l.Warn(ctx, "baz")
|
l.Warn("baz")
|
||||||
l.Error(ctx, "buz", KV{"a": "b"})
|
l.Error("buz", mctx.Annotate(ctx, "a", "b", "c", "d"))
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ WARN -- baz\n"),
|
assertOut(`{"level":"WARN","descr":"baz"}`),
|
||||||
assertOut("~ ERROR -- buz -- a=\"b\"\n"),
|
assertOut(`{"level":"ERROR","descr":"buz","annotations":{"/":{"a":"b","c":"d"}}}`),
|
||||||
))
|
))
|
||||||
|
|
||||||
l2 := l.Clone()
|
l2 := l.Clone()
|
||||||
@ -87,109 +69,12 @@ func TestLogger(t *T) {
|
|||||||
msg.Description = strings.ToUpper(msg.Description)
|
msg.Description = strings.ToUpper(msg.Description)
|
||||||
return h(msg)
|
return h(msg)
|
||||||
})
|
})
|
||||||
l2.Info(ctx, "bar")
|
l2.Info("bar")
|
||||||
l2.Warn(ctx, "baz")
|
l2.Warn("baz")
|
||||||
l.Error(ctx, "buz")
|
l.Error("buz")
|
||||||
massert.Fatal(t, massert.All(
|
massert.Fatal(t, massert.All(
|
||||||
assertOut("~ INFO -- BAR\n"),
|
assertOut(`{"level":"INFO","descr":"BAR"}`),
|
||||||
assertOut("~ WARN -- BAZ\n"),
|
assertOut(`{"level":"WARN","descr":"BAZ"}`),
|
||||||
assertOut("~ ERROR -- buz\n"),
|
assertOut(`{"level":"ERROR","descr":"buz"}`),
|
||||||
))
|
|
||||||
|
|
||||||
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()),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
55
mnet/mnet.go
55
mnet/mnet.go
@ -8,13 +8,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mcfg"
|
"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/mlog"
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
"github.com/mediocregopher/mediocre-go-lib/mrun"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MListener is returned by MListen and simply wraps a net.Listener.
|
// Listener is returned by WithListen and simply wraps a net.Listener.
|
||||||
type MListener struct {
|
type Listener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
@ -23,13 +23,13 @@ type MListener struct {
|
|||||||
NoCloseOnStop bool
|
NoCloseOnStop bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MListen returns an MListener which will be initialized when the start event
|
// WithListener returns a Listener which will be initialized when the start
|
||||||
// is triggered on the returned Context (see mrun.Start), and closed when the
|
// event is triggered on the returned Context (see mrun.Start), and closed when
|
||||||
// stop event is triggered on the returned Context (see mrun.Stop).
|
// the stop event is triggered on the returned Context (see mrun.Stop).
|
||||||
//
|
//
|
||||||
// network defaults to "tcp" if empty. defaultAddr defaults to ":0" if empty,
|
// network defaults to "tcp" if empty. defaultAddr defaults to ":0" if empty,
|
||||||
// and will be configurable via mcfg.
|
// 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 == "" {
|
if network == "" {
|
||||||
network = "tcp"
|
network = "tcp"
|
||||||
}
|
}
|
||||||
@ -37,59 +37,52 @@ func MListen(ctx context.Context, network, defaultAddr string) (context.Context,
|
|||||||
defaultAddr = ":0"
|
defaultAddr = ":0"
|
||||||
}
|
}
|
||||||
|
|
||||||
l := new(MListener)
|
l := &Listener{
|
||||||
|
ctx: mctx.NewChild(ctx, "net"),
|
||||||
|
}
|
||||||
|
|
||||||
// TODO the equivalent functionality as here will be added with annotations
|
var addr *string
|
||||||
//l.log = mlog.From(ctx)
|
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.log.SetKV(l)
|
l.ctx = mrun.WithStartHook(l.ctx, func(context.Context) error {
|
||||||
|
|
||||||
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 err error
|
var err error
|
||||||
if l.Listener, err = net.Listen(network, *addr); err != nil {
|
if l.Listener, err = net.Listen(network, *addr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mlog.Info(l.ctx, "listening")
|
l.ctx = mctx.Annotate(l.ctx, "addr", l.Addr().String())
|
||||||
|
mlog.Info("listening", l.ctx)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO track connections and wait for them to complete before shutting
|
// TODO track connections and wait for them to complete before shutting
|
||||||
// down?
|
// down?
|
||||||
ctx = mrun.OnStop(ctx, func(context.Context) error {
|
l.ctx = mrun.WithStopHook(l.ctx, func(context.Context) error {
|
||||||
if l.NoCloseOnStop {
|
if l.NoCloseOnStop {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
mlog.Info(l.ctx, "stopping listener")
|
mlog.Info("stopping listener", l.ctx)
|
||||||
return l.Close()
|
return l.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
l.ctx = ctx
|
return mctx.WithChild(ctx, l.ctx), l
|
||||||
return ctx, l
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept wraps a call to Accept on the underlying net.Listener, providing debug
|
// Accept wraps a call to Accept on the underlying net.Listener, providing debug
|
||||||
// logging.
|
// logging.
|
||||||
func (l *MListener) Accept() (net.Conn, error) {
|
func (l *Listener) Accept() (net.Conn, error) {
|
||||||
conn, err := l.Listener.Accept()
|
conn, err := l.Listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conn, err
|
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
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close wraps a call to Close on the underlying net.Listener, providing debug
|
// Close wraps a call to Close on the underlying net.Listener, providing debug
|
||||||
// logging.
|
// logging.
|
||||||
func (l *MListener) Close() error {
|
func (l *Listener) Close() error {
|
||||||
mlog.Debug(l.ctx, "listener closing")
|
mlog.Info("listener closing", l.ctx)
|
||||||
err := l.Listener.Close()
|
return 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()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -35,9 +35,9 @@ func TestIsReservedIP(t *T) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMListen(t *T) {
|
func TestWithListener(t *T) {
|
||||||
ctx := mtest.NewCtx()
|
ctx := mtest.Context()
|
||||||
ctx, l := MListen(ctx, "", "")
|
ctx, l := WithListener(ctx, "", "")
|
||||||
mtest.Run(ctx, t, func() {
|
mtest.Run(ctx, t, func() {
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := net.Dial("tcp", l.Addr().String())
|
conn, err := net.Dial("tcp", l.Addr().String())
|
||||||
|
44
mrun/hook.go
44
mrun/hook.go
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Hook describes a function which can be registered to trigger on an event via
|
// 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 Hook func(context.Context) error
|
||||||
|
|
||||||
type ctxKey int
|
type ctxKey int
|
||||||
@ -65,12 +65,12 @@ func getHookEls(ctx context.Context, userKey interface{}) ([]hookEl, int, int) {
|
|||||||
return hookEls, len(children), lastNumHooks
|
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
|
// TriggerHooks is called with that same key. Multiple Hooks can be registered
|
||||||
// for the same key, and will be called sequentially when triggered.
|
// for the same key, and will be called sequentially when triggered.
|
||||||
//
|
//
|
||||||
// Hooks will be called with whatever Context is passed into TriggerHooks.
|
// 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, numChildren, numHooks := getHookEls(ctx, key)
|
||||||
hookEls = append(hookEls, hookEl{hook: hook})
|
hookEls = append(hookEls, hookEl{hook: hook})
|
||||||
|
|
||||||
@ -100,9 +100,9 @@ func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TriggerHooks causes all Hooks registered with RegisterHook on the Context
|
// TriggerHooks causes all Hooks registered with WithHook on the Context (and
|
||||||
// (and its predecessors) under the given key to be called in the order they
|
// its predecessors) under the given key to be called in the order they were
|
||||||
// were registered.
|
// registered.
|
||||||
//
|
//
|
||||||
// If any Hook returns an error no further Hooks will be called and that error
|
// If any Hook returns an error no further Hooks will be called and that error
|
||||||
// will be returned.
|
// will be returned.
|
||||||
@ -113,15 +113,15 @@ func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl)
|
|||||||
//
|
//
|
||||||
// // parent context has hookA registered
|
// // parent context has hookA registered
|
||||||
// ctx := context.Background()
|
// ctx := context.Background()
|
||||||
// ctx = RegisterHook(ctx, 0, hookA)
|
// ctx = WithHook(ctx, 0, hookA)
|
||||||
//
|
//
|
||||||
// // child context has hookB registered
|
// // child context has hookB registered
|
||||||
// childCtx := mctx.NewChild(ctx, "child")
|
// 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
|
// ctx = mctx.WithChild(ctx, childCtx) // needed to link childCtx to ctx
|
||||||
//
|
//
|
||||||
// // parent context has another Hook, hookC, registered
|
// // 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
|
// // The Hooks will be triggered in the order: hookA, hookB, then hookC
|
||||||
// err := TriggerHooks(ctx, 0)
|
// err := TriggerHooks(ctx, 0)
|
||||||
@ -148,33 +148,33 @@ const (
|
|||||||
stop
|
stop
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnStart registers the given Hook to run when Start is called. This is a
|
// WithStartHook registers the given Hook to run when Start is called. This is a
|
||||||
// special case of RegisterHook.
|
// special case of WithHook.
|
||||||
//
|
//
|
||||||
// As a convention Hooks running on the start event should block only as long as
|
// 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
|
// 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
|
// 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
|
// 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
|
// go-routine to do their actual work. Long-lived tasks should set themselves up
|
||||||
// to stop on the stop event (see OnStop).
|
// to stop on the stop event (see WithStopHook).
|
||||||
func OnStart(ctx context.Context, hook Hook) context.Context {
|
func WithStartHook(ctx context.Context, hook Hook) context.Context {
|
||||||
return RegisterHook(ctx, start, hook)
|
return WithHook(ctx, start, hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start runs all Hooks registered using OnStart. This is a special case of
|
// Start runs all Hooks registered using WithStartHook. This is a special case
|
||||||
// TriggerHooks.
|
// of TriggerHooks.
|
||||||
func Start(ctx context.Context) error {
|
func Start(ctx context.Context) error {
|
||||||
return TriggerHooks(ctx, start)
|
return TriggerHooks(ctx, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnStop registers the given Hook to run when Stop is called. This is a special
|
// WithStopHook registers the given Hook to run when Stop is called. This is a
|
||||||
// case of RegisterHook.
|
// special case of WithHook.
|
||||||
func OnStop(ctx context.Context, hook Hook) context.Context {
|
func WithStopHook(ctx context.Context, hook Hook) context.Context {
|
||||||
return RegisterHook(ctx, stop, hook)
|
return WithHook(ctx, stop, hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop runs all Hooks registered using OnStop in the reverse order in which
|
// Stop runs all Hooks registered using WithStopHook in the reverse order in
|
||||||
// they were registered. This is a special case of TriggerHooks.
|
// which they were registered. This is a special case of TriggerHooks.
|
||||||
func Stop(ctx context.Context) error {
|
func Stop(ctx context.Context) error {
|
||||||
return TriggerHooksReverse(ctx, stop)
|
return TriggerHooksReverse(ctx, stop)
|
||||||
}
|
}
|
||||||
|
@ -18,20 +18,20 @@ func TestHooks(t *T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = RegisterHook(ctx, 0, mkHook(1))
|
ctx = WithHook(ctx, 0, mkHook(1))
|
||||||
ctx = RegisterHook(ctx, 0, mkHook(2))
|
ctx = WithHook(ctx, 0, mkHook(2))
|
||||||
|
|
||||||
ctxA := mctx.NewChild(ctx, "a")
|
ctxA := mctx.NewChild(ctx, "a")
|
||||||
ctxA = RegisterHook(ctxA, 0, mkHook(3))
|
ctxA = WithHook(ctxA, 0, mkHook(3))
|
||||||
ctxA = RegisterHook(ctxA, 999, mkHook(999)) // different key
|
ctxA = WithHook(ctxA, 999, mkHook(999)) // different key
|
||||||
ctx = mctx.WithChild(ctx, ctxA)
|
ctx = mctx.WithChild(ctx, ctxA)
|
||||||
|
|
||||||
ctx = RegisterHook(ctx, 0, mkHook(4))
|
ctx = WithHook(ctx, 0, mkHook(4))
|
||||||
|
|
||||||
ctxB := mctx.NewChild(ctx, "b")
|
ctxB := mctx.NewChild(ctx, "b")
|
||||||
ctxB = RegisterHook(ctxB, 0, mkHook(5))
|
ctxB = WithHook(ctxB, 0, mkHook(5))
|
||||||
ctxB1 := mctx.NewChild(ctxB, "1")
|
ctxB1 := mctx.NewChild(ctxB, "1")
|
||||||
ctxB1 = RegisterHook(ctxB1, 0, mkHook(6))
|
ctxB1 = WithHook(ctxB1, 0, mkHook(6))
|
||||||
ctxB = mctx.WithChild(ctxB, ctxB1)
|
ctxB = mctx.WithChild(ctxB, ctxB1)
|
||||||
ctx = mctx.WithChild(ctx, ctxB)
|
ctx = mctx.WithChild(ctx, ctxB)
|
||||||
|
|
||||||
|
@ -36,10 +36,10 @@ func (fe *futureErr) set(err error) {
|
|||||||
|
|
||||||
type threadCtxKey int
|
type threadCtxKey int
|
||||||
|
|
||||||
// Thread spawns a go-routine which executes the given function. The returned
|
// WithThread spawns a go-routine which executes the given function. The
|
||||||
// Context tracks this go-routine, which can then be passed into the Wait
|
// returned Context tracks this go-routine, which can then be passed into the
|
||||||
// function to block until the spawned go-routine returns.
|
// Wait function to block until the spawned go-routine returns.
|
||||||
func Thread(ctx context.Context, fn func() error) context.Context {
|
func WithThread(ctx context.Context, fn func() error) context.Context {
|
||||||
futErr := newFutureErr()
|
futErr := newFutureErr()
|
||||||
oldFutErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr)
|
oldFutErrs, _ := ctx.Value(threadCtxKey(0)).([]*futureErr)
|
||||||
futErrs := make([]*futureErr, len(oldFutErrs), len(oldFutErrs)+1)
|
futErrs := make([]*futureErr, len(oldFutErrs), len(oldFutErrs)+1)
|
||||||
|
@ -30,7 +30,7 @@ func TestThreadWait(t *T) {
|
|||||||
t.Run("noBlock", func(t *T) {
|
t.Run("noBlock", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = Thread(ctx, func() error { return nil })
|
ctx = WithThread(ctx, func() error { return nil })
|
||||||
if err := Wait(ctx, nil); err != nil {
|
if err := Wait(ctx, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = Thread(ctx, func() error { return testErr })
|
ctx = WithThread(ctx, func() error { return testErr })
|
||||||
if err := Wait(ctx, nil); err != testErr {
|
if err := Wait(ctx, nil); err != testErr {
|
||||||
t.Fatalf("should have got test error, got: %v", err)
|
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("block", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = Thread(ctx, func() error {
|
ctx = WithThread(ctx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -59,7 +59,7 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = Thread(ctx, func() error {
|
ctx = WithThread(ctx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
@ -70,7 +70,7 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("canceled", func(t *T) {
|
t.Run("canceled", func(t *T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = Thread(ctx, func() error {
|
ctx = WithThread(ctx, func() error {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
@ -90,7 +90,7 @@ func TestThreadWait(t *T) {
|
|||||||
t.Run("noBlock", func(t *T) {
|
t.Run("noBlock", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
childCtx = Thread(childCtx, func() error { return nil })
|
childCtx = WithThread(childCtx, func() error { return nil })
|
||||||
ctx = mctx.WithChild(ctx, childCtx)
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := Wait(ctx, nil); err != nil {
|
if err := Wait(ctx, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -99,7 +99,7 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
childCtx = Thread(childCtx, func() error { return testErr })
|
childCtx = WithThread(childCtx, func() error { return testErr })
|
||||||
ctx = mctx.WithChild(ctx, childCtx)
|
ctx = mctx.WithChild(ctx, childCtx)
|
||||||
if err := Wait(ctx, nil); err != testErr {
|
if err := Wait(ctx, nil); err != testErr {
|
||||||
t.Fatalf("should have got test error, got: %v", err)
|
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("block", func(t *T) {
|
||||||
t.Run("noErr", func(t *T) {
|
t.Run("noErr", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
childCtx = Thread(childCtx, func() error {
|
childCtx = WithThread(childCtx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -122,7 +122,7 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("err", func(t *T) {
|
t.Run("err", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
childCtx = Thread(childCtx, func() error {
|
childCtx = WithThread(childCtx, func() error {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
@ -134,7 +134,7 @@ func TestThreadWait(t *T) {
|
|||||||
|
|
||||||
t.Run("canceled", func(t *T) {
|
t.Run("canceled", func(t *T) {
|
||||||
ctx, childCtx := ctxWithChild()
|
ctx, childCtx := ctxWithChild()
|
||||||
childCtx = Thread(childCtx, func() error {
|
childCtx = WithThread(childCtx, func() error {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
return testErr
|
return testErr
|
||||||
})
|
})
|
||||||
|
@ -12,18 +12,18 @@ import (
|
|||||||
|
|
||||||
type envCtxKey int
|
type envCtxKey int
|
||||||
|
|
||||||
// NewCtx creates and returns a root Context suitable for testing.
|
// Context creates and returns a root Context suitable for testing.
|
||||||
func NewCtx() context.Context {
|
func Context() context.Context {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
logger := mlog.NewLogger()
|
logger := mlog.NewLogger()
|
||||||
logger.SetMaxLevel(mlog.DebugLevel)
|
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
|
// WithEnv sets the given environment variable on the given Context, such that
|
||||||
// will be used as if it was a real environment variable when the Run function
|
// it will be used as if it was a real environment variable when the Run
|
||||||
// from this package is called.
|
// function from this package is called.
|
||||||
func SetEnv(ctx context.Context, key, val string) context.Context {
|
func WithEnv(ctx context.Context, key, val string) context.Context {
|
||||||
prevEnv, _ := ctx.Value(envCtxKey(0)).([][2]string)
|
prevEnv, _ := ctx.Value(envCtxKey(0)).([][2]string)
|
||||||
env := make([][2]string, len(prevEnv), len(prevEnv)+1)
|
env := make([][2]string, len(prevEnv), len(prevEnv)+1)
|
||||||
copy(env, prevEnv)
|
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:
|
// 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
|
// - Calls mrun.Start
|
||||||
//
|
//
|
||||||
@ -42,7 +42,7 @@ func SetEnv(ctx context.Context, key, val string) context.Context {
|
|||||||
// - Calls mrun.Stop
|
// - Calls mrun.Stop
|
||||||
//
|
//
|
||||||
// The intention is that Run is used within a test on a Context created via
|
// 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()) {
|
func Run(ctx context.Context, t *testing.T, body func()) {
|
||||||
envTups, _ := ctx.Value(envCtxKey(0)).([][2]string)
|
envTups, _ := ctx.Value(envCtxKey(0)).([][2]string)
|
||||||
env := make([]string, 0, len(envTups))
|
env := make([]string, 0, len(envTups))
|
||||||
|
@ -7,9 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestRun(t *T) {
|
func TestRun(t *T) {
|
||||||
ctx := NewCtx()
|
ctx := Context()
|
||||||
ctx, arg := mcfg.RequiredString(ctx, "arg", "Required by this test")
|
ctx, arg := mcfg.WithRequiredString(ctx, "arg", "Required by this test")
|
||||||
ctx = SetEnv(ctx, "ARG", "foo")
|
ctx = WithEnv(ctx, "ARG", "foo")
|
||||||
Run(ctx, t, func() {
|
Run(ctx, t, func() {
|
||||||
if *arg != "foo" {
|
if *arg != "foo" {
|
||||||
t.Fatalf(`arg not set to "foo", is set to %q`, *arg)
|
t.Fatalf(`arg not set to "foo", is set to %q`, *arg)
|
||||||
|
Loading…
Reference in New Issue
Block a user