log, vendor: vendor in log15 inline into our codebase
This commit is contained in:
parent
29fac7de44
commit
ec7f81f4bc
11
log/CONTRIBUTORS
Normal file
11
log/CONTRIBUTORS
Normal file
@ -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
|
13
log/LICENSE
Normal file
13
log/LICENSE
Normal file
@ -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.
|
77
log/README.md
Normal file
77
log/README.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
![obligatory xkcd](http://imgs.xkcd.com/comics/standards.png)
|
||||||
|
|
||||||
|
# log15 [![godoc reference](https://godoc.org/github.com/inconshreveable/log15?status.png)](https://godoc.org/github.com/inconshreveable/log15) [![Build Status](https://travis-ci.org/inconshreveable/log15.svg?branch=master)](https://travis-ci.org/inconshreveable/log15)
|
||||||
|
|
||||||
|
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. If you want to rely on a stable API,
|
||||||
|
you must vendor the library.
|
||||||
|
|
||||||
|
## Importing
|
||||||
|
|
||||||
|
```go
|
||||||
|
import log "github.com/inconshreveable/log15"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.JsonFormat()))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Will result in output that looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
WARN[06-17|21:58:10] abnormal conn rate module=app/server rate=0.500 low=0.100 high=0.800
|
||||||
|
INFO[06-17|21:58:10] connection open module=app/server raddr=10.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Breaking API Changes
|
||||||
|
The following commits broke API stability. This reference is intended to help you understand the consequences of updating to a newer version
|
||||||
|
of log15.
|
||||||
|
|
||||||
|
- 57a084d014d4150152b19e4e531399a7145d1540 - Added a `Get()` method to the `Logger` interface to retrieve the current handler
|
||||||
|
- 93404652ee366648fa622b64d1e2b67d75a3094a - `Record` field `Call` changed to `stack.Call` with switch to `github.com/go-stack/stack`
|
||||||
|
- a5e7613673c73281f58e15a87d2cf0cf111e8152 - Restored `syslog.Priority` argument to the `SyslogXxx` handler constructors
|
||||||
|
|
||||||
|
## 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
|
4
log/README_ETHEREUM.md
Normal file
4
log/README_ETHEREUM.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
This package is a fork of https://github.com/inconshreveable/log15, with some
|
||||||
|
minor modifications required by the go-ethereum codebase:
|
||||||
|
|
||||||
|
* Support for log level `trace`
|
333
log/doc.go
Normal file
333
log/doc.go
Normal file
@ -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 "github.com/inconshreveable/log15"
|
||||||
|
|
||||||
|
|
||||||
|
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) error
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 github.com/go-stack/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 "github.com/inconshreveable/log15"
|
||||||
|
|
||||||
|
var Log = log.New()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Log.SetHandler(log.DiscardHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
Users of your library may then enable it if they like:
|
||||||
|
|
||||||
|
import "github.com/inconshreveable/log15"
|
||||||
|
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 "github.com/inconshreveable/log15/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 log
|
279
log/format.go
Normal file
279
log/format.go
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"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 {
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteByte('=')
|
||||||
|
buf.WriteString(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.String()
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := value.(time.Time); ok {
|
||||||
|
// Performance optimization: No need for escaping since the provided
|
||||||
|
// timeFormat doesn't have any escape characters, and escaping is
|
||||||
|
// expensive.
|
||||||
|
return t.Format(timeFormat)
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringBufPool = sync.Pool{
|
||||||
|
New: func() interface{} { return new(bytes.Buffer) },
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeString(s string) string {
|
||||||
|
needsQuotes := false
|
||||||
|
needsEscape := false
|
||||||
|
for _, r := range s {
|
||||||
|
if r <= ' ' || r == '=' || r == '"' {
|
||||||
|
needsQuotes = true
|
||||||
|
}
|
||||||
|
if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' {
|
||||||
|
needsEscape = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if needsEscape == false && needsQuotes == false {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
e := stringBufPool.Get().(*bytes.Buffer)
|
||||||
|
e.WriteByte('"')
|
||||||
|
for _, r := range s {
|
||||||
|
switch r {
|
||||||
|
case '\\', '"':
|
||||||
|
e.WriteByte('\\')
|
||||||
|
e.WriteByte(byte(r))
|
||||||
|
case '\n':
|
||||||
|
e.WriteString("\\n")
|
||||||
|
case '\r':
|
||||||
|
e.WriteString("\\r")
|
||||||
|
case '\t':
|
||||||
|
e.WriteString("\\t")
|
||||||
|
default:
|
||||||
|
e.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.WriteByte('"')
|
||||||
|
var ret string
|
||||||
|
if needsQuotes {
|
||||||
|
ret = e.String()
|
||||||
|
} else {
|
||||||
|
ret = string(e.Bytes()[1 : e.Len()-1])
|
||||||
|
}
|
||||||
|
e.Reset()
|
||||||
|
stringBufPool.Put(e)
|
||||||
|
return ret
|
||||||
|
}
|
356
log/handler.go
Normal file
356
log/handler.go
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-stack/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 {
|
||||||
|
r.Ctx = append(r.Ctx, "caller", fmt.Sprint(r.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 {
|
||||||
|
r.Ctx = append(r.Ctx, "fn", fmt.Sprintf("%+n", r.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
|
||||||
|
// package github.com/go-stack/stack for the list of supported formats.
|
||||||
|
func CallerStackHandler(format string, h Handler) Handler {
|
||||||
|
return FuncHandler(func(r *Record) error {
|
||||||
|
s := stack.Trace().TrimBelow(r.Call).TrimRuntime()
|
||||||
|
if len(s) > 0 {
|
||||||
|
r.Ctx = append(r.Ctx, "stack", fmt.Sprintf(format, s))
|
||||||
|
}
|
||||||
|
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.LvlError, 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.CallStack); ok {
|
||||||
|
v = cs.TrimBelow(r.Call).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))
|
||||||
|
}
|
26
log/handler_go13.go
Normal file
26
log/handler_go13.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// +build !go1.4
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
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 h.Get().Log(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *swapHandler) Get() Handler {
|
||||||
|
return *(*Handler)(atomic.LoadPointer(&h.handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *swapHandler) Swap(newHandler Handler) {
|
||||||
|
atomic.StorePointer(&h.handler, unsafe.Pointer(&newHandler))
|
||||||
|
}
|
23
log/handler_go14.go
Normal file
23
log/handler_go14.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// +build go1.4
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
import "sync/atomic"
|
||||||
|
|
||||||
|
// swapHandler wraps another handler that may be swapped out
|
||||||
|
// dynamically at runtime in a thread-safe fashion.
|
||||||
|
type swapHandler struct {
|
||||||
|
handler atomic.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *swapHandler) Log(r *Record) error {
|
||||||
|
return (*h.handler.Load().(*Handler)).Log(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *swapHandler) Swap(newHandler Handler) {
|
||||||
|
h.handler.Store(&newHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *swapHandler) Get() Handler {
|
||||||
|
return *h.handler.Load().(*Handler)
|
||||||
|
}
|
208
log/logger.go
Normal file
208
log/logger.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-stack/stack"
|
||||||
|
)
|
||||||
|
|
||||||
|
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{}
|
||||||
|
Call stack.Call
|
||||||
|
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
|
||||||
|
|
||||||
|
// GetHandler gets the handler associated with the logger.
|
||||||
|
GetHandler() Handler
|
||||||
|
|
||||||
|
// 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{}) {
|
||||||
|
l.h.Log(&Record{
|
||||||
|
Time: time.Now(),
|
||||||
|
Lvl: lvl,
|
||||||
|
Msg: msg,
|
||||||
|
Ctx: newContext(l.ctx, ctx),
|
||||||
|
Call: stack.Caller(2),
|
||||||
|
KeyNames: RecordKeyNames{
|
||||||
|
Time: timeKey,
|
||||||
|
Msg: msgKey,
|
||||||
|
Lvl: lvlKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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) GetHandler() Handler {
|
||||||
|
return l.h.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
67
log/root.go
Normal file
67
log/root.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log/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)
|
||||||
|
}
|
55
log/syslog.go
Normal file
55
log/syslog.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// +build !windows,!plan9
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
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(priority syslog.Priority, tag string, fmtr Format) (Handler, error) {
|
||||||
|
wr, err := syslog.New(priority, tag)
|
||||||
|
return sharedSyslog(fmtr, wr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyslogNetHandler opens a connection to a log daemon over the network and writes
|
||||||
|
// all log records to it.
|
||||||
|
func SyslogNetHandler(net, addr string, priority syslog.Priority, tag string, fmtr Format) (Handler, error) {
|
||||||
|
wr, err := syslog.Dial(net, addr, priority, 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(priority syslog.Priority, tag string, fmtr Format) Handler {
|
||||||
|
return must(SyslogHandler(priority, tag, fmtr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m muster) SyslogNetHandler(net, addr string, priority syslog.Priority, tag string, fmtr Format) Handler {
|
||||||
|
return must(SyslogNetHandler(net, addr, priority, tag, fmtr))
|
||||||
|
}
|
21
log/term/LICENSE
Normal file
21
log/term/LICENSE
Normal file
@ -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.
|
13
log/term/terminal_appengine.go
Normal file
13
log/term/terminal_appengine.go
Normal file
@ -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
|
||||||
|
}
|
13
log/term/terminal_darwin.go
Normal file
13
log/term/terminal_darwin.go
Normal file
@ -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
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA
|
||||||
|
|
||||||
|
type Termios syscall.Termios
|
18
log/term/terminal_freebsd.go
Normal file
18
log/term/terminal_freebsd.go
Normal file
@ -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
|
||||||
|
}
|
14
log/term/terminal_linux.go
Normal file
14
log/term/terminal_linux.go
Normal file
@ -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
|
7
log/term/terminal_netbsd.go
Normal file
7
log/term/terminal_netbsd.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package term
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA
|
||||||
|
|
||||||
|
type Termios syscall.Termios
|
20
log/term/terminal_notwindows.go
Normal file
20
log/term/terminal_notwindows.go
Normal file
@ -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 netbsd
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
7
log/term/terminal_openbsd.go
Normal file
7
log/term/terminal_openbsd.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package term
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA
|
||||||
|
|
||||||
|
type Termios syscall.Termios
|
9
log/term/terminal_solaris.go
Normal file
9
log/term/terminal_solaris.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package term
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
// IsTty returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTty(fd uintptr) bool {
|
||||||
|
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETA)
|
||||||
|
return err == nil
|
||||||
|
}
|
26
log/term/terminal_windows.go
Normal file
26
log/term/terminal_windows.go
Normal file
@ -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
|
||||||
|
}
|
13
vendor/github.com/go-stack/stack/LICENSE.md
generated
vendored
Normal file
13
vendor/github.com/go-stack/stack/LICENSE.md
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Copyright 2014 Chris Hines
|
||||||
|
|
||||||
|
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.
|
38
vendor/github.com/go-stack/stack/README.md
generated
vendored
Normal file
38
vendor/github.com/go-stack/stack/README.md
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[![GoDoc](https://godoc.org/github.com/go-stack/stack?status.svg)](https://godoc.org/github.com/go-stack/stack)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/go-stack/stack)](https://goreportcard.com/report/go-stack/stack)
|
||||||
|
[![TravisCI](https://travis-ci.org/go-stack/stack.svg?branch=master)](https://travis-ci.org/go-stack/stack)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/github/go-stack/stack/badge.svg?branch=master)](https://coveralls.io/github/go-stack/stack?branch=master)
|
||||||
|
|
||||||
|
# stack
|
||||||
|
|
||||||
|
Package stack implements utilities to capture, manipulate, and format call
|
||||||
|
stacks. It provides a simpler API than package runtime.
|
||||||
|
|
||||||
|
The implementation takes care of the minutia and special cases of interpreting
|
||||||
|
the program counter (pc) values returned by runtime.Callers.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Package stack publishes releases via [semver](http://semver.org/) compatible Git
|
||||||
|
tags prefixed with a single 'v'. The master branch always contains the latest
|
||||||
|
release. The develop branch contains unreleased commits.
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
Package stack's types implement fmt.Formatter, which provides a simple and
|
||||||
|
flexible way to declaratively configure formatting when used with logging or
|
||||||
|
error tracking packages.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func DoTheThing() {
|
||||||
|
c := stack.Caller(0)
|
||||||
|
log.Print(c) // "source.go:10"
|
||||||
|
log.Printf("%+v", c) // "pkg/path/source.go:10"
|
||||||
|
log.Printf("%n", c) // "DoTheThing"
|
||||||
|
|
||||||
|
s := stack.Trace().TrimRuntime()
|
||||||
|
log.Print(s) // "[source.go:15 caller.go:42 main.go:14]"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the docs for all of the supported formatting options.
|
349
vendor/github.com/go-stack/stack/stack.go
generated
vendored
Normal file
349
vendor/github.com/go-stack/stack/stack.go
generated
vendored
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
// Package stack implements utilities to capture, manipulate, and format call
|
||||||
|
// stacks. It provides a simpler API than package runtime.
|
||||||
|
//
|
||||||
|
// The implementation takes care of the minutia and special cases of
|
||||||
|
// interpreting the program counter (pc) values returned by runtime.Callers.
|
||||||
|
//
|
||||||
|
// Package stack's types implement fmt.Formatter, which provides a simple and
|
||||||
|
// flexible way to declaratively configure formatting when used with logging
|
||||||
|
// or error tracking packages.
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call records a single function invocation from a goroutine stack.
|
||||||
|
type Call struct {
|
||||||
|
fn *runtime.Func
|
||||||
|
pc uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller returns a Call from the stack of the current goroutine. The argument
|
||||||
|
// skip is the number of stack frames to ascend, with 0 identifying the
|
||||||
|
// calling function.
|
||||||
|
func Caller(skip int) Call {
|
||||||
|
var pcs [2]uintptr
|
||||||
|
n := runtime.Callers(skip+1, pcs[:])
|
||||||
|
|
||||||
|
var c Call
|
||||||
|
|
||||||
|
if n < 2 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
c.pc = pcs[1]
|
||||||
|
if runtime.FuncForPC(pcs[0]) != sigpanic {
|
||||||
|
c.pc--
|
||||||
|
}
|
||||||
|
c.fn = runtime.FuncForPC(c.pc)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", c).
|
||||||
|
func (c Call) String() string {
|
||||||
|
return fmt.Sprint(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler. It formats the Call the same
|
||||||
|
// as fmt.Sprintf("%v", c).
|
||||||
|
func (c Call) MarshalText() ([]byte, error) {
|
||||||
|
if c.fn == nil {
|
||||||
|
return nil, ErrNoFunc
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
fmt.Fprint(&buf, c)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNoFunc means that the Call has a nil *runtime.Func. The most likely
|
||||||
|
// cause is a Call with the zero value.
|
||||||
|
var ErrNoFunc = errors.New("no call stack information")
|
||||||
|
|
||||||
|
// 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 (c Call) Format(s fmt.State, verb rune) {
|
||||||
|
if c.fn == nil {
|
||||||
|
fmt.Fprintf(s, "%%!%c(NOFUNC)", verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch verb {
|
||||||
|
case 's', 'v':
|
||||||
|
file, line := c.fn.FileLine(c.pc)
|
||||||
|
switch {
|
||||||
|
case s.Flag('#'):
|
||||||
|
// done
|
||||||
|
case s.Flag('+'):
|
||||||
|
file = file[pkgIndex(file, c.fn.Name()):]
|
||||||
|
default:
|
||||||
|
const sep = "/"
|
||||||
|
if i := strings.LastIndex(file, sep); i != -1 {
|
||||||
|
file = file[i+len(sep):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
io.WriteString(s, file)
|
||||||
|
if verb == 'v' {
|
||||||
|
buf := [7]byte{':'}
|
||||||
|
s.Write(strconv.AppendInt(buf[:1], int64(line), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
_, line := c.fn.FileLine(c.pc)
|
||||||
|
buf := [6]byte{}
|
||||||
|
s.Write(strconv.AppendInt(buf[:0], int64(line), 10))
|
||||||
|
|
||||||
|
case 'n':
|
||||||
|
name := c.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):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
io.WriteString(s, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PC returns the program counter for this call frame; multiple frames may
|
||||||
|
// have the same PC value.
|
||||||
|
func (c Call) PC() uintptr {
|
||||||
|
return c.pc
|
||||||
|
}
|
||||||
|
|
||||||
|
// name returns the import path qualified name of the function containing the
|
||||||
|
// call.
|
||||||
|
func (c Call) name() string {
|
||||||
|
if c.fn == nil {
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
return c.fn.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Call) file() string {
|
||||||
|
if c.fn == nil {
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
file, _ := c.fn.FileLine(c.pc)
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Call) line() int {
|
||||||
|
if c.fn == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, line := c.fn.FileLine(c.pc)
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallStack records a sequence of function invocations from a goroutine
|
||||||
|
// stack.
|
||||||
|
type CallStack []Call
|
||||||
|
|
||||||
|
// String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", cs).
|
||||||
|
func (cs CallStack) String() string {
|
||||||
|
return fmt.Sprint(cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
openBracketBytes = []byte("[")
|
||||||
|
closeBracketBytes = []byte("]")
|
||||||
|
spaceBytes = []byte(" ")
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler. It formats the CallStack the
|
||||||
|
// same as fmt.Sprintf("%v", cs).
|
||||||
|
func (cs CallStack) MarshalText() ([]byte, error) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
buf.Write(openBracketBytes)
|
||||||
|
for i, pc := range cs {
|
||||||
|
if pc.fn == nil {
|
||||||
|
return nil, ErrNoFunc
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
buf.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
fmt.Fprint(&buf, pc)
|
||||||
|
}
|
||||||
|
buf.Write(closeBracketBytes)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format implements fmt.Formatter by printing the CallStack as square brackets
|
||||||
|
// ([, ]) surrounding a space separated list of Calls each formatted with the
|
||||||
|
// supplied verb and options.
|
||||||
|
func (cs CallStack) Format(s fmt.State, verb rune) {
|
||||||
|
s.Write(openBracketBytes)
|
||||||
|
for i, pc := range cs {
|
||||||
|
if i > 0 {
|
||||||
|
s.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
pc.Format(s, verb)
|
||||||
|
}
|
||||||
|
s.Write(closeBracketBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSigpanic intentionally executes faulting code to generate a stack trace
|
||||||
|
// containing an entry for runtime.sigpanic.
|
||||||
|
func findSigpanic() *runtime.Func {
|
||||||
|
var fn *runtime.Func
|
||||||
|
var p *int
|
||||||
|
func() int {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
var pcs [512]uintptr
|
||||||
|
n := runtime.Callers(2, pcs[:])
|
||||||
|
for _, pc := range pcs[:n] {
|
||||||
|
f := runtime.FuncForPC(pc)
|
||||||
|
if f.Name() == "runtime.sigpanic" {
|
||||||
|
fn = f
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// intentional nil pointer dereference to trigger sigpanic
|
||||||
|
return *p
|
||||||
|
}()
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigpanic = findSigpanic()
|
||||||
|
|
||||||
|
// Trace returns a CallStack for the current goroutine with element 0
|
||||||
|
// identifying the calling function.
|
||||||
|
func Trace() CallStack {
|
||||||
|
var pcs [512]uintptr
|
||||||
|
n := runtime.Callers(2, pcs[:])
|
||||||
|
cs := make([]Call, n)
|
||||||
|
|
||||||
|
for i, pc := range pcs[:n] {
|
||||||
|
pcFix := pc
|
||||||
|
if i > 0 && cs[i-1].fn != sigpanic {
|
||||||
|
pcFix--
|
||||||
|
}
|
||||||
|
cs[i] = Call{
|
||||||
|
fn: runtime.FuncForPC(pcFix),
|
||||||
|
pc: pcFix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimBelow returns a slice of the CallStack with all entries below c
|
||||||
|
// removed.
|
||||||
|
func (cs CallStack) TrimBelow(c Call) CallStack {
|
||||||
|
for len(cs) > 0 && cs[0].pc != c.pc {
|
||||||
|
cs = cs[1:]
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimAbove returns a slice of the CallStack with all entries above c
|
||||||
|
// removed.
|
||||||
|
func (cs CallStack) TrimAbove(c Call) CallStack {
|
||||||
|
for len(cs) > 0 && cs[len(cs)-1].pc != c.pc {
|
||||||
|
cs = cs[:len(cs)-1]
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkgIndex returns the index that results in file[index:] being the path of
|
||||||
|
// file relative to the compile time GOPATH, and file[:index] being the
|
||||||
|
// $GOPATH/src/ portion of file. funcName must be the name of a function in
|
||||||
|
// file as returned by runtime.Func.Name.
|
||||||
|
func pkgIndex(file, funcName string) int {
|
||||||
|
// As of Go 1.6.2 there is no direct way to know the compile time GOPATH
|
||||||
|
// at runtime, but we can infer the number of path segments in the GOPATH.
|
||||||
|
// We note that runtime.Func.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:
|
||||||
|
//
|
||||||
|
// file[:idx] == /home/user/src/
|
||||||
|
// file[idx:] == pkg/sub/file.go
|
||||||
|
//
|
||||||
|
// From this we can easily see that fn.Name() has one less path separator
|
||||||
|
// than our desired result for file[idx:]. We count separators from the
|
||||||
|
// end of the file path until it finds two more than in the function name
|
||||||
|
// and then move one character forward to preserve the initial path
|
||||||
|
// segment without a leading separator.
|
||||||
|
const sep = "/"
|
||||||
|
i := len(file)
|
||||||
|
for n := strings.Count(funcName, sep) + 2; n > 0; n-- {
|
||||||
|
i = strings.LastIndex(file[:i], sep)
|
||||||
|
if i == -1 {
|
||||||
|
i = -len(sep)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get back to 0 or trim the leading separator
|
||||||
|
return i + len(sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtimePath string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var pcs [1]uintptr
|
||||||
|
runtime.Callers(0, pcs[:])
|
||||||
|
fn := runtime.FuncForPC(pcs[0])
|
||||||
|
file, _ := fn.FileLine(pcs[0])
|
||||||
|
|
||||||
|
idx := pkgIndex(file, fn.Name())
|
||||||
|
|
||||||
|
runtimePath = file[:idx]
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
runtimePath = strings.ToLower(runtimePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inGoroot(c Call) bool {
|
||||||
|
file := c.file()
|
||||||
|
if len(file) == 0 || file[0] == '?' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
file = strings.ToLower(file)
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(file, runtimePath) || strings.HasSuffix(file, "/_testmain.go")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimRuntime returns a slice of the CallStack with the topmost entries from
|
||||||
|
// the go runtime removed. It considers any calls originating from unknown
|
||||||
|
// files, files under GOROOT, or _testmain.go as part of the runtime.
|
||||||
|
func (cs CallStack) TrimRuntime() CallStack {
|
||||||
|
for len(cs) > 0 && inGoroot(cs[len(cs)-1]) {
|
||||||
|
cs = cs[:len(cs)-1]
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@ -52,6 +52,12 @@
|
|||||||
"revision": "991cd3d3809135dc24daf6188dc6edcaf3d7d2d9",
|
"revision": "991cd3d3809135dc24daf6188dc6edcaf3d7d2d9",
|
||||||
"revisionTime": "2017-01-17T22:23:42Z"
|
"revisionTime": "2017-01-17T22:23:42Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "2sj/DbXoXdnPAfjAEyhS0Jj5QL0=",
|
||||||
|
"path": "github.com/go-stack/stack",
|
||||||
|
"revision": "100eb0c0a9c5b306ca2fb4f165df21d80ada4b82",
|
||||||
|
"revisionTime": "2016-05-14T03:44:11Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "p/8vSviYF91gFflhrt5vkyksroo=",
|
"checksumSHA1": "p/8vSviYF91gFflhrt5vkyksroo=",
|
||||||
"path": "github.com/golang/snappy",
|
"path": "github.com/golang/snappy",
|
||||||
|
Loading…
Reference in New Issue
Block a user