[lxc-devel] [lxd/master] Vendor our own copy of the old log15

stgraber on Github lxc-bot at linuxcontainers.org
Mon Nov 13 05:15:09 UTC 2017


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 790 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20171113/accd461f/attachment.bin>
-------------- next part --------------
From ada19e3dc78052c64b396f44df1de05a49ad5a2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sun, 12 Nov 2017 23:58:31 -0500
Subject: [PATCH 1/3] Vendor a copy of log15 in shared/log15
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 shared/log15/.travis.yml                 |   9 +
 shared/log15/CONTRIBUTORS                |  11 +
 shared/log15/LICENSE                     |  13 +
 shared/log15/README.md                   |  60 ++++
 shared/log15/RELEASING.md                |  19 ++
 shared/log15/bench_test.go               | 129 +++++++
 shared/log15/doc.go                      | 333 ++++++++++++++++++
 shared/log15/ext/ext_test.go             | 109 ++++++
 shared/log15/ext/handler.go              | 130 +++++++
 shared/log15/ext/id.go                   |  47 +++
 shared/log15/format.go                   | 257 ++++++++++++++
 shared/log15/handler.go                  | 371 ++++++++++++++++++++
 shared/log15/handler_appengine.go        |  26 ++
 shared/log15/handler_other.go            |  22 ++
 shared/log15/log15_test.go               | 566 +++++++++++++++++++++++++++++++
 shared/log15/logger.go                   | 201 +++++++++++
 shared/log15/root.go                     |  67 ++++
 shared/log15/stack/stack.go              | 225 ++++++++++++
 shared/log15/stack/stack_pool.go         |  19 ++
 shared/log15/stack/stack_pool_chan.go    |  27 ++
 shared/log15/stack/stack_test.go         | 219 ++++++++++++
 shared/log15/syslog.go                   |  55 +++
 shared/log15/term/LICENSE                |  21 ++
 shared/log15/term/terminal_appengine.go  |  13 +
 shared/log15/term/terminal_darwin.go     |  12 +
 shared/log15/term/terminal_freebsd.go    |  18 +
 shared/log15/term/terminal_linux.go      |  14 +
 shared/log15/term/terminal_notwindows.go |  20 ++
 shared/log15/term/terminal_openbsd.go    |   7 +
 shared/log15/term/terminal_windows.go    |  26 ++
 30 files changed, 3046 insertions(+)
 create mode 100644 shared/log15/.travis.yml
 create mode 100644 shared/log15/CONTRIBUTORS
 create mode 100644 shared/log15/LICENSE
 create mode 100644 shared/log15/README.md
 create mode 100644 shared/log15/RELEASING.md
 create mode 100644 shared/log15/bench_test.go
 create mode 100644 shared/log15/doc.go
 create mode 100644 shared/log15/ext/ext_test.go
 create mode 100644 shared/log15/ext/handler.go
 create mode 100644 shared/log15/ext/id.go
 create mode 100644 shared/log15/format.go
 create mode 100644 shared/log15/handler.go
 create mode 100644 shared/log15/handler_appengine.go
 create mode 100644 shared/log15/handler_other.go
 create mode 100644 shared/log15/log15_test.go
 create mode 100644 shared/log15/logger.go
 create mode 100644 shared/log15/root.go
 create mode 100644 shared/log15/stack/stack.go
 create mode 100644 shared/log15/stack/stack_pool.go
 create mode 100644 shared/log15/stack/stack_pool_chan.go
 create mode 100644 shared/log15/stack/stack_test.go
 create mode 100644 shared/log15/syslog.go
 create mode 100644 shared/log15/term/LICENSE
 create mode 100644 shared/log15/term/terminal_appengine.go
 create mode 100644 shared/log15/term/terminal_darwin.go
 create mode 100644 shared/log15/term/terminal_freebsd.go
 create mode 100644 shared/log15/term/terminal_linux.go
 create mode 100644 shared/log15/term/terminal_notwindows.go
 create mode 100644 shared/log15/term/terminal_openbsd.go
 create mode 100644 shared/log15/term/terminal_windows.go

