[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