provide a basic agent healthcheck

This commit is contained in:
Brad Rydzewski 2017-09-12 11:25:55 -07:00
parent c665e5b1d2
commit eca91f4ec7
14 changed files with 487 additions and 73 deletions

View file

@ -1,7 +1,9 @@
# docker build --rm -t drone/drone .
# docker build --rm -f Dockerfile.agent -t drone/agent .
FROM centurylink/ca-certs
ENV GODEBUG=netdns=go
ADD release/drone-agent /bin/
ENTRYPOINT ["/bin/drone-agent"]
HEALTHCHECK CMD ["/bin/drone-agent", "ping"]

View file

@ -1,5 +1,3 @@
# docker build --rm -t drone/drone .
FROM centurylink/ca-certs
ENV GODEBUG=netdns=go
ENV DRONE_PLATFORM=linux/arm

View file

@ -1,5 +1,3 @@
# docker build --rm -t drone/drone .
FROM centurylink/ca-certs
ENV GODEBUG=netdns=go
ENV DRONE_PLATFORM=linux/arm64

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"sync"
@ -46,6 +47,19 @@ func loop(c *cli.Context) error {
zerolog.SetGlobalLevel(zerolog.WarnLevel)
}
if c.Bool("pretty") {
log.Logger = log.Output(
zerolog.ConsoleWriter{
Out: os.Stderr,
NoColor: c.BoolT("nocolor"),
},
)
}
if c.BoolT("healthcheck") {
go http.ListenAndServe(":3000", nil)
}
// TODO pass version information to grpc server
// TODO authenticate to grpc server

51
cmd/drone-agent/health.go Normal file
View file

@ -0,0 +1,51 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/drone/drone/version"
"github.com/urfave/cli"
)
// the file implements some basic healthcheck logic based on the
// following specification:
// https://github.com/mozilla-services/Dockerflow
func init() {
http.HandleFunc("/__heartbeat__", handleHeartbeat)
http.HandleFunc("/__version__", handleVersion)
}
func handleHeartbeat(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
func handleVersion(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Add("Content-Type", "text/json")
json.NewEncoder(w).Encode(versionResp{
Source: "https://github.com/drone/drone",
Version: version.Version.String(),
})
}
type versionResp struct {
Version string `json:"version"`
Source string `json:"source"`
}
// handles pinging the endpoint and returns an error if the
// agent is in an unhealthy state.
func pinger(c *cli.Context) error {
resp, err := http.Get("http://localhost:3000/__heartbeat__")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("agent returned non-200 status code")
}
return nil
}

View file

