diff --git a/m/m.go b/m/m.go index 26d89e9..6674df1 100644 --- a/m/m.go +++ b/m/m.go @@ -1,130 +1,151 @@ -// Package m is the glue which holds all the other packages in this project -// together. While other packages in this project are intended to be able to be -// used separately and largely independently, this package combines them in ways -// which I specifically like. +// Package m implements functionality specific to how I like my programs to +// work. It acts as glue between many of the other packages in this framework, +// putting them together in the way I find most useful. package m import ( "context" "os" "os/signal" + "time" "github.com/mediocregopher/mediocre-go-lib/mcfg" + "github.com/mediocregopher/mediocre-go-lib/mcmp" "github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/merr" "github.com/mediocregopher/mediocre-go-lib/mlog" "github.com/mediocregopher/mediocre-go-lib/mrun" ) -type ctxKey int +type cmpKey int const ( - ctxKeyCfgSrc ctxKey = iota - ctxKeyInfoLog + cmpKeyCfgSrc cmpKey = iota + cmpKeyInfoLog ) -func debugLog(msg string, ctxs ...context.Context) { - fn := mlog.Debug +func debugLog(cmp *mcmp.Component, msg string, ctxs ...context.Context) { + level := mlog.DebugLevel if len(ctxs) > 0 { - if ok, _ := ctxs[0].Value(ctxKeyInfoLog).(bool); ok { - fn = mlog.Info + if ok, _ := ctxs[0].Value(cmpKeyInfoLog).(bool); ok { + level = mlog.InfoLevel } } - fn(msg, ctxs...) + + mlog.From(cmp).Log(mlog.Message{ + Level: level, + Description: msg, + Contexts: ctxs, + }) } -// ProcessContext returns a Context which should be used as the root Context -// when implementing most processes. +// RootComponent returns a Component which should be used as the root Component +// when implementing most programs. // -// The returned Context will automatically handle setting up global -// configuration parameters like "log-level", as well as an http endpoint where -// debug information about the running process can be accessed. -func ProcessContext() context.Context { - ctx := context.Background() +// The returned Component will automatically handle setting up global +// configuration parameters like "log-level", as well as parsing those +// and all other parameters when the Init even is triggered on it. +func RootComponent() *mcmp.Component { + cmp := new(mcmp.Component) // embed confuration source which should be used into the context. - ctx = context.WithValue(ctx, ctxKeyCfgSrc, mcfg.Source(new(mcfg.SourceCLI))) + cmp.SetValue(cmpKeyCfgSrc, mcfg.Source(new(mcfg.SourceCLI))) // set up log level handling logger := mlog.NewLogger() - ctx = mlog.WithLogger(ctx, logger) - ctx, logLevelStr := mcfg.WithString(ctx, "log-level", "info", "Maximum log level which will be printed.") - ctx = mrun.WithStartHook(ctx, func(context.Context) error { + mlog.SetLogger(cmp, logger) + + // set up parameter parsing + mrun.InitHook(cmp, func(context.Context) error { + src, _ := cmp.Value(cmpKeyCfgSrc).(mcfg.Source) + if src == nil { + return merr.New("Component not sourced from m package", cmp.Context()) + } else if err := mcfg.Populate(cmp, src); err != nil { + return merr.Wrap(err, cmp.Context()) + } + return nil + }) + + logLevelStr := mcfg.String(cmp, "log-level", + mcfg.ParamDefault("info"), + mcfg.ParamUsage("Maximum log level which will be printed.")) + mrun.InitHook(cmp, func(context.Context) error { logLevel := mlog.LevelFromString(*logLevelStr) if logLevel == nil { - ctx := mctx.Annotate(ctx, "log-level", *logLevelStr) - return merr.New("invalid log level", ctx) + return merr.New("invalid log level", cmp.Context(), + mctx.Annotated("log-level", *logLevelStr)) } logger.SetMaxLevel(logLevel) return nil }) - return ctx + return cmp } -// ServiceContext extends ProcessContext so that it better supports long running -// processes which are expected to handle requests from outside clients. +// RootServiceComponent extends RootComponent so that it better supports long +// running processes which are expected to handle requests from outside clients. // // Additional behavior it adds includes setting up an http endpoint where debug // information about the running process can be accessed. -func ServiceContext() context.Context { - ctx := ProcessContext() +func RootServiceComponent() *mcmp.Component { + cmp := RootComponent() // services expect to use many different configuration sources - ctx = context.WithValue(ctx, ctxKeyCfgSrc, mcfg.Source(mcfg.Sources{ + cmp.SetValue(cmpKeyCfgSrc, mcfg.Source(mcfg.Sources{ new(mcfg.SourceEnv), new(mcfg.SourceCLI), })) // it's useful to show debug entries (from this package specifically) as // info logs for long-running services. - ctx = context.WithValue(ctx, ctxKeyInfoLog, true) + cmp.SetValue(cmpKeyInfoLog, true) // TODO set up the debug endpoint. - return ctx + return cmp } -// Start performs the work of populating configuration parameters and triggering -// the start event. It will return once the Start event has completed running. -// -// This function returns a Context because there are cases where the Context -// will be modified during Start, such as if WithSubCommand was used. If the -// Context was not modified then the passed in Context will be returned. -func Start(ctx context.Context) context.Context { - src, _ := ctx.Value(ctxKeyCfgSrc).(mcfg.Source) - if src == nil { - mlog.Fatal("ctx not sourced from m package", ctx) - } +// MustInit will call mrun.Init on the given Component, which must have been +// created in this package, and exit the process if mrun.Init does not complete +// successfully. +func MustInit(cmp *mcmp.Component) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - // no logging should happen before populate, primarily because log-level - // hasn't been populated yet, but also because it makes help output on cli - // look weird. - ctx, err := mcfg.Populate(ctx, src) - if err != nil { - mlog.Fatal("error populating configuration", ctx, merr.Context(err)) - } else if err := mrun.Start(ctx); err != nil { - mlog.Fatal("error triggering start event", ctx, merr.Context(err)) + debugLog(cmp, "initializing") + if err := mrun.Init(ctx, cmp); err != nil { + mlog.From(cmp).Fatal("initialization failed", merr.Context(err)) } - debugLog("start hooks completed", ctx) - return ctx + debugLog(cmp, "initialization completed successfully") } -// StartWaitStop performs the work of populating configuration parameters, -// triggering the start event, waiting for an interrupt, and then triggering the -// stop event. Run will block until the stop event is done. If any errors are -// encountered a fatal is thrown. -func StartWaitStop(ctx context.Context) { - ctx = Start(ctx) +// MustShutdown is like MustInit, except that it triggers the Shutdown event on +// the Component. +func MustShutdown(cmp *mcmp.Component) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + debugLog(cmp, "shutting down") + if err := mrun.Shutdown(ctx, cmp); err != nil { + mlog.From(cmp).Fatal("shutdown failed", merr.Context(err)) + } + debugLog(cmp, "shutting down completed successfully") +} + +// Exec calls MustInit on the given Component, then blocks until an interrupt +// signal is received, then calls MustShutdown on the Component, until finally +// exiting the process. +func Exec(cmp *mcmp.Component) { + MustInit(cmp) { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) s := <-ch - debugLog("signal received, stopping", mctx.Annotate(ctx, "signal", s)) + debugLog(cmp, "signal received, stopping", mctx.Annotated("signal", s)) } + MustShutdown(cmp) - if err := mrun.Stop(ctx); err != nil { - mlog.Fatal("error triggering stop event", ctx, merr.Context(err)) - } - debugLog("exiting process", ctx) + debugLog(cmp, "exiting process") + os.Stdout.Sync() + os.Stderr.Sync() + os.Exit(0) } diff --git a/m/m_test.go b/m/m_test.go index e15d508..b59e8f5 100644 --- a/m/m_test.go +++ b/m/m_test.go @@ -6,47 +6,42 @@ import ( . "testing" "github.com/mediocregopher/mediocre-go-lib/mcfg" - "github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mlog" - "github.com/mediocregopher/mediocre-go-lib/mrun" "github.com/mediocregopher/mediocre-go-lib/mtest/massert" ) func TestServiceCtx(t *T) { t.Run("log-level", func(t *T) { - ctx := ServiceContext() + cmp := RootComponent() - // pull the Logger out of the ctx and set the Handler on it, so we can check - // the log level + // pull the Logger out of the component and set the Handler on it, so we + // can check the log level var msgs []mlog.Message - logger := mlog.From(ctx) + logger := mlog.GetLogger(cmp) logger.SetHandler(func(msg mlog.Message) error { msgs = append(msgs, msg) return nil }) + mlog.SetLogger(cmp, logger) - // create a child Context before running to ensure it the change propagates + // create a child Component before running to ensure it the change propagates // correctly. - ctxA := mctx.NewChild(ctx, "A") - ctx = mctx.WithChild(ctx, ctxA) + cmpA := cmp.Child("A") params := mcfg.ParamValues{{Name: "log-level", Value: json.RawMessage(`"DEBUG"`)}} - if _, err := mcfg.Populate(ctx, params); err != nil { - t.Fatal(err) - } else if err := mrun.Start(ctx); err != nil { - t.Fatal(err) - } + cmp.SetValue(cmpKeyCfgSrc, params) + MustInit(cmp) - mlog.From(ctxA).Info("foo", ctxA) - mlog.From(ctxA).Debug("bar", ctxA) + mlog.From(cmpA).Info("foo") + mlog.From(cmpA).Debug("bar") massert.Require(t, massert.Length(msgs, 2), massert.Equal(msgs[0].Level.String(), "INFO"), massert.Equal(msgs[0].Description, "foo"), - massert.Equal(msgs[0].Contexts, []context.Context{ctxA}), + massert.Equal(msgs[0].Contexts, []context.Context{cmpA.Context()}), massert.Equal(msgs[1].Level.String(), "DEBUG"), massert.Equal(msgs[1].Description, "bar"), - massert.Equal(msgs[1].Contexts, []context.Context{ctxA}), + massert.Equal(msgs[1].Contexts, []context.Context{cmpA.Context()}), ) }) } diff --git a/mlog/cmp.go b/mlog/cmp.go index dcebdd6..302e232 100644 --- a/mlog/cmp.go +++ b/mlog/cmp.go @@ -11,31 +11,45 @@ type cmpKey int // retrieved from the Component, or any of its children, using From. func SetLogger(cmp *mcmp.Component, l *Logger) { cmp.SetValue(cmpKey(0), l) + + // If the base Logger on this Component gets changed, then the cached Logger + // from From on this Component, and all of its Children, ought to be reset, + // so that any changes can be reflected in their loggers. + var resetFromLogger func(*mcmp.Component) + resetFromLogger = func(cmp *mcmp.Component) { + cmp.SetValue(cmpKey(1), nil) + for _, childCmp := range cmp.Children() { + resetFromLogger(childCmp) + } + } + resetFromLogger(cmp) } // DefaultLogger is an instance of Logger which is returned by From when a // Logger hasn't been previously set with SetLogger on the passed in Component. var DefaultLogger = NewLogger() -// From returns the Logger which was set on the Component, or one of its +// GetLogger returns the Logger which was set on the Component, or on of its // ancestors, using SetLogger. If no Logger was ever set then DefaultLogger is // returned. -// -// The returned Logger will be modified such that it will implicitly merge the -// Contexts of any Message into the given Component's Context. +func GetLogger(cmp *mcmp.Component) *Logger { + if l, ok := cmp.InheritedValue(cmpKey(0)); ok { + return l.(*Logger) + } + return DefaultLogger +} + +// From returns the result from GetLogger, modified so as to automatically add +// some annotations related to the Component itself to all Messages being +// logged. func From(cmp *mcmp.Component) *Logger { - var l *Logger - if l, _ = cmp.Value(cmpKey(1)).(*Logger); l != nil { + if l, _ := cmp.Value(cmpKey(1)).(*Logger); l != nil { return l - } else if lInt, ok := cmp.InheritedValue(cmpKey(0)); ok { - l = lInt.(*Logger) - } else { - l = DefaultLogger } // if we're here it means a modified Logger wasn't set on this particular // Component, and therefore the current one must be modified. - l = l.Clone() + l := GetLogger(cmp).Clone() oldHandler := l.Handler() l.SetHandler(func(msg Message) error { ctx := mctx.MergeAnnotationsInto(cmp.Context(), msg.Contexts...)