diff --git a/shared/log15/.travis.yml b/shared/log15/.travis.yml
new file mode 100644
index 000000000..56678f3b5
--- /dev/null
+++ b/shared/log15/.travis.yml
@@ -0,0 +1,9 @@
+language: go
+
+go:
+  - 1.0
+  - 1.1
+  - 1.2
+  - 1.3
+  - release
+  - tip
diff --git a/shared/log15/CONTRIBUTORS b/shared/log15/CONTRIBUTORS
new file mode 100644
index 000000000..a0866713b
--- /dev/null
+++ b/shared/log15/CONTRIBUTORS
@@ -0,0 +1,11 @@
+Contributors to log15:
+
+- Aaron L 
+- Alan Shreve 
+- Chris Hines 
+- Ciaran Downey 
+- Dmitry Chestnykh 
+- Evan Shaw 
+- Péter Szilágyi 
+- Trevor Gattis 
+- Vincent Vanackere 
diff --git a/shared/log15/LICENSE b/shared/log15/LICENSE
new file mode 100644
index 000000000..5f0d1fb6a
--- /dev/null
+++ b/shared/log15/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2014 Alan Shreve
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/shared/log15/README.md b/shared/log15/README.md
new file mode 100644
index 000000000..49313fffa
--- /dev/null
+++ b/shared/log15/README.md
@@ -0,0 +1,60 @@
+![obligatory xkcd](http://imgs.xkcd.com/comics/standards.png)
+
+# log15 [![godoc reference](https://godoc.org/gopkg.in/inconshreveable/log15.v2?status.png)](https://godoc.org/gopkg.in/inconshreveable/log15.v2)
+
+Package log15 provides an opinionated, simple toolkit for best-practice logging in Go (golang) that is both human and machine readable. It is modeled after the Go standard library's [`io`](http://golang.org/pkg/io/) and [`net/http`](http://golang.org/pkg/net/http/) packages and is an alternative to the standard library's [`log`](http://golang.org/pkg/log/) package. 
+
+## Features
+- A simple, easy-to-understand API
+- Promotes structured logging by encouraging use of key/value pairs
+- Child loggers which inherit and add their own private context
+- Lazy evaluation of expensive operations
+- Simple Handler interface allowing for construction of flexible, custom logging configurations with a tiny API.
+- Color terminal support
+- Built-in support for logging to files, streams, syslog, and the network
+- Support for forking records to multiple handlers, buffering records for output, failing over from failed handler writes, + more
+
+## Versioning
+The API of the master branch of log15 should always be considered unstable. Using a stable version
+of the log15 package is supported by gopkg.in. Include your dependency like so:
+
+```go
+import log "gopkg.in/inconshreveable/log15.v2"
+```
+
+## Examples
+
+```go
+// all loggers can have key/value context
+srvlog := log.New("module", "app/server")
+
+// all log messages can have key/value context 
+srvlog.Warn("abnormal conn rate", "rate", curRate, "low", lowRate, "high", highRate)
+
+// child loggers with inherited context
+connlog := srvlog.New("raddr", c.RemoteAddr())
+connlog.Info("connection open")
+
+// lazy evaluation
+connlog.Debug("ping remote", "latency", log.Lazy(pingRemote))
+
+// flexible configuration
+srvlog.SetHandler(log.MultiHandler(
+    log.StreamHandler(os.Stderr, log.LogfmtFormat()),
+    log.LvlFilterHandler(
+        log.LvlError,
+        log.Must.FileHandler("errors.json", log.JsonHandler())))
+```
+
+## FAQ
+
+### The varargs style is brittle and error prone! Can I have type safety please?
+Yes. Use `log.Ctx`:
+
+```go
+srvlog := log.New(log.Ctx{"module": "app/server"})
+srvlog.Warn("abnormal conn rate", log.Ctx{"rate": curRate, "low": lowRate, "high": highRate})
+```
+
+## License
+Apache
diff --git a/shared/log15/RELEASING.md b/shared/log15/RELEASING.md
new file mode 100644
index 000000000..589a4dcc6
--- /dev/null
+++ b/shared/log15/RELEASING.md
@@ -0,0 +1,19 @@
+# log15's release strategy
+
+log15 uses gopkg.in to manage versioning releases so that consumers who don't vendor dependencies can rely upon a stable API.
+
+## Master
+
+Master is considered to have no API stability guarantee, so merging new code that passes tests into master is always okay.
+
+## Releasing a new API-compatible version
+
+The process to release a new API-compatible version is described below. For the purposes of this example, we'll assume you're trying to release a new version of v2
+
+1. `git checkout v2`
+1. `git merge master`
+1. Audit the code for any imports of sub-packages. Modify any import references from `github.com/inconshrevealbe/log15/<pkg>` -> `gopkg.in/inconshreveable/log15.v2/<pkg>`
+1. `git commit`
+1. `git tag`, find the latest tag of the style v2.X.
+1. `git tag v2.X+1` If the last version was v2.6, you would run `git tag v2.7`
+1. `git push --tags git at github.com:inconshreveable/log15.git v2`
diff --git a/shared/log15/bench_test.go b/shared/log15/bench_test.go
new file mode 100644
index 000000000..e692e6193
--- /dev/null
+++ b/shared/log15/bench_test.go
@@ -0,0 +1,129 @@
+package log15
+
+import (
+	"bytes"
+	"testing"
+	"time"
+)
+
+func BenchmarkStreamNoCtx(b *testing.B) {
+	lg := New()
+
+	buf := bytes.Buffer{}
+	lg.SetHandler(StreamHandler(&buf, LogfmtFormat()))
+
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+		buf.Reset()
+	}
+}
+
+func BenchmarkDiscard(b *testing.B) {
+	lg := New()
+	lg.SetHandler(DiscardHandler())
+
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkCallerFileHandler(b *testing.B) {
+	lg := New()
+	lg.SetHandler(CallerFileHandler(DiscardHandler()))
+
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkCallerFuncHandler(b *testing.B) {
+	lg := New()
+	lg.SetHandler(CallerFuncHandler(DiscardHandler()))
+
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkLogfmtNoCtx(b *testing.B) {
+	r := Record{
+		Time: time.Now(),
+		Lvl:  LvlInfo,
+		Msg:  "test message",
+		Ctx:  []interface{}{},
+	}
+
+	logfmt := LogfmtFormat()
+	for i := 0; i < b.N; i++ {
+		logfmt.Format(&r)
+	}
+}
+
+func BenchmarkJsonNoCtx(b *testing.B) {
+	r := Record{
+		Time: time.Now(),
+		Lvl:  LvlInfo,
+		Msg:  "test message",
+		Ctx:  []interface{}{},
+	}
+
+	jsonfmt := JsonFormat()
+	for i := 0; i < b.N; i++ {
+		jsonfmt.Format(&r)
+	}
+}
+
+func BenchmarkMultiLevelFilter(b *testing.B) {
+	handler := MultiHandler(
+		LvlFilterHandler(LvlDebug, DiscardHandler()),
+		LvlFilterHandler(LvlError, DiscardHandler()),
+	)
+
+	lg := New()
+	lg.SetHandler(handler)
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkDescendant1(b *testing.B) {
+	lg := New()
+	lg.SetHandler(DiscardHandler())
+	lg = lg.New()
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkDescendant2(b *testing.B) {
+	lg := New()
+	lg.SetHandler(DiscardHandler())
+	for i := 0; i < 2; i++ {
+		lg = lg.New()
+	}
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkDescendant4(b *testing.B) {
+	lg := New()
+	lg.SetHandler(DiscardHandler())
+	for i := 0; i < 4; i++ {
+		lg = lg.New()
+	}
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
+
+func BenchmarkDescendant8(b *testing.B) {
+	lg := New()
+	lg.SetHandler(DiscardHandler())
+	for i := 0; i < 8; i++ {
+		lg = lg.New()
+	}
+	for i := 0; i < b.N; i++ {
+		lg.Info("test message")
+	}
+}
diff --git a/shared/log15/doc.go b/shared/log15/doc.go
new file mode 100644
index 000000000..64826d7c2
--- /dev/null
+++ b/shared/log15/doc.go
@@ -0,0 +1,333 @@
+/*
+Package log15 provides an opinionated, simple toolkit for best-practice logging that is
+both human and machine readable. It is modeled after the standard library's io and net/http
+packages.
+
+This package enforces you to only log key/value pairs. Keys must be strings. Values may be
+any type that you like. The default output format is logfmt, but you may also choose to use
+JSON instead if that suits you. Here's how you log:
+
+    log.Info("page accessed", "path", r.URL.Path, "user_id", user.id)
+
+This will output a line that looks like:
+
+     lvl=info t=2014-05-02T16:07:23-0700 msg="page accessed" path=/org/71/profile user_id=9
+
+Getting Started
+
+To get started, you'll want to import the library:
+
+    import log "gopkg.in/inconshreveable/log15.v2"
+
+
+Now you're ready to start logging:
+
+    func main() {
+        log.Info("Program starting", "args", os.Args())
+    }
+
+
+Convention
+
+Because recording a human-meaningful message is common and good practice, the first argument to every
+logging method is the value to the *implicit* key 'msg'.
+
+Additionally, the level you choose for a message will be automatically added with the key 'lvl', and so
+will the current timestamp with key 't'.
+
+You may supply any additional context as a set of key/value pairs to the logging function. log15 allows
+you to favor terseness, ordering, and speed over safety. This is a reasonable tradeoff for
+logging functions. You don't need to explicitly state keys/values, log15 understands that they alternate
+in the variadic argument list:
+
+    log.Warn("size out of bounds", "low", lowBound, "high", highBound, "val", val)
+
+If you really do favor your type-safety, you may choose to pass a log.Ctx instead:
+
+    log.Warn("size out of bounds", log.Ctx{"low": lowBound, "high": highBound, "val": val})
+
+
+Context loggers
+
+Frequently, you want to add context to a logger so that you can track actions associated with it. An http
+request is a good example. You can easily create new loggers that have context that is automatically included
+with each log line:
+
+    requestlogger := log.New("path", r.URL.Path)
+
+    // later
+    requestlogger.Debug("db txn commit", "duration", txnTimer.Finish())
+
+This will output a log line that includes the path context that is attached to the logger:
+
+    lvl=dbug t=2014-05-02T16:07:23-0700 path=/repo/12/add_hook msg="db txn commit" duration=0.12
+
+
+Handlers
+
+The Handler interface defines where log lines are printed to and how they are formated. Handler is a
+single interface that is inspired by net/http's handler interface:
+
+    type Handler interface {
+        Log(r *Record)
+    }
+
+
+Handlers can filter records, format them, or dispatch to multiple other Handlers.
+This package implements a number of Handlers for common logging patterns that are
+easily composed to create flexible, custom logging structures.
+
+Here's an example handler that prints logfmt output to Stdout:
+
+    handler := log.StreamHandler(os.Stdout, log.LogfmtFormat())
+
+Here's an example handler that defers to two other handlers. One handler only prints records
+from the rpc package in logfmt to standard out. The other prints records at Error level
+or above in JSON formatted output to the file /var/log/service.json
+
+    handler := log.MultiHandler(
+        log.LvlFilterHandler(log.LvlError, log.Must.FileHandler("/var/log/service.json", log.JsonFormat())),
+        log.MatchFilterHandler("pkg", "app/rpc" log.StdoutHandler())
+    )
+
+Logging File Names and Line Numbers
+
+This package implements three Handlers that add debugging information to the
+context, CallerFileHandler, CallerFuncHandler and CallerStackHandler. Here's
+an example that adds the source file and line number of each logging call to
+the context.
+
+    h := log.CallerFileHandler(log.StdoutHandler())
+    log.Root().SetHandler(h)
+    ...
+    log.Error("open file", "err", err)
+
+This will output a line that looks like:
+
+    lvl=eror t=2014-05-02T16:07:23-0700 msg="open file" err="file not found" caller=data.go:42
+
+Here's an example that logs the call stack rather than just the call site.
+
+    h := log.CallerStackHandler("%+v", log.StdoutHandler())
+    log.Root().SetHandler(h)
+    ...
+    log.Error("open file", "err", err)
+
+This will output a line that looks like:
+
+    lvl=eror t=2014-05-02T16:07:23-0700 msg="open file" err="file not found" stack="[pkg/data.go:42 pkg/cmd/main.go]"
+
+The "%+v" format instructs the handler to include the path of the source file
+relative to the compile time GOPATH. The log15/stack package documents the
+full list of formatting verbs and modifiers available.
+
+Custom Handlers
+
+The Handler interface is so simple that it's also trivial to write your own. Let's create an
+example handler which tries to write to one handler, but if that fails it falls back to
+writing to another handler and includes the error that it encountered when trying to write
+to the primary. This might be useful when trying to log over a network socket, but if that
+fails you want to log those records to a file on disk.
+
+    type BackupHandler struct {
+        Primary Handler
+        Secondary Handler
+    }
+
+    func (h *BackupHandler) Log (r *Record) error {
+        err := h.Primary.Log(r)
+        if err != nil {
+            r.Ctx = append(ctx, "primary_err", err)
+            return h.Secondary.Log(r)
+        }
+        return nil
+    }
+
+This pattern is so useful that a generic version that handles an arbitrary number of Handlers
+is included as part of this library called FailoverHandler.
+
+Logging Expensive Operations
+
+Sometimes, you want to log values that are extremely expensive to compute, but you don't want to pay
+the price of computing them if you haven't turned up your logging level to a high level of detail.
+
+This package provides a simple type to annotate a logging operation that you want to be evaluated
+lazily, just when it is about to be logged, so that it would not be evaluated if an upstream Handler
+filters it out. Just wrap any function which takes no arguments with the log.Lazy type. For example:
+
+    func factorRSAKey() (factors []int) {
+        // return the factors of a very large number
+    }
+
+    log.Debug("factors", log.Lazy{factorRSAKey})
+
+If this message is not logged for any reason (like logging at the Error level), then
+factorRSAKey is never evaluated.
+
+Dynamic context values
+
+The same log.Lazy mechanism can be used to attach context to a logger which you want to be
+evaluated when the message is logged, but not when the logger is created. For example, let's imagine
+a game where you have Player objects:
+
+    type Player struct {
+        name string
+        alive bool
+        log.Logger
+    }
+
+You always want to log a player's name and whether they're alive or dead, so when you create the player
+object, you might do:
+
+    p := &Player{name: name, alive: true}
+    p.Logger = log.New("name", p.name, "alive", p.alive)
+
+Only now, even after a player has died, the logger will still report they are alive because the logging
+context is evaluated when the logger was created. By using the Lazy wrapper, we can defer the evaluation
+of whether the player is alive or not to each log message, so that the log records will reflect the player's
+current state no matter when the log message is written:
+
+    p := &Player{name: name, alive: true}
+    isAlive := func() bool { return p.alive }
+    player.Logger = log.New("name", p.name, "alive", log.Lazy{isAlive})
+
+Terminal Format
+
+If log15 detects that stdout is a terminal, it will configure the default
+handler for it (which is log.StdoutHandler) to use TerminalFormat. This format
+logs records nicely for your terminal, including color-coded output based
+on log level.
+
+Error Handling
+
+Becasuse log15 allows you to step around the type system, there are a few ways you can specify
+invalid arguments to the logging functions. You could, for example, wrap something that is not
+a zero-argument function with log.Lazy or pass a context key that is not a string. Since logging libraries
+are typically the mechanism by which errors are reported, it would be onerous for the logging functions
+to return errors. Instead, log15 handles errors by making these guarantees to you:
+
+- Any log record containing an error will still be printed with the error explained to you as part of the log record.
+
+- Any log record containing an error will include the context key LOG15_ERROR, enabling you to easily
+(and if you like, automatically) detect if any of your logging calls are passing bad values.
+
+Understanding this, you might wonder why the Handler interface can return an error value in its Log method. Handlers
+are encouraged to return errors only if they fail to write their log records out to an external source like if the
+syslog daemon is not responding. This allows the construction of useful handlers which cope with those failures
+like the FailoverHandler.
+
+Library Use
+
+log15 is intended to be useful for library authors as a way to provide configurable logging to
+users of their library. Best practice for use in a library is to always disable all output for your logger
+by default and to provide a public Logger instance that consumers of your library can configure. Like so:
+
+    package yourlib
+
+    import "gopkg.in/inconshreveable/log15.v2"
+
+    var Log = log.New()
+
+    func init() {
+        Log.SetHandler(log.DiscardHandler())
+    }
+
+Users of your library may then enable it if they like:
+
+    import "gopkg.in/inconshreveable/log15.v2"
+    import "example.com/yourlib"
+
+    func main() {
+        handler := // custom handler setup
+        yourlib.Log.SetHandler(handler)
+    }
+
+Best practices attaching logger context
+
+The ability to attach context to a logger is a powerful one. Where should you do it and why?
+I favor embedding a Logger directly into any persistent object in my application and adding
+unique, tracing context keys to it. For instance, imagine I am writing a web browser:
+
+    type Tab struct {
+        url string
+        render *RenderingContext
+        // ...
+
+        Logger
+    }
+
+    func NewTab(url string) *Tab {
+        return &Tab {
+            // ...
+            url: url,
+
+            Logger: log.New("url", url),
+        }
+    }
+
+When a new tab is created, I assign a logger to it with the url of
+the tab as context so it can easily be traced through the logs.
+Now, whenever we perform any operation with the tab, we'll log with its
+embedded logger and it will include the tab title automatically:
+
+    tab.Debug("moved position", "idx", tab.idx)
+
+There's only one problem. What if the tab url changes? We could
+use log.Lazy to make sure the current url is always written, but that
+would mean that we couldn't trace a tab's full lifetime through our
+logs after the user navigate to a new URL.
+
+Instead, think about what values to attach to your loggers the
+same way you think about what to use as a key in a SQL database schema.
+If it's possible to use a natural key that is unique for the lifetime of the
+object, do so. But otherwise, log15's ext package has a handy RandId
+function to let you generate what you might call "surrogate keys"
+They're just random hex identifiers to use for tracing. Back to our
+Tab example, we would prefer to set up our Logger like so:
+
+        import logext "gopkg.in/inconshreveable/log15.v2/ext"
+
+        t := &Tab {
+            // ...
+            url: url,
+        }
+
+        t.Logger = log.New("id", logext.RandId(8), "url", log.Lazy{t.getUrl})
+        return t
+
+Now we'll have a unique traceable identifier even across loading new urls, but
+we'll still be able to see the tab's current url in the log messages.
+
+Must
+
+For all Handler functions which can return an error, there is a version of that
+function which will return no error but panics on failure. They are all available
+on the Must object. For example:
+
+    log.Must.FileHandler("/path", log.JsonFormat)
+    log.Must.NetHandler("tcp", ":1234", log.JsonFormat)
+
+Inspiration and Credit
+
+All of the following excellent projects inspired the design of this library:
+
+code.google.com/p/log4go
+
+github.com/op/go-logging
+
+github.com/technoweenie/grohl
+
+github.com/Sirupsen/logrus
+
+github.com/kr/logfmt
+
+github.com/spacemonkeygo/spacelog
+
+golang's stdlib, notably io and net/http
+
+The Name
+
+https://xkcd.com/927/
+
+*/
+package log15
diff --git a/shared/log15/ext/ext_test.go b/shared/log15/ext/ext_test.go
new file mode 100644
index 000000000..6971f0320
--- /dev/null
+++ b/shared/log15/ext/ext_test.go
@@ -0,0 +1,109 @@
+package ext
+
+import (
+	"errors"
+	log "github.com/lxc/lxd/shared/log15"
+	"math"
+	"testing"
+)
+
+func testHandler() (log.Handler, *log.Record) {
+	rec := new(log.Record)
+	return log.FuncHandler(func(r *log.Record) error {
+		*rec = *r
+		return nil
+	}), rec
+}
+
+func TestHotSwapHandler(t *testing.T) {
+	t.Parallel()
+
+	h1, r1 := testHandler()
+
+	l := log.New()
+	h := HotSwapHandler(h1)
+	l.SetHandler(h)
+
+	l.Info("to h1")
+	if r1.Msg != "to h1" {
+		t.Fatalf("didn't get expected message to h1")
+	}
+
+	h2, r2 := testHandler()
+	h.Swap(h2)
+	l.Info("to h2")
+	if r2.Msg != "to h2" {
+		t.Fatalf("didn't get expected message to h2")
+	}
+}
+
+func TestSpeculativeHandler(t *testing.T) {
+	t.Parallel()
+
+	// test with an even multiple of the buffer size, less than full buffer size
+	// and not a multiple of the buffer size
+	for _, count := range []int{10000, 50, 432} {
+		recs := make(chan *log.Record)
+		done := make(chan int)
+		spec := SpeculativeHandler(100, log.ChannelHandler(recs))
+
+		go func() {
+			defer close(done)
+			expectedCount := int(math.Min(float64(count), float64(100)))
+			expectedIdx := count - expectedCount
+			for r := range recs {
+				if r.Ctx[1] != expectedIdx {
+					t.Errorf("Bad ctx 'i', got %d expected %d", r.Ctx[1], expectedIdx)
+					return
+				}
+				expectedIdx++
+				expectedCount--
+
+				if expectedCount == 0 {
+					// got everything we expected
+					break
+				}
+			}
+
+			select {
+			case <-recs:
+				t.Errorf("got an extra record we shouldn't have!")
+			default:
+			}
+		}()
+
+		lg := log.New()
+		lg.SetHandler(spec)
+		for i := 0; i < count; i++ {
+			lg.Debug("test speculative", "i", i)
+		}
+
+		go spec.Flush()
+
+		// wait for the go routine to finish
+		<-done
+	}
+}
+
+func TestErrorHandler(t *testing.T) {
+	t.Parallel()
+
+	h, r := testHandler()
+	lg := log.New()
+	lg.SetHandler(EscalateErrHandler(
+		log.LvlFilterHandler(log.LvlError, h)))
+
+	lg.Debug("some function result", "err", nil)
+	if r.Msg != "" {
+		t.Fatalf("Expected debug level message to be filtered")
+	}
+
+	lg.Debug("some function result", "err", errors.New("failed operation"))
+	if r.Msg != "some function result" {
+		t.Fatalf("Expected debug level message to be escalated and pass lvlfilter")
+	}
+
+	if r.Lvl != log.LvlError {
+		t.Fatalf("Expected debug level message to be escalated to LvlError")
+	}
+}
diff --git a/shared/log15/ext/handler.go b/shared/log15/ext/handler.go
new file mode 100644
index 000000000..afb411623
--- /dev/null
+++ b/shared/log15/ext/handler.go
@@ -0,0 +1,130 @@
+package ext
+
+import (
+	"os"
+	"sync"
+	"sync/atomic"
+	"unsafe"
+
+	log "github.com/lxc/lxd/shared/log15"
+)
+
+// EscalateErrHandler wraps another handler and passes all records through
+// unchanged except if the logged context contains a non-nil error
+// value in its context. In that case, the record's level is raised
+// to LvlError unless it was already more serious (LvlCrit).
+//
+// This allows you to log the result of all functions for debugging
+// and still capture error conditions when in production with a single
+// log line. As an example, the following the log record will be written
+// out only if there was an error writing a value to redis:
+//
+//     logger := logext.EscalateErrHandler(
+//         log.LvlFilterHandler(log.LvlInfo, log.StdoutHandler))
+//
+//     reply, err := redisConn.Do("SET", "foo", "bar")
+//     logger.Debug("Wrote value to redis", "reply", reply, "err", err)
+//     if err != nil {
+//         return err
+//     }
+//
+func EscalateErrHandler(h log.Handler) log.Handler {
+	return log.FuncHandler(func(r *log.Record) error {
+		if r.Lvl > log.LvlError {
+			for i := 1; i < len(r.Ctx); i++ {
+				if v, ok := r.Ctx[i].(error); ok && v != nil {
+					r.Lvl = log.LvlError
+					break
+				}
+			}
+		}
+		return h.Log(r)
+	})
+}
+
+// SpeculativeHandler is a handler for speculative logging. It
+// keeps a ring buffer of the given size full of the last events
+// logged into it. When Flush is called, all buffered log records
+// are written to the wrapped handler. This is extremely for
+// continuously capturing debug level output, but only flushing those
+// log records if an exceptional condition is encountered.
+func SpeculativeHandler(size int, h log.Handler) *Speculative {
+	return &Speculative{
+		handler: h,
+		recs:    make([]*log.Record, size),
+	}
+}
+
+type Speculative struct {
+	mu      sync.Mutex
+	idx     int
+	recs    []*log.Record
+	handler log.Handler
+	full    bool
+}
+
+func (h *Speculative) Log(r *log.Record) error {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+	h.recs[h.idx] = r
+	h.idx = (h.idx + 1) % len(h.recs)
+	h.full = h.full || h.idx == 0
+	return nil
+}
+
+func (h *Speculative) Flush() {
+	recs := make([]*log.Record, 0)
+	func() {
+		h.mu.Lock()
+		defer h.mu.Unlock()
+		if h.full {
+			recs = append(recs, h.recs[h.idx:]...)
+		}
+		recs = append(recs, h.recs[:h.idx]...)
+
+		// reset state
+		h.full = false
+		h.idx = 0
+	}()
+
+	// don't hold the lock while we flush to the wrapped handler
+	for _, r := range recs {
+		h.handler.Log(r)
+	}
+}
+
+// HotSwapHandler wraps another handler that may swapped out
+// dynamically at runtime in a thread-safe fashion.
+// HotSwapHandler is the same functionality
+// used to implement the SetHandler method for the default
+// implementation of Logger.
+func HotSwapHandler(h log.Handler) *HotSwap {
+	hs := new(HotSwap)
+	hs.Swap(h)
+	return hs
+}
+
+type HotSwap struct {
+	handler unsafe.Pointer
+}
+
+func (h *HotSwap) Log(r *log.Record) error {
+	return (*(*log.Handler)(atomic.LoadPointer(&h.handler))).Log(r)
+}
+
+func (h *HotSwap) Swap(newHandler log.Handler) {
+	atomic.StorePointer(&h.handler, unsafe.Pointer(&newHandler))
+}
+
+// FatalHandler makes critical errors exit the program
+// immediately, much like the log.Fatal* methods from the
+// standard log package
+func FatalHandler(h log.Handler) log.Handler {
+	return log.FuncHandler(func(r *log.Record) error {
+		err := h.Log(r)
+		if r.Lvl == log.LvlCrit {
+			os.Exit(1)
+		}
+		return err
+	})
+}
diff --git a/shared/log15/ext/id.go b/shared/log15/ext/id.go
new file mode 100644
index 000000000..0bfb1551f
--- /dev/null
+++ b/shared/log15/ext/id.go
@@ -0,0 +1,47 @@
+package ext
+
+import (
+	"fmt"
+	"math/rand"
+	"sync"
+	"time"
+)
+
+var r = rand.New(&lockedSource{src: rand.NewSource(time.Now().Unix())})
+
+// RandId creates a random identifier of the requested length.
+// Useful for assigning mostly-unique identifiers for logging
+// and identification that are unlikely to collide because of
+// short lifespan or low set cardinality
+func RandId(idlen int) string {
+	b := make([]byte, idlen)
+	var randVal uint32
+	for i := 0; i < idlen; i++ {
+		byteIdx := i % 4
+		if byteIdx == 0 {
+			randVal = r.Uint32()
+		}
+		b[i] = byte((randVal >> (8 * uint(byteIdx))) & 0xFF)
+	}
+	return fmt.Sprintf("%x", b)
+}
+
+// lockedSource is a wrapper to allow a rand.Source to be used
+// concurrently (same type as the one used internally in math/rand).
+type lockedSource struct {
+	lk  sync.Mutex
+	src rand.Source
+}
+
+func (r *lockedSource) Int63() (n int64) {
+	r.lk.Lock()
+	n = r.src.Int63()
+	r.lk.Unlock()
+	return
+}
+
+func (r *lockedSource) Seed(seed int64) {
+	r.lk.Lock()
+	r.src.Seed(seed)
+	r.lk.Unlock()
+}
diff --git a/shared/log15/format.go b/shared/log15/format.go
new file mode 100644
index 000000000..76d260ee5
--- /dev/null
+++ b/shared/log15/format.go
@@ -0,0 +1,257 @@
+package log15
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const (
+	timeFormat     = "2006-01-02T15:04:05-0700"
+	termTimeFormat = "01-02|15:04:05"
+	floatFormat    = 'f'
+	termMsgJust    = 40
+)
+
+type Format interface {
+	Format(r *Record) []byte
+}
+
+// FormatFunc returns a new Format object which uses
+// the given function to perform record formatting.
+func FormatFunc(f func(*Record) []byte) Format {
+	return formatFunc(f)
+}
+
+type formatFunc func(*Record) []byte
+
+func (f formatFunc) Format(r *Record) []byte {
+	return f(r)
+}
+
+// TerminalFormat formats log records optimized for human readability on
+// a terminal with color-coded level output and terser human friendly timestamp.
+// This format should only be used for interactive programs or while developing.
+//
+//     [TIME] [LEVEL] MESAGE key=value key=value ...
+//
+// Example:
+//
+//     [May 16 20:58:45] [DBUG] remove route ns=haproxy addr=127.0.0.1:50002
+//
+func TerminalFormat() Format {
+	return FormatFunc(func(r *Record) []byte {
+		var color = 0
+		switch r.Lvl {
+		case LvlCrit:
+			color = 35
+		case LvlError:
+			color = 31
+		case LvlWarn:
+			color = 33
+		case LvlInfo:
+			color = 32
+		case LvlDebug:
+			color = 36
+		}
+
+		b := &bytes.Buffer{}
+		lvl := strings.ToUpper(r.Lvl.String())
+		if color > 0 {
+			fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), r.Msg)
+		} else {
+			fmt.Fprintf(b, "[%s] [%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg)
+		}
+
+		// try to justify the log output for short messages
+		if len(r.Ctx) > 0 && len(r.Msg) < termMsgJust {
+			b.Write(bytes.Repeat([]byte{' '}, termMsgJust-len(r.Msg)))
+		}
+
+		// print the keys logfmt style
+		logfmt(b, r.Ctx, color)
+		return b.Bytes()
+	})
+}
+
+// LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable
+// format for key/value pairs.
+//
+// For more details see: http://godoc.org/github.com/kr/logfmt
+//
+func LogfmtFormat() Format {
+	return FormatFunc(func(r *Record) []byte {
+		common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg}
+		buf := &bytes.Buffer{}
+		logfmt(buf, append(common, r.Ctx...), 0)
+		return buf.Bytes()
+	})
+}
+
+func logfmt(buf *bytes.Buffer, ctx []interface{}, color int) {
+	for i := 0; i < len(ctx); i += 2 {
+		if i != 0 {
+			buf.WriteByte(' ')
+		}
+
+		k, ok := ctx[i].(string)
+		v := formatLogfmtValue(ctx[i+1])
+		if !ok {
+			k, v = errorKey, formatLogfmtValue(k)
+		}
+
+		// XXX: we should probably check that all of your key bytes aren't invalid
+		if color > 0 {
+			fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=%s", color, k, v)
+		} else {
+			fmt.Fprintf(buf, "%s=%s", k, v)
+		}
+	}
+
+	buf.WriteByte('\n')
+}
+
+// JsonFormat formats log records as JSON objects separated by newlines.
+// It is the equivalent of JsonFormatEx(false, true).
+func JsonFormat() Format {
+	return JsonFormatEx(false, true)
+}
+
+// JsonFormatEx formats log records as JSON objects. If pretty is true,
+// records will be pretty-printed. If lineSeparated is true, records
+// will be logged with a new line between each record.
+func JsonFormatEx(pretty, lineSeparated bool) Format {
+	jsonMarshal := json.Marshal
+	if pretty {
+		jsonMarshal = func(v interface{}) ([]byte, error) {
+			return json.MarshalIndent(v, "", "    ")
+		}
+	}
+
+	return FormatFunc(func(r *Record) []byte {
+		props := make(map[string]interface{})
+
+		props[r.KeyNames.Time] = r.Time
+		props[r.KeyNames.Lvl] = r.Lvl
+		props[r.KeyNames.Msg] = r.Msg
+
+		for i := 0; i < len(r.Ctx); i += 2 {
+			k, ok := r.Ctx[i].(string)
+			if !ok {
+				props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i])
+			}
+			props[k] = formatJsonValue(r.Ctx[i+1])
+		}
+
+		b, err := jsonMarshal(props)
+		if err != nil {
+			b, _ = jsonMarshal(map[string]string{
+				errorKey: err.Error(),
+			})
+			return b
+		}
+
+		if lineSeparated {
+			b = append(b, '\n')
+		}
+
+		return b
+	})
+}
+
+func formatShared(value interface{}) (result interface{}) {
+	defer func() {
+		if err := recover(); err != nil {
+			if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
+				result = "nil"
+			} else {
+				panic(err)
+			}
+		}
+	}()
+
+	switch v := value.(type) {
+	case time.Time:
+		return v.Format(timeFormat)
+
+	case error:
+		return v.Error()
+
+	case fmt.Stringer:
+		return v.String()
+
+	default:
+		return v
+	}
+}
+
+func formatJsonValue(value interface{}) interface{} {
+	value = formatShared(value)
+	switch value.(type) {
+	case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string:
+		return value
+	default:
+		return fmt.Sprintf("%+v", value)
+	}
+}
+
+// formatValue formats a value for serialization
+func formatLogfmtValue(value interface{}) string {
+	if value == nil {
+		return "nil"
+	}
+
+	value = formatShared(value)
+	switch v := value.(type) {
+	case bool:
+		return strconv.FormatBool(v)
+	case float32:
+		return strconv.FormatFloat(float64(v), floatFormat, 3, 64)
+	case float64:
+		return strconv.FormatFloat(v, floatFormat, 3, 64)
+	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+		return fmt.Sprintf("%d", value)
+	case string:
+		return escapeString(v)
+	default:
+		return escapeString(fmt.Sprintf("%+v", value))
+	}
+}
+
+func escapeString(s string) string {
+	needQuotes := false
+	e := bytes.Buffer{}
+	e.WriteByte('"')
+	for _, r := range s {
+		if r <= ' ' || r == '=' || r == '"' {
+			needQuotes = true
+		}
+
+		switch r {
+		case '\\', '"':
+			e.WriteByte('\\')
+			e.WriteByte(byte(r))
+		case '\n':
+			e.WriteByte('\\')
+			e.WriteByte('n')
+		case '\r':
+			e.WriteByte('\\')
+			e.WriteByte('r')
+		case '\t':
+			e.WriteByte('\\')
+			e.WriteByte('t')
+		default:
+			e.WriteRune(r)
+		}
+	}
+	e.WriteByte('"')
+	start, stop := 0, e.Len()
+	if !needQuotes {
+		start, stop = 1, stop-1
+	}
+	return string(e.Bytes()[start:stop])
+}
diff --git a/shared/log15/handler.go b/shared/log15/handler.go
new file mode 100644
index 000000000..532ef5430
--- /dev/null
+++ b/shared/log15/handler.go
@@ -0,0 +1,371 @@
+package log15
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"reflect"
+	"sync"
+
+	"github.com/lxc/lxd/shared/log15/stack"
+)
+
+// A Logger prints its log records by writing to a Handler.
+// The Handler interface defines where and how log records are written.
+// Handlers are composable, providing you great flexibility in combining
+// them to achieve the logging structure that suits your applications.
+type Handler interface {
+	Log(r *Record) error
+}
+
+// FuncHandler returns a Handler that logs records with the given
+// function.
+func FuncHandler(fn func(r *Record) error) Handler {
+	return funcHandler(fn)
+}
+
+type funcHandler func(r *Record) error
+
+func (h funcHandler) Log(r *Record) error {
+	return h(r)
+}
+
+// StreamHandler writes log records to an io.Writer
+// with the given format. StreamHandler can be used
+// to easily begin writing log records to other
+// outputs.
+//
+// StreamHandler wraps itself with LazyHandler and SyncHandler
+// to evaluate Lazy objects and perform safe concurrent writes.
+func StreamHandler(wr io.Writer, fmtr Format) Handler {
+	h := FuncHandler(func(r *Record) error {
+		_, err := wr.Write(fmtr.Format(r))
+		return err
+	})
+	return LazyHandler(SyncHandler(h))
+}
+
+// SyncHandler can be wrapped around a handler to guarantee that
+// only a single Log operation can proceed at a time. It's necessary
+// for thread-safe concurrent writes.
+func SyncHandler(h Handler) Handler {
+	var mu sync.Mutex
+	return FuncHandler(func(r *Record) error {
+		defer mu.Unlock()
+		mu.Lock()
+		return h.Log(r)
+	})
+}
+
+// FileHandler returns a handler which writes log records to the give file
+// using the given format. If the path
+// already exists, FileHandler will append to the given file. If it does not,
+// FileHandler will create the file with mode 0644.
+func FileHandler(path string, fmtr Format) (Handler, error) {
+	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		return nil, err
+	}
+	return closingHandler{f, StreamHandler(f, fmtr)}, nil
+}
+
+// NetHandler opens a socket to the given address and writes records
+// over the connection.
+func NetHandler(network, addr string, fmtr Format) (Handler, error) {
+	conn, err := net.Dial(network, addr)
+	if err != nil {
+		return nil, err
+	}
+
+	return closingHandler{conn, StreamHandler(conn, fmtr)}, nil
+}
+
+// XXX: closingHandler is essentially unused at the moment
+// it's meant for a future time when the Handler interface supports
+// a possible Close() operation
+type closingHandler struct {
+	io.WriteCloser
+	Handler
+}
+
+func (h *closingHandler) Close() error {
+	return h.WriteCloser.Close()
+}
+
+// CallerFileHandler returns a Handler that adds the line number and file of
+// the calling function to the context with key "caller".
+func CallerFileHandler(h Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		call := stack.Call(r.CallPC[0])
+		r.Ctx = append(r.Ctx, "caller", fmt.Sprint(call))
+		return h.Log(r)
+	})
+}
+
+// CallerFuncHandler returns a Handler that adds the calling function name to
+// the context with key "fn".
+func CallerFuncHandler(h Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		call := stack.Call(r.CallPC[0])
+		r.Ctx = append(r.Ctx, "fn", fmt.Sprintf("%+n", call))
+		return h.Log(r)
+	})
+}
+
+// CallerStackHandler returns a Handler that adds a stack trace to the context
+// with key "stack". The stack trace is formated as a space separated list of
+// call sites inside matching []'s. The most recent call site is listed first.
+// Each call site is formatted according to format. See the documentation of
+// log15/stack.Call.Format for the list of supported formats.
+func CallerStackHandler(format string, h Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		s := stack.Callers().
+			TrimBelow(stack.Call(r.CallPC[0])).
+			TrimRuntime()
+		if len(s) > 0 {
+			buf := &bytes.Buffer{}
+			buf.WriteByte('[')
+			for i, pc := range s {
+				if i > 0 {
+					buf.WriteByte(' ')
+				}
+				fmt.Fprintf(buf, format, pc)
+			}
+			buf.WriteByte(']')
+			r.Ctx = append(r.Ctx, "stack", buf.String())
+		}
+		return h.Log(r)
+	})
+}
+
+// FilterHandler returns a Handler that only writes records to the
+// wrapped Handler if the given function evaluates true. For example,
+// to only log records where the 'err' key is not nil:
+//
+//    logger.SetHandler(FilterHandler(func(r *Record) bool {
+//        for i := 0; i < len(r.Ctx); i += 2 {
+//            if r.Ctx[i] == "err" {
+//                return r.Ctx[i+1] != nil
+//            }
+//        }
+//        return false
+//    }, h))
+//
+func FilterHandler(fn func(r *Record) bool, h Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		if fn(r) {
+			return h.Log(r)
+		}
+		return nil
+	})
+}
+
+// MatchFilterHandler returns a Handler that only writes records
+// to the wrapped Handler if the given key in the logged
+// context matches the value. For example, to only log records
+// from your ui package:
+//
+//    log.MatchFilterHandler("pkg", "app/ui", log.StdoutHandler)
+//
+func MatchFilterHandler(key string, value interface{}, h Handler) Handler {
+	return FilterHandler(func(r *Record) (pass bool) {
+		switch key {
+		case r.KeyNames.Lvl:
+			return r.Lvl == value
+		case r.KeyNames.Time:
+			return r.Time == value
+		case r.KeyNames.Msg:
+			return r.Msg == value
+		}
+
+		for i := 0; i < len(r.Ctx); i += 2 {
+			if r.Ctx[i] == key {
+				return r.Ctx[i+1] == value
+			}
+		}
+		return false
+	}, h)
+}
+
+// LvlFilterHandler returns a Handler that only writes
+// records which are less than the given verbosity
+// level to the wrapped Handler. For example, to only
+// log Error/Crit records:
+//
+//     log.LvlFilterHandler(log.Error, log.StdoutHandler)
+//
+func LvlFilterHandler(maxLvl Lvl, h Handler) Handler {
+	return FilterHandler(func(r *Record) (pass bool) {
+		return r.Lvl <= maxLvl
+	}, h)
+}
+
+// A MultiHandler dispatches any write to each of its handlers.
+// This is useful for writing different types of log information
+// to different locations. For example, to log to a file and
+// standard error:
+//
+//     log.MultiHandler(
+//         log.Must.FileHandler("/var/log/app.log", log.LogfmtFormat()),
+//         log.StderrHandler)
+//
+func MultiHandler(hs ...Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		for _, h := range hs {
+			// what to do about failures?
+			h.Log(r)
+		}
+		return nil
+	})
+}
+
+// A FailoverHandler writes all log records to the first handler
+// specified, but will failover and write to the second handler if
+// the first handler has failed, and so on for all handlers specified.
+// For example you might want to log to a network socket, but failover
+// to writing to a file if the network fails, and then to
+// standard out if the file write fails:
+//
+//     log.FailoverHandler(
+//         log.Must.NetHandler("tcp", ":9090", log.JsonFormat()),
+//         log.Must.FileHandler("/var/log/app.log", log.LogfmtFormat()),
+//         log.StdoutHandler)
+//
+// All writes that do not go to the first handler will add context with keys of
+// the form "failover_err_{idx}" which explain the error encountered while
+// trying to write to the handlers before them in the list.
+func FailoverHandler(hs ...Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		var err error
+		for i, h := range hs {
+			err = h.Log(r)
+			if err == nil {
+				return nil
+			} else {
+				r.Ctx = append(r.Ctx, fmt.Sprintf("failover_err_%d", i), err)
+			}
+		}
+
+		return err
+	})
+}
+
+// ChannelHandler writes all records to the given channel.
+// It blocks if the channel is full. Useful for async processing
+// of log messages, it's used by BufferedHandler.
+func ChannelHandler(recs chan<- *Record) Handler {
+	return FuncHandler(func(r *Record) error {
+		recs <- r
+		return nil
+	})
+}
+
+// BufferedHandler writes all records to a buffered
+// channel of the given size which flushes into the wrapped
+// handler whenever it is available for writing. Since these
+// writes happen asynchronously, all writes to a BufferedHandler
+// never return an error and any errors from the wrapped handler are ignored.
+func BufferedHandler(bufSize int, h Handler) Handler {
+	recs := make(chan *Record, bufSize)
+	go func() {
+		for m := range recs {
+			_ = h.Log(m)
+		}
+	}()
+	return ChannelHandler(recs)
+}
+
+// LazyHandler writes all values to the wrapped handler after evaluating
+// any lazy functions in the record's context. It is already wrapped
+// around StreamHandler and SyslogHandler in this library, you'll only need
+// it if you write your own Handler.
+func LazyHandler(h Handler) Handler {
+	return FuncHandler(func(r *Record) error {
+		// go through the values (odd indices) and reassign
+		// the values of any lazy fn to the result of its execution
+		hadErr := false
+		for i := 1; i < len(r.Ctx); i += 2 {
+			lz, ok := r.Ctx[i].(Lazy)
+			if ok {
+				v, err := evaluateLazy(lz)
+				if err != nil {
+					hadErr = true
+					r.Ctx[i] = err
+				} else {
+					if cs, ok := v.(stack.Trace); ok {
+						v = cs.TrimBelow(stack.Call(r.CallPC[0])).
+							TrimRuntime()
+					}
+					r.Ctx[i] = v
+				}
+			}
+		}
+
+		if hadErr {
+			r.Ctx = append(r.Ctx, errorKey, "bad lazy")
+		}
+
+		return h.Log(r)
+	})
+}
+
+func evaluateLazy(lz Lazy) (interface{}, error) {
+	t := reflect.TypeOf(lz.Fn)
+
+	if t.Kind() != reflect.Func {
+		return nil, fmt.Errorf("INVALID_LAZY, not func: %+v", lz.Fn)
+	}
+
+	if t.NumIn() > 0 {
+		return nil, fmt.Errorf("INVALID_LAZY, func takes args: %+v", lz.Fn)
+	}
+
+	if t.NumOut() == 0 {
+		return nil, fmt.Errorf("INVALID_LAZY, no func return val: %+v", lz.Fn)
+	}
+
+	value := reflect.ValueOf(lz.Fn)
+	results := value.Call([]reflect.Value{})
+	if len(results) == 1 {
+		return results[0].Interface(), nil
+	} else {
+		values := make([]interface{}, len(results))
+		for i, v := range results {
+			values[i] = v.Interface()
+		}
+		return values, nil
+	}
+}
+
+// DiscardHandler reports success for all writes but does nothing.
+// It is useful for dynamically disabling logging at runtime via
+// a Logger's SetHandler method.
+func DiscardHandler() Handler {
+	return FuncHandler(func(r *Record) error {
+		return nil
+	})
+}
+
+// The Must object provides the following Handler creation functions
+// which instead of returning an error parameter only return a Handler
+// and panic on failure: FileHandler, NetHandler, SyslogHandler, SyslogNetHandler
+var Must muster
+
+func must(h Handler, err error) Handler {
+	if err != nil {
+		panic(err)
+	}
+	return h
+}
+
+type muster struct{}
+
+func (m muster) FileHandler(path string, fmtr Format) Handler {
+	return must(FileHandler(path, fmtr))
+}
+
+func (m muster) NetHandler(network, addr string, fmtr Format) Handler {
+	return must(NetHandler(network, addr, fmtr))
+}
diff --git a/shared/log15/handler_appengine.go b/shared/log15/handler_appengine.go
new file mode 100644
index 000000000..f5e34d254
--- /dev/null
+++ b/shared/log15/handler_appengine.go
@@ -0,0 +1,26 @@
+// +build appengine
+
+package log15
+
+import "sync"
+
+// swapHandler wraps another handler that may be swapped out
+// dynamically at runtime in a thread-safe fashion.
+type swapHandler struct {
+	handler interface{}
+	lock    sync.RWMutex
+}
+
+func (h *swapHandler) Log(r *Record) error {
+	h.lock.RLock()
+	defer h.lock.RUnlock()
+
+	return h.handler.(Handler).Log(r)
+}
+
+func (h *swapHandler) Swap(newHandler Handler) {
+	h.lock.Lock()
+	defer h.lock.Unlock()
+
+	h.handler = newHandler
+}
diff --git a/shared/log15/handler_other.go b/shared/log15/handler_other.go
new file mode 100644
index 000000000..4da96745b
--- /dev/null
+++ b/shared/log15/handler_other.go
@@ -0,0 +1,22 @@
+// +build !appengine
+
+package log15
+
+import (
+	"sync/atomic"
+	"unsafe"
+)
+
+// swapHandler wraps another handler that may be swapped out
+// dynamically at runtime in a thread-safe fashion.
+type swapHandler struct {
+	handler unsafe.Pointer
+}
+
+func (h *swapHandler) Log(r *Record) error {
+	return (*(*Handler)(atomic.LoadPointer(&h.handler))).Log(r)
+}
+
+func (h *swapHandler) Swap(newHandler Handler) {
+	atomic.StorePointer(&h.handler, unsafe.Pointer(&newHandler))
+}
diff --git a/shared/log15/log15_test.go b/shared/log15/log15_test.go
new file mode 100644
index 000000000..6166dd5ca
--- /dev/null
+++ b/shared/log15/log15_test.go
@@ -0,0 +1,566 @@
+package log15
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net"
+	"regexp"
+	"runtime"
+	"sync"
+	"testing"
+	"time"
+)
+
+func testHandler() (Handler, *Record) {
+	rec := new(Record)
+	return FuncHandler(func(r *Record) error {
+		*rec = *r
+		return nil
+	}), rec
+}
+
+func testLogger() (Logger, Handler, *Record) {
+	l := New()
+	h, r := testHandler()
+	l.SetHandler(LazyHandler(h))
+	return l, h, r
+}
+
+func TestLazy(t *testing.T) {
+	t.Parallel()
+
+	x := 1
+	lazy := func() int {
+		return x
+	}
+
+	l, _, r := testLogger()
+	l.Info("", "x", Lazy{lazy})
+	if r.Ctx[1] != 1 {
+		t.Fatalf("Lazy function not evaluated, got %v, expected %d", r.Ctx[1], 1)
+	}
+
+	x = 2
+	l.Info("", "x", Lazy{lazy})
+	if r.Ctx[1] != 2 {
+		t.Fatalf("Lazy function not evaluated, got %v, expected %d", r.Ctx[1], 1)
+	}
+}
+
+func TestInvalidLazy(t *testing.T) {
+	t.Parallel()
+
+	l, _, r := testLogger()
+	validate := func() {
+		if len(r.Ctx) < 4 {
+			t.Fatalf("Invalid lazy, got %d args, expecting at least 4", len(r.Ctx))
+		}
+
+		if r.Ctx[2] != errorKey {
+			t.Fatalf("Invalid lazy, got key %s expecting %s", r.Ctx[2], errorKey)
+		}
+	}
+
+	l.Info("", "x", Lazy{1})
+	validate()
+
+	l.Info("", "x", Lazy{func(x int) int { return x }})
+	validate()
+
+	l.Info("", "x", Lazy{func() {}})
+	validate()
+}
+
+func TestCtx(t *testing.T) {
+	t.Parallel()
+
+	l, _, r := testLogger()
+	l.Info("", Ctx{"x": 1, "y": "foo", "tester": t})
+	if len(r.Ctx) != 6 {
+		t.Fatalf("Expecting Ctx tansformed into %d ctx args, got %d: %v", 6, len(r.Ctx), r.Ctx)
+	}
+}
+
+func testFormatter(f Format) (Logger, *bytes.Buffer) {
+	l := New()
+	var buf bytes.Buffer
+	l.SetHandler(StreamHandler(&buf, f))
+	return l, &buf
+}
+
+func TestJson(t *testing.T) {
+	t.Parallel()
+
+	l, buf := testFormatter(JsonFormat())
+	l.Error("some message", "x", 1, "y", 3.2)
+
+	var v map[string]interface{}
+	decoder := json.NewDecoder(buf)
+	if err := decoder.Decode(&v); err != nil {
+		t.Fatalf("Error decoding JSON: %v", v)
+	}
+
+	validate := func(key string, expected interface{}) {
+		if v[key] != expected {
+			t.Fatalf("Got %v expected %v for %v", v[key], expected, key)
+		}
+	}
+
+	validate("msg", "some message")
+	validate("x", float64(1)) // all numbers are floats in JSON land
+	validate("y", 3.2)
+}
+
+type testtype struct {
+	name string
+}
+
+func (tt testtype) String() string {
+	return tt.name
+}
+
+func TestLogfmt(t *testing.T) {
+	t.Parallel()
+
+	var nilVal *testtype
+
+	l, buf := testFormatter(LogfmtFormat())
+	l.Error("some message", "x", 1, "y", 3.2, "equals", "=", "quote", "\"", "nil", nilVal)
+
+	// skip timestamp in comparison
+	got := buf.Bytes()[27:buf.Len()]
+	expected := []byte(`lvl=eror msg="some message" x=1 y=3.200 equals="=" quote="\"" nil=nil` + "\n")
+	if !bytes.Equal(got, expected) {
+		t.Fatalf("Got %s, expected %s", got, expected)
+	}
+}
+
+func TestMultiHandler(t *testing.T) {
+	t.Parallel()
+
+	h1, r1 := testHandler()
+	h2, r2 := testHandler()
+	l := New()
+	l.SetHandler(MultiHandler(h1, h2))
+	l.Debug("clone")
+
+	if r1.Msg != "clone" {
+		t.Fatalf("wrong value for h1.Msg. Got %s expected %s", r1.Msg, "clone")
+	}
+
+	if r2.Msg != "clone" {
+		t.Fatalf("wrong value for h2.Msg. Got %s expected %s", r2.Msg, "clone")
+	}
+
+}
+
+type waitHandler struct {
+	ch chan Record
+}
+
+func (h *waitHandler) Log(r *Record) error {
+	h.ch <- *r
+	return nil
+}
+
+func TestBufferedHandler(t *testing.T) {
+	t.Parallel()
+
+	ch := make(chan Record)
+	l := New()
+	l.SetHandler(BufferedHandler(0, &waitHandler{ch}))
+
+	l.Debug("buffer")
+	if r := <-ch; r.Msg != "buffer" {
+		t.Fatalf("wrong value for r.Msg. Got %s expected %s", r.Msg, "")
+	}
+}
+
+func TestLogContext(t *testing.T) {
+	t.Parallel()
+
+	l, _, r := testLogger()
+	l = l.New("foo", "bar")
+	l.Crit("baz")
+
+	if len(r.Ctx) != 2 {
+		t.Fatalf("Expected logger context in record context. Got length %d, expected %d", len(r.Ctx), 2)
+	}
+
+	if r.Ctx[0] != "foo" {
+		t.Fatalf("Wrong context key, got %s expected %s", r.Ctx[0], "foo")
+	}
+
+	if r.Ctx[1] != "bar" {
+		t.Fatalf("Wrong context value, got %s expected %s", r.Ctx[1], "bar")
+	}
+}
+
+func TestMapCtx(t *testing.T) {
+	t.Parallel()
+
+	l, _, r := testLogger()
+	l.Crit("test", Ctx{"foo": "bar"})
+
+	if len(r.Ctx) != 2 {
+		t.Fatalf("Wrong context length, got %d, expected %d", len(r.Ctx), 2)
+	}
+
+	if r.Ctx[0] != "foo" {
+		t.Fatalf("Wrong context key, got %s expected %s", r.Ctx[0], "foo")
+	}
+
+	if r.Ctx[1] != "bar" {
+		t.Fatalf("Wrong context value, got %s expected %s", r.Ctx[1], "bar")
+	}
+}
+
+func TestLvlFilterHandler(t *testing.T) {
+	t.Parallel()
+
+	l := New()
+	h, r := testHandler()
+	l.SetHandler(LvlFilterHandler(LvlWarn, h))
+	l.Info("info'd")
+
+	if r.Msg != "" {
+		t.Fatalf("Expected zero record, but got record with msg: %v", r.Msg)
+	}
+
+	l.Warn("warned")
+	if r.Msg != "warned" {
+		t.Fatalf("Got record msg %s expected %s", r.Msg, "warned")
+	}
+
+	l.Warn("error'd")
+	if r.Msg != "error'd" {
+		t.Fatalf("Got record msg %s expected %s", r.Msg, "error'd")
+	}
+}
+
+func TestNetHandler(t *testing.T) {
+	t.Parallel()
+
+	l, err := net.Listen("tcp", "localhost:0")
+	if err != nil {
+		t.Fatalf("Failed to listen: %v", l)
+	}
+
+	errs := make(chan error)
+	go func() {
+		c, err := l.Accept()
+		if err != nil {
+			t.Errorf("Failed to accept connection: %v", err)
+			return
+		}
+
+		rd := bufio.NewReader(c)
+		s, err := rd.ReadString('\n')
+		if err != nil {
+			t.Errorf("Failed to read string: %v", err)
+		}
+
+		got := s[27:]
+		expected := "lvl=info msg=test x=1\n"
+		if got != expected {
+			t.Errorf("Got log line %s, expected %s", got, expected)
+		}
+
+		errs <- nil
+	}()
+
+	lg := New()
+	h, err := NetHandler("tcp", l.Addr().String(), LogfmtFormat())
+	if err != nil {
+		t.Fatal(err)
+	}
+	lg.SetHandler(h)
+	lg.Info("test", "x", 1)
+
+	select {
+	case <-time.After(time.Second):
+		t.Fatalf("Test timed out!")
+	case <-errs:
+		// ok
+	}
+}
+
+func TestMatchFilterHandler(t *testing.T) {
+	t.Parallel()
+
+	l, h, r := testLogger()
+	l.SetHandler(MatchFilterHandler("err", nil, h))
+
+	l.Crit("test", "foo", "bar")
+	if r.Msg != "" {
+		t.Fatalf("expected filter handler to discard msg")
+	}
+
+	l.Crit("test2", "err", "bad fd")
+	if r.Msg != "" {
+		t.Fatalf("expected filter handler to discard msg")
+	}
+
+	l.Crit("test3", "err", nil)
+	if r.Msg != "test3" {
+		t.Fatalf("expected filter handler to allow msg")
+	}
+}
+
+func TestMatchFilterBuiltin(t *testing.T) {
+	t.Parallel()
+
+	l, h, r := testLogger()
+	l.SetHandler(MatchFilterHandler("lvl", LvlError, h))
+	l.Info("does not pass")
+
+	if r.Msg != "" {
+		t.Fatalf("got info level record that should not have matched")
+	}
+
+	l.Error("error!")
+	if r.Msg != "error!" {
+		t.Fatalf("did not get error level record that should have matched")
+	}
+
+	r.Msg = ""
+	l.SetHandler(MatchFilterHandler("msg", "matching message", h))
+	l.Info("doesn't match")
+	if r.Msg != "" {
+		t.Fatalf("got record with wrong message matched")
+	}
+
+	l.Debug("matching message")
+	if r.Msg != "matching message" {
+		t.Fatalf("did not get record which matches")
+	}
+}
+
+type failingWriter struct {
+	fail bool
+}
+
+func (w *failingWriter) Write(buf []byte) (int, error) {
+	if w.fail {
+		return 0, errors.New("fail")
+	} else {
+		return len(buf), nil
+	}
+}
+
+func TestFailoverHandler(t *testing.T) {
+	t.Parallel()
+
+	l := New()
+	h, r := testHandler()
+	w := &failingWriter{false}
+
+	l.SetHandler(FailoverHandler(
+		StreamHandler(w, JsonFormat()),
+		h))
+
+	l.Debug("test ok")
+	if r.Msg != "" {
+		t.Fatalf("expected no failover")
+	}
+
+	w.fail = true
+	l.Debug("test failover", "x", 1)
+	if r.Msg != "test failover" {
+		t.Fatalf("expected failover")
+	}
+
+	if len(r.Ctx) != 4 {
+		t.Fatalf("expected additional failover ctx")
+	}
+
+	got := r.Ctx[2]
+	expected := "failover_err_0"
+	if got != expected {
+		t.Fatalf("expected failover ctx. got: %s, expected %s", got, expected)
+	}
+}
+
+// https://github.com/inconshreveable/log15/issues/16
+func TestIndependentSetHandler(t *testing.T) {
+	t.Parallel()
+
+	parent, _, r := testLogger()
+	child := parent.New()
+	child.SetHandler(DiscardHandler())
+	parent.Info("test")
+	if r.Msg != "test" {
+		t.Fatalf("parent handler affected by child")
+	}
+}
+
+// https://github.com/inconshreveable/log15/issues/16
+func TestInheritHandler(t *testing.T) {
+	t.Parallel()
+
+	parent, _, r := testLogger()
+	child := parent.New()
+	parent.SetHandler(DiscardHandler())
+	child.Info("test")
+	if r.Msg == "test" {
+		t.Fatalf("child handler affected not affected by parent")
+	}
+}
+
+func TestCallerFileHandler(t *testing.T) {
+	t.Parallel()
+
+	l := New()
+	h, r := testHandler()
+	l.SetHandler(CallerFileHandler(h))
+
+	l.Info("baz")
+	_, _, line, _ := runtime.Caller(0)
+
+	if len(r.Ctx) != 2 {
+		t.Fatalf("Expected caller in record context. Got length %d, expected %d", len(r.Ctx), 2)
+	}
+
+	const key = "caller"
+
+	if r.Ctx[0] != key {
+		t.Fatalf("Wrong context key, got %s expected %s", r.Ctx[0], key)
+	}
+
+	s, ok := r.Ctx[1].(string)
+	if !ok {
+		t.Fatalf("Wrong context value type, got %T expected string", r.Ctx[1])
+	}
+
+	exp := fmt.Sprint("log15_test.go:", line-1)
+	if s != exp {
+		t.Fatalf("Wrong context value, got %s expected string matching %s", s, exp)
+	}
+}
+
+func TestCallerFuncHandler(t *testing.T) {
+	t.Parallel()
+
+	l := New()
+	h, r := testHandler()
+	l.SetHandler(CallerFuncHandler(h))
+
+	l.Info("baz")
+
+	if len(r.Ctx) != 2 {
+		t.Fatalf("Expected caller in record context. Got length %d, expected %d", len(r.Ctx), 2)
+	}
+
+	const key = "fn"
+
+	if r.Ctx[0] != key {
+		t.Fatalf("Wrong context key, got %s expected %s", r.Ctx[0], key)
+	}
+
+	const regex = ".*\\.TestCallerFuncHandler"
+
+	s, ok := r.Ctx[1].(string)
+	if !ok {
+		t.Fatalf("Wrong context value type, got %T expected string", r.Ctx[1])
+	}
+
+	match, err := regexp.MatchString(regex, s)
+	if err != nil {
+		t.Fatalf("Error matching %s to regex %s: %v", s, regex, err)
+	}
+
+	if !match {
+		t.Fatalf("Wrong context value, got %s expected string matching %s", s, regex)
+	}
+}
+
+// https://github.com/inconshreveable/log15/issues/27
+func TestCallerStackHandler(t *testing.T) {
+	t.Parallel()
+
+	l := New()
+	h, r := testHandler()
+	l.SetHandler(CallerStackHandler("%#v", h))
+
+	lines := []int{}
+
+	func() {
+		l.Info("baz")
+		_, _, line, _ := runtime.Caller(0)
+		lines = append(lines, line-1)
+	}()
+	_, file, line, _ := runtime.Caller(0)
+	lines = append(lines, line-1)
+
+	if len(r.Ctx) != 2 {
+		t.Fatalf("Expected stack in record context. Got length %d, expected %d", len(r.Ctx), 2)
+	}
+
+	const key = "stack"
+
+	if r.Ctx[0] != key {
+		t.Fatalf("Wrong context key, got %s expected %s", r.Ctx[0], key)
+	}
+
+	s, ok := r.Ctx[1].(string)
+	if !ok {
+		t.Fatalf("Wrong context value type, got %T expected string", r.Ctx[1])
+	}
+
+	exp := "["
+	for i, line := range lines {
+		if i > 0 {
+			exp += " "
+		}
+		exp += fmt.Sprint(file, ":", line)
+	}
+	exp += "]"
+
+	if s != exp {
+		t.Fatalf("Wrong context value, got %s expected string matching %s", s, exp)
+	}
+}
+
+// tests that when logging concurrently to the same logger
+// from multiple goroutines that the calls are handled independently
+// this test tries to trigger a previous bug where concurrent calls could
+// corrupt each other's context values
+//
+// this test runs N concurrent goroutines each logging a fixed number of
+// records and a handler that buckets them based on the index passed in the context.
+// if the logger is not concurrent-safe then the values in the buckets will not all be the same
+//
+// https://github.com/inconshreveable/log15/pull/30
+func TestConcurrent(t *testing.T) {
+	root := New()
+	// this was the first value that triggered
+	// go to allocate extra capacity in the logger's context slice which
+	// was necessary to trigger the bug
+	const ctxLen = 34
+	l := root.New(make([]interface{}, ctxLen)...)
+	const goroutines = 8
+	var res [goroutines]int
+	l.SetHandler(SyncHandler(FuncHandler(func(r *Record) error {
+		res[r.Ctx[ctxLen+1].(int)]++
+		return nil
+	})))
+	var wg sync.WaitGroup
+	wg.Add(goroutines)
+	for i := 0; i < goroutines; i++ {
+		go func(idx int) {
+			defer wg.Done()
+			for j := 0; j < 10000; j++ {
+				l.Info("test message", "goroutine_idx", idx)
+			}
+		}(i)
+	}
+	wg.Wait()
+	for _, val := range res[:] {
+		if val != 10000 {
+			t.Fatalf("Wrong number of messages for context: %+v", res)
+		}
+	}
+}
diff --git a/shared/log15/logger.go b/shared/log15/logger.go
new file mode 100644
index 000000000..dcd7cf8db
--- /dev/null
+++ b/shared/log15/logger.go
@@ -0,0 +1,201 @@
+package log15
+
+import (
+	"fmt"
+	"runtime"
+	"time"
+)
+
+const timeKey = "t"
+const lvlKey = "lvl"
+const msgKey = "msg"
+const errorKey = "LOG15_ERROR"
+
+type Lvl int
+
+const (
+	LvlCrit Lvl = iota
+	LvlError
+	LvlWarn
+	LvlInfo
+	LvlDebug
+)
+
+// Returns the name of a Lvl
+func (l Lvl) String() string {
+	switch l {
+	case LvlDebug:
+		return "dbug"
+	case LvlInfo:
+		return "info"
+	case LvlWarn:
+		return "warn"
+	case LvlError:
+		return "eror"
+	case LvlCrit:
+		return "crit"
+	default:
+		panic("bad level")
+	}
+}
+
+// Returns the appropriate Lvl from a string name.
+// Useful for parsing command line args and configuration files.
+func LvlFromString(lvlString string) (Lvl, error) {
+	switch lvlString {
+	case "debug", "dbug":
+		return LvlDebug, nil
+	case "info":
+		return LvlInfo, nil
+	case "warn":
+		return LvlWarn, nil
+	case "error", "eror":
+		return LvlError, nil
+	case "crit":
+		return LvlCrit, nil
+	default:
+		return LvlDebug, fmt.Errorf("Unknown level: %v", lvlString)
+	}
+}
+
+// A Record is what a Logger asks its handler to write
+type Record struct {
+	Time     time.Time
+	Lvl      Lvl
+	Msg      string
+	Ctx      []interface{}
+	CallPC   [1]uintptr
+	KeyNames RecordKeyNames
+}
+
+type RecordKeyNames struct {
+	Time string
+	Msg  string
+	Lvl  string
+}
+
+// A Logger writes key/value pairs to a Handler
+type Logger interface {
+	// New returns a new Logger that has this logger's context plus the given context
+	New(ctx ...interface{}) Logger
+
+	// SetHandler updates the logger to write records to the specified handler.
+	SetHandler(h Handler)
+
+	// Log a message at the given level with context key/value pairs
+	Debug(msg string, ctx ...interface{})
+	Info(msg string, ctx ...interface{})
+	Warn(msg string, ctx ...interface{})
+	Error(msg string, ctx ...interface{})
+	Crit(msg string, ctx ...interface{})
+}
+
+type logger struct {
+	ctx []interface{}
+	h   *swapHandler
+}
+
+func (l *logger) write(msg string, lvl Lvl, ctx []interface{}) {
+	r := Record{
+		Time: time.Now(),
+		Lvl:  lvl,
+		Msg:  msg,
+		Ctx:  newContext(l.ctx, ctx),
+		KeyNames: RecordKeyNames{
+			Time: timeKey,
+			Msg:  msgKey,
+			Lvl:  lvlKey,
+		},
+	}
+	runtime.Callers(3, r.CallPC[:])
+	l.h.Log(&r)
+}
+
+func (l *logger) New(ctx ...interface{}) Logger {
+	child := &logger{newContext(l.ctx, ctx), new(swapHandler)}
+	child.SetHandler(l.h)
+	return child
+}
+
+func newContext(prefix []interface{}, suffix []interface{}) []interface{} {
+	normalizedSuffix := normalize(suffix)
+	newCtx := make([]interface{}, len(prefix)+len(normalizedSuffix))
+	n := copy(newCtx, prefix)
+	copy(newCtx[n:], normalizedSuffix)
+	return newCtx
+}
+
+func (l *logger) Debug(msg string, ctx ...interface{}) {
+	l.write(msg, LvlDebug, ctx)
+}
+
+func (l *logger) Info(msg string, ctx ...interface{}) {
+	l.write(msg, LvlInfo, ctx)
+}
+
+func (l *logger) Warn(msg string, ctx ...interface{}) {
+	l.write(msg, LvlWarn, ctx)
+}
+
+func (l *logger) Error(msg string, ctx ...interface{}) {
+	l.write(msg, LvlError, ctx)
+}
+
+func (l *logger) Crit(msg string, ctx ...interface{}) {
+	l.write(msg, LvlCrit, ctx)
+}
+
+func (l *logger) SetHandler(h Handler) {
+	l.h.Swap(h)
+}
+
+func normalize(ctx []interface{}) []interface{} {
+	// if the caller passed a Ctx object, then expand it
+	if len(ctx) == 1 {
+		if ctxMap, ok := ctx[0].(Ctx); ok {
+			ctx = ctxMap.toArray()
+		}
+	}
+
+	// ctx needs to be even because it's a series of key/value pairs
+	// no one wants to check for errors on logging functions,
+	// so instead of erroring on bad input, we'll just make sure
+	// that things are the right length and users can fix bugs
+	// when they see the output looks wrong
+	if len(ctx)%2 != 0 {
+		ctx = append(ctx, nil, errorKey, "Normalized odd number of arguments by adding nil")
+	}
+
+	return ctx
+}
+
+// Lazy allows you to defer calculation of a logged value that is expensive
+// to compute until it is certain that it must be evaluated with the given filters.
+//
+// Lazy may also be used in conjunction with a Logger's New() function
+// to generate a child logger which always reports the current value of changing
+// state.
+//
+// You may wrap any function which takes no arguments to Lazy. It may return any
+// number of values of any type.
+type Lazy struct {
+	Fn interface{}
+}
+
+// Ctx is a map of key/value pairs to pass as context to a log function
+// Use this only if you really need greater safety around the arguments you pass
+// to the logging functions.
+type Ctx map[string]interface{}
+
+func (c Ctx) toArray() []interface{} {
+	arr := make([]interface{}, len(c)*2)
+
+	i := 0
+	for k, v := range c {
+		arr[i] = k
+		arr[i+1] = v
+		i += 2
+	}
+
+	return arr
+}
diff --git a/shared/log15/root.go b/shared/log15/root.go
new file mode 100644
index 000000000..ed3a3cf54
--- /dev/null
+++ b/shared/log15/root.go
@@ -0,0 +1,67 @@
+package log15
+
+import (
+	"os"
+
+	"github.com/lxc/lxd/shared/log15/term"
+	"github.com/mattn/go-colorable"
+)
+
+var (
+	root          *logger
+	StdoutHandler = StreamHandler(os.Stdout, LogfmtFormat())
+	StderrHandler = StreamHandler(os.Stderr, LogfmtFormat())
+)
+
+func init() {
+	if term.IsTty(os.Stdout.Fd()) {
+		StdoutHandler = StreamHandler(colorable.NewColorableStdout(), TerminalFormat())
+	}
+
+	if term.IsTty(os.Stderr.Fd()) {
+		StderrHandler = StreamHandler(colorable.NewColorableStderr(), TerminalFormat())
+	}
+
+	root = &logger{[]interface{}{}, new(swapHandler)}
+	root.SetHandler(StdoutHandler)
+}
+
+// New returns a new logger with the given context.
+// New is a convenient alias for Root().New
+func New(ctx ...interface{}) Logger {
+	return root.New(ctx...)
+}
+
+// Root returns the root logger
+func Root() Logger {
+	return root
+}
+
+// The following functions bypass the exported logger methods (logger.Debug,
+// etc.) to keep the call depth the same for all paths to logger.write so
+// runtime.Caller(2) always refers to the call site in client code.
+
+// Debug is a convenient alias for Root().Debug
+func Debug(msg string, ctx ...interface{}) {
+	root.write(msg, LvlDebug, ctx)
+}
+
+// Info is a convenient alias for Root().Info
+func Info(msg string, ctx ...interface{}) {
+	root.write(msg, LvlInfo, ctx)
+}
+
+// Warn is a convenient alias for Root().Warn
+func Warn(msg string, ctx ...interface{}) {
+	root.write(msg, LvlWarn, ctx)
+}
+
+// Error is a convenient alias for Root().Error
+func Error(msg string, ctx ...interface{}) {
+	root.write(msg, LvlError, ctx)
+}
+
+// Crit is a convenient alias for Root().Crit
+func Crit(msg string, ctx ...interface{}) {
+	root.write(msg, LvlCrit, ctx)
+}
diff --git a/shared/log15/stack/stack.go b/shared/log15/stack/stack.go
new file mode 100644
index 000000000..ae3021cce
--- /dev/null
+++ b/shared/log15/stack/stack.go
@@ -0,0 +1,225 @@
+// Package stack implements utilities to capture, manipulate, and format call
+// stacks.
+package stack
+
+import (
+	"fmt"
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+// Call records a single function invocation from a goroutine stack. It is a
+// wrapper for the program counter values returned by runtime.Caller and
+// runtime.Callers and consumed by runtime.FuncForPC.
+type Call uintptr
+
+// Format implements fmt.Formatter with support for the following verbs.
+//
+//    %s    source file
+//    %d    line number
+//    %n    function name
+//    %v    equivalent to %s:%d
+//
+// It accepts the '+' and '#' flags for most of the verbs as follows.
+//
+//    %+s   path of source file relative to the compile time GOPATH
+//    %#s   full path of source file
+//    %+n   import path qualified function name
+//    %+v   equivalent to %+s:%d
+//    %#v   equivalent to %#s:%d
+func (pc Call) Format(s fmt.State, c rune) {
+	// BUG(ChrisHines): Subtracting one from pc is a work around for
+	// https://code.google.com/p/go/issues/detail?id=7690. The idea for this
+	// work around comes from rsc's initial patch at
+	// https://codereview.appspot.com/84100043/#ps20001, but as noted in the
+	// issue discussion, it is not a complete fix since it doesn't handle some
+	// cases involving signals. Just the same, it handles all of the other
+	// cases I have tested.
+	pcFix := uintptr(pc) - 1
+	fn := runtime.FuncForPC(pcFix)
+	if fn == nil {
+		fmt.Fprintf(s, "%%!%c(NOFUNC)", c)
+		return
+	}
+
+	switch c {
+	case 's', 'v':
+		file, line := fn.FileLine(pcFix)
+		switch {
+		case s.Flag('#'):
+			// done
+		case s.Flag('+'):
+			// Here we want to get the source file path relative to the
+			// compile time GOPATH. As of Go 1.3.x there is no direct way to
+			// know the compiled GOPATH at runtime, but we can infer the
+			// number of path segments in the GOPATH. We note that fn.Name()
+			// returns the function name qualified by the import path, which
+			// does not include the GOPATH. Thus we can trim segments from the
+			// beginning of the file path until the number of path separators
+			// remaining is one more than the number of path separators in the
+			// function name. For example, given:
+			//
+			//    GOPATH     /home/user
+			//    file       /home/user/src/pkg/sub/file.go
+			//    fn.Name()  pkg/sub.Type.Method
+			//
+			// We want to produce:
+			//
+			//    pkg/sub/file.go
+			//
+			// From this we can easily see that fn.Name() has one less path
+			// separator than our desired output.
+			const sep = "/"
+			impCnt := strings.Count(fn.Name(), sep) + 1
+			pathCnt := strings.Count(file, sep)
+			for pathCnt > impCnt {
+				i := strings.Index(file, sep)
+				if i == -1 {
+					break
+				}
+				file = file[i+len(sep):]
+				pathCnt--
+			}
+		default:
+			const sep = "/"
+			if i := strings.LastIndex(file, sep); i != -1 {
+				file = file[i+len(sep):]
+			}
+		}
+		fmt.Fprint(s, file)
+		if c == 'v' {
+			fmt.Fprint(s, ":", line)
+		}
+
+	case 'd':
+		_, line := fn.FileLine(pcFix)
+		fmt.Fprint(s, line)
+
+	case 'n':
+		name := fn.Name()
+		if !s.Flag('+') {
+			const pathSep = "/"
+			if i := strings.LastIndex(name, pathSep); i != -1 {
+				name = name[i+len(pathSep):]
+			}
+			const pkgSep = "."
+			if i := strings.Index(name, pkgSep); i != -1 {
+				name = name[i+len(pkgSep):]
+			}
+		}
+		fmt.Fprint(s, name)
+	}
+}
+
+// Callers returns a Trace for the current goroutine with element 0
+// identifying the calling function.
+func Callers() Trace {
+	pcs := poolBuf()
+	pcs = pcs[:cap(pcs)]
+	n := runtime.Callers(2, pcs)
+	cs := make([]Call, n)
+	for i, pc := range pcs[:n] {
+		cs[i] = Call(pc)
+	}
+	putPoolBuf(pcs)
+	return cs
+}
+
+// name returns the import path qualified name of the function containing the
+// call.
+func (pc Call) name() string {
+	pcFix := uintptr(pc) - 1 // work around for go issue #7690
+	fn := runtime.FuncForPC(pcFix)
+	if fn == nil {
+		return "???"
+	}
+	return fn.Name()
+}
+
+func (pc Call) file() string {
+	pcFix := uintptr(pc) - 1 // work around for go issue #7690
+	fn := runtime.FuncForPC(pcFix)
+	if fn == nil {
+		return "???"
+	}
+	file, _ := fn.FileLine(pcFix)
+	return file
+}
+
+// Trace records a sequence of function invocations from a goroutine stack.
+type Trace []Call
+
+// Format implements fmt.Formatter by printing the Trace as square brackes ([,
+// ]) surrounding a space separated list of Calls each formatted with the
+// supplied verb and options.
+func (pcs Trace) Format(s fmt.State, c rune) {
+	s.Write([]byte("["))
+	for i, pc := range pcs {
+		if i > 0 {
+			s.Write([]byte(" "))
+		}
+		pc.Format(s, c)
+	}
+	s.Write([]byte("]"))
+}
+
+// TrimBelow returns a slice of the Trace with all entries below pc removed.
+func (pcs Trace) TrimBelow(pc Call) Trace {
+	for len(pcs) > 0 && pcs[0] != pc {
+		pcs = pcs[1:]
+	}
+	return pcs
+}
+
+// TrimAbove returns a slice of the Trace with all entries above pc removed.
+func (pcs Trace) TrimAbove(pc Call) Trace {
+	for len(pcs) > 0 && pcs[len(pcs)-1] != pc {
+		pcs = pcs[:len(pcs)-1]
+	}
+	return pcs
+}
+
+// TrimBelowName returns a slice of the Trace with all entries below the
+// lowest with function name name removed.
+func (pcs Trace) TrimBelowName(name string) Trace {
+	for len(pcs) > 0 && pcs[0].name() != name {
+		pcs = pcs[1:]
+	}
+	return pcs
+}
+
+// TrimAboveName returns a slice of the Trace with all entries above the
+// highest with function name name removed.
+func (pcs Trace) TrimAboveName(name string) Trace {
+	for len(pcs) > 0 && pcs[len(pcs)-1].name() != name {
+		pcs = pcs[:len(pcs)-1]
+	}
+	return pcs
+}
+
+var goroot string
+
+func init() {
+	goroot = filepath.ToSlash(runtime.GOROOT())
+	if runtime.GOOS == "windows" {
+		goroot = strings.ToLower(goroot)
+	}
+}
+
+func inGoroot(path string) bool {
+	if runtime.GOOS == "windows" {
+		path = strings.ToLower(path)
+	}
+	return strings.HasPrefix(path, goroot)
+}
+
+// TrimRuntime returns a slice of the Trace with the topmost entries from the
+// go runtime removed. It considers any calls originating from files under
+// GOROOT as part of the runtime.
+func (pcs Trace) TrimRuntime() Trace {
+	for len(pcs) > 0 && inGoroot(pcs[len(pcs)-1].file()) {
+		pcs = pcs[:len(pcs)-1]
+	}
+	return pcs
+}
diff --git a/shared/log15/stack/stack_pool.go b/shared/log15/stack/stack_pool.go
new file mode 100644
index 000000000..34f2ca970
--- /dev/null
+++ b/shared/log15/stack/stack_pool.go
@@ -0,0 +1,19 @@
+// +build go1.3
+
+package stack
+
+import (
+	"sync"
+)
+
+var pcStackPool = sync.Pool{
+	New: func() interface{} { return make([]uintptr, 1000) },
+}
+
+func poolBuf() []uintptr {
+	return pcStackPool.Get().([]uintptr)
+}
+
+func putPoolBuf(p []uintptr) {
+	pcStackPool.Put(p)
+}
diff --git a/shared/log15/stack/stack_pool_chan.go b/shared/log15/stack/stack_pool_chan.go
new file mode 100644
index 000000000..a9d6c154d
--- /dev/null
+++ b/shared/log15/stack/stack_pool_chan.go
@@ -0,0 +1,27 @@
+// +build !go1.3 appengine
+
+package stack
+
+const (
+	stackPoolSize = 64
+)
+
+var (
+	pcStackPool = make(chan []uintptr, stackPoolSize)
+)
+
+func poolBuf() []uintptr {
+	select {
+	case p := <-pcStackPool:
+		return p
+	default:
+		return make([]uintptr, 1000)
+	}
+}
+
+func putPoolBuf(p []uintptr) {
+	select {
+	case pcStackPool <- p:
+	default:
+	}
+}
diff --git a/shared/log15/stack/stack_test.go b/shared/log15/stack/stack_test.go
new file mode 100644
index 000000000..1df1ef3dd
--- /dev/null
+++ b/shared/log15/stack/stack_test.go
@@ -0,0 +1,219 @@
+package stack_test
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/lxc/lxd/shared/log15/stack"
+)
+
+type testType struct{}
+
+func (tt testType) testMethod() (pc uintptr, file string, line int, ok bool) {
+	return runtime.Caller(0)
+}
+
+func TestCallFormat(t *testing.T) {
+	t.Parallel()
+
+	pc, file, line, ok := runtime.Caller(0)
+	if !ok {
+		t.Fatal("runtime.Caller(0) failed")
+	}
+
+	gopathSrc := filepath.Join(os.Getenv("GOPATH"), "src")
+	relFile, err := filepath.Rel(gopathSrc, file)
+	if err != nil {
+		t.Fatalf("failed to determine path relative to GOPATH: %v", err)
+	}
+	relFile = filepath.ToSlash(relFile)
+
+	pc2, file2, line2, ok2 := testType{}.testMethod()
+	if !ok2 {
+		t.Fatal("runtime.Caller(0) failed")
+	}
+	relFile2, err := filepath.Rel(gopathSrc, file)
+	if err != nil {
+		t.Fatalf("failed to determine path relative to GOPATH: %v", err)
+	}
+	relFile2 = filepath.ToSlash(relFile2)
+
+	data := []struct {
+		pc   uintptr
+		desc string
+		fmt  string
+		out  string
+	}{
+		{0, "error", "%s", "%!s(NOFUNC)"},
+
+		{pc, "func", "%s", path.Base(file)},
+		{pc, "func", "%+s", relFile},
+		{pc, "func", "%#s", file},
+		{pc, "func", "%d", fmt.Sprint(line)},
+		{pc, "func", "%n", "TestCallFormat"},
+		{pc, "func", "%+n", runtime.FuncForPC(pc).Name()},
+		{pc, "func", "%v", fmt.Sprint(path.Base(file), ":", line)},
+		{pc, "func", "%+v", fmt.Sprint(relFile, ":", line)},
+		{pc, "func", "%#v", fmt.Sprint(file, ":", line)},
+		{pc, "func", "%v|%[1]n()", fmt.Sprint(path.Base(file), ":", line, "|", "TestCallFormat()")},
+
+		{pc2, "meth", "%s", path.Base(file2)},
+		{pc2, "meth", "%+s", relFile2},
+		{pc2, "meth", "%#s", file2},
+		{pc2, "meth", "%d", fmt.Sprint(line2)},
+		{pc2, "meth", "%n", "testType.testMethod"},
+		{pc2, "meth", "%+n", runtime.FuncForPC(pc2).Name()},
+		{pc2, "meth", "%v", fmt.Sprint(path.Base(file2), ":", line2)},
+		{pc2, "meth", "%+v", fmt.Sprint(relFile2, ":", line2)},
+		{pc2, "meth", "%#v", fmt.Sprint(file2, ":", line2)},
+		{pc2, "meth", "%v|%[1]n()", fmt.Sprint(path.Base(file2), ":", line2, "|", "testType.testMethod()")},
+	}
+
+	for _, d := range data {
+		got := fmt.Sprintf(d.fmt, stack.Call(d.pc))
+		if got != d.out {
+			t.Errorf("fmt.Sprintf(%q, Call(%s)) = %s, want %s", d.fmt, d.desc, got, d.out)
+		}
+	}
+}
+
+func BenchmarkCallVFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprint(ioutil.Discard, stack.Call(pc))
+	}
+}
+
+func BenchmarkCallPlusVFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%+v", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallSharpVFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%#v", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallSFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%s", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallPlusSFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%+s", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallDFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%d", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallNFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%n", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallPlusNFmt(b *testing.B) {
+	pc, _, _, ok := runtime.Caller(0)
+	if !ok {
+		b.Fatal("runtime.Caller(0) failed")
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		fmt.Fprintf(ioutil.Discard, "%+n", stack.Call(pc))
+	}
+}
+
+func BenchmarkCallers(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		stack.Callers()
+	}
+}
+
+func deepStack(depth int, b *testing.B) stack.Trace {
+	if depth > 0 {
+		return deepStack(depth-1, b)
+	}
+	b.StartTimer()
+	s := stack.Callers()
+	b.StopTimer()
+	return s
+}
+
+func BenchmarkCallers10(b *testing.B) {
+	b.StopTimer()
+
+	for i := 0; i < b.N; i++ {
+		deepStack(10, b)
+	}
+}
+
+func BenchmarkCallers50(b *testing.B) {
+	b.StopTimer()
+
+	for i := 0; i < b.N; i++ {
+		deepStack(50, b)
+	}
+}
+
+func BenchmarkCallers100(b *testing.B) {
+	b.StopTimer()
+
+	for i := 0; i < b.N; i++ {
+		deepStack(100, b)
+	}
+}
diff --git a/shared/log15/syslog.go b/shared/log15/syslog.go
new file mode 100644
index 000000000..36c12b11f
--- /dev/null
+++ b/shared/log15/syslog.go
@@ -0,0 +1,55 @@
+// +build !windows,!plan9
+
+package log15
+
+import (
+	"log/syslog"
+	"strings"
+)
+
+// SyslogHandler opens a connection to the system syslog daemon by calling
+// syslog.New and writes all records to it.
+func SyslogHandler(tag string, fmtr Format) (Handler, error) {
+	wr, err := syslog.New(syslog.LOG_INFO, tag)
+	return sharedSyslog(fmtr, wr, err)
+}
+
+// SyslogHandler opens a connection to a log daemon over the network and writes
+// all log records to it.
+func SyslogNetHandler(net, addr string, tag string, fmtr Format) (Handler, error) {
+	wr, err := syslog.Dial(net, addr, syslog.LOG_INFO, tag)
+	return sharedSyslog(fmtr, wr, err)
+}
+
+func sharedSyslog(fmtr Format, sysWr *syslog.Writer, err error) (Handler, error) {
+	if err != nil {
+		return nil, err
+	}
+	h := FuncHandler(func(r *Record) error {
+		var syslogFn = sysWr.Info
+		switch r.Lvl {
+		case LvlCrit:
+			syslogFn = sysWr.Crit
+		case LvlError:
+			syslogFn = sysWr.Err
+		case LvlWarn:
+			syslogFn = sysWr.Warning
+		case LvlInfo:
+			syslogFn = sysWr.Info
+		case LvlDebug:
+			syslogFn = sysWr.Debug
+		}
+
+		s := strings.TrimSpace(string(fmtr.Format(r)))
+		return syslogFn(s)
+	})
+	return LazyHandler(&closingHandler{sysWr, h}), nil
+}
+
+func (m muster) SyslogHandler(tag string, fmtr Format) Handler {
+	return must(SyslogHandler(tag, fmtr))
+}
+
+func (m muster) SyslogNetHandler(net, addr string, tag string, fmtr Format) Handler {
+	return must(SyslogNetHandler(net, addr, tag, fmtr))
+}
diff --git a/shared/log15/term/LICENSE b/shared/log15/term/LICENSE
new file mode 100644
index 000000000..f090cb42f
--- /dev/null
+++ b/shared/log15/term/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Simon Eskildsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/shared/log15/term/terminal_appengine.go b/shared/log15/term/terminal_appengine.go
new file mode 100644
index 000000000..c1b5d2a3b
--- /dev/null
+++ b/shared/log15/term/terminal_appengine.go
@@ -0,0 +1,13 @@
+// Based on ssh/terminal:
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build appengine
+
+package term
+
+// IsTty always returns false on AppEngine.
+func IsTty(fd uintptr) bool {
+	return false
+}
diff --git a/shared/log15/term/terminal_darwin.go b/shared/log15/term/terminal_darwin.go
new file mode 100644
index 000000000..b05de4cb8
--- /dev/null
+++ b/shared/log15/term/terminal_darwin.go
@@ -0,0 +1,12 @@
+// Based on ssh/terminal:
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package term
+
+import "syscall"
+
+const ioctlReadTermios = syscall.TIOCGETA
+
+type Termios syscall.Termios
diff --git a/shared/log15/term/terminal_freebsd.go b/shared/log15/term/terminal_freebsd.go
new file mode 100644
index 000000000..cfaceab33
--- /dev/null
+++ b/shared/log15/term/terminal_freebsd.go
@@ -0,0 +1,18 @@
+package term
+
+import (
+	"syscall"
+)
+
+const ioctlReadTermios = syscall.TIOCGETA
+
+// Go 1.2 doesn't include Termios for FreeBSD. This should be added in 1.3 and this could be merged with terminal_darwin.
+type Termios struct {
+	Iflag  uint32
+	Oflag  uint32
+	Cflag  uint32
+	Lflag  uint32
+	Cc     [20]uint8
+	Ispeed uint32
+	Ospeed uint32
+}
diff --git a/shared/log15/term/terminal_linux.go b/shared/log15/term/terminal_linux.go
new file mode 100644
index 000000000..5290468d6
--- /dev/null
+++ b/shared/log15/term/terminal_linux.go
@@ -0,0 +1,14 @@
+// Based on ssh/terminal:
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !appengine
+
+package term
+
+import "syscall"
+
+const ioctlReadTermios = syscall.TCGETS
+
+type Termios syscall.Termios
diff --git a/shared/log15/term/terminal_notwindows.go b/shared/log15/term/terminal_notwindows.go
new file mode 100644
index 000000000..87df7d5b0
--- /dev/null
+++ b/shared/log15/term/terminal_notwindows.go
@@ -0,0 +1,20 @@
+// Based on ssh/terminal:
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build linux,!appengine darwin freebsd openbsd
+
+package term
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+// IsTty returns true if the given file descriptor is a terminal.
+func IsTty(fd uintptr) bool {
+	var termios Termios
+	_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
+	return err == 0
+}
diff --git a/shared/log15/term/terminal_openbsd.go b/shared/log15/term/terminal_openbsd.go
new file mode 100644
index 000000000..f9bb9e1c2
--- /dev/null
+++ b/shared/log15/term/terminal_openbsd.go
@@ -0,0 +1,7 @@
+package term
+
+import "syscall"
+
+const ioctlReadTermios = syscall.TIOCGETA
+
+type Termios syscall.Termios
diff --git a/shared/log15/term/terminal_windows.go b/shared/log15/term/terminal_windows.go
new file mode 100644
index 000000000..df3c30c15
--- /dev/null
+++ b/shared/log15/term/terminal_windows.go
@@ -0,0 +1,26 @@
+// Based on ssh/terminal:
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package term
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+var kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+var (
+	procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
+)
+
+// IsTty returns true if the given file descriptor is a terminal.
+func IsTty(fd uintptr) bool {
+	var st uint32
+	r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
+	return r != 0 && e == 0
+}