@ -16,6 +16,13 @@ func main() {
app.Version = version.Version.String()
app.Usage = "drone agent"
app.Action = loop
app.Commands = []cli.Command{
{
Name: "ping",
Usage: "ping the agent",
Action: pinger,
},
}
app.Flags = []cli.Flag{
cli.StringFlag{
EnvVar: "DRONE_SERVER",
@ -39,6 +46,16 @@ func main() {
Name: "debug",
Usage: "start the agent in debug mode",
},
cli.BoolFlag{
EnvVar: "DRONE_DEBUG_PRETTY",
Name: "pretty",
Usage: "enable pretty-printed debug output",
},
cli.BoolTFlag{
EnvVar: "DRONE_DEBUG_NOCOLOR",
Name: "nocolor",
Usage: "disable colored debug output",
},
cli.StringFlag{
EnvVar: "DRONE_HOSTNAME,HOSTNAME",
Name: "hostname",
@ -50,14 +67,19 @@ func main() {
},
cli.StringFlag{
EnvVar: "DRONE_FILTER",
Name: "drone-filter",
Usage: "A filter expression used to restrict builds by label",
Name: "filter",
Usage: "filter expression used to restrict builds by label",
},
cli.IntFlag{
EnvVar: "DRONE_MAX_PROCS",
Name: "max-procs",
Value: 1,
},
cli.BoolTFlag{
EnvVar: "DRONE_HEALTHCHECK",
Name: "healthcheck",
Usage: "enables the healthcheck endpoint",
},
}
if err := app.Run(os.Args); err != nil {

View file

@ -8,16 +8,20 @@ Zerolog's API is designed to provide both a great developer experience and stunn
The uber's [zap](https://godoc.org/go.uber.org/zap) library pioneered this approach. Zerolog is taking this concept to the next level with simpler to use API and even better performance.
To keep the code base and the API simple, zerolog focuses on JSON logging only. As [suggested on reddit](https://www.reddit.com/r/golang/comments/6c9k7n/zerolog_is_now_faster_than_zap/), you may use tools like [humanlog](https://github.com/aybabtme/humanlog) to pretty print JSON on the console during development.
To keep the code base and the API simple, zerolog focuses on JSON logging only. Pretty logging on the console is made possible using the provided (but inefficient) `zerolog.ConsoleWriter`.
![](pretty.png)
## Features
* Blazing fast
* Low to zero allocation
* Level logging
* Sampling
* Contextual fields
* `context.Context` integration
* `net/http` helpers
* Pretty logging for development
## Usage
@ -27,6 +31,13 @@ import "github.com/rs/zerolog/log"
### A global logger can be use for simple logging
```go
log.Print("hello world")
// Output: {"level":"debug","time":1494567715,"message":"hello world"}
```
```go
log.Info().Msg("hello world")
@ -96,6 +107,18 @@ if e := log.Debug(); e.Enabled() {
// Output: {"level":"info","time":1494567715,"message":"routed message"}
```
### Pretty logging
```go
if isConsole {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
log.Info().Str("foo", "bar").Msg("Hello world")
// Output: 1494567715 |INFO| Hello world foo=bar
```
### Sub dictionary
```go
@ -138,12 +161,31 @@ log.Logger = log.With().Str("foo", "bar").Logger()
### Log Sampling
```go
sampled := log.Sample(10)
sampled := log.Sample(&zerolog.BasicSampler{N: 10})
sampled.Info().Msg("will be logged every 10 messages")
// Output: {"time":1494567715,"sample":10,"message":"will be logged every 10 messages"}
// Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"}
```
More advanced sampling:
```go
// Will let 5 debug messages per period of 1 second.
// Over 5 debug message, 1 every 100 debug messages are logged.
// Other levels are not sampled.
sampled := log.Sample(zerolog.LevelSampler{
DebugSampler: &zerolog.BurstSampler{
Burst: 5,
Period: 1*time.Second,
NextSampler: &zerolog.BasicSampler{N: 100},
},
})
sampled.Debug().Msg("hello world")
// Output: {"time":1494567715,"level":"debug","message":"hello world"}
```
### Pass a sub-logger by context
```go
@ -233,7 +275,6 @@ Some settings can be changed and will by applied to all loggers:
* `zerolog.LevelFieldName`: Can be set to customize level field name.
* `zerolog.MessageFieldName`: Can be set to customize message field name.
* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
* `zerolog.SampleFieldName`: Can be set to customize the field name added when sampling is enabled.
* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with an empty string, times are formated as UNIX timestamp.
// DurationFieldUnit defines the unit for time.Duration type fields added
// using the Dur method.
@ -259,7 +300,7 @@ Some settings can be changed and will by applied to all loggers:
* `Dict`: Adds a sub-key/value as a field of the event.
* `Interface`: Uses reflection to marshal the type.
## Performance
## Benchmarks
All operations are allocation free (those numbers *include* JSON encoding):
@ -271,7 +312,12 @@ BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op
BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
```
Using Uber's zap [comparison benchmark](https://github.com/uber-go/zap#performance):
There are a few Go logging benchmarks and comparisons that include zerolog.
- [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
- [uber-common/zap](https://github.com/uber-go/zap#performance)
Using Uber's zap comparison benchmark:
Log a message and 10 fields:

117
vendor/github.com/rs/zerolog/console.go generated vendored Normal file
View file

@ -0,0 +1,117 @@
package zerolog
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"strconv"
"strings"
"sync"
)
const (
cReset = 0
cBold = 1
cRed = 31
cGreen = 32
cYellow = 33
cBlue = 34
cMagenta = 35
cCyan = 36
cGray = 37
cDarkGray = 90
)
var consoleBufPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 100))
},
}
// ConsoleWriter reads a JSON object per write operation and output an
// optionally colored human readable version on the Out writer.
type ConsoleWriter struct {
Out io.Writer
NoColor bool
}
func (w ConsoleWriter) Write(p []byte) (n int, err error) {
var event map[string]interface{}
err = json.Unmarshal(p, &event)
if err != nil {
return
}
buf := consoleBufPool.Get().(*bytes.Buffer)
defer consoleBufPool.Put(buf)
lvlColor := cReset
level := "????"
if l, ok := event[LevelFieldName].(string); ok {
if !w.NoColor {
lvlColor = levelColor(l)
}
level = strings.ToUpper(l)[0:4]
}
fmt.Fprintf(buf, "%s |%s| %s",
colorize(event[TimestampFieldName], cDarkGray, !w.NoColor),
colorize(level, lvlColor, !w.NoColor),
colorize(event[MessageFieldName], cReset, !w.NoColor))
fields := make([]string, 0, len(event))
for field := range event {
switch field {
case LevelFieldName, TimestampFieldName, MessageFieldName:
continue
}
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
fmt.Fprintf(buf, " %s=", colorize(field, cCyan, !w.NoColor))
switch value := event[field].(type) {
case string:
if needsQuote(value) {
buf.WriteString(strconv.Quote(value))
} else {
buf.WriteString(value)
}
default:
fmt.Fprint(buf, value)
}
}
buf.WriteByte('\n')
buf.WriteTo(w.Out)
n = len(p)
return
}
func colorize(s interface{}, color int, enabled bool) string {
if !enabled {
return fmt.Sprintf("%v", s)
}
return fmt.Sprintf("\x1b[%dm%v\x1b[0m", color, s)
}
func levelColor(level string) int {
switch level {
case "debug":
return cMagenta
case "info":
return cGreen
case "warn":
return cYellow
case "error", "fatal", "panic":
return cRed
default:
return cReset
}
}
func needsQuote(s string) bool {
for i := range s {
if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' {
return true
}
}
return false
}

25
vendor/github.com/rs/zerolog/ctx.go generated vendored
View file

@ -5,25 +5,42 @@ import (
"io/ioutil"
)
var disabledLogger = New(ioutil.Discard).Level(Disabled)
var disabledLogger *Logger
func init() {
l := New(ioutil.Discard).Level(Disabled)
disabledLogger = &l
}
type ctxKey struct{}
// WithContext returns a copy of ctx with l associated.
// WithContext returns a copy of ctx with l associated. If an instance of Logger
// is already in the context, the pointer to this logger is updated with l.
//
// For instance, to add a field to an existing logger in the context, use this
// notation:
//
// ctx := r.Context()
// l := zerolog.Ctx(ctx)
// ctx = l.With().Str("foo", "bar").WithContext(ctx)
func (l Logger) WithContext(ctx context.Context) context.Context {
if lp, ok := ctx.Value(ctxKey{}).(*Logger); ok {
// Update existing pointer.
*lp = l
return ctx
}
if l.level == Disabled {
// Do not store disabled logger.
return ctx
}
return context.WithValue(ctx, ctxKey{}, &l)
}
// Ctx returns the Logger associated with the ctx. If no logger
// is associated, a disabled logger is returned.
func Ctx(ctx context.Context) Logger {
func Ctx(ctx context.Context) *Logger {
if l, ok := ctx.Value(ctxKey{}).(*Logger); ok {
return *l
return l
}
return disabledLogger
}

View file

@ -16,9 +16,6 @@ var (
// ErrorFieldName is the field name used for error fields.
ErrorFieldName = "error"
// SampleFieldName is the name of the field used to report sampling.
SampleFieldName = "sample"
// TimeFieldFormat defines the time format of the Time field type.
// If set to an empty string, the time is formatted as an UNIX timestamp
// as integer.

98
vendor/github.com/rs/zerolog/log.go generated vendored
View file

@ -62,17 +62,17 @@
//
// Sample logs:
//
// sampled := log.Sample(10)
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
// sampled.Info().Msg("will be logged every 10 messages")
//
package zerolog
import (
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"sync/atomic"
"github.com/rs/zerolog/internal/json"
)
@ -115,15 +115,6 @@ func (l Level) String() string {
return ""
}
const (
// Often samples log every 10 events.
Often = 10
// Sometimes samples log every 100 events.
Sometimes = 100
// Rarely samples log every 1000 events.
Rarely = 1000
)
var disabledEvent = newEvent(levelWriterAdapter{ioutil.Discard}, 0, false)
// A Logger represents an active logging object that generates lines
@ -134,8 +125,7 @@ var disabledEvent = newEvent(levelWriterAdapter{ioutil.Discard}, 0, false)
type Logger struct {
w LevelWriter
level Level
sample uint32
counter *uint32
sampler Sampler
context []byte
}
@ -162,6 +152,16 @@ func Nop() Logger {
return New(nil).Level(Disabled)
}
// Output duplicates the current logger and sets w as its output.
func (l Logger) Output(w io.Writer) Logger {
l2 := New(w)
l2.level = l.level
l2.sampler = l.sampler
l2.context = make([]byte, len(l.context), cap(l.context))
copy(l2.context, l.context)
return l2
}
// With creates a child logger with the field added to its context.
func (l Logger) With() Context {
context := l.context
@ -175,34 +175,30 @@ func (l Logger) With() Context {
return Context{l}
}
// Level creates a child logger with the minimum accepted level set to level.
func (l Logger) Level(lvl Level) Logger {
return Logger{
w: l.w,
level: lvl,
sample: l.sample,
counter: l.counter,
context: l.context,
// UpdateContext updates the internal logger's context.
//
// Use this method with caution. If unsure, prefer the With method.
func (l *Logger) UpdateContext(update func(c Context) Context) {
if l == disabledLogger {
return
}
if cap(l.context) == 0 {
l.context = make([]byte, 1, 500) // first byte is timestamp flag
}
c := update(Context{*l})
l.context = c.l.context
}
// Sample returns a logger that only let one message out of every to pass thru.
func (l Logger) Sample(every int) Logger {
if every == 0 {
// Create a child with no sampling.
return Logger{
w: l.w,
level: l.level,
context: l.context,
}
}
return Logger{
w: l.w,
level: l.level,
sample: uint32(every),
counter: new(uint32),
context: l.context,
}
// Level creates a child logger with the minimum accepted level set to level.
func (l Logger) Level(lvl Level) Logger {
l.level = lvl
return l
}
// Sample returns a logger with the s sampler.
func (l Logger) Sample(s Sampler) Logger {
l.sampler = s
return l
}
// Debug starts a new message with debug level.
@ -283,6 +279,22 @@ func (l Logger) Log() *Event {
return l.newEvent(PanicLevel, false, nil)
}
// Print sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Print.
func (l Logger) Print(v ...interface{}) {
if e := l.Debug(); e.Enabled() {
e.Msg(fmt.Sprint(v...))
}
}
// Printf sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Printf.
func (l Logger) Printf(format string, v ...interface{}) {
if e := l.Debug(); e.Enabled() {
e.Msg(fmt.Sprintf(format, v...))
}
}
// Write implements the io.Writer interface. This is useful to set as a writer
// for the standard library log.
func (l Logger) Write(p []byte) (n int, err error) {
@ -304,7 +316,7 @@ func (l Logger) newEvent(level Level, addLevelField bool, done func(string)) *Ev
if addLevelField {
lvl = level
}
e := newEvent(l.w, lvl, enabled)
e := newEvent(l.w, lvl, true)
e.done = done
if l.context != nil && len(l.context) > 0 && l.context[0] > 0 {
// first byte of context is ts flag
@ -313,9 +325,6 @@ func (l Logger) newEvent(level Level, addLevelField bool, done func(string)) *Ev
if addLevelField {
e.Str(LevelFieldName, level.String())
}
if l.sample > 0 && SampleFieldName != "" {
e.Uint32(SampleFieldName, l.sample)
}
if l.context != nil && len(l.context) > 1 {
if len(e.buf) > 1 {
e.buf = append(e.buf, ',')
@ -330,9 +339,8 @@ func (l Logger) should(lvl Level) bool {
if lvl < l.level || lvl < globalLevel() {
return false
}
if l.sample > 0 && l.counter != nil && !samplingDisabled() {
c := atomic.AddUint32(l.counter, 1)
return c%l.sample == 0
if l.sampler != nil && !samplingDisabled() {
return l.sampler.Sample(lvl)
}
return true
}

View file

@ -3,6 +3,7 @@ package log
import (
"context"
"io"
"os"
"github.com/rs/zerolog"
@ -11,6 +12,11 @@ import (
// Logger is the global logger.
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
// Output duplicates the global logger and sets w as its output.
func Output(w io.Writer) zerolog.Logger {
return Logger.Output(w)
}
// With creates a child logger with the field added to its context.
func With() zerolog.Context {
return Logger.With()
@ -21,9 +27,9 @@ func Level(level zerolog.Level) zerolog.Logger {
return Logger.Level(level)
}
// Sample returns a logger that only let one message out of every to pass thru.
func Sample(every int) zerolog.Logger {
return Logger.Sample(every)
// Sample returns a logger with the s sampler.
func Sample(s zerolog.Sampler) zerolog.Logger {
return Logger.Sample(s)
}
// Debug starts a new message with debug level.
@ -78,8 +84,20 @@ func Log() *zerolog.Event {
return Logger.Log()
}
// Print sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Print.
func Print(v ...interface{}) {
Logger.Print(v...)
}
// Printf sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Printf.
func Printf(format string, v ...interface{}) {
Logger.Printf(format, v...)
}
// Ctx returns the Logger associated with the ctx. If no logger
// is associated, a disabled logger is returned.
func Ctx(ctx context.Context) zerolog.Logger {
func Ctx(ctx context.Context) *zerolog.Logger {
return zerolog.Ctx(ctx)
}

126
vendor/github.com/rs/zerolog/sampler.go generated vendored Normal file
View file

@ -0,0 +1,126 @@
package zerolog
import (
"math/rand"
"sync/atomic"
"time"
)
var (
// Often samples log every ~ 10 events.
Often = RandomSampler(10)
// Sometimes samples log every ~ 100 events.
Sometimes = RandomSampler(100)
// Rarely samples log every ~ 1000 events.
Rarely = RandomSampler(1000)
)
// Sampler defines an interface to a log sampler.
type Sampler interface {
// Sample returns true if the event should be part of the sample, false if
// the event should be dropped.
Sample(lvl Level) bool
}
// RandomSampler use a PRNG to randomly sample an event out of N events,
// regardless of their level.
type RandomSampler uint32
// Sample implements the Sampler interface.
func (s RandomSampler) Sample(lvl Level) bool {
if s <= 0 {
return false
}
if rand.Intn(int(s)) != 0 {
return false
}
return true
}
// BasicSampler is a sampler that will send every Nth events, regardless of
// there level.
type BasicSampler struct {
N uint32
counter uint32
}
// Sample implements the Sampler interface.
func (s *BasicSampler) Sample(lvl Level) bool {
c := atomic.AddUint32(&s.counter, 1)
return c%s.N == 0
}
// BurstSampler lets Burst events pass per Period then pass the decision to
// NextSampler. If Sampler is not set, all subsequent events are rejected.
type BurstSampler struct {
// Burst is the maximum number of event per period allowed before calling
// NextSampler.
Burst uint32
// Period defines the burst period. If 0, NextSampler is always called.
Period time.Duration
// NextSampler is the sampler used after the burst is reached. If nil,
// events are always rejected after the burst.
NextSampler Sampler
counter uint32
resetAt int64
}
// Sample implements the Sampler interface.
func (s *BurstSampler) Sample(lvl Level) bool {
if s.Burst > 9 && s.Period > 0 {
if s.inc() <= s.Burst {
return true
}
}
if s.NextSampler == nil {
return false
}
return s.NextSampler.Sample(lvl)
}
func (s *BurstSampler) inc() uint32 {
now := time.Now().UnixNano()
resetAt := atomic.LoadInt64(&s.resetAt)
var c uint32
if now > resetAt {
c = 1
atomic.StoreUint32(&s.counter, c)
newResetAt := now + s.Period.Nanoseconds()
reset := atomic.CompareAndSwapInt64(&s.resetAt, resetAt, newResetAt)
if !reset {
// Lost the race with another goroutine trying to reset.
c = atomic.AddUint32(&s.counter, 1)
}
} else {
c = atomic.AddUint32(&s.counter, 1)
}
return c
}
// LevelSampler applies a different sampler for each level.
type LevelSampler struct {
DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler
}
func (s LevelSampler) Sample(lvl Level) bool {
switch lvl {
case DebugLevel:
if s.DebugSampler != nil {
return s.DebugSampler.Sample(lvl)
}
case InfoLevel:
if s.InfoSampler != nil {
return s.InfoSampler.Sample(lvl)
}
case WarnLevel:
if s.WarnSampler != nil {
return s.WarnSampler.Sample(lvl)
}
case ErrorLevel:
if s.ErrorSampler != nil {
return s.ErrorSampler.Sample(lvl)
}
}
return true
}

8
vendor/vendor.json vendored
View file

@ -762,10 +762,10 @@
"revisionTime": "2017-04-24T20:45:52Z"
},
{
"checksumSHA1": "fGBeb3o1grSXGUNAjwptkBWfch0=",
"checksumSHA1": "3Ie7HG2k47G/gwz8prjymTMLEms=",
"path": "github.com/rs/zerolog",
"revision": "89ff8dbc5f047ae9957523b07e627891079f7967",
"revisionTime": "2017-07-27T06:42:12Z"
"revision": "9d194eb6f50e8718a6d6f8f1e1f0bf3ddf4065f1",
"revisionTime": "2017-09-11T21:52:32Z"
},
{
"checksumSHA1": "AREhk6LKIp2I/4Njd756bqU6JSQ=",
@ -774,7 +774,7 @@
"revisionTime": "2017-07-27T06:42:12Z"
},
{
"checksumSHA1": "kolarHDX6fkauW+1KWx1SFqSF2o=",
"checksumSHA1": "VFgakVFNczJTf9gtUeIpkgcFRf0=",
"path": "github.com/rs/zerolog/log",
"revision": "89ff8dbc5f047ae9957523b07e627891079f7967",
"revisionTime": "2017-07-27T06:42:12Z"