bugsnag integration

Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
This commit is contained in:
Jean-Laurent de Morlhon 2015-12-08 12:13:59 +01:00
parent 2051e6eeae
commit 4f0c24483b
16 changed files with 400 additions and 79 deletions

View File

@ -88,6 +88,7 @@ func main() {
return
}
log.Logger = log.NewLogrusMachineLogger()
setDebugOutputLevel()
cli.AppHelpTemplate = AppHelpTemplate
cli.CommandHelpTemplate = CommandHelpTemplate
@ -149,6 +150,12 @@ func main() {
Name: "native-ssh",
Usage: "Use the native (Go-based) SSH implementation.",
},
cli.StringFlag{
EnvVar: "MACHINE_BUGSNAG_API_TOKEN",
Name: "bugsnag-api-token",
Usage: "BugSnag API token for crash reporting",
Value: "",
},
}
if err := app.Run(os.Args); err != nil {

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/machine/commands/mcndirs"
"github.com/docker/machine/libmachine"
"github.com/docker/machine/libmachine/cert"
"github.com/docker/machine/libmachine/crashreport"
"github.com/docker/machine/libmachine/host"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/libmachine/mcnutils"
@ -102,6 +103,8 @@ func fatalOnError(command func(commandLine CommandLine, api libmachine.API) erro
api.GithubAPIToken = context.GlobalString("github-api-token")
api.Filestore.Path = context.GlobalString("storage-path")
crashreport.Configure(context.GlobalString("bugsnag-api-token"))
// TODO (nathanleclaire): These should ultimately be accessed
// through the libmachine client by the rest of the code and
// not through their respective modules. For now, however,

View File

@ -139,3 +139,21 @@ Make sure to specify the machine name as an argument:
$ docker-machine stop dev
$ docker-machine start dev
# Crash Reporting
Provisioning a host is a complex matter that can fail for a lot of reasons.
Some of those reasons lies in your very workstation that can have a wide
variety of shell, network configuration, vpn, proxy and firewalls or from reasons
on the other end of the chain: your cloud provider or the network in between.
To help `docker-machine` be as stable as possible, we added a monitoring of crashes
whenever you try to `create` or `upgrade` a host. This will send over https to bugsnag
a couple of information : your docker-machine version, build, OS, ARCH, the path to your
current shell and the history of the last command as you could see it with a `-D` option.
Those data are only there to help us pinpoint recurring issue with docker-machine and will only
be transmitted in the case of a crash of docker-machine.
If you're worried about thatm you can create a `no-error-report` in the `$HOME/.docker/machine`
directory, and we will not gather nor send any data.

View File

@ -0,0 +1,111 @@
package crashreport
import (
"fmt"
"os"
"runtime"
"bytes"
"os/exec"
"path/filepath"
"errors"
"github.com/bugsnag/bugsnag-go"
"github.com/docker/machine/commands/mcndirs"
"github.com/docker/machine/libmachine/log"
"github.com/docker/machine/version"
)
// We bundle a bugnsag key, but we disable its usage for now
// this ease testing just set useDefaultKey to true
// and yet we bundle the code without activating it by default.
var useDefaultKey = false
const defaultAPIKey = "a9697f9a010c33ee218a65e5b1f3b0c1"
var apiKey string
// Configure the apikey for bugnag
func Configure(key string) {
if key != "" {
apiKey = key
return
}
if useDefaultKey {
apiKey = defaultAPIKey
}
}
// Send through http the crash report to bugsnag need a call to Configure(apiKey) before
func Send(error error) error {
if noReportFileExist() {
err := errors.New("Not sending report since the optout file exist.")
log.Debug(err)
return err
}
if apiKey == "" {
err := errors.New("Not sending report since no api key has been set.")
log.Debug(err)
return err
}
bugsnag.Configure(bugsnag.Configuration{
APIKey: apiKey,
// XXX we need to abuse bugsnag metrics to get the OS/ARCH information as a usable filter
// Can do that with either "stage" or "hostname"
ReleaseStage: fmt.Sprintf("%s (%s)", runtime.GOOS, runtime.GOARCH),
ProjectPackages: []string{"github.com/docker/machine/[^v]*"},
AppVersion: version.FullVersion(),
Synchronous: true,
PanicHandler: func() {},
Logger: new(logger),
})
metaData := bugsnag.MetaData{}
metaData.Add("app", "compiler", fmt.Sprintf("%s (%s)", runtime.Compiler, runtime.Version()))
metaData.Add("device", "os", runtime.GOOS)
metaData.Add("device", "arch", runtime.GOARCH)
detectRunningShell(&metaData)
detectUname(&metaData)
var buffer bytes.Buffer
for _, message := range log.History() {
buffer.WriteString(message + "\n")
}
metaData.Add("history", "trace", buffer.String())
return bugsnag.Notify(error, metaData)
}
func noReportFileExist() bool {
optOutFilePath := filepath.Join(mcndirs.GetBaseDir(), "no-error-report")
if _, err := os.Stat(optOutFilePath); os.IsNotExist(err) {
return false
}
return true
}
func detectRunningShell(metaData *bugsnag.MetaData) {
shell := os.Getenv("SHELL")
if shell != "" {
metaData.Add("device", "shell", shell)
}
shell = os.Getenv("__fish_bin_dir")
if shell != "" {
metaData.Add("device", "shell", shell)
}
}
func detectUname(metaData *bugsnag.MetaData) {
cmd := exec.Command("uname", "-s")
output, err := cmd.Output()
if err != nil {
return
}
metaData.Add("device", "uname", string(output))
}

View File

@ -0,0 +1,9 @@
package crashreport
import "github.com/docker/machine/libmachine/log"
type logger struct{}
func (d *logger) Printf(fmtString string, args ...interface{}) {
log.Debugf(fmtString, args)
}

View File

@ -23,8 +23,8 @@ var (
)
const (
pluginOutPrefix = "(%s) "
pluginErrPrefix = "(%s) DBG | "
pluginOut = "(%s) %s"
pluginErr = "(%s) DBG | %s"
PluginEnvKey = "MACHINE_PLUGIN_TOKEN"
PluginEnvVal = "42"
PluginEnvDriverName = "MACHINE_PLUGIN_DRIVER_NAME"
@ -210,9 +210,9 @@ func (lbp *Plugin) execServer() error {
for {
select {
case out := <-stdOutCh:
log.Info(fmt.Sprintf(pluginOutPrefix, lbp.MachineName), out)
log.Infof(pluginOut, lbp.MachineName, out)
case err := <-stdErrCh:
log.Debug(fmt.Sprintf(pluginErrPrefix, lbp.MachineName), err)
log.Debugf(pluginErr, lbp.MachineName, err)
case _ = <-lbp.stopCh:
stopStdoutCh <- true
stopStderrCh <- true

View File

@ -78,11 +78,9 @@ func TestLocalBinaryPluginClose(t *testing.T) {
}
func TestExecServer(t *testing.T) {
log.SetDebug(true)
machineName := "test"
logReader, logWriter := io.Pipe()
log.SetDebug(true)
log.SetOutput(logWriter)
defer func() {
@ -98,6 +96,7 @@ func TestExecServer(t *testing.T) {
stderr: stderrReader,
}
machineName := "test"
lbp := &Plugin{
MachineName: machineName,
Executor: fe,
@ -112,12 +111,10 @@ func TestExecServer(t *testing.T) {
finalErr <- lbp.execServer()
}()
expectedAddr := "127.0.0.1:12345"
expectedPluginOut := "Doing some fun plugin stuff..."
expectedPluginErr := "Uh oh, something in plugin went wrong..."
logScanner := bufio.NewScanner(logReader)
// Write the ip address
expectedAddr := "127.0.0.1:12345"
if _, err := io.WriteString(stdoutWriter, expectedAddr+"\n"); err != nil {
t.Fatalf("Error attempting to write plugin address: %s", err)
}
@ -126,22 +123,24 @@ func TestExecServer(t *testing.T) {
t.Fatalf("Expected to read the expected address properly in server but did not")
}
expectedOut := fmt.Sprintf("%s%s", fmt.Sprintf(pluginOutPrefix, machineName), expectedPluginOut)
// Write a log in stdout
expectedPluginOut := "Doing some fun plugin stuff..."
if _, err := io.WriteString(stdoutWriter, expectedPluginOut+"\n"); err != nil {
t.Fatalf("Error attempting to write to out in plugin: %s", err)
}
expectedOut := fmt.Sprintf(pluginOut, machineName, expectedPluginOut)
if logScanner.Scan(); logScanner.Text() != expectedOut {
t.Fatalf("Output written to log was not what we expected\nexpected: %s\nactual: %s", expectedOut, logScanner.Text())
}
expectedErr := fmt.Sprintf("%s%s", fmt.Sprintf(pluginErrPrefix, machineName), expectedPluginErr)
// Write a log in stderr
expectedPluginErr := "Uh oh, something in plugin went wrong..."
if _, err := io.WriteString(stderrWriter, expectedPluginErr+"\n"); err != nil {
t.Fatalf("Error attempting to write to err in plugin: %s", err)
}
expectedErr := fmt.Sprintf(pluginErr, machineName, expectedPluginErr)
if logScanner.Scan(); logScanner.Text() != expectedErr {
t.Fatalf("Error written to log was not what we expected\nexpected: %s\nactual: %s", expectedErr, logScanner.Text())
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/docker/machine/libmachine/auth"
"github.com/docker/machine/libmachine/crashreport"
"github.com/docker/machine/libmachine/drivers"
"github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/log"
@ -134,11 +135,13 @@ func (h *Host) Upgrade() error {
provisioner, err := provision.DetectProvisioner(h.Driver)
if err != nil {
crashreport.Send(err)
return err
}
log.Info("Upgrading docker...")
if err := provisioner.Package("docker", pkgaction.Upgrade); err != nil {
crashreport.Send(err)
return err
}

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/machine/libmachine/auth"
"github.com/docker/machine/libmachine/cert"
"github.com/docker/machine/libmachine/check"
"github.com/docker/machine/libmachine/crashreport"
"github.com/docker/machine/libmachine/drivers"
"github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/host"
@ -96,6 +97,18 @@ func (api *Client) Create(h *host.Host) error {
log.Info("Creating machine...")
if err := api.performCreate(h); err != nil {
crashreport.Send(err)
return err
}
log.Debug("Reticulating splines...")
return nil
}
func (api *Client) performCreate(h *host.Host) error {
if err := h.Driver.Create(); err != nil {
return fmt.Errorf("Error in driver during machine creation: %s", err)
}
@ -136,7 +149,6 @@ func (api *Client) Create(h *host.Host) error {
log.Info("Docker is up and running!")
}
log.Debug("Reticulating splines...")
return nil
}

View File

@ -0,0 +1,83 @@
package log
import (
"fmt"
"io"
"os"
)
type FmtMachineLogger struct {
out io.Writer
err io.Writer
debug bool
}
// NewFmtMachineLogger creates a MachineLogger implementation used by the drivers
func NewFmtMachineLogger() MachineLogger {
return &FmtMachineLogger{
out: os.Stdout,
err: os.Stderr,
debug: false,
}
}
func (ml *FmtMachineLogger) RedirectStdOutToStdErr() {
ml.out = ml.err
}
func (ml *FmtMachineLogger) SetDebug(debug bool) {
ml.debug = debug
}
func (ml *FmtMachineLogger) SetOutput(out io.Writer) {
ml.out = out
ml.err = out
}
func (ml *FmtMachineLogger) Debug(args ...interface{}) {
if ml.debug {
fmt.Fprintln(ml.err, args...)
}
}
func (ml *FmtMachineLogger) Debugf(fmtString string, args ...interface{}) {
if ml.debug {
fmt.Fprintf(ml.err, fmtString+"\n", args...)
}
}
func (ml *FmtMachineLogger) Error(args ...interface{}) {
fmt.Fprintln(ml.out, args...)
}
func (ml *FmtMachineLogger) Errorf(fmtString string, args ...interface{}) {
fmt.Fprintf(ml.out, fmtString+"\n", args...)
}
func (ml *FmtMachineLogger) Info(args ...interface{}) {
fmt.Fprintln(ml.out, args...)
}
func (ml *FmtMachineLogger) Infof(fmtString string, args ...interface{}) {
fmt.Fprintf(ml.out, fmtString+"\n", args...)
}
func (ml *FmtMachineLogger) Fatal(args ...interface{}) {
fmt.Fprintln(ml.out, args...)
}
func (ml *FmtMachineLogger) Fatalf(fmtString string, args ...interface{}) {
fmt.Fprintf(ml.out, fmtString+"\n", args...)
}
func (ml *FmtMachineLogger) Warn(args ...interface{}) {
fmt.Fprintln(ml.out, args...)
}
func (ml *FmtMachineLogger) Warnf(fmtString string, args ...interface{}) {
fmt.Fprintf(ml.out, fmtString+"\n", args...)
}
func (ml *FmtMachineLogger) History() []string {
return []string{}
}

View File

@ -0,0 +1,40 @@
package log
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFmtDebug(t *testing.T) {
testLogger := NewFmtMachineLogger()
testLogger.SetDebug(true)
result := captureOutput(testLogger, func() { testLogger.Debug("debug") })
assert.Equal(t, result, "debug")
}
func TestFmtInfo(t *testing.T) {
testLogger := NewFmtMachineLogger()
result := captureOutput(testLogger, func() { testLogger.Info("info") })
assert.Equal(t, result, "info")
}
func TestFmtWarn(t *testing.T) {
testLogger := NewFmtMachineLogger()
result := captureOutput(testLogger, func() { testLogger.Warn("warn") })
assert.Equal(t, result, "warn")
}
func TestFmtError(t *testing.T) {
testLogger := NewFmtMachineLogger()
result := captureOutput(testLogger, func() { testLogger.Error("error") })
assert.Equal(t, result, "error")
}

View File

@ -2,65 +2,65 @@ package log
import "io"
var logger MachineLogger
var Logger MachineLogger
func init() {
logger = NewMachineLogger()
Logger = NewFmtMachineLogger()
}
// RedirectStdOutToStdErr prevents any log from corrupting the output
func RedirectStdOutToStdErr() {
logger.RedirectStdOutToStdErr()
Logger.RedirectStdOutToStdErr()
}
func Debug(args ...interface{}) {
logger.Debug(args...)
Logger.Debug(args...)
}
func Debugf(fmtString string, args ...interface{}) {
logger.Debugf(fmtString, args...)
Logger.Debugf(fmtString, args...)
}
func Error(args ...interface{}) {
logger.Error(args...)
Logger.Error(args...)
}
func Errorf(fmtString string, args ...interface{}) {
logger.Errorf(fmtString, args...)
Logger.Errorf(fmtString, args...)
}
func Info(args ...interface{}) {
logger.Info(args...)
Logger.Info(args...)
}
func Infof(fmtString string, args ...interface{}) {
logger.Infof(fmtString, args...)
Logger.Infof(fmtString, args...)
}
func Fatal(args ...interface{}) {
logger.Fatal(args...)
Logger.Fatal(args...)
}
func Fatalf(fmtString string, args ...interface{}) {
logger.Fatalf(fmtString, args...)
Logger.Fatalf(fmtString, args...)
}
func Warn(args ...interface{}) {
logger.Warn(args...)
Logger.Warn(args...)
}
func Warnf(fmtString string, args ...interface{}) {
logger.Warnf(fmtString, args...)
}
func Logger() interface{} {
return logger
Logger.Warnf(fmtString, args...)
}
func SetDebug(debug bool) {
logger.SetDebug(debug)
Logger.SetDebug(debug)
}
func SetOutput(out io.Writer) {
logger.SetOutput(out)
Logger.SetOutput(out)
}
func History() []string {
return Logger.History()
}

View File

@ -1,9 +0,0 @@
package log
import (
"testing"
)
func TestFormatterIsSet(t *testing.T) {
}

View File

@ -3,26 +3,33 @@ package log
import (
"io"
"fmt"
"sync"
"github.com/Sirupsen/logrus"
)
type LogrusMachineLogger struct {
logger *logrus.Logger
history []string
historyLock sync.Locker
logger *logrus.Logger
}
func NewMachineLogger() MachineLogger {
// NewLogrusMachineLogger creates the MachineLogger implementation used by the docker-machine
func NewLogrusMachineLogger() MachineLogger {
logrusLogger := logrus.New()
logrusLogger.Level = logrus.InfoLevel
logrusLogger.Formatter = new(MachineFormatter)
return LogrusMachineLogger{logrusLogger}
return &LogrusMachineLogger{[]string{}, &sync.Mutex{}, logrusLogger}
}
// RedirectStdOutToStdErr prevents any log from corrupting the output
func (ml LogrusMachineLogger) RedirectStdOutToStdErr() {
func (ml *LogrusMachineLogger) RedirectStdOutToStdErr() {
ml.logger.Level = logrus.ErrorLevel
}
func (ml LogrusMachineLogger) SetDebug(debug bool) {
func (ml *LogrusMachineLogger) SetDebug(debug bool) {
if debug {
ml.logger.Level = logrus.DebugLevel
} else {
@ -30,50 +37,76 @@ func (ml LogrusMachineLogger) SetDebug(debug bool) {
}
}
func (ml LogrusMachineLogger) SetOutput(out io.Writer) {
func (ml *LogrusMachineLogger) SetOutput(out io.Writer) {
ml.logger.Out = out
}
func (ml LogrusMachineLogger) Logger() interface{} {
func (ml *LogrusMachineLogger) Logger() *logrus.Logger {
return ml.logger
}
func (ml LogrusMachineLogger) Debug(args ...interface{}) {
func (ml *LogrusMachineLogger) Debug(args ...interface{}) {
ml.record(args...)
ml.logger.Debug(args...)
}
func (ml LogrusMachineLogger) Debugf(fmtString string, args ...interface{}) {
func (ml *LogrusMachineLogger) Debugf(fmtString string, args ...interface{}) {
ml.recordf(fmtString, args...)
ml.logger.Debugf(fmtString, args...)
}
func (ml LogrusMachineLogger) Error(args ...interface{}) {
func (ml *LogrusMachineLogger) Error(args ...interface{}) {
ml.record(args...)
ml.logger.Error(args...)
}
func (ml LogrusMachineLogger) Errorf(fmtString string, args ...interface{}) {
func (ml *LogrusMachineLogger) Errorf(fmtString string, args ...interface{}) {
ml.recordf(fmtString, args...)
ml.logger.Errorf(fmtString, args...)
}
func (ml LogrusMachineLogger) Info(args ...interface{}) {
func (ml *LogrusMachineLogger) Info(args ...interface{}) {
ml.record(args...)
ml.logger.Info(args...)
}
func (ml LogrusMachineLogger) Infof(fmtString string, args ...interface{}) {
func (ml *LogrusMachineLogger) Infof(fmtString string, args ...interface{}) {
ml.recordf(fmtString, args...)
ml.logger.Infof(fmtString, args...)
}
func (ml LogrusMachineLogger) Fatal(args ...interface{}) {
func (ml *LogrusMachineLogger) Fatal(args ...interface{}) {
ml.record(args...)
ml.logger.Fatal(args...)
}
func (ml LogrusMachineLogger) Fatalf(fmtString string, args ...interface{}) {
func (ml *LogrusMachineLogger) Fatalf(fmtString string, args ...interface{}) {
ml.recordf(fmtString, args...)
ml.logger.Fatalf(fmtString, args...)
}
func (ml LogrusMachineLogger) Warn(args ...interface{}) {
func (ml *LogrusMachineLogger) Warn(args ...interface{}) {
ml.record(args...)
ml.logger.Warn(args...)
}
func (ml LogrusMachineLogger) Warnf(fmtString string, args ...interface{}) {
func (ml *LogrusMachineLogger) Warnf(fmtString string, args ...interface{}) {
ml.recordf(fmtString, args...)
ml.logger.Warnf(fmtString, args...)
}
func (ml *LogrusMachineLogger) History() []string {
return ml.history
}
func (ml *LogrusMachineLogger) record(args ...interface{}) {
ml.historyLock.Lock()
defer ml.historyLock.Unlock()
ml.history = append(ml.history, fmt.Sprint(args...))
}
func (ml *LogrusMachineLogger) recordf(fmtString string, args ...interface{}) {
ml.historyLock.Lock()
defer ml.historyLock.Unlock()
ml.history = append(ml.history, fmt.Sprintf(fmtString, args...))
}

View File

@ -11,31 +11,31 @@ import (
)
func TestDefaultLevelIsInfo(t *testing.T) {
testLogger := NewMachineLogger()
assert.Equal(t, testLogger.Logger().(*logrus.Logger).Level, logrus.InfoLevel)
testLogger := NewLogrusMachineLogger().(*LogrusMachineLogger)
assert.Equal(t, testLogger.Logger().Level, logrus.InfoLevel)
}
func TestSetDebugToTrue(t *testing.T) {
testLogger := NewMachineLogger()
testLogger := NewLogrusMachineLogger().(*LogrusMachineLogger)
testLogger.SetDebug(true)
assert.Equal(t, testLogger.Logger().(*logrus.Logger).Level, logrus.DebugLevel)
assert.Equal(t, testLogger.Logger().Level, logrus.DebugLevel)
}
func TestSetDebugToFalse(t *testing.T) {
testLogger := NewMachineLogger()
testLogger := NewLogrusMachineLogger().(*LogrusMachineLogger)
testLogger.SetDebug(true)
testLogger.SetDebug(false)
assert.Equal(t, testLogger.Logger().(*logrus.Logger).Level, logrus.InfoLevel)
assert.Equal(t, testLogger.Logger().Level, logrus.InfoLevel)
}
func TestSetSilenceOutput(t *testing.T) {
testLogger := NewMachineLogger()
testLogger := NewLogrusMachineLogger().(*LogrusMachineLogger)
testLogger.RedirectStdOutToStdErr()
assert.Equal(t, testLogger.Logger().(*logrus.Logger).Level, logrus.ErrorLevel)
assert.Equal(t, testLogger.Logger().Level, logrus.ErrorLevel)
}
func TestDebug(t *testing.T) {
testLogger := NewMachineLogger()
func TestDebugOutput(t *testing.T) {
testLogger := NewLogrusMachineLogger()
testLogger.SetDebug(true)
result := captureOutput(testLogger, func() { testLogger.Debug("debug") })
@ -43,30 +43,42 @@ func TestDebug(t *testing.T) {
assert.Equal(t, result, "debug")
}
func TestInfo(t *testing.T) {
testLogger := NewMachineLogger()
func TestInfoOutput(t *testing.T) {
testLogger := NewLogrusMachineLogger()
result := captureOutput(testLogger, func() { testLogger.Info("info") })
assert.Equal(t, result, "info")
}
func TestWarn(t *testing.T) {
testLogger := NewMachineLogger()
func TestWarnOutput(t *testing.T) {
testLogger := NewLogrusMachineLogger()
result := captureOutput(testLogger, func() { testLogger.Warn("warn") })
assert.Equal(t, result, "warn")
}
func TestError(t *testing.T) {
testLogger := NewMachineLogger()
func TestErrorOutput(t *testing.T) {
testLogger := NewLogrusMachineLogger()
result := captureOutput(testLogger, func() { testLogger.Error("error") })
assert.Equal(t, result, "error")
}
func TestEntriesAreCollected(t *testing.T) {
testLogger := NewLogrusMachineLogger()
testLogger.RedirectStdOutToStdErr()
testLogger.Debug("debug")
testLogger.Info("info")
testLogger.Error("error")
assert.Equal(t, 3, len(testLogger.History()))
assert.Equal(t, "debug", testLogger.History()[0])
assert.Equal(t, "info", testLogger.History()[1])
assert.Equal(t, "error", testLogger.History()[2])
}
func captureOutput(testLogger MachineLogger, lambda func()) string {
pipeReader, pipeWriter := io.Pipe()
scanner := bufio.NewScanner(pipeReader)

View File

@ -24,5 +24,5 @@ type MachineLogger interface {
Warn(args ...interface{})
Warnf(fmtString string, args ...interface{})
Logger() interface{}
History() []string
}