From 69b2794495bf998508dc87d3ff0a983547d18bec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 13 Nov 2017 00:05:17 -0500
Subject: [PATCH 2/3] Revert "Temporary workaround for log15 API breakage"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit 30c859499e24ba6255508cd664c09cf4c1731bbe.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 shared/logging/log_posix.go | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/shared/logging/log_posix.go b/shared/logging/log_posix.go
index 5ada8081e..ffc55da5d 100644
--- a/shared/logging/log_posix.go
+++ b/shared/logging/log_posix.go
@@ -3,8 +3,6 @@
 package logging
 
 import (
-	slog "log/syslog"
-
 	log "gopkg.in/inconshreveable/log15.v2"
 )
 
@@ -15,11 +13,11 @@ func getSystemHandler(syslog string, debug bool, format log.Format) log.Handler
 		if !debug {
 			return log.LvlFilterHandler(
 				log.LvlInfo,
-				log.Must.SyslogHandler(slog.LOG_INFO, syslog, format),
+				log.Must.SyslogHandler(syslog, format),
 			)
 		}
 
-		return log.Must.SyslogHandler(slog.LOG_INFO, syslog, format)
+		return log.Must.SyslogHandler(syslog, format)
 	}
 
 	return nil

From 0635414ef749bbee221a5191e6e71dd61be24056 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 13 Nov 2017 00:00:02 -0500
Subject: [PATCH 3/3] Switch to the built-in log15
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/api.go                    | 2 +-
 lxd/api_internal.go           | 2 +-
 lxd/apparmor.go               | 2 +-
 lxd/config/safe.go            | 2 +-
 lxd/container_exec.go         | 2 +-
 lxd/container_lxc.go          | 2 +-
 lxd/containers.go             | 2 +-
 lxd/containers_post.go        | 2 +-
 lxd/daemon.go                 | 2 +-
 lxd/daemon_config.go          | 2 +-
 lxd/daemon_images.go          | 2 +-
 lxd/db/containers.go          | 2 +-
 lxd/debug/memory_test.go      | 2 +-
 lxd/devices.go                | 2 +-
 lxd/endpoints/endpoints.go    | 2 +-
 lxd/endpoints/network.go      | 2 +-
 lxd/events.go                 | 2 +-
 lxd/images.go                 | 2 +-
 lxd/logging.go                | 2 +-
 lxd/main_subcommand.go        | 2 +-
 lxd/networks.go               | 2 +-
 lxd/patches.go                | 2 +-
 lxd/profiles.go               | 2 +-
 lxd/sys/apparmor.go           | 2 +-
 lxd/sys/os.go                 | 2 +-
 lxd/util/http.go              | 2 +-
 lxd/util/sys.go               | 2 +-
 shared/logging/format.go      | 2 +-
 shared/logging/log.go         | 4 ++--
 shared/logging/log_posix.go   | 2 +-
 shared/logging/log_windows.go | 2 +-
 shared/logging/testing.go     | 2 +-
 32 files changed, 33 insertions(+), 33 deletions(-)

