Update packages github.com/Sirupsen/logrus, github.com/gorilla/context and github.com/garyburd/redigo to new version

This commit is contained in:
Wenkai Yin 2018-04-03 11:48:06 +08:00
parent 1296a09034
commit 46ec8c70fa
62 changed files with 2024 additions and 657 deletions

17
src/Gopkg.lock generated
View File

@ -4,7 +4,8 @@
[[projects]] [[projects]]
name = "github.com/Sirupsen/logrus" name = "github.com/Sirupsen/logrus"
packages = ["."] packages = ["."]
revision = "a283a10442df8dc09befd873fab202bf8a253d6a" revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]] [[projects]]
name = "github.com/Unknwon/goconfig" name = "github.com/Unknwon/goconfig"
@ -118,8 +119,8 @@
"internal", "internal",
"redis" "redis"
] ]
revision = "47dc60e71eed504e3ef8e77ee3c6fe720f3be57f" revision = "a69d19351219b6dd56f274f96d85a7014a2ec34e"
version = "v1.3.0" version = "v1.6.0"
[[projects]] [[projects]]
name = "github.com/go-sql-driver/mysql" name = "github.com/go-sql-driver/mysql"
@ -147,7 +148,8 @@
[[projects]] [[projects]]
name = "github.com/gorilla/context" name = "github.com/gorilla/context"
packages = ["."] packages = ["."]
revision = "aed02d124ae4a0e94fea4541c8effd05bf0c8296" revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]] [[projects]]
name = "github.com/gorilla/handlers" name = "github.com/gorilla/handlers"
@ -210,7 +212,10 @@
[[projects]] [[projects]]
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = ["pbkdf2"] packages = [
"pbkdf2",
"ssh/terminal"
]
revision = "5f961cd492ac9d43fc33a8ef646bae79d113fd97" revision = "5f961cd492ac9d43fc33a8ef646bae79d113fd97"
[[projects]] [[projects]]
@ -270,6 +275,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "ce26a28bd76c7bcfc4ed4dcd95a1522968cbe752a45cf7759b9e13734a31afc2" inputs-digest = "e690fed3ef15e012cf97d5a1eb70fcb90ea051b54d7472e4c804291d08a58c80"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -66,4 +66,16 @@ ignored = ["github.com/vmware/harbor/tests*"]
[[constraint]] [[constraint]]
name = "github.com/gorilla/mux" name = "github.com/gorilla/mux"
version = "=1.6.0"
[[override]]
name = "github.com/Sirupsen/logrus"
version = "=1.0.5"
[[override]]
name = "github.com/gorilla/context"
version = "=1.1"
[[override]]
name = "github.com/garyburd/redigo"
version = "=1.6.0" version = "=1.6.0"

View File

@ -1,10 +1,15 @@
language: go language: go
go: go:
- 1.3 - 1.6.x
- 1.4 - 1.7.x
- 1.5 - 1.8.x
- 1.6
- tip - tip
env:
- GOMAXPROCS=4 GORACE=halt_on_error=1
install: install:
- go get -t ./... - go get github.com/stretchr/testify/assert
script: GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./... - go get gopkg.in/gemnasium/logrus-airbrake-hook.v2
- go get golang.org/x/sys/unix
- go get golang.org/x/sys/windows
script:
- go test -race -v ./...

View File

