From 4d3dfb2c15fdad3600d533653a14abd8e57365de Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Thu, 13 Feb 2014 15:08:22 +0100 Subject: [PATCH] Prepare for public release. --- .gitignore | 4 + AUTHORS | 6 + LICENSE | 29 +++++ debian/changelog | 43 ++++++++ debian/compat | 1 + debian/control | 21 ++++ debian/copyright | 35 ++++++ debian/install | 1 + debian/lintian-overrides | 3 + debian/rules | 16 +++ debian/source/format | 1 + doc.go | 9 ++ logging.go | 53 +++++++++ runtime.go | 186 +++++++++++++++++++++++++++++++ runtime_test.go | 39 +++++++ server.go | 233 +++++++++++++++++++++++++++++++++++++++ 16 files changed, 680 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/install create mode 100644 debian/lintian-overrides create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 doc.go create mode 100644 logging.go create mode 100644 runtime.go create mode 100644 runtime_test.go create mode 100644 server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2fa8a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*~ +debian/files +debian/tmp +debian/golang-phoenix-dev* \ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..1040d00 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +# Names should be added to this file like so: +# Name +# +# Please keep the list sorted. + +Lance Cooper diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7619fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2014, struktur AG +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..faae4a9 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,43 @@ +phoenix (0.1.0) precise; urgency=low + + * Initial public release. + + -- Simon Eisenmann Thu, 13 Feb 2014 14:43:07 +0100 + +phoenix (0.0.6) precise; urgency=low + + * Fix handling of panics during server startup. + * Add support for setting configuration defaults prior to + server startup. + + -- Lance Cooper Thu, 02 Jan 2014 17:06:25 -0600 + +phoenix (0.0.5) precise; urgency=low + + * Fix exception on startup when no configfile was specified. + + -- Lance Cooper Mon, 21 Oct 2013 14:58:05 -0500 + +phoenix (0.0.4) precise; urgency=low + + * Fix build and installed dependencies. + + -- Lance Cooper Wed, 16 Oct 2013 11:28:11 -0500 + +phoenix (0.0.3) precise; urgency=low + + * Add ability to fetch app name and version strings. + + -- Lance Cooper Wed, 02 Oct 2013 11:34:43 +0200 + +phoenix (0.0.2) precise; urgency=low + + * Breaking change: fix blocking portion of api. + + -- Lance Cooper Fri, 27 Sep 2013 15:23:40 +0200 + +phoenix (0.0.1) precise; urgency=low + + * Initial release. + + -- Lance Cooper Fri, 27 Sep 2013 13:27:17 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..45a4fb7 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +8 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..63a2a1f --- /dev/null +++ b/debian/control @@ -0,0 +1,21 @@ +Source: phoenix +Section: golang +Priority: extra +Maintainer: Lance Cooper +Build-Depends: debhelper (>= 8), + dh-golang, + golang-go (>= 1.1), + golang-goconf-dev, + golang-httputils-dev +Standards-Version: 3.9.3 + +Package: golang-phoenix-dev +Pre-Depends: dpkg (>= 1.15.6~) +Architecture: all +Depends: ${shlibs:Depends}, + ${misc:Depends}, + golang-goconf-dev, + golang-httputils-dev +Description: phoenix + is a Golang library providing support functionality for application startup, + configuration, logging, and profiling. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..9a854e7 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,35 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: phoenix +Source: http://github.com/strukturag/phoenix + +Files: * +Copyright: Copyright (c) 2014, struktur AG +License: BSD-3-clause + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..3e409b1 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +usr/share/gocode/src diff --git a/debian/lintian-overrides b/debian/lintian-overrides new file mode 100644 index 0000000..f5f32d7 --- /dev/null +++ b/debian/lintian-overrides @@ -0,0 +1,3 @@ +# Lintian doesn't know aboug section golang yet, +# but this is fine since it's in debian and ubuntu next. +golang-phoenix-dev: unknown-section golang diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..39f2449 --- /dev/null +++ b/debian/rules @@ -0,0 +1,16 @@ +#!/usr/bin/make -f + +#export DH_VERBOSE=1 + +export DH_OPTIONS + +export DH_GOPKG := golang.struktur.de/phoenix +TMPGOPATH = $(CURDIR)/debian/tmp/usr/share/gocode + +override_dh_auto_install: + mkdir -p ${TMPGOPATH}/src/${DH_GOPKG} +# Copy all .go files to /usr/share/gocode (we compile and ship). + find . -path ./debian -prune -o -type f -name "*.go" -exec tar cf - {} + | (cd "${TMPGOPATH}/src/${DH_GOPKG}" && tar xvf -) + +%: + dh $@ --with=golang diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..0745104 --- /dev/null +++ b/doc.go @@ -0,0 +1,9 @@ +// Copyright 2014 struktur AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package phoenix provides runtime support for long running server processes. +// +// In particular, it provides standardized mechanisms for handling logging, +// configuration, and HTTP server startup, as well as profiling support. +package phoenix diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..6b52093 --- /dev/null +++ b/logging.go @@ -0,0 +1,53 @@ +// Copyright 2014 struktur AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package phoenix + +import ( + "io" + "os" + "path" + "sync" +) + +func openLogWriter(logfile string) (wc io.WriteCloser, err error) { + // NOTE(lcooper): Closing stderr is generally considered a "bad thing". + wc = nopWriteCloser(os.Stderr) + if logfile != "" { + wc, err = os.OpenFile(path.Clean(logfile), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640) + } + wc = newLockingWriteCloser(wc) + return +} + +type lockingWriteCloser struct { + sync.Mutex + io.WriteCloser +} + +// NOTE(lcooper): this shouldn't be a bottleneck in the general case, +// as the logger implementation already locks. However it does +// make the writer safe for access from multiple loggers at once. +// We don't lock on Close() since we're the only ones who call it. +func newLockingWriteCloser(wc io.WriteCloser) io.WriteCloser { + return &lockingWriteCloser{WriteCloser: wc} +} + +func (wc *lockingWriteCloser) Write(bytes []byte) (int, error) { + wc.Lock() + defer wc.Unlock() + return wc.WriteCloser.Write(bytes) +} + +type nopCloser struct { + io.Writer +} + +func nopWriteCloser(w io.Writer) io.WriteCloser { + return &nopCloser{w} +} + +func (closer *nopCloser) Close() error { + return nil +} diff --git a/runtime.go b/runtime.go new file mode 100644 index 0000000..751c5b6 --- /dev/null +++ b/runtime.go @@ -0,0 +1,186 @@ +// Copyright 2014 struktur AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package phoenix + +import ( + "code.google.com/p/goconf/conf" + "errors" + "github.com/strukturag/httputils" + "log" + "net/http" + "time" +) + +// Config provides read access to the application's configuration. +type Config interface { + GetBool(section string, option string) (bool, error) + GetInt(section string, option string) (int, error) + GetString(section string, option string) (string, error) +} + +// Logger provides a log-only interface to the application Logger. +// +// Presently only methods for logging at the default (debug) level +// are provided, this may change in the future. +type Logger interface { + Print(...interface{}) + Printf(string, ...interface{}) +} + +// Metadata provides access to application information such as name and version. +type Metadata interface { + // Name returns the the configured application name, + // or "app" if none was set. + Name() string + + // Version returns the configured version string, + // or "unreleased" if no version string was provided. + Version() string +} + +// Container provides access to system data, configuration, and +// logging. +// +// Typically subinterfaces should be used when possible. +type Container interface { + Config + Logger + Metadata +} + +// Runtime provides application runtime support and +// server process launch functionality. +type Runtime interface { + Container + + // DefaultHTTPHandler specifies a handler which will be run + // using the default HTTP server configuration. + // + // The results of calling this method after Start() has been + // called are undefined. + DefaultHTTPHandler(http.Handler) + + // Start runs all registered servers and blocks until they terminate. + Start() error +} + +type startFunc func(Runtime) error + +type stopFunc func(Runtime) + +type callback struct { + start startFunc + stop stopFunc +} + +type runtime struct { + name, version string + *log.Logger + *conf.ConfigFile + callbacks []callback + server *httputils.Server + runFunc RunFunc +} + +func newRuntime(name, version string, logger *log.Logger, configFile *conf.ConfigFile, runFunc RunFunc) *runtime { + return &runtime{name, version, logger, configFile, make([]callback, 0), nil, runFunc} +} + +func (runtime *runtime) Callback(start startFunc, stop stopFunc) { + runtime.callbacks = append(runtime.callbacks, callback{start, stop}) +} + +func (runtime *runtime) OnStart(start startFunc) { + runtime.Callback(start, func(_ Runtime) {}) +} + +func (runtime *runtime) OnStop(stop stopFunc) { + runtime.Callback(func(_ Runtime) error { return nil }, stop) +} + +func (runtime *runtime) Run() (err error) { + defer func() { + if err != nil { + runtime.Print(err) + } + }() + + err = runtime.runFunc(runtime) + return +} + +func (runtime *runtime) Start() error { + if runtime.server == nil { + return errors.New("No HTTP server was registered") + } + + stopCallbacks := make([]callback, 0) + defer func() { + for _, cb := range stopCallbacks { + cb.stop(runtime) + } + }() + + for _, cb := range runtime.callbacks { + if err := cb.start(runtime); err != nil { + return err + } else { + stopCallbacks = append([]callback{cb}, stopCallbacks...) + } + } + + return runtime.server.ListenAndServe() +} + +func (runtime *runtime) DefaultHTTPHandler(handler http.Handler) { + listen, err := runtime.GetString("http", "listen") + if err != nil { + listen = "127.0.0.1:8080" + } + + readtimeout, err := runtime.GetInt("http", "readtimeout") + if err != nil { + readtimeout = 10 + } + + writetimeout, err := runtime.GetInt("http", "writetimeout") + if err != nil { + writetimeout = 10 + } + + runtime.server = &httputils.Server{ + Server: http.Server{ + Addr: listen, + Handler: handler, + ReadTimeout: time.Duration(readtimeout) * time.Second, + WriteTimeout: time.Duration(writetimeout) * time.Second, + MaxHeaderBytes: 1 << 20, + }, + Logger: runtime.Logger, + } + + runtime.OnStop(func(runtime Runtime) { + runtime.Print("Server shutdown.") + }) + + runtime.OnStart(func(r Runtime) error { + runtime.Printf("Starting HTTP server on %s", listen) + return nil + }) +} + +func (runtime *runtime) Name() string { + if runtime.name == "" { + return "app" + } + return runtime.name +} + +func (runtime *runtime) Version() string { + if runtime.version == "" { + return "unreleased" + } + return runtime.version +} diff --git a/runtime_test.go b/runtime_test.go new file mode 100644 index 0000000..4e16a7b --- /dev/null +++ b/runtime_test.go @@ -0,0 +1,39 @@ +// Copyright 2014 struktur AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package phoenix + +import ( + "testing" +) + +func TestRuntime_NameUsesTheGivenValue(t *testing.T) { + name := "spreed-app" + runtime := newRuntime(name, "", nil, nil, nil) + if actualName := runtime.Name(); actualName != name { + t.Errorf("Expected app name to be '%s', but was '%s'", name, actualName) + } +} + +func TestRuntime_NameDefaultstoAppIfUnset(t *testing.T) { + runtime := newRuntime("", "", nil, nil, nil) + if expected, actual := "app", runtime.Name(); expected != actual { + t.Errorf("Expected app name to be '%s', but was '%s'", expected, actual) + } +} + +func TestRuntime_VersionUsesTheGivenValue(t *testing.T) { + version := "0.9.4b1" + runtime := newRuntime("", version, nil, nil, nil) + if actualVersion := runtime.Version(); actualVersion != version { + t.Errorf("Expected app name to be '%s', but was '%s'", version, actualVersion) + } +} + +func TestRuntime_NameDefaultstoUnreleasedIfUnset(t *testing.T) { + runtime := newRuntime("", "", nil, nil, nil) + if expected, actual := "unreleased", runtime.Version(); expected != actual { + t.Errorf("Expected app version to be '%s', but was '%s'", expected, actual) + } +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..30b841d --- /dev/null +++ b/server.go @@ -0,0 +1,233 @@ +// Copyright 2014 struktur AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package phoenix + +import ( + "code.google.com/p/goconf/conf" + "fmt" + "io" + "log" + _ "net/http/pprof" + "os" + "path" + goruntime "runtime" + "runtime/pprof" +) + +// RunFunc is the completion callback for server setup. +type RunFunc func(Runtime) error + +// Server provides pre-startup configuration and application boot functionality. +type Server interface { + // DefaultOption sets the default value of the named option in the given + // section. + DefaultOption(section, option, value string) Server + + // OverrideOption forces the named option in the given section + // to have the given value regardless of it's state in the + // config file. + OverrideOption(section, option, value string) Server + + // Config sets the path to the application's main config file. + Config(path *string) Server + + // Log sets the path to the application's logfile. Defaults to stderr if unset. + Log(path *string) Server + + // CpuProfile runs the application with CPU profiling enabled, + // writing the results to path. + CpuProfile(path *string) Server + + // MemProfile runs the application with memory profiling enabled, + // writing the results to path. + MemProfile(path *string) Server + + // Run initializes a Runtime instance and provides it to the runner callback, + // returning any errors produced by the callback. + // + // Any errors resulting from loading the configuration or opening the log + // will be returned without calling runner. + Run(runner RunFunc) error +} + +type server struct { + Name, Version string + configPath, logPath *string + cpuProfile, memProfile *string + Defaults, Overrides *conf.ConfigFile +} + +// NewServer creates a Server instance with the given name and version string. +func NewServer(name, version string) Server { + return &server{ + Name: name, + Version: version, + Defaults: conf.NewConfigFile(), + Overrides: conf.NewConfigFile(), + } +} + +func (server *server) DefaultOption(section, name, value string) Server { + server.Defaults.AddOption(section, name, value) + return server +} + +func (server *server) OverrideOption(section, name, value string) Server { + server.Overrides.AddOption(section, name, value) + return server +} + +func (server *server) Config(path *string) Server { + server.configPath = path + return server +} + +func (server *server) Log(path *string) Server { + server.logPath = path + return server +} + +func (server *server) CpuProfile(path *string) Server { + server.cpuProfile = path + return server +} + +func (server *server) MemProfile(path *string) Server { + server.memProfile = path + return server +} + +func (server *server) Run(runFunc RunFunc) (err error) { + bootLogger := server.makeLogger(os.Stderr) + + configFile, err := server.loadConfig() + if err != nil { + bootLogger.Printf("Failed to load configuration file: %v", err) + return err + } + + var logfile string + if server.logPath == nil || *server.logPath == "" { + logfile, err = configFile.GetString("log", "logfile") + } else { + logfile = *server.logPath + } + + logwriter, err := openLogWriter(logfile) + if err != nil { + bootLogger.Printf("Unable to open log file: %s", err) + return err + } + defer logwriter.Close() + + // Set the core logging package to log to our logwriter. + server.setSystemLogger(logwriter) + + // And create our internal logger instance. + logger := server.makeLogger(logwriter) + + // Now that logging is started, install a panic handler. + defer func() { + if recovered := recover(); recovered != nil { + if panicedError, ok := recovered.(error); ok { + err = panicedError + } else { + err = fmt.Errorf("%v", recovered) + } + + stackTrace := make([]byte, 1024) + for { + n := goruntime.Stack(stackTrace, false) + if n < len(stackTrace) { + stackTrace = stackTrace[0:n] + break + } + stackTrace = make([]byte, len(stackTrace)*2) + } + + logger.Printf("%v\n%s", err, stackTrace) + } + }() + + runtime := newRuntime(server.Name, server.Version, logger, configFile, runFunc) + + if server.cpuProfile != nil && *server.cpuProfile != "" { + runtime.OnStart(func(runtime Runtime) error { + cpuprofilepath := path.Clean(*server.cpuProfile) + runtime.Printf("Writing CPU profile to %s", cpuprofilepath) + + f, err := os.Create(cpuprofilepath) + if err != nil { + return fmt.Errorf("Failed to open CPU profile: %v", err) + } + return pprof.StartCPUProfile(f) + }) + + runtime.OnStop(func(_ Runtime) { + pprof.StopCPUProfile() + }) + } + + if server.memProfile != nil && *server.memProfile != "" { + memprofilepath := path.Clean(*server.memProfile) + var profileData io.WriteCloser + runtime.OnStart(func(runtime Runtime) (err error) { + runtime.Printf("A memory profile will be written to %s on exit.", memprofilepath) + profileData, err = os.Create(memprofilepath) + return + }) + + runtime.OnStop(func(runtime Runtime) { + runtime.Printf("Writing memory profile to %s", memprofilepath) + defer profileData.Close() + if err := pprof.Lookup("heap").WriteTo(profileData, 0); err != nil { + runtime.Printf("Failed to create memory profile: %v", err) + } + }) + } + + err = runtime.Run() + return +} + +func (server *server) loadConfig() (mainConfig *conf.ConfigFile, err error) { + if server.configPath != nil { + mainConfig, err = conf.ReadConfigFile(*server.configPath) + if err != nil { + return + } + } else { + mainConfig = conf.NewConfigFile() + } + + for _, section := range server.Defaults.GetSections() { + options, _ := server.Defaults.GetOptions(section) + for _, option := range options { + if !mainConfig.HasOption(section, option) { + value, _ := server.Defaults.GetRawString(section, option) + mainConfig.AddOption(section, option, value) + } + } + } + + for _, section := range server.Overrides.GetSections() { + options, _ := server.Overrides.GetOptions(section) + for _, option := range options { + value, _ := server.Overrides.GetRawString(section, option) + mainConfig.AddOption(section, option, value) + } + } + return +} + +func (server *server) makeLogger(w io.Writer) *log.Logger { + return log.New(w, server.Name+" ", log.LstdFlags) +} + +func (server *server) setSystemLogger(w io.Writer) { + log.SetOutput(w) + log.SetPrefix(server.Name + " ") + log.SetFlags(log.LstdFlags) +}