diff --git a/lxd/api.go b/lxd/api.go
index d5d6e01ab..94ba9c285 100644
--- a/lxd/api.go
+++ b/lxd/api.go
@@ -3,7 +3,7 @@ package main
 import (
 	"net/http"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 
 	"github.com/gorilla/mux"
 	"github.com/lxc/lxd/shared/logger"
diff --git a/lxd/api_internal.go b/lxd/api_internal.go
index b73095bac..4a253db7f 100644
--- a/lxd/api_internal.go
+++ b/lxd/api_internal.go
@@ -19,7 +19,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/osarch"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 var apiInternal = []Command{
diff --git a/lxd/apparmor.go b/lxd/apparmor.go
index 5057e91b7..ef1c2d438 100644
--- a/lxd/apparmor.go
+++ b/lxd/apparmor.go
@@ -12,7 +12,7 @@ import (
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 const (
diff --git a/lxd/config/safe.go b/lxd/config/safe.go
index f6a8c22f8..7a869f762 100644
--- a/lxd/config/safe.go
+++ b/lxd/config/safe.go
@@ -3,7 +3,7 @@ package config
 import (
 	"fmt"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 
 	"github.com/lxc/lxd/shared/logger"
 )
diff --git a/lxd/container_exec.go b/lxd/container_exec.go
index e09682ab4..a174a8e49 100644
--- a/lxd/container_exec.go
+++ b/lxd/container_exec.go
@@ -21,7 +21,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/version"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 type execWs struct {
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index e23b4fcb1..c32791f62 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -34,7 +34,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/osarch"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // Operation locking
diff --git a/lxd/containers.go b/lxd/containers.go
index c4a169770..46f06854a 100644
--- a/lxd/containers.go
+++ b/lxd/containers.go
@@ -11,7 +11,7 @@ import (
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 var containersCmd = Command{
diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index 200002915..76d2a615a 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -18,7 +18,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/osarch"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 func createFromImage(d *Daemon, req *api.ContainersPost) Response {
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 6968e08ae..3a22932cb 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -35,7 +35,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/version"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // A Daemon can respond to requests from a shared client.
diff --git a/lxd/daemon_config.go b/lxd/daemon_config.go
index 508600e2e..c0b7fa722 100644
--- a/lxd/daemon_config.go
+++ b/lxd/daemon_config.go
@@ -11,8 +11,8 @@ import (
 	"strings"
 	"sync"
 
+	log "github.com/lxc/lxd/shared/log15"
 	"golang.org/x/crypto/scrypt"
-	log "gopkg.in/inconshreveable/log15.v2"
 
 	dbapi "github.com/lxc/lxd/lxd/db"
 	"github.com/lxc/lxd/shared"
diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go
index 43c875aeb..abc81a965 100644
--- a/lxd/daemon_images.go
+++ b/lxd/daemon_images.go
@@ -24,7 +24,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/version"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // Simplestream cache
diff --git a/lxd/db/containers.go b/lxd/db/containers.go
index 9eac650d5..fa74d13a8 100644
--- a/lxd/db/containers.go
+++ b/lxd/db/containers.go
@@ -9,7 +9,7 @@ import (
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // ContainerArgs is a value object holding all db-related details about a
diff --git a/lxd/debug/memory_test.go b/lxd/debug/memory_test.go
index ad48cafec..0860386e3 100644
--- a/lxd/debug/memory_test.go
+++ b/lxd/debug/memory_test.go
@@ -7,9 +7,9 @@ import (
 	"testing"
 	"time"
 
+	log "github.com/lxc/lxd/shared/log15"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	log "gopkg.in/inconshreveable/log15.v2"
 
 	"github.com/lxc/lxd/lxd/debug"
 	"github.com/lxc/lxd/shared/logging"
diff --git a/lxd/devices.go b/lxd/devices.go
index fd9211d3b..9b6edbfc1 100644
--- a/lxd/devices.go
+++ b/lxd/devices.go
@@ -25,7 +25,7 @@ import (
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 var deviceSchedRebalance = make(chan []string, 2)
diff --git a/lxd/endpoints/endpoints.go b/lxd/endpoints/endpoints.go
index 77f766b7e..d1b82859b 100644
--- a/lxd/endpoints/endpoints.go
+++ b/lxd/endpoints/endpoints.go
@@ -8,8 +8,8 @@ import (
 
 	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
+	log "github.com/lxc/lxd/shared/log15"
 	"github.com/lxc/lxd/shared/logger"
-	log "gopkg.in/inconshreveable/log15.v2"
 	tomb "gopkg.in/tomb.v2"
 )
 
diff --git a/lxd/endpoints/network.go b/lxd/endpoints/network.go
index 8d78d4d2e..4d306e457 100644
--- a/lxd/endpoints/network.go
+++ b/lxd/endpoints/network.go
@@ -10,8 +10,8 @@ import (
 
 	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
+	log "github.com/lxc/lxd/shared/log15"
 	"github.com/lxc/lxd/shared/logger"
-	log "gopkg.in/inconshreveable/log15.v2"
 )
 
 // NetworkPublicKey returns the public key of the TLS certificate used by the
diff --git a/lxd/events.go b/lxd/events.go
index d7aed0422..4d0c1e9b5 100644
--- a/lxd/events.go
+++ b/lxd/events.go
@@ -9,8 +9,8 @@ import (
 	"time"
 
 	"github.com/gorilla/websocket"
+	log "github.com/lxc/lxd/shared/log15"
 	"github.com/pborman/uuid"
-	log "gopkg.in/inconshreveable/log15.v2"
 
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
diff --git a/lxd/images.go b/lxd/images.go
index 7276723fa..68fd51f6d 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -35,7 +35,7 @@ import (
 	"github.com/lxc/lxd/shared/osarch"
 	"github.com/lxc/lxd/shared/version"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 /* We only want a single publish running at any one time.
diff --git a/lxd/logging.go b/lxd/logging.go
index 1b9d3447e..6587149cd 100644
--- a/lxd/logging.go
+++ b/lxd/logging.go
@@ -12,7 +12,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"golang.org/x/net/context"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // This task function expires logs when executed. It's started by the Daemon
diff --git a/lxd/main_subcommand.go b/lxd/main_subcommand.go
index 2d634dd59..ed5575be2 100644
--- a/lxd/main_subcommand.go
+++ b/lxd/main_subcommand.go
@@ -3,7 +3,7 @@ package main
 import (
 	"fmt"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/cmd"
diff --git a/lxd/networks.go b/lxd/networks.go
index 94d80fd98..7e78acea2 100644
--- a/lxd/networks.go
+++ b/lxd/networks.go
@@ -13,7 +13,7 @@ import (
 	"strings"
 
 	"github.com/gorilla/mux"
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 
 	"github.com/lxc/lxd/lxd/db"
 	"github.com/lxc/lxd/lxd/state"
diff --git a/lxd/patches.go b/lxd/patches.go
index e8d74208b..985a14b04 100644
--- a/lxd/patches.go
+++ b/lxd/patches.go
@@ -12,7 +12,7 @@ import (
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 /* Patches are one-time actions that are sometimes needed to update
diff --git a/lxd/profiles.go b/lxd/profiles.go
index 510908f84..6b36e2203 100644
--- a/lxd/profiles.go
+++ b/lxd/profiles.go
@@ -18,7 +18,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/version"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 /* This is used for both profiles post and profile put */
diff --git a/lxd/sys/apparmor.go b/lxd/sys/apparmor.go
index 7518139fb..d724e06a4 100644
--- a/lxd/sys/apparmor.go
+++ b/lxd/sys/apparmor.go
@@ -12,7 +12,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/syndtr/gocapability/capability"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // Initialize AppArmor-specific attributes.
diff --git a/lxd/sys/os.go b/lxd/sys/os.go
index 9ef307a5e..b88482aa2 100644
--- a/lxd/sys/os.go
+++ b/lxd/sys/os.go
@@ -3,7 +3,7 @@ package sys
 import (
 	"path/filepath"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 
 	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
diff --git a/lxd/util/http.go b/lxd/util/http.go
index 0cb66cba0..d33c6cb63 100644
--- a/lxd/util/http.go
+++ b/lxd/util/http.go
@@ -19,7 +19,7 @@ import (
 
 	"golang.org/x/net/context"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logger"
diff --git a/lxd/util/sys.go b/lxd/util/sys.go
index 75f5df092..a9d91f634 100644
--- a/lxd/util/sys.go
+++ b/lxd/util/sys.go
@@ -5,7 +5,7 @@ import (
 	"strconv"
 	"strings"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 	golxc "gopkg.in/lxc/go-lxc.v2"
 
 	"github.com/lxc/lxd/shared/idmap"
diff --git a/shared/logging/format.go b/shared/logging/format.go
index d60b7e481..250f4cdb7 100644
--- a/shared/logging/format.go
+++ b/shared/logging/format.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"time"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 const (
diff --git a/shared/logging/log.go b/shared/logging/log.go
index b35346a86..94dc47725 100644
--- a/shared/logging/log.go
+++ b/shared/logging/log.go
@@ -6,8 +6,8 @@ import (
 	"path/filepath"
 	"time"
 
-	log "gopkg.in/inconshreveable/log15.v2"
-	"gopkg.in/inconshreveable/log15.v2/term"
+	log "github.com/lxc/lxd/shared/log15"
+	"github.com/lxc/lxd/shared/log15/term"
 
 	"github.com/lxc/lxd/shared/logger"
 )
diff --git a/shared/logging/log_posix.go b/shared/logging/log_posix.go
index ffc55da5d..b1c67fbbe 100644
--- a/shared/logging/log_posix.go
+++ b/shared/logging/log_posix.go
@@ -3,7 +3,7 @@
 package logging
 
 import (
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // getSystemHandler on Linux writes messages to syslog.
diff --git a/shared/logging/log_windows.go b/shared/logging/log_windows.go
index 009326e30..467f9323c 100644
--- a/shared/logging/log_windows.go
+++ b/shared/logging/log_windows.go
@@ -3,7 +3,7 @@
 package logging
 
 import (
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // getSystemHandler on Windows does nothing.
diff --git a/shared/logging/testing.go b/shared/logging/testing.go
index 370f2a5e0..d92241d8f 100644
--- a/shared/logging/testing.go
+++ b/shared/logging/testing.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"testing"
 
-	log "gopkg.in/inconshreveable/log15.v2"
+	log "github.com/lxc/lxd/shared/log15"
 )
 
 // Testing installs a global logger that emits messages using the t.Logf method


More information about the lxc-devel mailing list