@ -1,3 +1,60 @@
# 1.0.5
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4
* Fix race when adding hooks (#612)
* Fix terminal check in AppEngine (#635)
# 1.0.3
* Replace example files with testable examples
# 1.0.2
* bug: quote non-string values in text formatter (#583)
* Make (*Logger) SetLevel a public method
# 1.0.1
* bug: fix escaping in text formatter (#575)
# 1.0.0
* Officially changed name to lower-case
* bug: colors on Windows 10 (#541)
* bug: fix race in accessing level (#512)
# 0.11.5
* feature: add writer and writerlevel to entry (#372)
# 0.11.4
* bug: fix undefined variable on solaris (#493)
# 0.11.3
* formatter: configure quoting of empty values (#484)
* formatter: configure quoting character (default is `"`) (#484)
* bug: fix not importing io correctly in non-linux environments (#481)
# 0.11.2
* bug: fix windows terminal detection (#476)
# 0.11.1
* bug: fix tty detection with custom out (#471)
# 0.11.0
* performance: Use bufferpool to allocate (#370)
* terminal: terminal detection for app-engine (#343)
* feature: exit handler (#375)
# 0.10.0 # 0.10.0
* feature: Add a test hook (#180) * feature: Add a test hook (#180)

View File

@ -1,11 +1,24 @@
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)&nbsp;[![GoDoc](https://godoc.org/github.com/Sirupsen/logrus?status.svg)](https://godoc.org/github.com/Sirupsen/logrus) # Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus)&nbsp;[![GoDoc](https://godoc.org/github.com/sirupsen/logrus?status.svg)](https://godoc.org/github.com/sirupsen/logrus)
Logrus is a structured logger for Go (golang), completely API compatible with Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not the standard library logger.
yet stable (pre 1.0). Logrus itself is completely stable and has been used in
many large deployments. The core API is unlikely to change much but please **Seeing weird case-sensitive problems?** It's in the past been possible to
version control your Logrus to make sure you aren't fetching latest `master` on import Logrus as both upper- and lower-case. Due to the Go package environment,
every build.** this caused issues in the community and we needed a standard. Some environments
experienced problems with the upper-case variant, so the lower-case was decided.
Everything using `logrus` will need to use the lower-case:
`github.com/sirupsen/logrus`. Any package that isn't, should be changed.
To fix Glide, see [these
comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437).
For an in-depth explanation of the casing issue, see [this
comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276).
**Are you interested in assisting in maintaining Logrus?** Currently I have a
lot of obligations, and I am unable to provide Logrus with the maintainership it
needs. If you'd like to help, please reach out to me at `simon at author's
username dot com`.
Nicely color-coded in development (when a TTY is attached, otherwise just Nicely color-coded in development (when a TTY is attached, otherwise just
plain text): plain text):
@ -46,6 +59,12 @@ time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x20822
exit status 1 exit status 1
``` ```
#### Case-sensitivity
The organization's name was changed to lower-case--and this will not be changed
back. If you are getting import conflicts due to case sensitivity, please use
the lower-case import: `github.com/sirupsen/logrus`.
#### Example #### Example
The simplest way to use Logrus is simply the package-level exported logger: The simplest way to use Logrus is simply the package-level exported logger:
@ -54,7 +73,7 @@ The simplest way to use Logrus is simply the package-level exported logger:
package main package main
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func main() { func main() {
@ -65,7 +84,7 @@ func main() {
``` ```
Note that it's completely api-compatible with the stdlib logger, so you can Note that it's completely api-compatible with the stdlib logger, so you can
replace your `log` imports everywhere with `log "github.com/Sirupsen/logrus"` replace your `log` imports everywhere with `log "github.com/sirupsen/logrus"`
and you'll now have the flexibility of Logrus. You can customize it all you and you'll now have the flexibility of Logrus. You can customize it all you
want: want:
@ -74,15 +93,16 @@ package main
import ( import (
"os" "os"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func init() { func init() {
// Log as JSON instead of the default ASCII formatter. // Log as JSON instead of the default ASCII formatter.
log.SetFormatter(&log.JSONFormatter{}) log.SetFormatter(&log.JSONFormatter{})
// Output to stderr instead of stdout, could also be a file. // Output to stdout instead of the default stderr
log.SetOutput(os.Stderr) // Can be any io.Writer, see below for File example
log.SetOutput(os.Stdout)
// Only log the warning severity or above. // Only log the warning severity or above.
log.SetLevel(log.WarnLevel) log.SetLevel(log.WarnLevel)
@ -123,7 +143,8 @@ application, you can also create an instance of the `logrus` Logger:
package main package main
import ( import (
"github.com/Sirupsen/logrus" "os"
"github.com/sirupsen/logrus"
) )
// Create a new instance of the logger. You can have any number of instances. // Create a new instance of the logger. You can have any number of instances.
@ -132,7 +153,15 @@ var log = logrus.New()
func main() { func main() {
// The API for setting attributes is a little different than the package level // The API for setting attributes is a little different than the package level
// exported logger. See Godoc. // exported logger. See Godoc.
log.Out = os.Stderr log.Out = os.Stdout
// You could set this to any `io.Writer` such as a file
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
// if err == nil {
// log.Out = file
// } else {
// log.Info("Failed to log to file, using default stderr")
// }
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"animal": "walrus", "animal": "walrus",
@ -143,7 +172,7 @@ func main() {
#### Fields #### Fields
Logrus encourages careful, structured logging though logging fields instead of Logrus encourages careful, structured logging through logging fields instead of
long, unparseable error messages. For example, instead of: `log.Fatalf("Failed long, unparseable error messages. For example, instead of: `log.Fatalf("Failed
to send event %s to topic %s with key %d")`, you should log the much more to send event %s to topic %s with key %d")`, you should log the much more
discoverable: discoverable:
@ -165,6 +194,20 @@ In general, with Logrus using any of the `printf`-family functions should be
seen as a hint you should add a field, however, you can still use the seen as a hint you should add a field, however, you can still use the
`printf`-family functions with Logrus. `printf`-family functions with Logrus.
#### Default Fields
Often it's helpful to have fields _always_ attached to log statements in an
application or parts of one. For example, you may want to always log the
`request_id` and `user_ip` in the context of a request. Instead of writing
`log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})` on
every line, you can create a `logrus.Entry` to pass around instead:
```go
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request") # will log request_id and user_ip
requestLogger.Warn("something not great happened")
```
#### Hooks #### Hooks
You can add hooks for logging levels. For example to send errors to an exception You can add hooks for logging levels. For example to send errors to an exception
@ -176,9 +219,9 @@ Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
```go ```go
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "aibrake" "gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
"log/syslog" "log/syslog"
) )
@ -200,35 +243,58 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| Hook | Description | | Hook | Description |
| ----- | ----------- | | ----- | ----------- |
| [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. |
| [Airbrake "legacy"](https://github.com/gemnasium/logrus-airbrake-legacy-hook) | Send errors to an exception tracking service compatible with the Airbrake API V2. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. | | [Airbrake "legacy"](https://github.com/gemnasium/logrus-airbrake-legacy-hook) | Send errors to an exception tracking service compatible with the Airbrake API V2. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. |
| [Papertrail](https://github.com/polds/logrus-papertrail-hook) | Send errors to the [Papertrail](https://papertrailapp.com) hosted logging service via UDP. | | [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. |
| [Syslog](https://github.com/Sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. | | [Amazon Kinesis](https://github.com/evalphobia/logrus_kinesis) | Hook for logging to [Amazon Kinesis](https://aws.amazon.com/kinesis/) |
| [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [Sentry](https://github.com/evalphobia/logrus_sentry) | Send errors to the Sentry error logging and aggregation service. |
| [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. |
| [Logrusly](https://github.com/sebest/logrusly) | Send logs to [Loggly](https://www.loggly.com/) |
| [Slackrus](https://github.com/johntdyer/slackrus) | Hook for Slack chat. |
| [Journalhook](https://github.com/wercker/journalhook) | Hook for logging to `systemd-journald` |
| [Graylog](https://github.com/gemnasium/logrus-graylog-hook) | Hook for logging to [Graylog](http://graylog2.org/) |
| [Raygun](https://github.com/squirkle/logrus-raygun-hook) | Hook for logging to [Raygun.io](http://raygun.io/) |
| [LFShook](https://github.com/rifflock/lfshook) | Hook for logging to the local filesystem |
| [Honeybadger](https://github.com/agonzalezro/logrus_honeybadger) | Hook for sending exceptions to Honeybadger |
| [Mail](https://github.com/zbindenren/logrus_mail) | Hook for sending exceptions via mail |
| [Rollrus](https://github.com/heroku/rollrus) | Hook for sending errors to rollbar |
| [Fluentd](https://github.com/evalphobia/logrus_fluent) | Hook for logging to fluentd |
| [Mongodb](https://github.com/weekface/mgorus) | Hook for logging to mongodb |
| [Influxus] (http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB] (http://influxdata.com/) |
| [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
| [Octokit](https://github.com/dorajistyle/logrus-octokit-hook) | Hook for logging to github via octokit |
| [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
| [Redis-Hook](https://github.com/rogierlommers/logrus-redis-hook) | Hook for logging to a ELK stack (through Redis) |
| [Amqp-Hook](https://github.com/vladoatanasov/logrus_amqp) | Hook for logging to Amqp broker (Like RabbitMQ) | | [Amqp-Hook](https://github.com/vladoatanasov/logrus_amqp) | Hook for logging to Amqp broker (Like RabbitMQ) |
| [KafkaLogrus](https://github.com/goibibo/KafkaLogrus) | Hook for logging to kafka | | [Application Insights](https://github.com/jjcollinge/logrus-appinsights) | Hook for logging to [Application Insights](https://azure.microsoft.com/en-us/services/application-insights/)
| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) | | [AzureTableHook](https://github.com/kpfaulkner/azuretablehook/) | Hook for logging to Azure Table Storage|
| [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
| [Discordrus](https://github.com/kz/discordrus) | Hook for logging to [Discord](https://discordapp.com/) |
| [ElasticSearch](https://github.com/sohlich/elogrus) | Hook for logging to ElasticSearch| | [ElasticSearch](https://github.com/sohlich/elogrus) | Hook for logging to ElasticSearch|
| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)| | [Firehose](https://github.com/beaubrewer/logrus_firehose) | Hook for logging to [Amazon Firehose](https://aws.amazon.com/kinesis/firehose/)
| [Fluentd](https://github.com/evalphobia/logrus_fluent) | Hook for logging to fluentd |
| [Go-Slack](https://github.com/multiplay/go-slack) | Hook for logging to [Slack](https://slack.com) |
| [Graylog](https://github.com/gemnasium/logrus-graylog-hook) | Hook for logging to [Graylog](http://graylog2.org/) |
| [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. |
| [Honeybadger](https://github.com/agonzalezro/logrus_honeybadger) | Hook for sending exceptions to Honeybadger |
| [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
| [Influxus](http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB](http://influxdata.com/) |
| [Journalhook](https://github.com/wercker/journalhook) | Hook for logging to `systemd-journald` |
| [KafkaLogrus](https://github.com/tracer0tong/kafkalogrus) | Hook for logging to Kafka |
| [Kafka REST Proxy](https://github.com/Nordstrom/logrus-kafka-rest-proxy) | Hook for logging to [Kafka REST Proxy](https://docs.confluent.io/current/kafka-rest/docs) |
| [LFShook](https://github.com/rifflock/lfshook) | Hook for logging to the local filesystem |
| [Logbeat](https://github.com/macandmia/logbeat) | Hook for logging to [Opbeat](https://opbeat.com/) |
| [Logentries](https://github.com/jcftang/logentriesrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logentrus](https://github.com/puddingfactory/logentrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logmatic.io](https://github.com/logmatic/logmatic-go) | Hook for logging to [Logmatic.io](http://logmatic.io/) |
| [Logrusly](https://github.com/sebest/logrusly) | Send logs to [Loggly](https://www.loggly.com/) |
| [Logstash](https://github.com/bshuster-repo/logrus-logstash-hook) | Hook for logging to [Logstash](https://www.elastic.co/products/logstash) | | [Logstash](https://github.com/bshuster-repo/logrus-logstash-hook) | Hook for logging to [Logstash](https://www.elastic.co/products/logstash) |
| [Mail](https://github.com/zbindenren/logrus_mail) | Hook for sending exceptions via mail |
| [Mattermost](https://github.com/shuLhan/mattermost-integration/tree/master/hooks/logrus) | Hook for logging to [Mattermost](https://mattermost.com/) |
| [Mongodb](https://github.com/weekface/mgorus) | Hook for logging to mongodb |
| [NATS-Hook](https://github.com/rybit/nats_logrus_hook) | Hook for logging to [NATS](https://nats.io) |
| [Octokit](https://github.com/dorajistyle/logrus-octokit-hook) | Hook for logging to github via octokit |
| [Papertrail](https://github.com/polds/logrus-papertrail-hook) | Send errors to the [Papertrail](https://papertrailapp.com) hosted logging service via UDP. |
| [PostgreSQL](https://github.com/gemnasium/logrus-postgresql-hook) | Send logs to [PostgreSQL](http://postgresql.org) |
| [Promrus](https://github.com/weaveworks/promrus) | Expose number of log messages as [Prometheus](https://prometheus.io/) metrics |
| [Pushover](https://github.com/toorop/logrus_pushover) | Send error via [Pushover](https://pushover.net) |
| [Raygun](https://github.com/squirkle/logrus-raygun-hook) | Hook for logging to [Raygun.io](http://raygun.io/) |
| [Redis-Hook](https://github.com/rogierlommers/logrus-redis-hook) | Hook for logging to a ELK stack (through Redis) |
| [Rollrus](https://github.com/heroku/rollrus) | Hook for sending errors to rollbar |
| [Scribe](https://github.com/sagar8192/logrus-scribe-hook) | Hook for logging to [Scribe](https://github.com/facebookarchive/scribe)|
| [Sentry](https://github.com/evalphobia/logrus_sentry) | Send errors to the Sentry error logging and aggregation service. |
| [Slackrus](https://github.com/johntdyer/slackrus) | Hook for Slack chat. |
| [Stackdriver](https://github.com/knq/sdhook) | Hook for logging to [Google Stackdriver](https://cloud.google.com/logging/) |
| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)|
| [Syslog](https://github.com/sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. |
| [Syslog TLS](https://github.com/shinji62/logrus-syslog-ng) | Send errors to remote syslog server with TLS support. |
| [Telegram](https://github.com/rossmcdonald/telegram_hook) | Hook for logging errors to [Telegram](https://telegram.org/) |
| [TraceView](https://github.com/evalphobia/logrus_appneta) | Hook for logging to [AppNeta TraceView](https://www.appneta.com/products/traceview/) |
| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) |
| [logz.io](https://github.com/ripcurld00d/logrus-logzio-hook) | Hook for logging to [logz.io](https://logz.io), a Log as a Service using Logstash |
| [SQS-Hook](https://github.com/tsarpaul/logrus_sqs) | Hook for logging to [Amazon Simple Queue Service (SQS)](https://aws.amazon.com/sqs/) |
#### Level logging #### Level logging
@ -277,7 +343,7 @@ could do:
```go ```go
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
init() { init() {
@ -304,11 +370,15 @@ The built-in logging formatters are:
without colors. without colors.
* *Note:* to force colored output when there is no TTY, set the `ForceColors` * *Note:* to force colored output when there is no TTY, set the `ForceColors`
field to `true`. To force no colored output even if there is a TTY set the field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true` `DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON. * `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).
Third party logging formatters: Third party logging formatters:
* [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can be parsed by Kubernetes and Google Container Engine.
* [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events. * [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
* [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout. * [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦. * [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
@ -354,6 +424,18 @@ srv := http.Server{
Each line written to that writer will be printed the usual way, using formatters Each line written to that writer will be printed the usual way, using formatters
and hooks. The level for those entries is `info`. and hooks. The level for those entries is `info`.
This means that we can override the standard library logger easily:
```go
logger := logrus.New()
logger.Formatter = &logrus.JSONFormatter{}
// Use logrus for standard log output
// Note that `log` here references stdlib's log
// Not logrus imported under the name `log`.
log.SetOutput(logger.Writer())
```
#### Rotation #### Rotation
Log rotation is not provided with Logrus. Log rotation should be done by an Log rotation is not provided with Logrus. Log rotation should be done by an
@ -365,6 +447,7 @@ entries. It should not be a feature of the application-level logger.
| Tool | Description | | Tool | Description |
| ---- | ----------- | | ---- | ----------- |
|[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will generated with different config at different environment.| |[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will generated with different config at different environment.|
|[Logrus Viper Helper](https://github.com/heirko/go-contrib/tree/master/logrusHelper)|An Helper around Logrus to wrap with spf13/Viper to load configuration with fangs! And to simplify Logrus configuration use some behavior of [Logrus Mate](https://github.com/gogap/logrus_mate). [sample](https://github.com/heirko/iris-contrib/blob/master/middleware/logrus-logger/example) |
#### Testing #### Testing
@ -374,15 +457,24 @@ Logrus has a built in facility for asserting the presence of log messages. This
* a test logger (`test.NewNullLogger`) that just records log messages (and does not output any): * a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
```go ```go
logger, hook := NewNullLogger() import(
logger.Error("Hello error") "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"testing"
)
assert.Equal(1, len(hook.Entries)) func TestSomething(t*testing.T){
assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level) logger, hook := test.NewNullLogger()
assert.Equal("Hello error", hook.LastEntry().Message) logger.Error("Helloerror")
hook.Reset() assert.Equal(t, 1, len(hook.Entries))
assert.Nil(hook.LastEntry()) assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
assert.Equal(t, "Helloerror", hook.LastEntry().Message)
hook.Reset()
assert.Nil(t, hook.LastEntry())
}
``` ```
#### Fatal handlers #### Fatal handlers
@ -400,3 +492,20 @@ handler := func() {
logrus.RegisterExitHandler(handler) logrus.RegisterExitHandler(handler)
... ...
``` ```
#### Thread safety
By default Logger is protected by mutex for concurrent writes, this mutex is invoked when calling hooks and writing logs.
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
Situation when locking is not needed includes:
* You have no hooks registered, or hooks calling is already thread-safe.
* Writing to logger.Out is already thread-safe, for example:
1) logger.Out is protected by locks.
2) logger.Out is a os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allow multi-thread/multi-process writing)
(Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)

View File

@ -1,7 +1,7 @@
package logrus package logrus
// The following code was sourced and modified from the // The following code was sourced and modified from the
// https://bitbucket.org/tebeka/atexit package governed by the following license: // https://github.com/tebeka/atexit package governed by the following license:
// //
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>. // Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
// //

View File

@ -2,7 +2,10 @@ package logrus
import ( import (
"io/ioutil" "io/ioutil"
"log"
"os"
"os/exec" "os/exec"
"path/filepath"
"testing" "testing"
"time" "time"
) )
@ -11,30 +14,36 @@ func TestRegister(t *testing.T) {
current := len(handlers) current := len(handlers)
RegisterExitHandler(func() {}) RegisterExitHandler(func() {})
if len(handlers) != current+1 { if len(handlers) != current+1 {
t.Fatalf("can't add handler") t.Fatalf("expected %d handlers, got %d", current+1, len(handlers))
} }
} }
func TestHandler(t *testing.T) { func TestHandler(t *testing.T) {
gofile := "/tmp/testprog.go" tempDir, err := ioutil.TempDir("", "test_handler")
if err != nil {
log.Fatalf("can't create temp dir. %q", err)
}
defer os.RemoveAll(tempDir)
gofile := filepath.Join(tempDir, "gofile.go")
if err := ioutil.WriteFile(gofile, testprog, 0666); err != nil { if err := ioutil.WriteFile(gofile, testprog, 0666); err != nil {
t.Fatalf("can't create go file") t.Fatalf("can't create go file. %q", err)
} }
outfile := "/tmp/testprog.out" outfile := filepath.Join(tempDir, "outfile.out")
arg := time.Now().UTC().String() arg := time.Now().UTC().String()
err := exec.Command("go", "run", gofile, outfile, arg).Run() err = exec.Command("go", "run", gofile, outfile, arg).Run()
if err == nil { if err == nil {
t.Fatalf("completed normally, should have failed") t.Fatalf("completed normally, should have failed")
} }
data, err := ioutil.ReadFile(outfile) data, err := ioutil.ReadFile(outfile)
if err != nil { if err != nil {
t.Fatalf("can't read output file %s", outfile) t.Fatalf("can't read output file %s. %q", outfile, err)
} }
if string(data) != arg { if string(data) != arg {
t.Fatalf("bad data") t.Fatalf("bad data. Expected %q, got %q", data, arg)
} }
} }
@ -44,7 +53,7 @@ var testprog = []byte(`
package main package main
import ( import (
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"

14
src/vendor/github.com/Sirupsen/logrus/appveyor.yml generated vendored Normal file
View File

@ -0,0 +1,14 @@
version: "{build}"
platform: x64
clone_folder: c:\gopath\src\github.com\sirupsen\logrus
environment:
GOPATH: c:\gopath
branches:
only:
- master
install:
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
- go version
build_script:
- go get -t
- go test

View File

@ -7,7 +7,7 @@ The simplest way to use Logrus is simply the package-level exported logger:
package main package main
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func main() { func main() {
@ -21,6 +21,6 @@ The simplest way to use Logrus is simply the package-level exported logger:
Output: Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10 time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/Sirupsen/logrus For a full guide visit https://github.com/sirupsen/logrus
*/ */
package logrus package logrus

View File

@ -3,11 +3,21 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"sync"
"time" "time"
) )
var bufferPool *sync.Pool
func init() {
bufferPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
}
// Defines the key when adding errors using WithError. // Defines the key when adding errors using WithError.
var ErrorKey = "error" var ErrorKey = "error"
@ -25,10 +35,14 @@ type Entry struct {
Time time.Time Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic // Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
Level Level Level Level
// Message passed to Debug, Info, Warn, Error, Fatal or Panic // Message passed to Debug, Info, Warn, Error, Fatal or Panic
Message string Message string
// When formatter is called in entry.log(), an Buffer may be set to entry
Buffer *bytes.Buffer
} }
func NewEntry(logger *Logger) *Entry { func NewEntry(logger *Logger) *Entry {
@ -39,21 +53,15 @@ func NewEntry(logger *Logger) *Entry {
} }
} }
// Returns a reader for the entry, which is a proxy to the formatter.
func (entry *Entry) Reader() (*bytes.Buffer, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
return bytes.NewBuffer(serialized), err
}
// Returns the string representation from the reader and ultimately the // Returns the string representation from the reader and ultimately the
// formatter. // formatter.
func (entry *Entry) String() (string, error) { func (entry *Entry) String() (string, error) {
reader, err := entry.Reader() serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil { if err != nil {
return "", err return "", err
} }
str := string(serialized)
return reader.String(), err return str, nil
} }
// Add an error as single field (using the key defined in ErrorKey) to the Entry. // Add an error as single field (using the key defined in ErrorKey) to the Entry.
@ -81,30 +89,21 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
// This function is not declared with a pointer value because otherwise // This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines // race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) { func (entry Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
entry.Time = time.Now() entry.Time = time.Now()
entry.Level = level entry.Level = level
entry.Message = msg entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil { entry.fireHooks()
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()
}
reader, err := entry.Reader() buffer = bufferPool.Get().(*bytes.Buffer)
if err != nil { buffer.Reset()
entry.Logger.mu.Lock() defer bufferPool.Put(buffer)
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) entry.Buffer = buffer
entry.Logger.mu.Unlock()
}
entry.Logger.mu.Lock() entry.write()
defer entry.Logger.mu.Unlock()
_, err = io.Copy(entry.Logger.Out, reader) entry.Buffer = nil
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
// To avoid Entry#log() returning a value that only would make sense for // To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking // panic() to use in Entry#Panic(), we avoid the allocation by checking
@ -114,8 +113,33 @@ func (entry Entry) log(level Level, msg string) {
} }
} }
// This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines
func (entry Entry) fireHooks() {
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
err := entry.Logger.Hooks.Fire(entry.Level, &entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
}
}
func (entry *Entry) write() {
serialized, err := entry.Logger.Formatter.Format(entry)
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
} else {
_, err = entry.Logger.Out.Write(serialized)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
}
func (entry *Entry) Debug(args ...interface{}) { func (entry *Entry) Debug(args ...interface{}) {
if entry.Logger.Level >= DebugLevel { if entry.Logger.level() >= DebugLevel {
entry.log(DebugLevel, fmt.Sprint(args...)) entry.log(DebugLevel, fmt.Sprint(args...))
} }
} }
@ -125,13 +149,13 @@ func (entry *Entry) Print(args ...interface{}) {
} }
func (entry *Entry) Info(args ...interface{}) { func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.Level >= InfoLevel { if entry.Logger.level() >= InfoLevel {
entry.log(InfoLevel, fmt.Sprint(args...)) entry.log(InfoLevel, fmt.Sprint(args...))
} }
} }
func (entry *Entry) Warn(args ...interface{}) { func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.Level >= WarnLevel { if entry.Logger.level() >= WarnLevel {
entry.log(WarnLevel, fmt.Sprint(args...)) entry.log(WarnLevel, fmt.Sprint(args...))
} }
} }
@ -141,20 +165,20 @@ func (entry *Entry) Warning(args ...interface{}) {
} }
func (entry *Entry) Error(args ...interface{}) { func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel { if entry.Logger.level() >= ErrorLevel {
entry.log(ErrorLevel, fmt.Sprint(args...)) entry.log(ErrorLevel, fmt.Sprint(args...))
} }
} }
func (entry *Entry) Fatal(args ...interface{}) { func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.Level >= FatalLevel { if entry.Logger.level() >= FatalLevel {
entry.log(FatalLevel, fmt.Sprint(args...)) entry.log(FatalLevel, fmt.Sprint(args...))
} }
Exit(1) Exit(1)
} }
func (entry *Entry) Panic(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.Level >= PanicLevel { if entry.Logger.level() >= PanicLevel {
entry.log(PanicLevel, fmt.Sprint(args...)) entry.log(PanicLevel, fmt.Sprint(args...))
} }
panic(fmt.Sprint(args...)) panic(fmt.Sprint(args...))
@ -163,13 +187,13 @@ func (entry *Entry) Panic(args ...interface{}) {
// Entry Printf family functions // Entry Printf family functions
func (entry *Entry) Debugf(format string, args ...interface{}) { func (entry *Entry) Debugf(format string, args ...interface{}) {
if entry.Logger.Level >= DebugLevel { if entry.Logger.level() >= DebugLevel {
entry.Debug(fmt.Sprintf(format, args...)) entry.Debug(fmt.Sprintf(format, args...))
} }
} }
func (entry *Entry) Infof(format string, args ...interface{}) { func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.Level >= InfoLevel { if entry.Logger.level() >= InfoLevel {
entry.Info(fmt.Sprintf(format, args...)) entry.Info(fmt.Sprintf(format, args...))
} }
} }
@ -179,7 +203,7 @@ func (entry *Entry) Printf(format string, args ...interface{}) {
} }
func (entry *Entry) Warnf(format string, args ...interface{}) { func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.Level >= WarnLevel { if entry.Logger.level() >= WarnLevel {
entry.Warn(fmt.Sprintf(format, args...)) entry.Warn(fmt.Sprintf(format, args...))
} }
} }
@ -189,20 +213,20 @@ func (entry *Entry) Warningf(format string, args ...interface{}) {
} }
func (entry *Entry) Errorf(format string, args ...interface{}) { func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.Level >= ErrorLevel { if entry.Logger.level() >= ErrorLevel {
entry.Error(fmt.Sprintf(format, args...)) entry.Error(fmt.Sprintf(format, args...))
} }
} }
func (entry *Entry) Fatalf(format string, args ...interface{}) { func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.Level >= FatalLevel { if entry.Logger.level() >= FatalLevel {
entry.Fatal(fmt.Sprintf(format, args...)) entry.Fatal(fmt.Sprintf(format, args...))
} }
Exit(1) Exit(1)
} }
func (entry *Entry) Panicf(format string, args ...interface{}) { func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.Level >= PanicLevel { if entry.Logger.level() >= PanicLevel {
entry.Panic(fmt.Sprintf(format, args...)) entry.Panic(fmt.Sprintf(format, args...))
} }
} }
@ -210,13 +234,13 @@ func (entry *Entry) Panicf(format string, args ...interface{}) {
// Entry Println family functions // Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) { func (entry *Entry) Debugln(args ...interface{}) {
if entry.Logger.Level >= DebugLevel { if entry.Logger.level() >= DebugLevel {
entry.Debug(entry.sprintlnn(args...)) entry.Debug(entry.sprintlnn(args...))
} }
} }
func (entry *Entry) Infoln(args ...interface{}) { func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.Level >= InfoLevel { if entry.Logger.level() >= InfoLevel {
entry.Info(entry.sprintlnn(args...)) entry.Info(entry.sprintlnn(args...))
} }
} }
@ -226,7 +250,7 @@ func (entry *Entry) Println(args ...interface{}) {
} }
func (entry *Entry) Warnln(args ...interface{}) { func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.Level >= WarnLevel { if entry.Logger.level() >= WarnLevel {
entry.Warn(entry.sprintlnn(args...)) entry.Warn(entry.sprintlnn(args...))
} }
} }
@ -236,20 +260,20 @@ func (entry *Entry) Warningln(args ...interface{}) {
} }
func (entry *Entry) Errorln(args ...interface{}) { func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel { if entry.Logger.level() >= ErrorLevel {
entry.Error(entry.sprintlnn(args...)) entry.Error(entry.sprintlnn(args...))
} }
} }
func (entry *Entry) Fatalln(args ...interface{}) { func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.Level >= FatalLevel { if entry.Logger.level() >= FatalLevel {
entry.Fatal(entry.sprintlnn(args...)) entry.Fatal(entry.sprintlnn(args...))
} }
Exit(1) Exit(1)
} }
func (entry *Entry) Panicln(args ...interface{}) { func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.Level >= PanicLevel { if entry.Logger.level() >= PanicLevel {
entry.Panic(entry.sprintlnn(args...)) entry.Panic(entry.sprintlnn(args...))
} }
} }

View File

@ -75,3 +75,41 @@ func TestEntryPanicf(t *testing.T) {
entry := NewEntry(logger) entry := NewEntry(logger)
entry.WithField("err", errBoom).Panicf("kaboom %v", true) entry.WithField("err", errBoom).Panicf("kaboom %v", true)
} }
const (
badMessage = "this is going to panic"
panicMessage = "this is broken"
)
type panickyHook struct{}
func (p *panickyHook) Levels() []Level {
return []Level{InfoLevel}
}
func (p *panickyHook) Fire(entry *Entry) error {
if entry.Message == badMessage {
panic(panicMessage)
}
return nil
}
func TestEntryHooksPanic(t *testing.T) {
logger := New()
logger.Out = &bytes.Buffer{}
logger.Level = InfoLevel
logger.Hooks.Add(&panickyHook{})
defer func() {
p := recover()
assert.NotNil(t, p)
assert.Equal(t, panicMessage, p)
entry := NewEntry(logger)
entry.Info("another message")
}()
entry := NewEntry(logger)
entry.Info(badMessage)
}

View File

@ -0,0 +1,69 @@
package logrus_test
import (
"github.com/sirupsen/logrus"
"os"
)
func Example_basic() {
var log = logrus.New()
log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) //default
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
log.Level = logrus.DebugLevel
log.Out = os.Stdout
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
// if err == nil {
// log.Out = file
// } else {
// log.Info("Failed to log to file, using default stderr")
// }
defer func() {
err := recover()
if err != nil {
entry := err.(*logrus.Entry)
log.WithFields(logrus.Fields{
"omg": true,
"err_animal": entry.Data["animal"],
"err_size": entry.Data["size"],
"err_level": entry.Level,
"err_message": entry.Message,
"number": 100,
}).Error("The ice breaks!") // or use Fatal() to force the process to exit with a nonzero code
}
}()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{
"animal": "orca",
"size": 9009,
}).Panic("It's over 9000!")
// Output:
// level=debug msg="Started observing beach" animal=walrus number=8
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
// level=warning msg="The group's number increased tremendously!" number=122 omg=true
// level=debug msg="Temperature changes" temperature=-4
// level=panic msg="It's over 9000!" animal=orca size=9009
// level=error msg="The ice breaks!" err_animal=orca err_level=panic err_message="It's over 9000!" err_size=9009 number=100 omg=true
}

View File

@ -0,0 +1,35 @@
package logrus_test
import (
"github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
"os"
)
func Example_hook() {
var log = logrus.New()
log.Formatter = new(logrus.TextFormatter) // default
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
log.Out = os.Stdout
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Error("The ice breaks!")
// Output:
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
// level=warning msg="The group's number increased tremendously!" number=122 omg=true
// level=error msg="The ice breaks!" number=100 omg=true
}

View File

@ -1,50 +0,0 @@
package main
import (
"github.com/Sirupsen/logrus"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) // default
log.Level = logrus.DebugLevel
}
func main() {
defer func() {
err := recover()
if err != nil {
log.WithFields(logrus.Fields{
"omg": true,
"err": err,
"number": 100,
}).Fatal("The ice breaks!")
}
}()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{
"animal": "orca",
"size": 9009,
}).Panic("It's over 9000!")
}

View File

@ -1,30 +0,0 @@
package main
import (
"github.com/Sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.TextFormatter) // default
log.Hooks.Add(airbrake.NewHook(123, "xyz", "development"))
}
func main() {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}

View File

@ -31,14 +31,14 @@ func SetFormatter(formatter Formatter) {
func SetLevel(level Level) { func SetLevel(level Level) {
std.mu.Lock() std.mu.Lock()
defer std.mu.Unlock() defer std.mu.Unlock()
std.Level = level std.SetLevel(level)
} }
// GetLevel returns the standard logger level. // GetLevel returns the standard logger level.
func GetLevel() Level { func GetLevel() Level {
std.mu.Lock() std.mu.Lock()
defer std.mu.Unlock() defer std.mu.Unlock()
return std.Level return std.level()
} }
// AddHook adds a hook to the standard logger hooks. // AddHook adds a hook to the standard logger hooks.

View File

@ -2,7 +2,7 @@ package logrus
import "time" import "time"
const DefaultTimestampFormat = time.RFC3339 const defaultTimestampFormat = time.RFC3339
// The Formatter interface is used to implement a custom Formatter. It takes an // The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones: // `Entry`. It exposes all the fields, including the default ones:

View File

@ -80,11 +80,14 @@ func BenchmarkLargeJSONFormatter(b *testing.B) {
} }
func doBenchmark(b *testing.B, formatter Formatter, fields Fields) { func doBenchmark(b *testing.B, formatter Formatter, fields Fields) {
logger := New()
entry := &Entry{ entry := &Entry{
Time: time.Time{}, Time: time.Time{},
Level: InfoLevel, Level: InfoLevel,
Message: "message", Message: "message",
Data: fields, Data: fields,
Logger: logger,
} }
var d []byte var d []byte
var err error var err error

View File

@ -1,6 +1,7 @@
package logrus package logrus
import ( import (
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -120,3 +121,24 @@ func TestErrorHookShouldFireOnError(t *testing.T) {
assert.Equal(t, hook.Fired, true) assert.Equal(t, hook.Fired, true)
}) })
} }
func TestAddHookRace(t *testing.T) {
var wg sync.WaitGroup
wg.Add(2)
hook := new(ErrorHook)
LogAndAssertJSON(t, func(log *Logger) {
go func() {
defer wg.Done()
log.AddHook(hook)
}()
go func() {
defer wg.Done()
log.Error("test")
}()
wg.Wait()
}, func(fields Fields) {
// the line may have been logged
// before the hook was added, so we can't
// actually assert on the hook
})
}

View File

@ -5,13 +5,13 @@
```go ```go
import ( import (
"log/syslog" "log/syslog"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" lSyslog "github.com/sirupsen/logrus/hooks/syslog"
) )
func main() { func main() {
log := logrus.New() log := logrus.New()
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "") hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err == nil { if err == nil {
log.Hooks.Add(hook) log.Hooks.Add(hook)
@ -24,16 +24,16 @@ If you want to connect to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "
```go ```go
import ( import (
"log/syslog" "log/syslog"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" lSyslog "github.com/sirupsen/logrus/hooks/syslog"
) )
func main() { func main() {
log := logrus.New() log := logrus.New()
hook, err := logrus_syslog.NewSyslogHook("", "", syslog.LOG_INFO, "") hook, err := lSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "")
if err == nil { if err == nil {
log.Hooks.Add(hook) log.Hooks.Add(hook)
} }
} }
``` ```

View File

@ -1,12 +1,13 @@
// +build !windows,!nacl,!plan9 // +build !windows,!nacl,!plan9
package logrus_syslog package syslog
import ( import (
"fmt" "fmt"
"github.com/Sirupsen/logrus"
"log/syslog" "log/syslog"
"os" "os"
"github.com/sirupsen/logrus"
) )
// SyslogHook to send logs via syslog. // SyslogHook to send logs via syslog.

View File

@ -1,9 +1,10 @@
package logrus_syslog package syslog
import ( import (
"github.com/Sirupsen/logrus"
"log/syslog" "log/syslog"
"testing" "testing"
"github.com/sirupsen/logrus"
) )
func TestLocalhostAddAndPrint(t *testing.T) { func TestLocalhostAddAndPrint(t *testing.T) {

View File

@ -1,17 +1,25 @@
// The Test package is used for testing logrus. It is here for backwards
// compatibility from when logrus' organization was upper-case. Please use
// lower-case logrus and the `null` package instead of this one.
package test package test
import ( import (
"io/ioutil" "io/ioutil"
"sync"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// test.Hook is a hook designed for dealing with logs in test scenarios. // Hook is a hook designed for dealing with logs in test scenarios.
type Hook struct { type Hook struct {
// Entries is an array of all entries that have been received by this hook.
// For safe access, use the AllEntries() method, rather than reading this
// value directly.
Entries []*logrus.Entry Entries []*logrus.Entry
mu sync.RWMutex
} }
// Installs a test hook for the global logger. // NewGlobal installs a test hook for the global logger.
func NewGlobal() *Hook { func NewGlobal() *Hook {
hook := new(Hook) hook := new(Hook)
@ -21,7 +29,7 @@ func NewGlobal() *Hook {
} }
// Installs a test hook for a given local logger. // NewLocal installs a test hook for a given local logger.
func NewLocal(logger *logrus.Logger) *Hook { func NewLocal(logger *logrus.Logger) *Hook {
hook := new(Hook) hook := new(Hook)
@ -31,7 +39,7 @@ func NewLocal(logger *logrus.Logger) *Hook {
} }
// Creates a discarding logger and installs the test hook. // NewNullLogger creates a discarding logger and installs the test hook.
func NewNullLogger() (*logrus.Logger, *Hook) { func NewNullLogger() (*logrus.Logger, *Hook) {
logger := logrus.New() logger := logrus.New()
@ -42,6 +50,8 @@ func NewNullLogger() (*logrus.Logger, *Hook) {
} }
func (t *Hook) Fire(e *logrus.Entry) error { func (t *Hook) Fire(e *logrus.Entry) error {
t.mu.Lock()
defer t.mu.Unlock()
t.Entries = append(t.Entries, e) t.Entries = append(t.Entries, e)
return nil return nil
} }
@ -51,17 +61,35 @@ func (t *Hook) Levels() []logrus.Level {
} }
// LastEntry returns the last entry that was logged or nil. // LastEntry returns the last entry that was logged or nil.
func (t *Hook) LastEntry() (l *logrus.Entry) { func (t *Hook) LastEntry() *logrus.Entry {
t.mu.RLock()
if i := len(t.Entries) - 1; i < 0 { defer t.mu.RUnlock()
i := len(t.Entries) - 1
if i < 0 {
return nil return nil
} else {
return t.Entries[i]
} }
// Make a copy, for safety
e := *t.Entries[i]
return &e
}
// AllEntries returns all entries that were logged.
func (t *Hook) AllEntries() []*logrus.Entry {
t.mu.RLock()
defer t.mu.RUnlock()
// Make a copy so the returned value won't race with future log requests
entries := make([]*logrus.Entry, len(t.Entries))
for i, entry := range t.Entries {
// Make a copy, for safety
e := *entry
entries[i] = &e
}
return entries
} }
// Reset removes all Entries from this test hook. // Reset removes all Entries from this test hook.
func (t *Hook) Reset() { func (t *Hook) Reset() {
t.mu.Lock()
defer t.mu.Unlock()
t.Entries = make([]*logrus.Entry, 0) t.Entries = make([]*logrus.Entry, 0)
} }

View File

@ -1,14 +1,14 @@
package test package test
import ( import (
"sync"
"testing" "testing"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAllHooks(t *testing.T) { func TestAllHooks(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
logger, hook := NewNullLogger() logger, hook := NewNullLogger()
@ -35,5 +35,27 @@ func TestAllHooks(t *testing.T) {
assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level) assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
assert.Equal("Hello error", hook.LastEntry().Message) assert.Equal("Hello error", hook.LastEntry().Message)
assert.Equal(1, len(hook.Entries)) assert.Equal(1, len(hook.Entries))
}
func TestLoggingWithHooksRace(t *testing.T) {
assert := assert.New(t)
logger, hook := NewNullLogger()
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
logger.Info("info")
wg.Done()
}()
}
assert.Equal(logrus.InfoLevel, hook.LastEntry().Level)
assert.Equal("info", hook.LastEntry().Message)
wg.Wait()
entries := hook.AllEntries()
assert.Equal(100, len(entries))
} }

View File

@ -5,18 +5,54 @@ import (
"fmt" "fmt"
) )
type fieldKey string
// FieldMap allows customization of the key names for default fields.
type FieldMap map[fieldKey]string
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
)
func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok {
return k
}
return string(key)
}
// JSONFormatter formats logs into parsable json
type JSONFormatter struct { type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps. // TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string TimestampFormat string
// DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &JSONFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// },
// }
FieldMap FieldMap
} }
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3) data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data { for k, v := range entry.Data {
switch v := v.(type) { switch v := v.(type) {
case error: case error:
// Otherwise errors are ignored by `encoding/json` // Otherwise errors are ignored by `encoding/json`
// https://github.com/Sirupsen/logrus/issues/137 // https://github.com/sirupsen/logrus/issues/137
data[k] = v.Error() data[k] = v.Error()
default: default:
data[k] = v data[k] = v
@ -26,12 +62,14 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat timestampFormat = defaultTimestampFormat
} }
data["time"] = entry.Time.Format(timestampFormat) if !f.DisableTimestamp {
data["msg"] = entry.Message data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
data["level"] = entry.Level.String() }
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
serialized, err := json.Marshal(data) serialized, err := json.Marshal(data)
if err != nil { if err != nil {

View File

@ -3,7 +3,7 @@ package logrus
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"strings"
"testing" "testing"
) )
@ -118,3 +118,82 @@ func TestJSONEntryEndsWithNewline(t *testing.T) {
t.Fatal("Expected JSON log entry to end with a newline") t.Fatal("Expected JSON log entry to end with a newline")
} }
} }
func TestJSONMessageKey(t *testing.T) {
formatter := &JSONFormatter{
FieldMap: FieldMap{
FieldKeyMsg: "message",
},
}
b, err := formatter.Format(&Entry{Message: "oh hai"})
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
s := string(b)
if !(strings.Contains(s, "message") && strings.Contains(s, "oh hai")) {
t.Fatal("Expected JSON to format message key")
}
}
func TestJSONLevelKey(t *testing.T) {
formatter := &JSONFormatter{
FieldMap: FieldMap{
FieldKeyLevel: "somelevel",
},
}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
s := string(b)
if !strings.Contains(s, "somelevel") {
t.Fatal("Expected JSON to format level key")
}
}
func TestJSONTimeKey(t *testing.T) {
formatter := &JSONFormatter{
FieldMap: FieldMap{
FieldKeyTime: "timeywimey",
},
}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
s := string(b)
if !strings.Contains(s, "timeywimey") {
t.Fatal("Expected JSON to format time key")
}
}
func TestJSONDisableTimestamp(t *testing.T) {
formatter := &JSONFormatter{
DisableTimestamp: true,
}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
s := string(b)
if strings.Contains(s, FieldKeyTime) {
t.Error("Did not prevent timestamp", s)
}
}
func TestJSONEnableTimestamp(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
s := string(b)
if !strings.Contains(s, FieldKeyTime) {
t.Error("Timestamp not present", s)
}
}

View File

@ -4,6 +4,7 @@ import (
"io" "io"
"os" "os"
"sync" "sync"
"sync/atomic"
) )
type Logger struct { type Logger struct {
@ -24,10 +25,33 @@ type Logger struct {
Formatter Formatter Formatter Formatter
// The logging level the logger should log at. This is typically (and defaults // The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged. `logrus.Debug` is useful in // logged.
Level Level Level Level
// Used to sync writing to the log. // Used to sync writing to the log. Locking is enabled by Default
mu sync.Mutex mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
}
type MutexWrap struct {
lock sync.Mutex
disabled bool
}
func (mw *MutexWrap) Lock() {
if !mw.disabled {
mw.lock.Lock()
}
}
func (mw *MutexWrap) Unlock() {
if !mw.disabled {
mw.lock.Unlock()
}
}
func (mw *MutexWrap) Disable() {
mw.disabled = true
} }
// Creates a new logger. Configuration should be set by changing `Formatter`, // Creates a new logger. Configuration should be set by changing `Formatter`,
@ -51,162 +75,249 @@ func New() *Logger {
} }
} }
func (logger *Logger) newEntry() *Entry {
entry, ok := logger.entryPool.Get().(*Entry)
if ok {
return entry
}
return NewEntry(logger)
}
func (logger *Logger) releaseEntry(entry *Entry) {
logger.entryPool.Put(entry)
}
// Adds a field to the log entry, note that it doesn't log until you call // Adds a field to the log entry, note that it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry. // Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`. // If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry { func (logger *Logger) WithField(key string, value interface{}) *Entry {
return NewEntry(logger).WithField(key, value) entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
} }
// Adds a struct of fields to the log entry. All it does is call `WithField` for // Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`. // each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry { func (logger *Logger) WithFields(fields Fields) *Entry {
return NewEntry(logger).WithFields(fields) entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithFields(fields)
} }
// Add an error as single field to the log entry. All it does is call // Add an error as single field to the log entry. All it does is call
// `WithError` for the given `error`. // `WithError` for the given `error`.
func (logger *Logger) WithError(err error) *Entry { func (logger *Logger) WithError(err error) *Entry {
return NewEntry(logger).WithError(err) entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithError(err)
} }
func (logger *Logger) Debugf(format string, args ...interface{}) { func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.Level >= DebugLevel { if logger.level() >= DebugLevel {
NewEntry(logger).Debugf(format, args...) entry := logger.newEntry()
entry.Debugf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Infof(format string, args ...interface{}) { func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel { if logger.level() >= InfoLevel {
NewEntry(logger).Infof(format, args...) entry := logger.newEntry()
entry.Infof(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Printf(format string, args ...interface{}) { func (logger *Logger) Printf(format string, args ...interface{}) {
NewEntry(logger).Printf(format, args...) entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
} }
func (logger *Logger) Warnf(format string, args ...interface{}) { func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.Level >= WarnLevel { if logger.level() >= WarnLevel {
NewEntry(logger).Warnf(format, args...) entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Warningf(format string, args ...interface{}) { func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.Level >= WarnLevel { if logger.level() >= WarnLevel {
NewEntry(logger).Warnf(format, args...) entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Errorf(format string, args ...interface{}) { func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.Level >= ErrorLevel { if logger.level() >= ErrorLevel {
NewEntry(logger).Errorf(format, args...) entry := logger.newEntry()
entry.Errorf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Fatalf(format string, args ...interface{}) { func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.Level >= FatalLevel { if logger.level() >= FatalLevel {
NewEntry(logger).Fatalf(format, args...) entry := logger.newEntry()
entry.Fatalf(format, args...)
logger.releaseEntry(entry)
} }
Exit(1) Exit(1)
} }
func (logger *Logger) Panicf(format string, args ...interface{}) { func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.Level >= PanicLevel { if logger.level() >= PanicLevel {
NewEntry(logger).Panicf(format, args...) entry := logger.newEntry()
entry.Panicf(format, args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Debug(args ...interface{}) { func (logger *Logger) Debug(args ...interface{}) {
if logger.Level >= DebugLevel { if logger.level() >= DebugLevel {
NewEntry(logger).Debug(args...) entry := logger.newEntry()
entry.Debug(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Info(args ...interface{}) { func (logger *Logger) Info(args ...interface{}) {
if logger.Level >= InfoLevel { if logger.level() >= InfoLevel {
NewEntry(logger).Info(args...) entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Print(args ...interface{}) { func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Info(args...) entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
} }
func (logger *Logger) Warn(args ...interface{}) { func (logger *Logger) Warn(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.level() >= WarnLevel {
NewEntry(logger).Warn(args...) entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Warning(args ...interface{}) { func (logger *Logger) Warning(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.level() >= WarnLevel {
NewEntry(logger).Warn(args...) entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Error(args ...interface{}) { func (logger *Logger) Error(args ...interface{}) {
if logger.Level >= ErrorLevel { if logger.level() >= ErrorLevel {
NewEntry(logger).Error(args...) entry := logger.newEntry()
entry.Error(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Fatal(args ...interface{}) { func (logger *Logger) Fatal(args ...interface{}) {
if logger.Level >= FatalLevel { if logger.level() >= FatalLevel {
NewEntry(logger).Fatal(args...) entry := logger.newEntry()
entry.Fatal(args...)
logger.releaseEntry(entry)
} }
Exit(1) Exit(1)
} }
func (logger *Logger) Panic(args ...interface{}) { func (logger *Logger) Panic(args ...interface{}) {
if logger.Level >= PanicLevel { if logger.level() >= PanicLevel {
NewEntry(logger).Panic(args...) entry := logger.newEntry()
entry.Panic(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Debugln(args ...interface{}) { func (logger *Logger) Debugln(args ...interface{}) {
if logger.Level >= DebugLevel { if logger.level() >= DebugLevel {
NewEntry(logger).Debugln(args...) entry := logger.newEntry()
entry.Debugln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Infoln(args ...interface{}) { func (logger *Logger) Infoln(args ...interface{}) {
if logger.Level >= InfoLevel { if logger.level() >= InfoLevel {
NewEntry(logger).Infoln(args...) entry := logger.newEntry()
entry.Infoln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Println(args ...interface{}) { func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...) entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
} }
func (logger *Logger) Warnln(args ...interface{}) { func (logger *Logger) Warnln(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.level() >= WarnLevel {
NewEntry(logger).Warnln(args...) entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Warningln(args ...interface{}) { func (logger *Logger) Warningln(args ...interface{}) {
if logger.Level >= WarnLevel { if logger.level() >= WarnLevel {
NewEntry(logger).Warnln(args...) entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Errorln(args ...interface{}) { func (logger *Logger) Errorln(args ...interface{}) {
if logger.Level >= ErrorLevel { if logger.level() >= ErrorLevel {
NewEntry(logger).Errorln(args...) entry := logger.newEntry()
entry.Errorln(args...)
logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Fatalln(args ...interface{}) { func (logger *Logger) Fatalln(args ...interface{}) {
if logger.Level >= FatalLevel { if logger.level() >= FatalLevel {
NewEntry(logger).Fatalln(args...) entry := logger.newEntry()
entry.Fatalln(args...)
logger.releaseEntry(entry)
} }
Exit(1) Exit(1)
} }
func (logger *Logger) Panicln(args ...interface{}) { func (logger *Logger) Panicln(args ...interface{}) {
if logger.Level >= PanicLevel { if logger.level() >= PanicLevel {
NewEntry(logger).Panicln(args...) entry := logger.newEntry()
entry.Panicln(args...)
logger.releaseEntry(entry)
} }
} }
//When file is opened with appending mode, it's safe to
//write concurrently to a file (within 4k message on Linux).
//In these cases user can choose to disable the lock.
func (logger *Logger) SetNoLock() {
logger.mu.Disable()
}
func (logger *Logger) level() Level {
return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
}
func (logger *Logger) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
}
func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Hooks.Add(hook)
}

View File

@ -0,0 +1,61 @@
package logrus
import (
"os"
"testing"
)
// smallFields is a small size data set for benchmarking
var loggerFields = Fields{
"foo": "bar",
"baz": "qux",
"one": "two",
"three": "four",
}
func BenchmarkDummyLogger(b *testing.B) {
nullf, err := os.OpenFile("/dev/null", os.O_WRONLY, 0666)
if err != nil {
b.Fatalf("%v", err)
}
defer nullf.Close()
doLoggerBenchmark(b, nullf, &TextFormatter{DisableColors: true}, smallFields)
}
func BenchmarkDummyLoggerNoLock(b *testing.B) {
nullf, err := os.OpenFile("/dev/null", os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
b.Fatalf("%v", err)
}
defer nullf.Close()
doLoggerBenchmarkNoLock(b, nullf, &TextFormatter{DisableColors: true}, smallFields)
}
func doLoggerBenchmark(b *testing.B, out *os.File, formatter Formatter, fields Fields) {
logger := Logger{
Out: out,
Level: InfoLevel,
Formatter: formatter,
}
entry := logger.WithFields(fields)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
entry.Info("aaa")
}
})
}
func doLoggerBenchmarkNoLock(b *testing.B, out *os.File, formatter Formatter, fields Fields) {
logger := Logger{
Out: out,
Level: InfoLevel,
Formatter: formatter,
}
logger.SetNoLock()
entry := logger.WithFields(fields)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
entry.Info("aaa")
}
})
}

View File

@ -10,7 +10,7 @@ import (
type Fields map[string]interface{} type Fields map[string]interface{}
// Level type // Level type
type Level uint8 type Level uint32
// Convert the Level to a string. E.g. PanicLevel becomes "panic". // Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string { func (level Level) String() string {

View File

@ -359,3 +359,28 @@ func TestLogrusInterface(t *testing.T) {
e := logger.WithField("another", "value") e := logger.WithField("another", "value")
fn(e) fn(e)
} }
// Implements io.Writer using channels for synchronization, so we can wait on
// the Entry.Writer goroutine to write in a non-racey way. This does assume that
// there is a single call to Logger.Out for each message.
type channelWriter chan []byte
func (cw channelWriter) Write(p []byte) (int, error) {
cw <- p
return len(p), nil
}
func TestEntryWriter(t *testing.T) {
cw := channelWriter(make(chan []byte, 1))
log := New()
log.Out = cw
log.Formatter = new(JSONFormatter)
log.WithField("foo", "bar").WriterLevel(WarnLevel).Write([]byte("hello\n"))
bs := <-cw
var fields Fields
err := json.Unmarshal(bs, &fields)
assert.Nil(t, err)
assert.Equal(t, fields["foo"], "bar")
assert.Equal(t, fields["level"], "warning")
}

View File

@ -1,9 +1,10 @@
// +build darwin freebsd openbsd netbsd dragonfly // +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine,!gopherjs
package logrus package logrus
import "syscall" import "golang.org/x/sys/unix"
const ioctlReadTermios = syscall.TIOCGETA const ioctlReadTermios = unix.TIOCGETA
type Termios syscall.Termios type Termios unix.Termios

View File

@ -0,0 +1,11 @@
// +build appengine gopherjs
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return true
}

View File

@ -0,0 +1,19 @@
// +build !appengine,!gopherjs
package logrus
import (
"io"
"os"
"golang.org/x/crypto/ssh/terminal"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return terminal.IsTerminal(int(v.Fd()))
default:
return false
}
}

View File

@ -3,10 +3,12 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !appengine,!gopherjs
package logrus package logrus
import "syscall" import "golang.org/x/sys/unix"
const ioctlReadTermios = syscall.TCGETS const ioctlReadTermios = unix.TCGETS
type Termios syscall.Termios type Termios unix.Termios

View File

@ -1,21 +0,0 @@
// 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 darwin freebsd openbsd netbsd dragonfly
package logrus
import (
"syscall"
"unsafe"
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stderr
var termios Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

View File

@ -1,15 +0,0 @@
// +build solaris
package logrus
import (
"os"
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal() bool {
_, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), unix.TCGETA)
return err == nil
}

View File

@ -1,27 +0,0 @@
// 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 logrus
import (
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stderr
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}

View File

@ -3,9 +3,9 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"runtime"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
) )
@ -14,24 +14,19 @@ const (
red = 31 red = 31
green = 32 green = 32
yellow = 33 yellow = 33
blue = 34 blue = 36
gray = 37 gray = 37
) )
var ( var (
baseTimestamp time.Time baseTimestamp time.Time
isTerminal bool
) )
func init() { func init() {
baseTimestamp = time.Now() baseTimestamp = time.Now()
isTerminal = IsTerminal()
}
func miniTS() int {
return int(time.Since(baseTimestamp) / time.Second)
} }
// TextFormatter formats logs into text
type TextFormatter struct { type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors. // Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool ForceColors bool
@ -54,10 +49,26 @@ type TextFormatter struct {
// that log extremely frequently and don't use the JSON formatter this may not // that log extremely frequently and don't use the JSON formatter this may not
// be desired. // be desired.
DisableSorting bool DisableSorting bool
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal
isTerminal bool
sync.Once
} }
func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil {
f.isTerminal = checkIfTerminal(entry.Logger.Out)
}
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var keys []string = make([]string, 0, len(entry.Data)) var b *bytes.Buffer
keys := make([]string, 0, len(entry.Data))
for k := range entry.Data { for k := range entry.Data {
keys = append(keys, k) keys = append(keys, k)
} }
@ -65,17 +76,21 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
if !f.DisableSorting { if !f.DisableSorting {
sort.Strings(keys) sort.Strings(keys)
} }
if entry.Buffer != nil {
b := &bytes.Buffer{} b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
prefixFieldClashes(entry.Data) prefixFieldClashes(entry.Data)
isColorTerminal := isTerminal && (runtime.GOOS != "windows") f.Do(func() { f.init(entry) })
isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat timestampFormat = defaultTimestampFormat
} }
if isColored { if isColored {
f.printColored(b, entry, keys, timestampFormat) f.printColored(b, entry, keys, timestampFormat)
@ -111,23 +126,29 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
levelText := strings.ToUpper(entry.Level.String())[0:4] levelText := strings.ToUpper(entry.Level.String())[0:4]
if !f.FullTimestamp { if f.DisableTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message)
} else if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message)
} else { } else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
} }
for _, k := range keys { for _, k := range keys {
v := entry.Data[k] v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v) fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
} }
} }
func needsQuoting(text string) bool { func (f *TextFormatter) needsQuoting(text string) bool {
if f.QuoteEmptyFields && len(text) == 0 {
return true
}
for _, ch := range text { for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') || if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || (ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') { ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
return true return true
} }
} }
@ -135,27 +156,23 @@ func needsQuoting(text string) bool {
} }
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteString(key) b.WriteString(key)
b.WriteByte('=') b.WriteByte('=')
f.appendValue(b, value)
}
switch value := value.(type) { func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
case string: stringVal, ok := value.(string)
if !needsQuoting(value) { if !ok {
b.WriteString(value) stringVal = fmt.Sprint(value)
} else {
fmt.Fprintf(b, "%q", value)
}
case error:
errmsg := value.Error()
if !needsQuoting(errmsg) {
b.WriteString(errmsg)
} else {
fmt.Fprintf(b, "%q", value)
}
default:
fmt.Fprint(b, value)
} }
b.WriteByte(' ') if !f.needsQuoting(stringVal) {
b.WriteString(stringVal)
} else {
b.WriteString(fmt.Sprintf("%q", stringVal))
}
} }

View File

@ -3,17 +3,38 @@ package logrus
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"strings"
"testing" "testing"
"time" "time"
) )
func TestFormatting(t *testing.T) {
tf := &TextFormatter{DisableColors: true}
testCases := []struct {
value string
expected string
}{
{`foo`, "time=\"0001-01-01T00:00:00Z\" level=panic test=foo\n"},
}
for _, tc := range testCases {
b, _ := tf.Format(WithField("test", tc.value))
if string(b) != tc.expected {
t.Errorf("formatting expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
}
}
}
func TestQuoting(t *testing.T) { func TestQuoting(t *testing.T) {
tf := &TextFormatter{DisableColors: true} tf := &TextFormatter{DisableColors: true}
checkQuoting := func(q bool, value interface{}) { checkQuoting := func(q bool, value interface{}) {
b, _ := tf.Format(WithField("test", value)) b, _ := tf.Format(WithField("test", value))
idx := bytes.Index(b, ([]byte)("test=")) idx := bytes.Index(b, ([]byte)("test="))
cont := bytes.Contains(b[idx+5:], []byte{'"'}) cont := bytes.Contains(b[idx+5:], []byte("\""))
if cont != q { if cont != q {
if q { if q {
t.Errorf("quoting expected for: %#v", value) t.Errorf("quoting expected for: %#v", value)
@ -23,14 +44,67 @@ func TestQuoting(t *testing.T) {
} }
} }
checkQuoting(false, "")
checkQuoting(false, "abcd") checkQuoting(false, "abcd")
checkQuoting(false, "v1.0") checkQuoting(false, "v1.0")
checkQuoting(false, "1234567890") checkQuoting(false, "1234567890")
checkQuoting(true, "/foobar") checkQuoting(false, "/foobar")
checkQuoting(false, "foo_bar")
checkQuoting(false, "foo@bar")
checkQuoting(false, "foobar^")
checkQuoting(false, "+/-_^@f.oobar")
checkQuoting(true, "foobar$")
checkQuoting(true, "&foobar")
checkQuoting(true, "x y") checkQuoting(true, "x y")
checkQuoting(true, "x,y") checkQuoting(true, "x,y")
checkQuoting(false, errors.New("invalid")) checkQuoting(false, errors.New("invalid"))
checkQuoting(true, errors.New("invalid argument")) checkQuoting(true, errors.New("invalid argument"))
// Test for quoting empty fields.
tf.QuoteEmptyFields = true
checkQuoting(true, "")
checkQuoting(false, "abcd")
checkQuoting(true, errors.New("invalid argument"))
}
func TestEscaping(t *testing.T) {
tf := &TextFormatter{DisableColors: true}
testCases := []struct {
value string
expected string
}{
{`ba"r`, `ba\"r`},
{`ba'r`, `ba'r`},
}
for _, tc := range testCases {
b, _ := tf.Format(WithField("test", tc.value))
if !bytes.Contains(b, []byte(tc.expected)) {
t.Errorf("escaping expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
}
}
}
func TestEscaping_Interface(t *testing.T) {
tf := &TextFormatter{DisableColors: true}
ts := time.Now()
testCases := []struct {
value interface{}
expected string
}{
{ts, fmt.Sprintf("\"%s\"", ts.String())},
{errors.New("error: something went wrong"), "\"error: something went wrong\""},
}
for _, tc := range testCases {
b, _ := tf.Format(WithField("test", tc.value))
if !bytes.Contains(b, []byte(tc.expected)) {
t.Errorf("escaping expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
}
}
} }
func TestTimestampFormat(t *testing.T) { func TestTimestampFormat(t *testing.T) {
@ -39,10 +113,7 @@ func TestTimestampFormat(t *testing.T) {
customStr, _ := customFormatter.Format(WithField("test", "test")) customStr, _ := customFormatter.Format(WithField("test", "test"))
timeStart := bytes.Index(customStr, ([]byte)("time=")) timeStart := bytes.Index(customStr, ([]byte)("time="))
timeEnd := bytes.Index(customStr, ([]byte)("level=")) timeEnd := bytes.Index(customStr, ([]byte)("level="))
timeStr := customStr[timeStart+5 : timeEnd-1] timeStr := customStr[timeStart+5+len("\"") : timeEnd-1-len("\"")]
if timeStr[0] == '"' && timeStr[len(timeStr)-1] == '"' {
timeStr = timeStr[1 : len(timeStr)-1]
}
if format == "" { if format == "" {
format = time.RFC3339 format = time.RFC3339
} }
@ -57,5 +128,14 @@ func TestTimestampFormat(t *testing.T) {
checkTimeStr("") checkTimeStr("")
} }
func TestDisableTimestampWithColoredOutput(t *testing.T) {
tf := &TextFormatter{DisableTimestamp: true, ForceColors: true}
b, _ := tf.Format(WithField("test", "test"))
if strings.Contains(string(b), "[0000]") {
t.Error("timestamp not expected when DisableTimestamp is true")
}
}
// TODO add tests for sorting etc., this requires a parser for the text // TODO add tests for sorting etc., this requires a parser for the text
// formatter output. // formatter output.

View File

@ -11,39 +11,48 @@ func (logger *Logger) Writer() *io.PipeWriter {
} }
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter { func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
return NewEntry(logger).WriterLevel(level)
}
func (entry *Entry) Writer() *io.PipeWriter {
return entry.WriterLevel(InfoLevel)
}
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe() reader, writer := io.Pipe()
var printFunc func(args ...interface{}) var printFunc func(args ...interface{})
switch level { switch level {
case DebugLevel: case DebugLevel:
printFunc = logger.Debug printFunc = entry.Debug
case InfoLevel: case InfoLevel:
printFunc = logger.Info printFunc = entry.Info
case WarnLevel: case WarnLevel:
printFunc = logger.Warn printFunc = entry.Warn
case ErrorLevel: case ErrorLevel:
printFunc = logger.Error printFunc = entry.Error
case FatalLevel: case FatalLevel:
printFunc = logger.Fatal printFunc = entry.Fatal
case PanicLevel: case PanicLevel:
printFunc = logger.Panic printFunc = entry.Panic
default: default:
printFunc = logger.Print printFunc = entry.Print
} }
go logger.writerScanner(reader, printFunc) go entry.writerScanner(reader, printFunc)
runtime.SetFinalizer(writer, writerFinalizer) runtime.SetFinalizer(writer, writerFinalizer)
return writer return writer
} }
func (logger *Logger) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) { func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
for scanner.Scan() { for scanner.Scan() {
printFunc(scanner.Text()) printFunc(scanner.Text())
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
logger.Errorf("Error while reading from Writer: %s", err) entry.Errorf("Error while reading from Writer: %s", err)
} }
reader.Close() reader.Close()
} }

View File

@ -5,11 +5,12 @@ services:
go: go:
- 1.4 - 1.4
- 1.5 - 1.5.x
- 1.6 - 1.6.x
- 1.7 - 1.7.x
- 1.8 - 1.8.x
- 1.9 - 1.9.x
- 1.10.x
- tip - tip
script: script:

View File

@ -29,6 +29,10 @@ import (
"time" "time"
) )
var (
_ ConnWithTimeout = (*conn)(nil)
)
// conn is the low-level implementation of Conn // conn is the low-level implementation of Conn
type conn struct { type conn struct {
// Shared // Shared
@ -72,6 +76,7 @@ type DialOption struct {
type dialOptions struct { type dialOptions struct {
readTimeout time.Duration readTimeout time.Duration
writeTimeout time.Duration writeTimeout time.Duration
dialer *net.Dialer
dial func(network, addr string) (net.Conn, error) dial func(network, addr string) (net.Conn, error)
db int db int
password string password string
@ -94,17 +99,27 @@ func DialWriteTimeout(d time.Duration) DialOption {
}} }}
} }
// DialConnectTimeout specifies the timeout for connecting to the Redis server. // DialConnectTimeout specifies the timeout for connecting to the Redis server when
// no DialNetDial option is specified.
func DialConnectTimeout(d time.Duration) DialOption { func DialConnectTimeout(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) { return DialOption{func(do *dialOptions) {
dialer := net.Dialer{Timeout: d} do.dialer.Timeout = d
do.dial = dialer.Dial }}
}
// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server
// when no DialNetDial option is specified.
// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then
// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected.
func DialKeepAlive(d time.Duration) DialOption {
return DialOption{func(do *dialOptions) {
do.dialer.KeepAlive = d
}} }}
} }
// DialNetDial specifies a custom dial function for creating TCP // DialNetDial specifies a custom dial function for creating TCP
// connections. If this option is left out, then net.Dial is // connections, otherwise a net.Dialer customized via the other options is used.
// used. DialNetDial overrides DialConnectTimeout. // DialNetDial overrides DialConnectTimeout and DialKeepAlive.
func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption {
return DialOption{func(do *dialOptions) { return DialOption{func(do *dialOptions) {
do.dial = dial do.dial = dial
@ -154,11 +169,16 @@ func DialUseTLS(useTLS bool) DialOption {
// address using the specified options. // address using the specified options.
func Dial(network, address string, options ...DialOption) (Conn, error) { func Dial(network, address string, options ...DialOption) (Conn, error) {
do := dialOptions{ do := dialOptions{
dial: net.Dial, dialer: &net.Dialer{
KeepAlive: time.Minute * 5,
},
} }
for _, option := range options { for _, option := range options {
option.f(&do) option.f(&do)
} }
if do.dial == nil {
do.dial = do.dialer.Dial
}
netConn, err := do.dial(network, address) netConn, err := do.dial(network, address)
if err != nil { if err != nil {
@ -166,7 +186,12 @@ func Dial(network, address string, options ...DialOption) (Conn, error) {
} }
if do.useTLS { if do.useTLS {
tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify) var tlsConfig *tls.Config
if do.tlsConfig == nil {
tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify}
} else {
tlsConfig = cloneTLSConfig(do.tlsConfig)
}
if tlsConfig.ServerName == "" { if tlsConfig.ServerName == "" {
host, _, err := net.SplitHostPort(address) host, _, err := net.SplitHostPort(address)
if err != nil { if err != nil {
@ -555,10 +580,17 @@ func (c *conn) Flush() error {
return nil return nil
} }
func (c *conn) Receive() (reply interface{}, err error) { func (c *conn) Receive() (interface{}, error) {
if c.readTimeout != 0 { return c.ReceiveWithTimeout(c.readTimeout)
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) }
func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
var deadline time.Time
if timeout != 0 {
deadline = time.Now().Add(timeout)
} }
c.conn.SetReadDeadline(deadline)
if reply, err = c.readReply(); err != nil { if reply, err = c.readReply(); err != nil {
return nil, c.fatal(err) return nil, c.fatal(err)
} }
@ -581,6 +613,10 @@ func (c *conn) Receive() (reply interface{}, err error) {
} }
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) { func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
return c.DoWithTimeout(c.readTimeout, cmd, args...)
}
func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
c.mu.Lock() c.mu.Lock()
pending := c.pending pending := c.pending
c.pending = 0 c.pending = 0
@ -604,9 +640,11 @@ func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
return nil, c.fatal(err) return nil, c.fatal(err)
} }
if c.readTimeout != 0 { var deadline time.Time
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) if readTimeout != 0 {
deadline = time.Now().Add(readTimeout)
} }
c.conn.SetReadDeadline(deadline)
if cmd == "" { if cmd == "" {
reply := make([]interface{}, pending) reply := make([]interface{}, pending)

View File

@ -34,14 +34,16 @@ import (
type testConn struct { type testConn struct {
io.Reader io.Reader
io.Writer io.Writer
readDeadline time.Time
writeDeadline time.Time
} }
func (*testConn) Close() error { return nil } func (*testConn) Close() error { return nil }
func (*testConn) LocalAddr() net.Addr { return nil } func (*testConn) LocalAddr() net.Addr { return nil }
func (*testConn) RemoteAddr() net.Addr { return nil } func (*testConn) RemoteAddr() net.Addr { return nil }
func (*testConn) SetDeadline(t time.Time) error { return nil } func (c *testConn) SetDeadline(t time.Time) error { c.readDeadline = t; c.writeDeadline = t; return nil }
func (*testConn) SetReadDeadline(t time.Time) error { return nil } func (c *testConn) SetReadDeadline(t time.Time) error { c.readDeadline = t; return nil }
func (*testConn) SetWriteDeadline(t time.Time) error { return nil } func (c *testConn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t; return nil }
func dialTestConn(r string, w io.Writer) redis.DialOption { func dialTestConn(r string, w io.Writer) redis.DialOption {
return redis.DialNetDial(func(network, addr string) (net.Conn, error) { return redis.DialNetDial(func(network, addr string) (net.Conn, error) {
@ -821,3 +823,45 @@ Bjqn3yoLHaoZVvbWOi0C2TCN4FjXjaLNZGifQPbIcaA=
clientTLSConfig.RootCAs = x509.NewCertPool() clientTLSConfig.RootCAs = x509.NewCertPool()
clientTLSConfig.RootCAs.AddCert(certificate) clientTLSConfig.RootCAs.AddCert(certificate)
} }
func TestWithTimeout(t *testing.T) {
for _, recv := range []bool{true, false} {
for _, defaultTimout := range []time.Duration{0, time.Minute} {
var buf bytes.Buffer
nc := &testConn{Reader: strings.NewReader("+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n"), Writer: &buf}
c, _ := redis.Dial("", "", redis.DialReadTimeout(defaultTimout), redis.DialNetDial(func(network, addr string) (net.Conn, error) { return nc, nil }))
for i := 0; i < 4; i++ {
var minDeadline, maxDeadline time.Time
// Alternate between default and specified timeout.
if i%2 == 0 {
if defaultTimout != 0 {
minDeadline = time.Now().Add(defaultTimout)
}
if recv {
c.Receive()
} else {
c.Do("PING")
}
if defaultTimout != 0 {
maxDeadline = time.Now().Add(defaultTimout)
}
} else {
timeout := 10 * time.Minute
minDeadline = time.Now().Add(timeout)
if recv {
redis.ReceiveWithTimeout(c, timeout)
} else {
redis.DoWithTimeout(c, timeout, "PING")
}
maxDeadline = time.Now().Add(timeout)
}
// Expect set deadline in expected range.
if nc.readDeadline.Before(minDeadline) || nc.readDeadline.After(maxDeadline) {
t.Errorf("recv %v, %d: do deadline error: %v, %v, %v", recv, i, minDeadline, nc.readDeadline, maxDeadline)
}
}
}
}
}

View File

@ -4,11 +4,7 @@ package redis
import "crypto/tls" import "crypto/tls"
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case func cloneTLSConfig(cfg *tls.Config) *tls.Config {
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
if cfg == nil {
return &tls.Config{InsecureSkipVerify: skipVerify}
}
return &tls.Config{ return &tls.Config{
Rand: cfg.Rand, Rand: cfg.Rand,
Time: cfg.Time, Time: cfg.Time,

View File

@ -1,14 +1,10 @@
// +build go1.7 // +build go1.7,!go1.8
package redis package redis
import "crypto/tls" import "crypto/tls"
// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case func cloneTLSConfig(cfg *tls.Config) *tls.Config {
func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config {
if cfg == nil {
return &tls.Config{InsecureSkipVerify: skipVerify}
}
return &tls.Config{ return &tls.Config{
Rand: cfg.Rand, Rand: cfg.Rand,
Time: cfg.Time, Time: cfg.Time,

9
src/vendor/github.com/garyburd/redigo/redis/go18.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
// +build go1.8
package redis
import "crypto/tls"
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
return cfg.Clone()
}

View File

@ -0,0 +1,85 @@
// Copyright 2018 Gary Burd
//
// 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.
// +build go1.9
package redis
import "testing"
func TestPoolList(t *testing.T) {
var idle idleList
var a, b, c idleConn
check := func(ics ...*idleConn) {
if idle.count != len(ics) {
t.Fatal("idle.count != len(ics)")
}
if len(ics) == 0 {
if idle.front != nil {
t.Fatalf("front not nil")
}
if idle.back != nil {
t.Fatalf("back not nil")
}
return
}
if idle.front != ics[0] {
t.Fatal("front != ics[0]")
}
if idle.back != ics[len(ics)-1] {
t.Fatal("back != ics[len(ics)-1]")
}
if idle.front.prev != nil {
t.Fatal("front.prev != nil")
}
if idle.back.next != nil {
t.Fatal("back.next != nil")
}
for i := 1; i < len(ics)-1; i++ {
if ics[i-1].next != ics[i] {
t.Fatal("ics[i-1].next != ics[i]")
}
if ics[i+1].prev != ics[i] {
t.Fatal("ics[i+1].prev != ics[i]")
}
}
}
idle.pushFront(&c)
check(&c)
idle.pushFront(&b)
check(&b, &c)
idle.pushFront(&a)
check(&a, &b, &c)
idle.popFront()
check(&b, &c)
idle.popFront()
check(&c)
idle.popFront()
check()
idle.pushFront(&c)
check(&c)
idle.pushFront(&b)
check(&b, &c)
idle.pushFront(&a)
check(&a, &b, &c)
idle.popBack()
check(&a, &b)
idle.popBack()
check(&a)
idle.popBack()
check()
}

View File

@ -18,6 +18,11 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"time"
)
var (
_ ConnWithTimeout = (*loggingConn)(nil)
) )
// NewLoggingConn returns a logging wrapper around a connection. // NewLoggingConn returns a logging wrapper around a connection.
@ -104,6 +109,12 @@ func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{},
return reply, err return reply, err
} }
func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) {
reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...)
c.print("DoWithTimeout", commandName, args, reply, err)
return reply, err
}
func (c *loggingConn) Send(commandName string, args ...interface{}) error { func (c *loggingConn) Send(commandName string, args ...interface{}) error {
err := c.Conn.Send(commandName, args...) err := c.Conn.Send(commandName, args...)
c.print("Send", commandName, args, nil, err) c.print("Send", commandName, args, nil, err)
@ -115,3 +126,9 @@ func (c *loggingConn) Receive() (interface{}, error) {
c.print("Receive", "", nil, reply, err) c.print("Receive", "", nil, reply, err)
return reply, err return reply, err
} }
func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
reply, err := ReceiveWithTimeout(c.Conn, timeout)
c.print("ReceiveWithTimeout", "", nil, reply, err)
return reply, err
}

View File

@ -16,18 +16,23 @@ package redis
import ( import (
"bytes" "bytes"
"container/list"
"crypto/rand" "crypto/rand"
"crypto/sha1" "crypto/sha1"
"errors" "errors"
"io" "io"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/garyburd/redigo/internal" "github.com/garyburd/redigo/internal"
) )
var (
_ ConnWithTimeout = (*pooledConnection)(nil)
_ ConnWithTimeout = (*errorConnection)(nil)
)
var nowFunc = time.Now // for testing var nowFunc = time.Now // for testing
// ErrPoolExhausted is returned from a pool connection method (Do, Send, // ErrPoolExhausted is returned from a pool connection method (Do, Send,
@ -96,7 +101,7 @@ var (
// return nil, err // return nil, err
// } // }
// return c, nil // return c, nil
// } // },
// } // }
// //
// Use the TestOnBorrow function to check the health of an idle connection // Use the TestOnBorrow function to check the health of an idle connection
@ -145,19 +150,13 @@ type Pool struct {
// for a connection to be returned to the pool before returning. // for a connection to be returned to the pool before returning.
Wait bool Wait bool
// mu protects fields defined below. chInitialized uint32 // set to 1 when field ch is initialized
mu sync.Mutex
cond *sync.Cond
closed bool
active int
// Stack of idleConn with most recently used at the front. mu sync.Mutex // mu protects the following fields
idle list.List closed bool // set to true when the pool is closed.
} active int // the number of open connections in the pool
ch chan struct{} // limits open connections when p.Wait is true
type idleConn struct { idle idleList // idle connections
c Conn
t time.Time
} }
// NewPool creates a new pool. // NewPool creates a new pool.
@ -173,7 +172,7 @@ func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
// getting an underlying connection, then the connection Err, Do, Send, Flush // getting an underlying connection, then the connection Err, Do, Send, Flush
// and Receive methods return that error. // and Receive methods return that error.
func (p *Pool) Get() Conn { func (p *Pool) Get() Conn {
c, err := p.get() c, err := p.get(nil)
if err != nil { if err != nil {
return errorConnection{err} return errorConnection{err}
} }
@ -182,7 +181,8 @@ func (p *Pool) Get() Conn {
// PoolStats contains pool statistics. // PoolStats contains pool statistics.
type PoolStats struct { type PoolStats struct {
// ActiveCount is the number of connections in the pool. The count includes idle connections and connections in use. // ActiveCount is the number of connections in the pool. The count includes
// idle connections and connections in use.
ActiveCount int ActiveCount int
// IdleCount is the number of idle connections in the pool. // IdleCount is the number of idle connections in the pool.
IdleCount int IdleCount int
@ -193,14 +193,15 @@ func (p *Pool) Stats() PoolStats {
p.mu.Lock() p.mu.Lock()
stats := PoolStats{ stats := PoolStats{
ActiveCount: p.active, ActiveCount: p.active,
IdleCount: p.idle.Len(), IdleCount: p.idle.count,
} }
p.mu.Unlock() p.mu.Unlock()
return stats return stats
} }
// ActiveCount returns the number of connections in the pool. The count includes idle connections and connections in use. // ActiveCount returns the number of connections in the pool. The count
// includes idle connections and connections in use.
func (p *Pool) ActiveCount() int { func (p *Pool) ActiveCount() int {
p.mu.Lock() p.mu.Lock()
active := p.active active := p.active
@ -211,7 +212,7 @@ func (p *Pool) ActiveCount() int {
// IdleCount returns the number of idle connections in the pool. // IdleCount returns the number of idle connections in the pool.
func (p *Pool) IdleCount() int { func (p *Pool) IdleCount() int {
p.mu.Lock() p.mu.Lock()
idle := p.idle.Len() idle := p.idle.count
p.mu.Unlock() p.mu.Unlock()
return idle return idle
} }
@ -219,132 +220,146 @@ func (p *Pool) IdleCount() int {
// Close releases the resources used by the pool. // Close releases the resources used by the pool.
func (p *Pool) Close() error { func (p *Pool) Close() error {
p.mu.Lock() p.mu.Lock()
idle := p.idle if p.closed {
p.idle.Init() p.mu.Unlock()
return nil
}
p.closed = true p.closed = true
p.active -= idle.Len() p.active -= p.idle.count
if p.cond != nil { ic := p.idle.front
p.cond.Broadcast() p.idle.count = 0
p.idle.front, p.idle.back = nil, nil
if p.ch != nil {
close(p.ch)
} }
p.mu.Unlock() p.mu.Unlock()
for e := idle.Front(); e != nil; e = e.Next() { for ; ic != nil; ic = ic.next {
e.Value.(idleConn).c.Close() ic.c.Close()
} }
return nil return nil
} }
// release decrements the active count and signals waiters. The caller must func (p *Pool) lazyInit() {
// hold p.mu during the call. // Fast path.
func (p *Pool) release() { if atomic.LoadUint32(&p.chInitialized) == 1 {
p.active -= 1 return
if p.cond != nil {
p.cond.Signal()
} }
// Slow path.
p.mu.Lock()
if p.chInitialized == 0 {
p.ch = make(chan struct{}, p.MaxActive)
if p.closed {
close(p.ch)
} else {
for i := 0; i < p.MaxActive; i++ {
p.ch <- struct{}{}
}
}
atomic.StoreUint32(&p.chInitialized, 1)
}
p.mu.Unlock()
} }
// get prunes stale connections and returns a connection from the idle list or // get prunes stale connections and returns a connection from the idle list or
// creates a new connection. // creates a new connection.
func (p *Pool) get() (Conn, error) { func (p *Pool) get(ctx interface {
Done() <-chan struct{}
Err() error
}) (Conn, error) {
// Handle limit for p.Wait == true.
if p.Wait && p.MaxActive > 0 {
p.lazyInit()
if ctx == nil {
<-p.ch
} else {
select {
case <-p.ch:
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
p.mu.Lock() p.mu.Lock()
// Prune stale connections. // Prune stale connections at the back of the idle list.
if p.IdleTimeout > 0 {
if timeout := p.IdleTimeout; timeout > 0 { n := p.idle.count
for i, n := 0, p.idle.Len(); i < n; i++ { for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ {
e := p.idle.Back() c := p.idle.back.c
if e == nil { p.idle.popBack()
break
}
ic := e.Value.(idleConn)
if ic.t.Add(timeout).After(nowFunc()) {
break
}
p.idle.Remove(e)
p.release()
p.mu.Unlock() p.mu.Unlock()
ic.c.Close() c.Close()
p.mu.Lock() p.mu.Lock()
p.active--
} }
} }
for { // Get idle connection from the front of idle list.
// Get idle connection. for p.idle.front != nil {
ic := p.idle.front
for i, n := 0, p.idle.Len(); i < n; i++ { p.idle.popFront()
e := p.idle.Front() p.mu.Unlock()
if e == nil { if p.TestOnBorrow == nil || p.TestOnBorrow(ic.c, ic.t) == nil {
break return ic.c, nil
}
ic := e.Value.(idleConn)
p.idle.Remove(e)
test := p.TestOnBorrow
p.mu.Unlock()
if test == nil || test(ic.c, ic.t) == nil {
return ic.c, nil
}
ic.c.Close()
p.mu.Lock()
p.release()
} }
ic.c.Close()
// Check for pool closed before dialing a new connection. p.mu.Lock()
p.active--
if p.closed {
p.mu.Unlock()
return nil, errors.New("redigo: get on closed pool")
}
// Dial new connection if under limit.
if p.MaxActive == 0 || p.active < p.MaxActive {
dial := p.Dial
p.active += 1
p.mu.Unlock()
c, err := dial()
if err != nil {
p.mu.Lock()
p.release()
p.mu.Unlock()
c = nil
}
return c, err
}
if !p.Wait {
p.mu.Unlock()
return nil, ErrPoolExhausted
}
if p.cond == nil {
p.cond = sync.NewCond(&p.mu)
}
p.cond.Wait()
} }
// Check for pool closed before dialing a new connection.
if p.closed {
p.mu.Unlock()
return nil, errors.New("redigo: get on closed pool")
}
// Handle limit for p.Wait == false.
if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive {
p.mu.Unlock()
return nil, ErrPoolExhausted
}
p.active++
p.mu.Unlock()
c, err := p.Dial()
if err != nil {
c = nil
p.mu.Lock()
p.active--
if p.ch != nil && !p.closed {
p.ch <- struct{}{}
}
p.mu.Unlock()
}
return c, err
} }
func (p *Pool) put(c Conn, forceClose bool) error { func (p *Pool) put(c Conn, forceClose bool) error {
err := c.Err()
p.mu.Lock() p.mu.Lock()
if !p.closed && err == nil && !forceClose { if !p.closed && !forceClose {
p.idle.PushFront(idleConn{t: nowFunc(), c: c}) p.idle.pushFront(&idleConn{t: nowFunc(), c: c})
if p.idle.Len() > p.MaxIdle { if p.idle.count > p.MaxIdle {
c = p.idle.Remove(p.idle.Back()).(idleConn).c c = p.idle.back.c
p.idle.popBack()
} else { } else {
c = nil c = nil
} }
} }
if c == nil { if c != nil {
if p.cond != nil {
p.cond.Signal()
}
p.mu.Unlock() p.mu.Unlock()
return nil c.Close()
p.mu.Lock()
p.active--
} }
p.release() if p.ch != nil && !p.closed {
p.ch <- struct{}{}
}
p.mu.Unlock() p.mu.Unlock()
return c.Close() return nil
} }
type pooledConnection struct { type pooledConnection struct {
@ -404,7 +419,7 @@ func (pc *pooledConnection) Close() error {
} }
} }
c.Do("") c.Do("")
pc.p.put(c, pc.state != 0) pc.p.put(c, pc.state != 0 || c.Err() != nil)
return nil return nil
} }
@ -418,6 +433,16 @@ func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply i
return pc.c.Do(commandName, args...) return pc.c.Do(commandName, args...)
} }
func (pc *pooledConnection) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) {
cwt, ok := pc.c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear
return cwt.DoWithTimeout(timeout, commandName, args...)
}
func (pc *pooledConnection) Send(commandName string, args ...interface{}) error { func (pc *pooledConnection) Send(commandName string, args ...interface{}) error {
ci := internal.LookupCommandInfo(commandName) ci := internal.LookupCommandInfo(commandName)
pc.state = (pc.state | ci.Set) &^ ci.Clear pc.state = (pc.state | ci.Set) &^ ci.Clear
@ -432,11 +457,71 @@ func (pc *pooledConnection) Receive() (reply interface{}, err error) {
return pc.c.Receive() return pc.c.Receive()
} }
func (pc *pooledConnection) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) {
cwt, ok := pc.c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
return cwt.ReceiveWithTimeout(timeout)
}
type errorConnection struct{ err error } type errorConnection struct{ err error }
func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err }
func (ec errorConnection) Send(string, ...interface{}) error { return ec.err } func (ec errorConnection) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) {
func (ec errorConnection) Err() error { return ec.err } return nil, ec.err
func (ec errorConnection) Close() error { return ec.err } }
func (ec errorConnection) Flush() error { return ec.err } func (ec errorConnection) Send(string, ...interface{}) error { return ec.err }
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err } func (ec errorConnection) Err() error { return ec.err }
func (ec errorConnection) Close() error { return nil }
func (ec errorConnection) Flush() error { return ec.err }
func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err }
func (ec errorConnection) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err }
type idleList struct {
count int
front, back *idleConn
}
type idleConn struct {
c Conn
t time.Time
next, prev *idleConn
}
func (l *idleList) pushFront(ic *idleConn) {
ic.next = l.front
ic.prev = nil
if l.count == 0 {
l.back = ic
} else {
l.front.prev = ic
}
l.front = ic
l.count++
return
}
func (l *idleList) popFront() {
ic := l.front
l.count--
if l.count == 0 {
l.front, l.back = nil, nil
} else {
ic.next.prev = nil
l.front = ic.next
}
ic.next, ic.prev = nil, nil
}
func (l *idleList) popBack() {
ic := l.back
l.count--
if l.count == 0 {
l.front, l.back = nil, nil
} else {
ic.prev.next = nil
l.back = ic.prev
}
ic.next, ic.prev = nil, nil
}

35
src/vendor/github.com/garyburd/redigo/redis/pool17.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2018 Gary Burd
//
// 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.
// +build go1.7
package redis
import "context"
// GetContext gets a connection using the provided context.
//
// The provided Context must be non-nil. If the context expires before the
// connection is complete, an error is returned. Any expiration on the context
// will not affect the returned connection.
//
// If the function completes without error, then the application must close the
// returned connection.
func (p *Pool) GetContext(ctx context.Context) (Conn, error) {
c, err := p.get(ctx)
if err != nil {
return errorConnection{err}, err
}
return &pooledConnection{p: p, c: c}, nil
}

View File

@ -0,0 +1,58 @@
// Copyright 2018 Gary Burd
//
// 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.
// +build go1.7
package redis_test
import (
"context"
"testing"
"github.com/garyburd/redigo/redis"
)
func TestWaitPoolGetAfterClose(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 1,
MaxActive: 1,
Dial: d.dial,
Wait: true,
}
p.Close()
_, err := p.GetContext(context.Background())
if err == nil {
t.Fatal("expected error")
}
}
func TestWaitPoolGetCanceledContext(t *testing.T) {
d := poolDialer{t: t}
p := &redis.Pool{
MaxIdle: 1,
MaxActive: 1,
Dial: d.dial,
Wait: true,
}
defer p.Close()
ctx, f := context.WithCancel(context.Background())
f()
c := p.Get()
defer c.Close()
_, err := p.GetContext(ctx)
if err != context.Canceled {
t.Fatalf("got error %v, want %v", err, context.Canceled)
}
}

View File

@ -231,7 +231,7 @@ func TestPoolTimeout(t *testing.T) {
d.check("1", p, 1, 1, 0) d.check("1", p, 1, 1, 0)
now = now.Add(p.IdleTimeout) now = now.Add(p.IdleTimeout + 1)
c = p.Get() c = p.Get()
c.Do("PING") c.Do("PING")
@ -445,9 +445,6 @@ func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error
}() }()
} }
// Wait for goroutines to block.
time.Sleep(time.Second / 4)
return errs return errs
} }

View File

@ -14,7 +14,10 @@
package redis package redis
import "errors" import (
"errors"
"time"
)
// Subscription represents a subscribe or unsubscribe notification. // Subscription represents a subscribe or unsubscribe notification.
type Subscription struct { type Subscription struct {
@ -103,7 +106,17 @@ func (c PubSubConn) Ping(data string) error {
// or error. The return value is intended to be used directly in a type switch // or error. The return value is intended to be used directly in a type switch
// as illustrated in the PubSubConn example. // as illustrated in the PubSubConn example.
func (c PubSubConn) Receive() interface{} { func (c PubSubConn) Receive() interface{} {
reply, err := Values(c.Conn.Receive()) return c.receiveInternal(c.Conn.Receive())
}
// ReceiveWithTimeout is like Receive, but it allows the application to
// override the connection's default timeout.
func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} {
return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout))
}
func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} {
reply, err := Values(replyArg, errArg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,165 @@
// Copyright 2012 Gary Burd
//
// 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.
// +build go1.7
package redis_test
import (
"context"
"fmt"
"time"
"github.com/garyburd/redigo/redis"
)
// listenPubSubChannels listens for messages on Redis pubsub channels. The
// onStart function is called after the channels are subscribed. The onMessage
// function is called for each message.
func listenPubSubChannels(ctx context.Context, redisServerAddr string,
onStart func() error,
onMessage func(channel string, data []byte) error,
channels ...string) error {
// A ping is set to the server with this period to test for the health of
// the connection and server.
const healthCheckPeriod = time.Minute
c, err := redis.Dial("tcp", redisServerAddr,
// Read timeout on server should be greater than ping period.
redis.DialReadTimeout(healthCheckPeriod+10*time.Second),
redis.DialWriteTimeout(10*time.Second))
if err != nil {
return err
}
defer c.Close()
psc := redis.PubSubConn{Conn: c}
if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil {
return err
}
done := make(chan error, 1)
// Start a goroutine to receive notifications from the server.
go func() {
for {
switch n := psc.Receive().(type) {
case error:
done <- n
return
case redis.Message:
if err := onMessage(n.Channel, n.Data); err != nil {
done <- err
return
}
case redis.Subscription:
switch n.Count {
case len(channels):
// Notify application when all channels are subscribed.
if err := onStart(); err != nil {
done <- err
return
}
case 0:
// Return from the goroutine when all channels are unsubscribed.
done <- nil
return
}
}
}
}()
ticker := time.NewTicker(healthCheckPeriod)
defer ticker.Stop()
loop:
for err == nil {
select {
case <-ticker.C:
// Send ping to test health of connection and server. If
// corresponding pong is not received, then receive on the
// connection will timeout and the receive goroutine will exit.
if err = psc.Ping(""); err != nil {
break loop
}
case <-ctx.Done():
break loop
case err := <-done:
// Return error from the receive goroutine.
return err
}
}
// Signal the receiving goroutine to exit by unsubscribing from all channels.
psc.Unsubscribe()
// Wait for goroutine to complete.
return <-done
}
func publish() {
c, err := dial()
if err != nil {
fmt.Println(err)
return
}
defer c.Close()
c.Do("PUBLISH", "c1", "hello")
c.Do("PUBLISH", "c2", "world")
c.Do("PUBLISH", "c1", "goodbye")
}
// This example shows how receive pubsub notifications with cancelation and
// health checks.
func ExamplePubSubConn() {
redisServerAddr, err := serverAddr()
if err != nil {
fmt.Println(err)
return
}
ctx, cancel := context.WithCancel(context.Background())
err = listenPubSubChannels(ctx,
redisServerAddr,
func() error {
// The start callback is a good place to backfill missed
// notifications. For the purpose of this example, a goroutine is
// started to send notifications.
go publish()
return nil
},
func(channel string, message []byte) error {
fmt.Printf("channel: %s, message: %s\n", channel, message)
// For the purpose of this example, cancel the listener's context
// after receiving last message sent by publish().
if string(message) == "goodbye" {
cancel()
}
return nil
},
"c1", "c2")
if err != nil {
fmt.Println(err)
return
}
// Output:
// channel: c1, message: hello
// channel: c2, message: world
// channel: c1, message: goodbye
}

View File

@ -15,93 +15,13 @@
package redis_test package redis_test
import ( import (
"fmt"
"reflect" "reflect"
"sync"
"testing" "testing"
"time"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
) )
func publish(channel, value interface{}) {
c, err := dial()
if err != nil {
fmt.Println(err)
return
}
defer c.Close()
c.Do("PUBLISH", channel, value)
}
// Applications can receive pushed messages from one goroutine and manage subscriptions from another goroutine.
func ExamplePubSubConn() {
c, err := dial()
if err != nil {
fmt.Println(err)
return
}
defer c.Close()
var wg sync.WaitGroup
wg.Add(2)
psc := redis.PubSubConn{Conn: c}
// This goroutine receives and prints pushed notifications from the server.
// The goroutine exits when the connection is unsubscribed from all
// channels or there is an error.
go func() {
defer wg.Done()
for {
switch n := psc.Receive().(type) {
case redis.Message:
fmt.Printf("Message: %s %s\n", n.Channel, n.Data)
case redis.PMessage:
fmt.Printf("PMessage: %s %s %s\n", n.Pattern, n.Channel, n.Data)
case redis.Subscription:
fmt.Printf("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count)
if n.Count == 0 {
return
}
case error:
fmt.Printf("error: %v\n", n)
return
}
}
}()
// This goroutine manages subscriptions for the connection.
go func() {
defer wg.Done()
psc.Subscribe("example")
psc.PSubscribe("p*")
// The following function calls publish a message using another
// connection to the Redis server.
publish("example", "hello")
publish("example", "world")
publish("pexample", "foo")
publish("pexample", "bar")
// Unsubscribe from all connections. This will cause the receiving
// goroutine to exit.
psc.Unsubscribe()
psc.PUnsubscribe()
}()
wg.Wait()
// Output:
// Subscription: subscribe example 1
// Subscription: psubscribe p* 2
// Message: example hello
// Message: example world
// PMessage: p* pexample foo
// PMessage: p* pexample bar
// Subscription: unsubscribe example 1
// Subscription: punsubscribe p* 0
}
func expectPushed(t *testing.T, c redis.PubSubConn, message string, expected interface{}) { func expectPushed(t *testing.T, c redis.PubSubConn, message string, expected interface{}) {
actual := c.Receive() actual := c.Receive()
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
@ -145,4 +65,10 @@ func TestPushed(t *testing.T) {
c.Conn.Send("PING") c.Conn.Send("PING")
c.Conn.Flush() c.Conn.Flush()
expectPushed(t, c, `Send("PING")`, redis.Pong{}) expectPushed(t, c, `Send("PING")`, redis.Pong{})
c.Ping("timeout")
got := c.ReceiveWithTimeout(time.Minute)
if want := (redis.Pong{Data: "timeout"}); want != got {
t.Errorf("recv /w timeout got %v, want %v", got, want)
}
} }

View File

@ -14,6 +14,11 @@
package redis package redis
import (
"errors"
"time"
)
// Error represents an error returned in a command reply. // Error represents an error returned in a command reply.
type Error string type Error string
@ -59,3 +64,54 @@ type Scanner interface {
// loss of information. // loss of information.
RedisScan(src interface{}) error RedisScan(src interface{}) error
} }
// ConnWithTimeout is an optional interface that allows the caller to override
// a connection's default read timeout. This interface is useful for executing
// the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the
// server.
//
// A connection's default read timeout is set with the DialReadTimeout dial
// option. Applications should rely on the default timeout for commands that do
// not block at the server.
//
// All of the Conn implementations in this package satisfy the ConnWithTimeout
// interface.
//
// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify
// use of this interface.
type ConnWithTimeout interface {
Conn
// Do sends a command to the server and returns the received reply.
// The timeout overrides the read timeout set when dialing the
// connection.
DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error)
// Receive receives a single reply from the Redis server. The timeout
// overrides the read timeout set when dialing the connection.
ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error)
}
var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout")
// DoWithTimeout executes a Redis command with the specified read timeout. If
// the connection does not satisfy the ConnWithTimeout interface, then an error
// is returned.
func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
cwt, ok := c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
return cwt.DoWithTimeout(timeout, cmd, args...)
}
// ReceiveWithTimeout receives a reply with the specified read timeout. If the
// connection does not satisfy the ConnWithTimeout interface, then an error is
// returned.
func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) {
cwt, ok := c.(ConnWithTimeout)
if !ok {
return nil, errTimeoutNotSupported
}
return cwt.ReceiveWithTimeout(timeout)
}

View File

@ -0,0 +1,71 @@
// Copyright 2017 Gary Burd
//
// 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.
package redis_test
import (
"testing"
"time"
"github.com/garyburd/redigo/redis"
)
type timeoutTestConn int
func (tc timeoutTestConn) Do(string, ...interface{}) (interface{}, error) {
return time.Duration(-1), nil
}
func (tc timeoutTestConn) DoWithTimeout(timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
return timeout, nil
}
func (tc timeoutTestConn) Receive() (interface{}, error) {
return time.Duration(-1), nil
}
func (tc timeoutTestConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
return timeout, nil
}
func (tc timeoutTestConn) Send(string, ...interface{}) error { return nil }
func (tc timeoutTestConn) Err() error { return nil }
func (tc timeoutTestConn) Close() error { return nil }
func (tc timeoutTestConn) Flush() error { return nil }
func testTimeout(t *testing.T, c redis.Conn) {
r, err := c.Do("PING")
if r != time.Duration(-1) || err != nil {
t.Errorf("Do() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil)
}
r, err = redis.DoWithTimeout(c, time.Minute, "PING")
if r != time.Minute || err != nil {
t.Errorf("DoWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil)
}
r, err = c.Receive()
if r != time.Duration(-1) || err != nil {
t.Errorf("Receive() = %v, %v, want %v, %v", r, err, time.Duration(-1), nil)
}
r, err = redis.ReceiveWithTimeout(c, time.Minute)
if r != time.Minute || err != nil {
t.Errorf("ReceiveWithTimeout() = %v, %v, want %v, %v", r, err, time.Minute, nil)
}
}
func TestConnTimeout(t *testing.T) {
testTimeout(t, timeoutTestConn(0))
}
func TestPoolConnTimeout(t *testing.T) {
p := &redis.Pool{Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil }}
testTimeout(t, p.Get())
}

View File

@ -140,6 +140,11 @@ func dial() (redis.Conn, error) {
return redis.DialDefaultServer() return redis.DialDefaultServer()
} }
// serverAddr wraps DefaultServerAddr() with a more suitable function name for examples.
func serverAddr() (string, error) {
return redis.DefaultServerAddr()
}
func ExampleBool() { func ExampleBool() {
c, err := dial() c, err := dial()
if err != nil { if err != nil {

View File

@ -84,8 +84,8 @@ var scanConversionTests = []struct {
{"1m", durationScan{Duration: time.Minute}}, {"1m", durationScan{Duration: time.Minute}},
{[]byte("1m"), durationScan{Duration: time.Minute}}, {[]byte("1m"), durationScan{Duration: time.Minute}},
{time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}}, {time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}},
{[]interface{}{[]byte("1m")}, []durationScan{durationScan{Duration: time.Minute}}}, {[]interface{}{[]byte("1m")}, []durationScan{{Duration: time.Minute}}},
{[]interface{}{[]byte("1m")}, []*durationScan{&durationScan{Duration: time.Minute}}}, {[]interface{}{[]byte("1m")}, []*durationScan{{Duration: time.Minute}}},
} }
func TestScanConversion(t *testing.T) { func TestScanConversion(t *testing.T) {
@ -318,7 +318,7 @@ var scanSliceTests = []struct {
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
nil, nil,
true, true,
[]*struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, []*struct{ A, B string }{{A: "a1", B: "b1"}, {A: "a2", B: "b2"}},
}, },
{ {
[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},

View File

@ -38,6 +38,7 @@ var (
ErrNegativeInt = errNegativeInt ErrNegativeInt = errNegativeInt
serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary") serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary")
serverAddress = flag.String("redis-address", "127.0.0.1", "The address of the server")
serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers") serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers")
serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`") serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`")
serverLog = ioutil.Discard serverLog = ioutil.Discard
@ -126,28 +127,32 @@ func stopDefaultServer() {
} }
} }
// startDefaultServer starts the default server if not already running. // DefaultServerAddr starts the test server if not already started and returns
func startDefaultServer() error { // the address of that server.
func DefaultServerAddr() (string, error) {
defaultServerMu.Lock() defaultServerMu.Lock()
defer defaultServerMu.Unlock() defer defaultServerMu.Unlock()
addr := fmt.Sprintf("%v:%d", *serverAddress, *serverBasePort)
if defaultServer != nil || defaultServerErr != nil { if defaultServer != nil || defaultServerErr != nil {
return defaultServerErr return addr, defaultServerErr
} }
defaultServer, defaultServerErr = NewServer( defaultServer, defaultServerErr = NewServer(
"default", "default",
"--port", strconv.Itoa(*serverBasePort), "--port", strconv.Itoa(*serverBasePort),
"--bind", *serverAddress,
"--save", "", "--save", "",
"--appendonly", "no") "--appendonly", "no")
return defaultServerErr return addr, defaultServerErr
} }
// DialDefaultServer starts the test server if not already started and dials a // DialDefaultServer starts the test server if not already started and dials a
// connection to the server. // connection to the server.
func DialDefaultServer() (Conn, error) { func DialDefaultServer() (Conn, error) {
if err := startDefaultServer(); err != nil { addr, err := DefaultServerAddr()
if err != nil {
return nil, err return nil, err
} }
c, err := Dial("tcp", fmt.Sprintf(":%d", *serverBasePort), DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second)) c, err := Dial("tcp", addr, DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,11 +8,12 @@ matrix:
- go: 1.5 - go: 1.5
- go: 1.6 - go: 1.6
- go: tip - go: tip
allow_failures:
- go: tip install:
- go get golang.org/x/tools/cmd/vet
script: script:
- go get -t -v ./... - go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .) - diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/) - go tool vet .
- go test -v -race ./... - go test -v -race ./...

View File

@ -69,7 +69,7 @@ func TestContext(t *testing.T) {
// GetAllOk() for empty request // GetAllOk() for empty request
values, ok = GetAllOk(emptyR) values, ok = GetAllOk(emptyR)
assertEqual(len(values), 0) assertEqual(value, nil)
assertEqual(ok, false) assertEqual(ok, false)
// Delete() // Delete()