This commit is contained in:
Manfred Touron
2017-05-18 18:54:23 +02:00
parent dc386661ca
commit 5448f25fd6
645 changed files with 55908 additions and 33297 deletions

View File

@@ -5,7 +5,7 @@ examples/profilesvc/profilesvc
examples/stringsvc1/stringsvc1
examples/stringsvc2/stringsvc2
examples/stringsvc3/stringsvc3
*.coverprofile
gover.coverprofile
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o

View File

@@ -3,6 +3,7 @@ language: go
script: go test -race -v ./...
go:
- 1.7.5
- 1.8
- 1.5.4
- 1.6.3
- 1.7.1
- tip

View File

@@ -1,4 +1,4 @@
# Go kit [![Circle CI](https://circleci.com/gh/go-kit/kit.svg?style=svg)](https://circleci.com/gh/go-kit/kit) [![Travis CI](https://travis-ci.org/go-kit/kit.svg?branch=master)](https://travis-ci.org/go-kit/kit) [![GoDoc](https://godoc.org/github.com/go-kit/kit?status.svg)](https://godoc.org/github.com/go-kit/kit) [![Coverage Status](https://coveralls.io/repos/go-kit/kit/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-kit/kit?branch=master) [![Go Report Card](https://goreportcard.com/badge/go-kit/kit)](https://goreportcard.com/report/go-kit/kit) [![Sourcegraph](https://sourcegraph.com/github.com/go-kit/kit/-/badge.svg)](https://sourcegraph.com/github.com/go-kit/kit?badge)
# Go kit [![Circle CI](https://circleci.com/gh/go-kit/kit.svg?style=svg)](https://circleci.com/gh/go-kit/kit) [![Drone.io](https://drone.io/github.com/go-kit/kit/status.png)](https://drone.io/github.com/go-kit/kit/latest) [![Travis CI](https://travis-ci.org/go-kit/kit.svg?branch=master)](https://travis-ci.org/go-kit/kit) [![GoDoc](https://godoc.org/github.com/go-kit/kit?status.svg)](https://godoc.org/github.com/go-kit/kit) [![Coverage Status](https://coveralls.io/repos/go-kit/kit/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-kit/kit?branch=master) [![Go Report Card](https://goreportcard.com/badge/go-kit/kit)](https://goreportcard.com/report/go-kit/kit)
**Go kit** is a **distributed programming toolkit** for building microservices
in large organizations. We solve common problems in distributed systems, so
@@ -108,14 +108,10 @@ Projects with a ★ have had particular influence on Go kit's design (or vice-ve
- [Goji](https://github.com/zenazn/goji)
- [Martini](https://github.com/go-martini/martini)
- [Beego](http://beego.me/)
- [Revel](https://revel.github.io/) (considered [harmful](https://github.com/go-kit/kit/issues/350))
- [Revel](https://revel.github.io/) (considered harmful)
## Additional reading
- [Architecting for the Cloud](http://fr.slideshare.net/stonse/architecting-for-the-cloud-using-netflixoss-codemash-workshop-29852233) — Netflix
- [Dapper, a Large-Scale Distributed Systems Tracing Infrastructure](http://research.google.com/pubs/pub36356.html) — Google
- [Your Server as a Function](http://monkey.org/~marius/funsrv.pdf) (PDF) — Twitter
---
Development supported by [DigitalOcean](https://digitalocean.com).

View File

@@ -92,7 +92,7 @@ Example of use in a server:
```go
import (
"context"
"golang.org/x/net/context"
"github.com/go-kit/kit/auth/jwt"
"github.com/go-kit/kit/log"

View File

@@ -1,10 +1,10 @@
package jwt
import (
"context"
"errors"
jwt "github.com/dgrijalva/jwt-go"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)
@@ -15,8 +15,7 @@ const (
// JWTTokenContextKey holds the key used to store a JWT Token in the
// context.
JWTTokenContextKey contextKey = "JWTToken"
// JWTClaimsContextKey holds the key used to store the JWT Claims in the
// JWTClaimsContxtKey holds the key used to store the JWT Claims in the
// context.
JWTClaimsContextKey contextKey = "JWTClaims"
)
@@ -25,26 +24,20 @@ var (
// ErrTokenContextMissing denotes a token was not passed into the parsing
// middleware's context.
ErrTokenContextMissing = errors.New("token up for parsing was not passed through the context")
// ErrTokenInvalid denotes a token was not able to be validated.
ErrTokenInvalid = errors.New("JWT Token was invalid")
// ErrTokenExpired denotes a token's expire header (exp) has since passed.
ErrTokenExpired = errors.New("JWT Token is expired")
// ErrTokenMalformed denotes a token was not formatted as a JWT token.
ErrTokenMalformed = errors.New("JWT Token is malformed")
// ErrTokenNotActive denotes a token's not before header (nbf) is in the
// future.
ErrTokenNotActive = errors.New("token is not valid yet")
// ErrUnexpectedSigningMethod denotes a token was signed with an unexpected
// ErrUncesptedSigningMethod denotes a token was signed with an unexpected
// signing method.
ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
)
// Claims is a map of arbitrary claim data.
type Claims map[string]interface{}
// NewSigner creates a new JWT token generating middleware, specifying key ID,

View File

@@ -1,10 +1,11 @@
package jwt
import (
"context"
"testing"
jwt "github.com/dgrijalva/jwt-go"
"golang.org/x/net/context"
)
var (

View File

@@ -1,11 +1,11 @@
package jwt
import (
"context"
"fmt"
stdhttp "net/http"
"strings"
"golang.org/x/net/context"
"google.golang.org/grpc/metadata"
"github.com/go-kit/kit/transport/grpc"
@@ -44,10 +44,10 @@ func FromHTTPContext() http.RequestFunc {
// ToGRPCContext moves JWT token from grpc metadata to context. Particularly
// userful for servers.
func ToGRPCContext() grpc.ServerRequestFunc {
return func(ctx context.Context, md metadata.MD) context.Context {
func ToGRPCContext() grpc.RequestFunc {
return func(ctx context.Context, md *metadata.MD) context.Context {
// capital "Key" is illegal in HTTP/2.
authHeader, ok := md["authorization"]
authHeader, ok := (*md)["authorization"]
if !ok {
return ctx
}
@@ -63,7 +63,7 @@ func ToGRPCContext() grpc.ServerRequestFunc {
// FromGRPCContext moves JWT token from context to grpc metadata. Particularly
// useful for clients.
func FromGRPCContext() grpc.ClientRequestFunc {
func FromGRPCContext() grpc.RequestFunc {
return func(ctx context.Context, md *metadata.MD) context.Context {
token, ok := ctx.Value(JWTTokenContextKey).(string)
if ok {

View File

@@ -1,12 +1,13 @@
package jwt
import (
"context"
"fmt"
"net/http"
"testing"
"google.golang.org/grpc/metadata"
"golang.org/x/net/context"
)
func TestToHTTPContext(t *testing.T) {
@@ -69,7 +70,7 @@ func TestToGRPCContext(t *testing.T) {
reqFunc := ToGRPCContext()
// No Authorization header is passed
ctx := reqFunc(context.Background(), md)
ctx := reqFunc(context.Background(), &md)
token := ctx.Value(JWTTokenContextKey)
if token != nil {
t.Error("Context should not contain a JWT Token")
@@ -77,7 +78,7 @@ func TestToGRPCContext(t *testing.T) {
// Invalid Authorization header is passed
md["authorization"] = []string{fmt.Sprintf("%s", signedKey)}
ctx = reqFunc(context.Background(), md)
ctx = reqFunc(context.Background(), &md)
token = ctx.Value(JWTTokenContextKey)
if token != nil {
t.Error("Context should not contain a JWT Token")
@@ -85,7 +86,7 @@ func TestToGRPCContext(t *testing.T) {
// Authorization header is correct
md["authorization"] = []string{fmt.Sprintf("Bearer %s", signedKey)}
ctx = reqFunc(context.Background(), md)
ctx = reqFunc(context.Background(), &md)
token, ok := ctx.Value(JWTTokenContextKey).(string)
if !ok {
t.Fatal("JWT Token not passed to context correctly")

View File

@@ -1,26 +0,0 @@
machine:
pre:
- curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0
- sudo rm -rf /usr/local/go
- curl -sSL https://storage.googleapis.com/golang/go1.8.linux-amd64.tar.gz | sudo tar xz -C /usr/local
services:
- docker
dependencies:
pre:
- sudo curl -L "https://github.com/docker/compose/releases/download/1.10.0/docker-compose-linux-x86_64" -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
- docker-compose -f docker-compose-integration.yml up -d --force-recreate
test:
pre:
- mkdir -p /home/ubuntu/.go_workspace/src/github.com/go-kit
- mv /home/ubuntu/kit /home/ubuntu/.go_workspace/src/github.com/go-kit
- ln -s /home/ubuntu/.go_workspace/src/github.com/go-kit/kit /home/ubuntu/kit
- go get github.com/go-kit/kit/...
override:
- go test -v -race -tags integration github.com/go-kit/kit/...:
environment:
ETCD_ADDR: http://localhost:2379
CONSUL_ADDR: localhost:8500
ZK_ADDR: localhost:2181

View File

@@ -1,9 +1,8 @@
package circuitbreaker
import (
"context"
"github.com/sony/gobreaker"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,10 +1,10 @@
package circuitbreaker
import (
"context"
"time"
"github.com/streadway/handy/breaker"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,9 +1,8 @@
package circuitbreaker
import (
"context"
"github.com/afex/hystrix-go/hystrix"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,7 +1,6 @@
package circuitbreaker_test
import (
"context"
"errors"
"fmt"
"path/filepath"
@@ -9,6 +8,8 @@ import (
"testing"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)
@@ -66,7 +67,7 @@ func testFailingEndpoint(
type mock struct {
through int
err error
err error
}
func (m *mock) endpoint(context.Context, interface{}) (interface{}, error) {

View File

@@ -1,16 +0,0 @@
version: '2'
services:
etcd:
image: quay.io/coreos/etcd
ports:
- "2379:2379"
command: /usr/local/bin/etcd -advertise-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 -listen-client-urls "http://0.0.0.0:2379,http://0.0.0.0:4001"
consul:
image: progrium/consul
ports:
- "8500:8500"
command: -server -bootstrap
zk:
image: zookeeper
ports:
- "2181:2181"

View File

@@ -1,7 +1,7 @@
package endpoint
import (
"context"
"golang.org/x/net/context"
)
// Endpoint is the fundamental building block of servers and clients.

View File

@@ -1,9 +1,10 @@
package endpoint_test
import (
"context"
"fmt"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -67,10 +67,10 @@ func New(instance string, tracer stdopentracing.Tracer, logger log.Logger) (adds
).Endpoint()
concatEndpoint = opentracing.TraceClient(tracer, "Concat")(concatEndpoint)
concatEndpoint = limiter(concatEndpoint)
concatEndpoint = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{
sumEndpoint = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "Concat",
Timeout: 30 * time.Second,
}))(concatEndpoint)
}))(sumEndpoint)
}
return addsvc.Endpoints{

View File

@@ -1,7 +1,6 @@
package main
import (
"context"
"flag"
"fmt"
"os"
@@ -13,6 +12,7 @@ import (
"github.com/lightstep/lightstep-tracer-go"
stdopentracing "github.com/opentracing/opentracing-go"
zipkin "github.com/openzipkin/zipkin-go-opentracing"
"golang.org/x/net/context"
"google.golang.org/grpc"
"sourcegraph.com/sourcegraph/appdash"
appdashot "sourcegraph.com/sourcegraph/appdash/opentracing"
@@ -40,8 +40,7 @@ func main() {
thriftProtocol = flag.String("thrift.protocol", "binary", "binary, compact, json, simplejson")
thriftBufferSize = flag.Int("thrift.buffer.size", 0, "0 for unbuffered")
thriftFramed = flag.Bool("thrift.framed", false, "true to enable framing")
zipkinAddr = flag.String("zipkin.addr", "", "Enable Zipkin tracing via a Zipkin HTTP Collector endpoint")
zipkinKafkaAddr = flag.String("zipkin.kafka.addr", "", "Enable Zipkin tracing via a Kafka server host:port")
zipkinAddr = flag.String("zipkin.addr", "", "Enable Zipkin tracing via a Kafka Collector host:port")
appdashAddr = flag.String("appdash.addr", "", "Enable Appdash tracing via an Appdash server host:port")
lightstepToken = flag.String("lightstep.token", "", "Enable LightStep tracing via a LightStep access token")
method = flag.String("method", "sum", "sum, concat")
@@ -58,34 +57,16 @@ func main() {
var tracer stdopentracing.Tracer
{
if *zipkinAddr != "" {
// endpoint typically looks like: http://zipkinhost:9411/api/v1/spans
collector, err := zipkin.NewHTTPCollector(*zipkinAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
defer collector.Close()
tracer, err = zipkin.NewTracer(
zipkin.NewRecorder(collector, false, "0.0.0.0:0", "addcli"),
)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
} else if *zipkinKafkaAddr != "" {
collector, err := zipkin.NewKafkaCollector(
strings.Split(*zipkinKafkaAddr, ","),
strings.Split(*zipkinAddr, ","),
zipkin.KafkaLogger(log.NewNopLogger()),
)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
defer collector.Close()
tracer, err = zipkin.NewTracer(
zipkin.NewRecorder(collector, false, "0.0.0.0:0", "addcli"),
zipkin.NewRecorder(collector, false, "localhost:8000", "addcli"),
)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)

View File

@@ -1,7 +1,6 @@
package main
package main
import (
"context"
"flag"
"fmt"
"net"
@@ -17,6 +16,7 @@ import (
stdopentracing "github.com/opentracing/opentracing-go"
zipkin "github.com/openzipkin/zipkin-go-opentracing"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
"google.golang.org/grpc"
"sourcegraph.com/sourcegraph/appdash"
appdashot "sourcegraph.com/sourcegraph/appdash/opentracing"
@@ -40,8 +40,7 @@ func main() {
thriftProtocol = flag.String("thrift.protocol", "binary", "binary, compact, json, simplejson")
thriftBufferSize = flag.Int("thrift.buffer.size", 0, "0 for unbuffered")
thriftFramed = flag.Bool("thrift.framed", false, "true to enable framing")
zipkinAddr = flag.String("zipkin.addr", "", "Enable Zipkin tracing via a Zipkin HTTP Collector endpoint")
zipkinKafkaAddr = flag.String("zipkin.kafka.addr", "", "Enable Zipkin tracing via a Kafka server host:port")
zipkinAddr = flag.String("zipkin.addr", "", "Enable Zipkin tracing via a Kafka server host:port")
appdashAddr = flag.String("appdash.addr", "", "Enable Appdash tracing via an Appdash server host:port")
lightstepToken = flag.String("lightstep.token", "", "Enable LightStep tracing via a LightStep access token")
)
@@ -51,8 +50,8 @@ func main() {
var logger log.Logger
{
logger = log.NewLogfmtLogger(os.Stdout)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.With(logger, "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
logger = log.NewContext(logger).With("caller", log.DefaultCaller)
}
logger.Log("msg", "hello")
defer logger.Log("msg", "goodbye")
@@ -86,38 +85,16 @@ func main() {
var tracer stdopentracing.Tracer
{
if *zipkinAddr != "" {
logger := log.With(logger, "tracer", "ZipkinHTTP")
logger := log.NewContext(logger).With("tracer", "Zipkin")
logger.Log("addr", *zipkinAddr)
// endpoint typically looks like: http://zipkinhost:9411/api/v1/spans
collector, err := zipkin.NewHTTPCollector(*zipkinAddr)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
defer collector.Close()
tracer, err = zipkin.NewTracer(
zipkin.NewRecorder(collector, false, "localhost:80", "addsvc"),
)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
} else if *zipkinKafkaAddr != "" {
logger := log.With(logger, "tracer", "ZipkinKafka")
logger.Log("addr", *zipkinKafkaAddr)
collector, err := zipkin.NewKafkaCollector(
strings.Split(*zipkinKafkaAddr, ","),
zipkin.KafkaLogger(log.NewNopLogger()),
strings.Split(*zipkinAddr, ","),
zipkin.KafkaLogger(logger),
)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
defer collector.Close()
tracer, err = zipkin.NewTracer(
zipkin.NewRecorder(collector, false, "localhost:80", "addsvc"),
)
@@ -126,18 +103,18 @@ func main() {
os.Exit(1)
}
} else if *appdashAddr != "" {
logger := log.With(logger, "tracer", "Appdash")
logger := log.NewContext(logger).With("tracer", "Appdash")
logger.Log("addr", *appdashAddr)
tracer = appdashot.NewTracer(appdash.NewRemoteCollector(*appdashAddr))
} else if *lightstepToken != "" {
logger := log.With(logger, "tracer", "LightStep")
logger := log.NewContext(logger).With("tracer", "LightStep")
logger.Log() // probably don't want to print out the token :)
tracer = lightstep.NewTracer(lightstep.Options{
AccessToken: *lightstepToken,
})
defer lightstep.FlushLightStepTracer(tracer)
} else {
logger := log.With(logger, "tracer", "none")
logger := log.NewContext(logger).With("tracer", "none")
logger.Log()
tracer = stdopentracing.GlobalTracer() // no-op
}
@@ -155,7 +132,7 @@ func main() {
var sumEndpoint endpoint.Endpoint
{
sumDuration := duration.With("method", "Sum")
sumLogger := log.With(logger, "method", "Sum")
sumLogger := log.NewContext(logger).With("method", "Sum")
sumEndpoint = addsvc.MakeSumEndpoint(service)
sumEndpoint = opentracing.TraceServer(tracer, "Sum")(sumEndpoint)
@@ -165,7 +142,7 @@ func main() {
var concatEndpoint endpoint.Endpoint
{
concatDuration := duration.With("method", "Concat")
concatLogger := log.With(logger, "method", "Concat")
concatLogger := log.NewContext(logger).With("method", "Concat")
concatEndpoint = addsvc.MakeConcatEndpoint(service)
concatEndpoint = opentracing.TraceServer(tracer, "Concat")(concatEndpoint)
@@ -190,7 +167,7 @@ func main() {
// Debug listener.
go func() {
logger := log.With(logger, "transport", "debug")
logger := log.NewContext(logger).With("transport", "debug")
m := http.NewServeMux()
m.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
@@ -206,15 +183,15 @@ func main() {
// HTTP transport.
go func() {
logger := log.With(logger, "transport", "HTTP")
h := addsvc.MakeHTTPHandler(endpoints, tracer, logger)
logger := log.NewContext(logger).With("transport", "HTTP")
h := addsvc.MakeHTTPHandler(ctx, endpoints, tracer, logger)
logger.Log("addr", *httpAddr)
errc <- http.ListenAndServe(*httpAddr, h)
}()
// gRPC transport.
go func() {
logger := log.With(logger, "transport", "gRPC")
logger := log.NewContext(logger).With("transport", "gRPC")
ln, err := net.Listen("tcp", *grpcAddr)
if err != nil {
@@ -222,7 +199,7 @@ func main() {
return
}
srv := addsvc.MakeGRPCServer(endpoints, tracer, logger)
srv := addsvc.MakeGRPCServer(ctx, endpoints, tracer, logger)
s := grpc.NewServer()
pb.RegisterAddServer(s, srv)
@@ -232,7 +209,7 @@ func main() {
// Thrift transport.
go func() {
logger := log.With(logger, "transport", "Thrift")
logger := log.NewContext(logger).With("transport", "Thrift")
var protocolFactory thrift.TProtocolFactory
switch *thriftProtocol {

View File

@@ -6,10 +6,11 @@ package addsvc
// formats. It also includes endpoint middlewares.
import (
"context"
"fmt"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"

View File

@@ -4,10 +4,11 @@ package addsvc
// implementation. It also includes service middlewares.
import (
"context"
"errors"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"
)

View File

@@ -4,10 +4,8 @@ package addsvc
// It utilizes the transport/grpc.Server.
import (
"context"
stdopentracing "github.com/opentracing/opentracing-go"
oldcontext "golang.org/x/net/context"
"golang.org/x/net/context"
"github.com/go-kit/kit/examples/addsvc/pb"
"github.com/go-kit/kit/log"
@@ -16,18 +14,20 @@ import (
)
// MakeGRPCServer makes a set of endpoints available as a gRPC AddServer.
func MakeGRPCServer(endpoints Endpoints, tracer stdopentracing.Tracer, logger log.Logger) pb.AddServer {
func MakeGRPCServer(ctx context.Context, endpoints Endpoints, tracer stdopentracing.Tracer, logger log.Logger) pb.AddServer {
options := []grpctransport.ServerOption{
grpctransport.ServerErrorLogger(logger),
}
return &grpcServer{
sum: grpctransport.NewServer(
ctx,
endpoints.SumEndpoint,
DecodeGRPCSumRequest,
EncodeGRPCSumResponse,
append(options, grpctransport.ServerBefore(opentracing.FromGRPCRequest(tracer, "Sum", logger)))...,
),
concat: grpctransport.NewServer(
ctx,
endpoints.ConcatEndpoint,
DecodeGRPCConcatRequest,
EncodeGRPCConcatResponse,
@@ -41,7 +41,7 @@ type grpcServer struct {
concat grpctransport.Handler
}
func (s *grpcServer) Sum(ctx oldcontext.Context, req *pb.SumRequest) (*pb.SumReply, error) {
func (s *grpcServer) Sum(ctx context.Context, req *pb.SumRequest) (*pb.SumReply, error) {
_, rep, err := s.sum.ServeGRPC(ctx, req)
if err != nil {
return nil, err
@@ -49,7 +49,7 @@ func (s *grpcServer) Sum(ctx oldcontext.Context, req *pb.SumRequest) (*pb.SumRep
return rep.(*pb.SumReply), nil
}
func (s *grpcServer) Concat(ctx oldcontext.Context, req *pb.ConcatRequest) (*pb.ConcatReply, error) {
func (s *grpcServer) Concat(ctx context.Context, req *pb.ConcatRequest) (*pb.ConcatReply, error) {
_, rep, err := s.concat.ServeGRPC(ctx, req)
if err != nil {
return nil, err

View File

@@ -5,13 +5,13 @@ package addsvc
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
stdopentracing "github.com/opentracing/opentracing-go"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/tracing/opentracing"
@@ -20,19 +20,21 @@ import (
// MakeHTTPHandler returns a handler that makes a set of endpoints available
// on predefined paths.
func MakeHTTPHandler(endpoints Endpoints, tracer stdopentracing.Tracer, logger log.Logger) http.Handler {
func MakeHTTPHandler(ctx context.Context, endpoints Endpoints, tracer stdopentracing.Tracer, logger log.Logger) http.Handler {
options := []httptransport.ServerOption{
httptransport.ServerErrorEncoder(errorEncoder),
httptransport.ServerErrorLogger(logger),
}
m := http.NewServeMux()
m.Handle("/sum", httptransport.NewServer(
ctx,
endpoints.SumEndpoint,
DecodeHTTPSumRequest,
EncodeHTTPGenericResponse,
append(options, httptransport.ServerBefore(opentracing.FromHTTPRequest(tracer, "Sum", logger)))...,
))
m.Handle("/concat", httptransport.NewServer(
ctx,
endpoints.ConcatEndpoint,
DecodeHTTPConcatRequest,
EncodeHTTPGenericResponse,
@@ -45,9 +47,18 @@ func errorEncoder(_ context.Context, err error, w http.ResponseWriter) {
code := http.StatusInternalServerError
msg := err.Error()
switch err {
case ErrTwoZeroes, ErrMaxSizeExceeded, ErrIntOverflow:
code = http.StatusBadRequest
if e, ok := err.(httptransport.Error); ok {
msg = e.Err.Error()
switch e.Domain {
case httptransport.DomainDecode:
code = http.StatusBadRequest
case httptransport.DomainDo:
switch e.Err {
case ErrTwoZeroes, ErrMaxSizeExceeded, ErrIntOverflow:
code = http.StatusBadRequest
}
}
}
w.WriteHeader(code)

View File

@@ -7,7 +7,7 @@ package addsvc
// yet. See https://github.com/go-kit/kit/issues/184.
import (
"context"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
thriftadd "github.com/go-kit/kit/examples/addsvc/thrift/gen-go/addsvc"

View File

@@ -2,7 +2,6 @@ package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
@@ -19,6 +18,7 @@ import (
"github.com/gorilla/mux"
"github.com/hashicorp/consul/api"
stdopentracing "github.com/opentracing/opentracing-go"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/examples/addsvc"
@@ -44,8 +44,8 @@ func main() {
var logger log.Logger
{
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.With(logger, "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
logger = log.NewContext(logger).With("caller", log.DefaultCaller)
}
// Service discovery domain. In this example we use Consul.
@@ -104,7 +104,7 @@ func main() {
// HTTP handler, and just install it under a particular path prefix in
// our router.
r.PathPrefix("/addsvc").Handler(http.StripPrefix("/addsvc", addsvc.MakeHTTPHandler(endpoints, tracer, logger)))
r.PathPrefix("addsvc/").Handler(addsvc.MakeHTTPHandler(ctx, endpoints, tracer, logger))
}
// stringsvc routes.
@@ -140,8 +140,8 @@ func main() {
// have to do provide it with the encode and decode functions for our
// stringsvc methods.
r.Handle("/stringsvc/uppercase", httptransport.NewServer(uppercase, decodeUppercaseRequest, encodeJSONResponse))
r.Handle("/stringsvc/count", httptransport.NewServer(count, decodeCountRequest, encodeJSONResponse))
r.Handle("/stringsvc/uppercase", httptransport.NewServer(ctx, uppercase, decodeUppercaseRequest, encodeJSONResponse))
r.Handle("/stringsvc/count", httptransport.NewServer(ctx, count, decodeCountRequest, encodeJSONResponse))
}
// Interrupt handler.

View File

@@ -1,4 +1,4 @@
package main
package main
import (
"flag"
@@ -8,6 +8,8 @@ import (
"os/signal"
"syscall"
"golang.org/x/net/context"
"github.com/go-kit/kit/examples/profilesvc"
"github.com/go-kit/kit/log"
)
@@ -21,8 +23,13 @@ func main() {
var logger log.Logger
{
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.With(logger, "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
logger = log.NewContext(logger).With("caller", log.DefaultCaller)
}
var ctx context.Context
{
ctx = context.Background()
}
var s profilesvc.Service
@@ -33,7 +40,7 @@ func main() {
var h http.Handler
{
h = profilesvc.MakeHTTPHandler(s, log.With(logger, "component", "HTTP"))
h = profilesvc.MakeHTTPHandler(ctx, s, log.NewContext(logger).With("component", "HTTP"))
}
errs := make(chan error)

View File

@@ -1,10 +1,11 @@
package profilesvc
import (
"context"
"net/url"
"strings"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
)

View File

@@ -1,9 +1,10 @@
package profilesvc
import (
"context"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
)

View File

@@ -1,9 +1,10 @@
package profilesvc
import (
"context"
"errors"
"sync"
"golang.org/x/net/context"
)
// Service is a simple CRUD interface for user profiles.

View File

@@ -4,14 +4,15 @@ package profilesvc
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/url"
"github.com/gorilla/mux"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
@@ -25,7 +26,7 @@ var (
// MakeHTTPHandler mounts all of the service endpoints into an http.Handler.
// Useful in a profilesvc server.
func MakeHTTPHandler(s Service, logger log.Logger) http.Handler {
func MakeHTTPHandler(ctx context.Context, s Service, logger log.Logger) http.Handler {
r := mux.NewRouter()
e := MakeServerEndpoints(s)
options := []httptransport.ServerOption{
@@ -44,54 +45,63 @@ func MakeHTTPHandler(s Service, logger log.Logger) http.Handler {
// DELETE /profiles/:id/addresses/:addressID remove an address
r.Methods("POST").Path("/profiles/").Handler(httptransport.NewServer(
ctx,
e.PostProfileEndpoint,
decodePostProfileRequest,
encodeResponse,
options...,
))
r.Methods("GET").Path("/profiles/{id}").Handler(httptransport.NewServer(
ctx,
e.GetProfileEndpoint,
decodeGetProfileRequest,
encodeResponse,
options...,
))
r.Methods("PUT").Path("/profiles/{id}").Handler(httptransport.NewServer(
ctx,
e.PutProfileEndpoint,
decodePutProfileRequest,
encodeResponse,
options...,
))
r.Methods("PATCH").Path("/profiles/{id}").Handler(httptransport.NewServer(
ctx,
e.PatchProfileEndpoint,
decodePatchProfileRequest,
encodeResponse,
options...,
))
r.Methods("DELETE").Path("/profiles/{id}").Handler(httptransport.NewServer(
ctx,
e.DeleteProfileEndpoint,
decodeDeleteProfileRequest,
encodeResponse,
options...,
))
r.Methods("GET").Path("/profiles/{id}/addresses/").Handler(httptransport.NewServer(
ctx,
e.GetAddressesEndpoint,
decodeGetAddressesRequest,
encodeResponse,
options...,
))
r.Methods("GET").Path("/profiles/{id}/addresses/{addressID}").Handler(httptransport.NewServer(
ctx,
e.GetAddressEndpoint,
decodeGetAddressRequest,
encodeResponse,
options...,
))
r.Methods("POST").Path("/profiles/{id}/addresses/").Handler(httptransport.NewServer(
ctx,
e.PostAddressEndpoint,
decodePostAddressRequest,
encodeResponse,
options...,
))
r.Methods("DELETE").Path("/profiles/{id}/addresses/{addressID}").Handler(httptransport.NewServer(
ctx,
e.DeleteAddressEndpoint,
decodeDeleteAddressRequest,
encodeResponse,
@@ -395,6 +405,16 @@ func codeFrom(err error) int {
case ErrAlreadyExists, ErrInconsistentIDs:
return http.StatusBadRequest
default:
if e, ok := err.(httptransport.Error); ok {
switch e.Domain {
case httptransport.DomainDecode:
return http.StatusBadRequest
case httptransport.DomainDo:
return http.StatusServiceUnavailable
default:
return http.StatusInternalServerError
}
}
return http.StatusInternalServerError
}
}

View File

@@ -1,9 +1,10 @@
package booking
import (
"context"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/examples/shipping/cargo"
@@ -42,10 +43,10 @@ type loadCargoResponse struct {
func (r loadCargoResponse) error() error { return r.Err }
func makeLoadCargoEndpoint(s Service) endpoint.Endpoint {
func makeLoadCargoEndpoint(bs Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(loadCargoRequest)
c, err := s.LoadCargo(req.ID)
c, err := bs.LoadCargo(req.ID)
return loadCargoResponse{Cargo: &c, Err: err}, nil
}
}

View File

@@ -1,13 +1,13 @@
package booking
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/gorilla/mux"
"golang.org/x/net/context"
kitlog "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
@@ -17,49 +17,56 @@ import (
)
// MakeHandler returns a handler for the booking service.
func MakeHandler(bs Service, logger kitlog.Logger) http.Handler {
func MakeHandler(ctx context.Context, bs Service, logger kitlog.Logger) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorLogger(logger),
kithttp.ServerErrorEncoder(encodeError),
}
bookCargoHandler := kithttp.NewServer(
ctx,
makeBookCargoEndpoint(bs),
decodeBookCargoRequest,
encodeResponse,
opts...,
)
loadCargoHandler := kithttp.NewServer(
ctx,
makeLoadCargoEndpoint(bs),
decodeLoadCargoRequest,
encodeResponse,
opts...,
)
requestRoutesHandler := kithttp.NewServer(
ctx,
makeRequestRoutesEndpoint(bs),
decodeRequestRoutesRequest,
encodeResponse,
opts...,
)
assignToRouteHandler := kithttp.NewServer(
ctx,
makeAssignToRouteEndpoint(bs),
decodeAssignToRouteRequest,
encodeResponse,
opts...,
)
changeDestinationHandler := kithttp.NewServer(
ctx,
makeChangeDestinationEndpoint(bs),
decodeChangeDestinationRequest,
encodeResponse,
opts...,
)
listCargosHandler := kithttp.NewServer(
ctx,
makeListCargosEndpoint(bs),
decodeListCargosRequest,
encodeResponse,
opts...,
)
listLocationsHandler := kithttp.NewServer(
ctx,
makeListLocationsEndpoint(bs),
decodeListLocationsRequest,
encodeResponse,

View File

@@ -1,9 +1,10 @@
package handling
import (
"context"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/examples/shipping/cargo"

View File

@@ -1,12 +1,12 @@
package handling
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/gorilla/mux"
"golang.org/x/net/context"
kitlog "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
@@ -17,7 +17,7 @@ import (
)
// MakeHandler returns a handler for the handling service.
func MakeHandler(hs Service, logger kitlog.Logger) http.Handler {
func MakeHandler(ctx context.Context, hs Service, logger kitlog.Logger) http.Handler {
r := mux.NewRouter()
opts := []kithttp.ServerOption{
@@ -26,6 +26,7 @@ func MakeHandler(hs Service, logger kitlog.Logger) http.Handler {
}
registerIncidentHandler := kithttp.NewServer(
ctx,
makeRegisterIncidentEndpoint(hs),
decodeRegisterIncidentRequest,
encodeResponse,

View File

@@ -1,9 +1,7 @@
// Package inspection provides means to inspect cargos.
package inspection
import (
"github.com/go-kit/kit/examples/shipping/cargo"
)
import "github.com/go-kit/kit/examples/shipping/cargo"
// EventHandler provides means of subscribing to inspection events.
type EventHandler interface {

View File

@@ -1,9 +1,7 @@
// Package location provides the Location aggregate.
package location
import (
"errors"
)
import "errors"
// UNLocode is the United Nations location code that uniquely identifies a
// particular location.

View File

@@ -1,7 +1,6 @@
package main
import (
"context"
"flag"
"fmt"
"net/http"
@@ -12,6 +11,7 @@ import (
"time"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
@@ -47,7 +47,7 @@ func main() {
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = &serializedLogger{Logger: logger}
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
var (
cargos = inmem.NewCargoRepository()
@@ -78,7 +78,7 @@ func main() {
var bs booking.Service
bs = booking.NewService(cargos, locations, handlingEvents, rs)
bs = booking.NewLoggingService(log.With(logger, "component", "booking"), bs)
bs = booking.NewLoggingService(log.NewContext(logger).With("component", "booking"), bs)
bs = booking.NewInstrumentingService(
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "api",
@@ -97,7 +97,7 @@ func main() {
var ts tracking.Service
ts = tracking.NewService(cargos, handlingEvents)
ts = tracking.NewLoggingService(log.With(logger, "component", "tracking"), ts)
ts = tracking.NewLoggingService(log.NewContext(logger).With("component", "tracking"), ts)
ts = tracking.NewInstrumentingService(
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "api",
@@ -116,7 +116,7 @@ func main() {
var hs handling.Service
hs = handling.NewService(handlingEvents, handlingEventFactory, handlingEventHandler)
hs = handling.NewLoggingService(log.With(logger, "component", "handling"), hs)
hs = handling.NewLoggingService(log.NewContext(logger).With("component", "handling"), hs)
hs = handling.NewInstrumentingService(
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "api",
@@ -133,13 +133,13 @@ func main() {
hs,
)
httpLogger := log.With(logger, "component", "http")
httpLogger := log.NewContext(logger).With("component", "http")
mux := http.NewServeMux()
mux.Handle("/booking/v1/", booking.MakeHandler(bs, httpLogger))
mux.Handle("/tracking/v1/", tracking.MakeHandler(ts, httpLogger))
mux.Handle("/handling/v1/", handling.MakeHandler(hs, httpLogger))
mux.Handle("/booking/v1/", booking.MakeHandler(ctx, bs, httpLogger))
mux.Handle("/tracking/v1/", tracking.MakeHandler(ctx, ts, httpLogger))
mux.Handle("/handling/v1/", handling.MakeHandler(ctx, hs, httpLogger))
http.Handle("/", accessControl(mux))
http.Handle("/metrics", stdprometheus.Handler())

View File

@@ -1,12 +1,13 @@
package routing
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
"golang.org/x/net/context"
"github.com/go-kit/kit/circuitbreaker"
"github.com/go-kit/kit/endpoint"
kithttp "github.com/go-kit/kit/transport/http"

View File

@@ -3,9 +3,7 @@
// bounded context.
package routing
import (
"github.com/go-kit/kit/examples/shipping/cargo"
)
import "github.com/go-kit/kit/examples/shipping/cargo"
// Service provides access to an external routing service.
type Service interface {

View File

@@ -1,7 +1,7 @@
package tracking
import (
"context"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,12 +1,12 @@
package tracking
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/gorilla/mux"
"golang.org/x/net/context"
kitlog "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
@@ -15,7 +15,7 @@ import (
)
// MakeHandler returns a handler for the tracking service.
func MakeHandler(ts Service, logger kitlog.Logger) http.Handler {
func MakeHandler(ctx context.Context, ts Service, logger kitlog.Logger) http.Handler {
r := mux.NewRouter()
opts := []kithttp.ServerOption{
@@ -24,6 +24,7 @@ func MakeHandler(ts Service, logger kitlog.Logger) http.Handler {
}
trackCargoHandler := kithttp.NewServer(
ctx,
makeTrackCargoEndpoint(ts),
decodeTrackCargoRequest,
encodeResponse,

View File

@@ -1,13 +1,14 @@
package main
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"strings"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
)
@@ -32,15 +33,18 @@ func (stringService) Count(s string) int {
}
func main() {
ctx := context.Background()
svc := stringService{}
uppercaseHandler := httptransport.NewServer(
ctx,
makeUppercaseEndpoint(svc),
decodeUppercaseRequest,
encodeResponse,
)
countHandler := httptransport.NewServer(
ctx,
makeCountEndpoint(svc),
decodeCountRequest,
encodeResponse,

View File

@@ -5,6 +5,7 @@ import (
"os"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
@@ -12,6 +13,7 @@ import (
)
func main() {
ctx := context.Background()
logger := log.NewLogfmtLogger(os.Stderr)
fieldKeys := []string{"method", "error"}
@@ -40,12 +42,14 @@ func main() {
svc = instrumentingMiddleware{requestCount, requestLatency, countResult, svc}
uppercaseHandler := httptransport.NewServer(
ctx,
makeUppercaseEndpoint(svc),
decodeUppercaseRequest,
encodeResponse,
)
countHandler := httptransport.NewServer(
ctx,
makeCountEndpoint(svc),
decodeCountRequest,
encodeResponse,

View File

@@ -1,10 +1,11 @@
package main
import (
"context"
"encoding/json"
"net/http"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,12 +1,12 @@
package main
import (
"context"
"flag"
"net/http"
"os"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
"github.com/go-kit/kit/log"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
@@ -22,7 +22,9 @@ func main() {
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "listen", *listen, "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("listen", *listen).With("caller", log.DefaultCaller)
ctx := context.Background()
fieldKeys := []string{"method", "error"}
requestCount := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
@@ -46,16 +48,18 @@ func main() {
var svc StringService
svc = stringService{}
svc = proxyingMiddleware(context.Background(), *proxy, logger)(svc)
svc = proxyingMiddleware(*proxy, ctx, logger)(svc)
svc = loggingMiddleware(logger)(svc)
svc = instrumentingMiddleware(requestCount, requestLatency, countResult)(svc)
uppercaseHandler := httptransport.NewServer(
ctx,
makeUppercaseEndpoint(svc),
decodeUppercaseRequest,
encodeResponse,
)
countHandler := httptransport.NewServer(
ctx,
makeCountEndpoint(svc),
decodeCountRequest,
encodeResponse,

View File

@@ -1,7 +1,6 @@
package main
import (
"context"
"errors"
"fmt"
"net/url"
@@ -10,6 +9,7 @@ import (
jujuratelimit "github.com/juju/ratelimit"
"github.com/sony/gobreaker"
"golang.org/x/net/context"
"github.com/go-kit/kit/circuitbreaker"
"github.com/go-kit/kit/endpoint"
@@ -20,7 +20,7 @@ import (
httptransport "github.com/go-kit/kit/transport/http"
)
func proxyingMiddleware(ctx context.Context, instances string, logger log.Logger) ServiceMiddleware {
func proxyingMiddleware(instances string, ctx context.Context, logger log.Logger) ServiceMiddleware {
// If instances is empty, don't proxy.
if instances == "" {
logger.Log("proxy_to", "none")

View File

@@ -2,11 +2,12 @@ package main
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,22 +1,20 @@
# package log
`package log` provides a minimal interface for structured logging in services.
It may be wrapped to encode conventions, enforce type-safety, provide leveled
logging, and so on. It can be used for both typical application log events,
and log-structured data streams.
It may be wrapped to encode conventions, enforce type-safety, provide leveled logging, and so on.
It can be used for both typical application log events, and log-structured data streams.
## Structured logging
Structured logging is, basically, conceding to the reality that logs are
_data_, and warrant some level of schematic rigor. Using a stricter,
key/value-oriented message format for our logs, containing contextual and
semantic information, makes it much easier to get insight into the
operational activity of the systems we build. Consequently, `package log` is
of the strong belief that "[the benefits of structured logging outweigh the
minimal effort involved](https://www.thoughtworks.com/radar/techniques/structured-logging)".
Structured logging is, basically, conceding to the reality that logs are _data_,
and warrant some level of schematic rigor.
Using a stricter, key/value-oriented message format for our logs,
containing contextual and semantic information,
makes it much easier to get insight into the operational activity of the systems we build.
Consequently, `package log` is of the strong belief that
"[the benefits of structured logging outweigh the minimal effort involved](https://www.thoughtworks.com/radar/techniques/structured-logging)".
Migrating from unstructured to structured logging is probably a lot easier
than you'd expect.
Migrating from unstructured to structured logging is probably a lot easier than you'd expect.
```go
// Unstructured
@@ -39,17 +37,17 @@ logger.Log("question", "what is the meaning of life?", "answer", 42)
// question="what is the meaning of life?" answer=42
```
### Contextual Loggers
### Log contexts
```go
func main() {
var logger log.Logger
logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
logger = log.With(logger, "instance_id", 123)
logger = log.NewContext(logger).With("instance_id", 123)
logger.Log("msg", "starting")
NewWorker(log.With(logger, "component", "worker")).Run()
NewSlacker(log.With(logger, "component", "slacker")).Run()
NewWorker(log.NewContext(logger).With("component", "worker")).Run()
NewSlacker(log.NewContext(logger).With("component", "slacker")).Run()
}
// Output:
@@ -79,8 +77,9 @@ func main() {
// {"msg":"I sure like pie","ts":"2016/01/01 12:34:56"}
```
Or, if, for legacy reasons, you need to pipe all of your logging through the
stdlib log package, you can redirect Go kit logger to the stdlib logger.
Or, if, for legacy reasons,
you need to pipe all of your logging through the stdlib log package,
you can redirect Go kit logger to the stdlib logger.
```go
logger := kitlog.NewLogfmtLogger(kitlog.StdlibWriter{})
@@ -95,7 +94,7 @@ logger.Log("legacy", true, "msg", "at least it's something")
```go
var logger log.Logger
logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)
logger.Log("msg", "hello")
@@ -105,7 +104,7 @@ logger.Log("msg", "hello")
## Supported output formats
- [Logfmt](https://brandur.org/logfmt) ([see also](https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write))
- [Logfmt](https://brandur.org/logfmt)
- JSON
## Enhancements
@@ -118,25 +117,27 @@ type Logger interface {
}
```
This interface, and its supporting code like is the product of much iteration
and evaluation. For more details on the evolution of the Logger interface,
see [The Hunt for a Logger Interface](http://go-talks.appspot.com/github.com/ChrisHines/talks/structured-logging/structured-logging.slide#1),
a talk by [Chris Hines](https://github.com/ChrisHines).
This interface, and its supporting code like [log.Context](https://godoc.org/github.com/go-kit/kit/log#Context),
is the product of much iteration and evaluation.
For more details on the evolution of the Logger interface,
see [The Hunt for a Logger Interface](http://go-talks.appspot.com/github.com/ChrisHines/talks/structured-logging/structured-logging.slide#1),
a talk by [Chris Hines](https://github.com/ChrisHines).
Also, please see
[#63](https://github.com/go-kit/kit/issues/63),
[#76](https://github.com/go-kit/kit/pull/76),
[#131](https://github.com/go-kit/kit/issues/131),
[#157](https://github.com/go-kit/kit/pull/157),
[#164](https://github.com/go-kit/kit/issues/164), and
[#252](https://github.com/go-kit/kit/pull/252)
to review historical conversations about package log and the Logger interface.
[#63](https://github.com/go-kit/kit/issues/63),
[#76](https://github.com/go-kit/kit/pull/76),
[#131](https://github.com/go-kit/kit/issues/131),
[#157](https://github.com/go-kit/kit/pull/157),
[#164](https://github.com/go-kit/kit/issues/164), and
[#252](https://github.com/go-kit/kit/pull/252)
to review historical conversations about package log and the Logger interface.
Value-add packages and suggestions,
like improvements to [the leveled logger](https://godoc.org/github.com/go-kit/kit/log/level),
are of course welcome. Good proposals should
like improvements to [the leveled logger](https://godoc.org/github.com/go-kit/kit/log/levels),
are of course welcome.
Good proposals should
- Be composable with [contextual loggers](https://godoc.org/github.com/go-kit/kit/log#With),
- Not break the behavior of [log.Caller](https://godoc.org/github.com/go-kit/kit/log#Caller) in any wrapped contextual loggers, and
- Be composable with [log.Context](https://godoc.org/github.com/go-kit/kit/log#Context),
- Not break the behavior of [log.Caller](https://godoc.org/github.com/go-kit/kit/log#Caller) in any wrapped context, and
- Be friendly to packages that accept only an unadorned log.Logger.
## Benchmarks & comparisons

View File

@@ -7,7 +7,7 @@ import (
)
func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) {
lc := log.With(logger, "common_key", "common_value")
lc := log.NewContext(logger).With("common_key", "common_value")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -17,5 +17,5 @@ func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) {
var (
baseMessage = func(logger log.Logger) { logger.Log("foo_key", "foo_value") }
withMessage = func(logger log.Logger) { log.With(logger, "a", "b").Log("c", "d") }
withMessage = func(logger log.Logger) { log.NewContext(logger).With("a", "b").Log("c", "d") }
)

View File

@@ -35,15 +35,14 @@
// idea to log simple values without formatting them. This practice allows
// the chosen logger to encode values in the most appropriate way.
//
// Contextual Loggers
// Log Context
//
// A contextual logger stores keyvals that it includes in all log events.
// Building appropriate contextual loggers reduces repetition and aids
// consistency in the resulting log output. With and WithPrefix add context to
// a logger. We can use With to improve the RunTask example.
// A log context stores keyvals that it includes in all log events. Building
// appropriate log contexts reduces repetition and aids consistency in the
// resulting log output. We can use a context to improve the RunTask example.
//
// func RunTask(task Task, logger log.Logger) string {
// logger = log.With(logger, "taskID", task.ID)
// logger = log.NewContext(logger).With("taskID", task.ID)
// logger.Log("event", "starting task")
// ...
// taskHelper(task.Cmd, logger)
@@ -52,18 +51,19 @@
// }
//
// The improved version emits the same log events as the original for the
// first and last calls to Log. Passing the contextual logger to taskHelper
// enables each log event created by taskHelper to include the task.ID even
// though taskHelper does not have access to that value. Using contextual
// loggers this way simplifies producing log output that enables tracing the
// life cycle of individual tasks. (See the Contextual example for the full
// code of the above snippet.)
// first and last calls to Log. The call to taskHelper highlights that a
// context may be passed as a logger to other functions. Each log event
// created by the called function will include the task.ID even though the
// function does not have access to that value. Using log contexts this way
// simplifies producing log output that enables tracing the life cycle of
// individual tasks. (See the Context example for the full code of the
// above snippet.)
//
// Dynamic Contextual Values
// Dynamic Context Values
//
// A Valuer function stored in a contextual logger generates a new value each
// time an event is logged. The Valuer example demonstrates how this feature
// works.
// A Valuer function stored in a log context generates a new value each time
// the context logs an event. The Valuer example demonstrates how this
// feature works.
//
// Valuers provide the basis for consistently logging timestamps and source
// code location. The log package defines several valuers for that purpose.
@@ -72,7 +72,7 @@
// entries contain a timestamp and source location looks like this:
//
// logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
// logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)
// logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)
//
// Concurrent Safety
//
@@ -90,27 +90,4 @@
// handled atomically within the wrapped logger, but it typically serializes
// both the formatting and output logic. Use a SyncLogger if the formatting
// logger may perform multiple writes per log event.
//
// Error Handling
//
// This package relies on the practice of wrapping or decorating loggers with
// other loggers to provide composable pieces of functionality. It also means
// that Logger.Log must return an error because some
// implementations—especially those that output log data to an io.Writer—may
// encounter errors that cannot be handled locally. This in turn means that
// Loggers that wrap other loggers should return errors from the wrapped
// logger up the stack.
//
// Fortunately, the decorator pattern also provides a way to avoid the
// necessity to check for errors every time an application calls Logger.Log.
// An application required to panic whenever its Logger encounters
// an error could initialize its logger as follows.
//
// fmtlogger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
// logger := log.LoggerFunc(func(keyvals ...interface{}) error {
// if err := fmtlogger.Log(keyvals...); err != nil {
// panic(err)
// }
// return nil
// })
package log

View File

@@ -1,9 +1,7 @@
package log_test
import (
"math/rand"
"os"
"sync"
"time"
"github.com/go-kit/kit/log"
@@ -29,7 +27,7 @@ func Example_basic() {
// taskID=1 event="task complete"
}
func Example_contextual() {
func Example_context() {
logger := log.NewLogfmtLogger(os.Stdout)
type Task struct {
@@ -43,7 +41,7 @@ func Example_contextual() {
}
RunTask := func(task Task, logger log.Logger) {
logger = log.With(logger, "taskID", task.ID)
logger = log.NewContext(logger).With("taskID", task.ID)
logger.Log("event", "starting task")
taskHelper(task.Cmd, logger)
@@ -68,7 +66,7 @@ func Example_valuer() {
return count
}
logger = log.With(logger, "count", log.Valuer(counter))
logger = log.NewContext(logger).With("count", log.Valuer(counter))
logger.Log("call", "first")
logger.Log("call", "second")
@@ -88,7 +86,7 @@ func Example_debugInfo() {
return baseTime
}
logger = log.With(logger, "time", log.Timestamp(mockTime), "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("time", log.Timestamp(mockTime), "caller", log.DefaultCaller)
logger.Log("call", "first")
logger.Log("call", "second")
@@ -98,40 +96,7 @@ func Example_debugInfo() {
logger.Log("call", "third")
// Output:
// time=2015-02-03T10:00:01Z caller=example_test.go:93 call=first
// time=2015-02-03T10:00:02Z caller=example_test.go:94 call=second
// time=2015-02-03T10:00:03Z caller=example_test.go:98 call=third
}
func Example_syncWriter() {
w := log.NewSyncWriter(os.Stdout)
logger := log.NewLogfmtLogger(w)
type Task struct {
ID int
}
var wg sync.WaitGroup
RunTask := func(task Task, logger log.Logger) {
logger.Log("taskID", task.ID, "event", "starting task")
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
logger.Log("taskID", task.ID, "event", "task complete")
wg.Done()
}
wg.Add(2)
go RunTask(Task{ID: 1}, logger)
go RunTask(Task{ID: 2}, logger)
wg.Wait()
// Unordered output:
// taskID=1 event="starting task"
// taskID=2 event="starting task"
// taskID=1 event="task complete"
// taskID=2 event="task complete"
// time=2015-02-03T10:00:01Z caller=example_test.go:91 call=first
// time=2015-02-03T10:00:02Z caller=example_test.go:92 call=second
// time=2015-02-03T10:00:03Z caller=example_test.go:96 call=third
}

View File

@@ -0,0 +1,65 @@
package level_test
import (
"io/ioutil"
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/experimental_level"
)
func BenchmarkNopBaseline(b *testing.B) {
benchmarkRunner(b, log.NewNopLogger())
}
func BenchmarkNopDisallowedLevel(b *testing.B) {
benchmarkRunner(b, level.New(log.NewNopLogger(), level.Config{
Allowed: level.AllowInfoAndAbove(),
}))
}
func BenchmarkNopAllowedLevel(b *testing.B) {
benchmarkRunner(b, level.New(log.NewNopLogger(), level.Config{
Allowed: level.AllowAll(),
}))
}
func BenchmarkJSONBaseline(b *testing.B) {
benchmarkRunner(b, log.NewJSONLogger(ioutil.Discard))
}
func BenchmarkJSONDisallowedLevel(b *testing.B) {
benchmarkRunner(b, level.New(log.NewJSONLogger(ioutil.Discard), level.Config{
Allowed: level.AllowInfoAndAbove(),
}))
}
func BenchmarkJSONAllowedLevel(b *testing.B) {
benchmarkRunner(b, level.New(log.NewJSONLogger(ioutil.Discard), level.Config{
Allowed: level.AllowAll(),
}))
}
func BenchmarkLogfmtBaseline(b *testing.B) {
benchmarkRunner(b, log.NewLogfmtLogger(ioutil.Discard))
}
func BenchmarkLogfmtDisallowedLevel(b *testing.B) {
benchmarkRunner(b, level.New(log.NewLogfmtLogger(ioutil.Discard), level.Config{
Allowed: level.AllowInfoAndAbove(),
}))
}
func BenchmarkLogfmtAllowedLevel(b *testing.B) {
benchmarkRunner(b, level.New(log.NewLogfmtLogger(ioutil.Discard), level.Config{
Allowed: level.AllowAll(),
}))
}
func benchmarkRunner(b *testing.B, logger log.Logger) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
level.Debug(logger).Log("foo", "bar")
}
}

View File

@@ -0,0 +1,27 @@
// Package level is an EXPERIMENTAL levelled logging package. The API will
// definitely have breaking changes and may be deleted altogether. Be warned!
//
// To use the level package, create a logger as per normal in your func main,
// and wrap it with level.New.
//
// var logger log.Logger
// logger = log.NewLogfmtLogger(os.Stderr)
// logger = level.New(logger, level.Config{Allowed: level.AllowInfoAndAbove}) // <--
// logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
//
// Then, at the callsites, use one of the level.Debug, Info, Warn, or Error
// helper methods to emit leveled log events.
//
// logger.Log("foo", "bar") // as normal, no level
// level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get())
// if value > 100 {
// level.Error(logger).Log("value", value)
// }
//
// The leveled logger allows precise control over what should happen if a log
// event is emitted without a level key, or if a squelched level is used. Check
// the Config struct for details. And, you can easily use non-default level
// values: create new string constants for whatever you want to change, pass
// them explicitly to the Config struct, and write your own level.Foo-style
// helper methods.
package level

View File

@@ -0,0 +1,146 @@
package level
import (
"github.com/go-kit/kit/log"
)
var (
levelKey = "level"
errorLevelValue = "error"
warnLevelValue = "warn"
infoLevelValue = "info"
debugLevelValue = "debug"
)
// AllowAll is an alias for AllowDebugAndAbove.
func AllowAll() []string {
return AllowDebugAndAbove()
}
// AllowDebugAndAbove allows all of the four default log levels.
// Its return value may be provided as the Allowed parameter in the Config.
func AllowDebugAndAbove() []string {
return []string{errorLevelValue, warnLevelValue, infoLevelValue, debugLevelValue}
}
// AllowInfoAndAbove allows the default info, warn, and error log levels.
// Its return value may be provided as the Allowed parameter in the Config.
func AllowInfoAndAbove() []string {
return []string{errorLevelValue, warnLevelValue, infoLevelValue}
}
// AllowWarnAndAbove allows the default warn and error log levels.
// Its return value may be provided as the Allowed parameter in the Config.
func AllowWarnAndAbove() []string {
return []string{errorLevelValue, warnLevelValue}
}
// AllowErrorOnly allows only the default error log level.
// Its return value may be provided as the Allowed parameter in the Config.
func AllowErrorOnly() []string {
return []string{errorLevelValue}
}
// AllowNone allows none of the default log levels.
// Its return value may be provided as the Allowed parameter in the Config.
func AllowNone() []string {
return []string{}
}
// Error returns a logger with the level key set to ErrorLevelValue.
func Error(logger log.Logger) log.Logger {
return log.NewContext(logger).With(levelKey, errorLevelValue)
}
// Warn returns a logger with the level key set to WarnLevelValue.
func Warn(logger log.Logger) log.Logger {
return log.NewContext(logger).With(levelKey, warnLevelValue)
}
// Info returns a logger with the level key set to InfoLevelValue.
func Info(logger log.Logger) log.Logger {
return log.NewContext(logger).With(levelKey, infoLevelValue)
}
// Debug returns a logger with the level key set to DebugLevelValue.
func Debug(logger log.Logger) log.Logger {
return log.NewContext(logger).With(levelKey, debugLevelValue)
}
// Config parameterizes the leveled logger.
type Config struct {
// Allowed enumerates the accepted log levels. If a log event is encountered
// with a level key set to a value that isn't explicitly allowed, the event
// will be squelched, and ErrNotAllowed returned.
Allowed []string
// ErrNotAllowed is returned to the caller when Log is invoked with a level
// key that hasn't been explicitly allowed. By default, ErrNotAllowed is
// nil; in this case, the log event is squelched with no error.
ErrNotAllowed error
// SquelchNoLevel will squelch log events with no level key, so that they
// don't proceed through to the wrapped logger. If SquelchNoLevel is set to
// true and a log event is squelched in this way, ErrNoLevel is returned to
// the caller.
SquelchNoLevel bool
// ErrNoLevel is returned to the caller when SquelchNoLevel is true, and Log
// is invoked without a level key. By default, ErrNoLevel is nil; in this
// case, the log event is squelched with no error.
ErrNoLevel error
}
// New wraps the logger and implements level checking. See the commentary on the
// Config object for a detailed description of how to configure levels.
func New(next log.Logger, config Config) log.Logger {
return &logger{
next: next,
allowed: makeSet(config.Allowed),
errNotAllowed: config.ErrNotAllowed,
squelchNoLevel: config.SquelchNoLevel,
errNoLevel: config.ErrNoLevel,
}
}
type logger struct {
next log.Logger
allowed map[string]struct{}
errNotAllowed error
squelchNoLevel bool
errNoLevel error
}
func (l *logger) Log(keyvals ...interface{}) error {
var hasLevel, levelAllowed bool
for i := 0; i < len(keyvals); i += 2 {
if k, ok := keyvals[i].(string); !ok || k != levelKey {
continue
}
hasLevel = true
if i >= len(keyvals) {
continue
}
v, ok := keyvals[i+1].(string)
if !ok {
continue
}
_, levelAllowed = l.allowed[v]
break
}
if !hasLevel && l.squelchNoLevel {
return l.errNoLevel
}
if hasLevel && !levelAllowed {
return l.errNotAllowed
}
return l.next.Log(keyvals...)
}
func makeSet(a []string) map[string]struct{} {
m := make(map[string]struct{}, len(a))
for _, s := range a {
m[s] = struct{}{}
}
return m
}

View File

@@ -0,0 +1,154 @@
package level_test
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/experimental_level"
)
func TestVariousLevels(t *testing.T) {
for _, testcase := range []struct {
allowed []string
want string
}{
{
level.AllowAll(),
strings.Join([]string{
`{"level":"debug","this is":"debug log"}`,
`{"level":"info","this is":"info log"}`,
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
level.AllowDebugAndAbove(),
strings.Join([]string{
`{"level":"debug","this is":"debug log"}`,
`{"level":"info","this is":"info log"}`,
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
level.AllowInfoAndAbove(),
strings.Join([]string{
`{"level":"info","this is":"info log"}`,
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
level.AllowWarnAndAbove(),
strings.Join([]string{
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
level.AllowErrorOnly(),
strings.Join([]string{
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
level.AllowNone(),
``,
},
} {
var buf bytes.Buffer
logger := level.New(log.NewJSONLogger(&buf), level.Config{Allowed: testcase.allowed})
level.Debug(logger).Log("this is", "debug log")
level.Info(logger).Log("this is", "info log")
level.Warn(logger).Log("this is", "warn log")
level.Error(logger).Log("this is", "error log")
if want, have := testcase.want, strings.TrimSpace(buf.String()); want != have {
t.Errorf("given Allowed=%v: want\n%s\nhave\n%s", testcase.allowed, want, have)
}
}
}
func TestErrNotAllowed(t *testing.T) {
myError := errors.New("squelched!")
logger := level.New(log.NewNopLogger(), level.Config{
Allowed: level.AllowWarnAndAbove(),
ErrNotAllowed: myError,
})
if want, have := myError, level.Info(logger).Log("foo", "bar"); want != have {
t.Errorf("want %#+v, have %#+v", want, have)
}
if want, have := error(nil), level.Warn(logger).Log("foo", "bar"); want != have {
t.Errorf("want %#+v, have %#+v", want, have)
}
}
func TestErrNoLevel(t *testing.T) {
myError := errors.New("no level specified")
var buf bytes.Buffer
logger := level.New(log.NewJSONLogger(&buf), level.Config{
SquelchNoLevel: true,
ErrNoLevel: myError,
})
if want, have := myError, logger.Log("foo", "bar"); want != have {
t.Errorf("want %v, have %v", want, have)
}
if want, have := ``, strings.TrimSpace(buf.String()); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestAllowNoLevel(t *testing.T) {
var buf bytes.Buffer
logger := level.New(log.NewJSONLogger(&buf), level.Config{
SquelchNoLevel: false,
ErrNoLevel: errors.New("I should never be returned!"),
})
if want, have := error(nil), logger.Log("foo", "bar"); want != have {
t.Errorf("want %v, have %v", want, have)
}
if want, have := `{"foo":"bar"}`, strings.TrimSpace(buf.String()); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestLevelContext(t *testing.T) {
var buf bytes.Buffer
// Wrapping the level logger with a context allows users to use
// log.DefaultCaller as per normal.
var logger log.Logger
logger = log.NewLogfmtLogger(&buf)
logger = level.New(logger, level.Config{Allowed: level.AllowAll()})
logger = log.NewContext(logger).With("caller", log.DefaultCaller)
level.Info(logger).Log("foo", "bar")
if want, have := `caller=level_test.go:134 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestContextLevel(t *testing.T) {
var buf bytes.Buffer
// Wrapping a context with the level logger still works, but requires users
// to specify a higher callstack depth value.
var logger log.Logger
logger = log.NewLogfmtLogger(&buf)
logger = log.NewContext(logger).With("caller", log.Caller(5))
logger = level.New(logger, level.Config{Allowed: level.AllowAll()})
level.Info(logger).Log("foo", "bar")
if want, have := `caller=level_test.go:150 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have {
t.Errorf("want %q, have %q", want, have)
}
}

View File

@@ -13,7 +13,7 @@ func TestJSONLoggerCaller(t *testing.T) {
t.Parallel()
buf := &bytes.Buffer{}
logger := log.NewJSONLogger(buf)
logger = log.With(logger, "caller", log.DefaultCaller)
logger = log.NewContext(logger).With("caller", log.DefaultCaller)
if err := logger.Log(); err != nil {
t.Fatal(err)

View File

@@ -1,72 +0,0 @@
package level_test
import (
"io/ioutil"
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
)
func Benchmark(b *testing.B) {
contexts := []struct {
name string
context func(log.Logger) log.Logger
}{
{"NoContext", func(l log.Logger) log.Logger {
return l
}},
{"TimeContext", func(l log.Logger) log.Logger {
return log.With(l, "time", log.DefaultTimestampUTC)
}},
{"CallerContext", func(l log.Logger) log.Logger {
return log.With(l, "caller", log.DefaultCaller)
}},
{"TimeCallerReqIDContext", func(l log.Logger) log.Logger {
return log.With(l, "time", log.DefaultTimestampUTC, "caller", log.DefaultCaller, "reqID", 29)
}},
}
loggers := []struct {
name string
logger log.Logger
}{
{"Nop", log.NewNopLogger()},
{"Logfmt", log.NewLogfmtLogger(ioutil.Discard)},
{"JSON", log.NewJSONLogger(ioutil.Discard)},
}
filters := []struct {
name string
filter func(log.Logger) log.Logger
}{
{"Baseline", func(l log.Logger) log.Logger {
return l
}},
{"DisallowedLevel", func(l log.Logger) log.Logger {
return level.NewFilter(l, level.AllowInfo())
}},
{"AllowedLevel", func(l log.Logger) log.Logger {
return level.NewFilter(l, level.AllowAll())
}},
}
for _, c := range contexts {
b.Run(c.name, func(b *testing.B) {
for _, f := range filters {
b.Run(f.name, func(b *testing.B) {
for _, l := range loggers {
b.Run(l.name, func(b *testing.B) {
logger := c.context(f.filter(l.logger))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
level.Debug(logger).Log("foo", "bar")
}
})
}
})
}
})
}
}

View File

@@ -1,22 +0,0 @@
// Package level implements leveled logging on top of package log. To use the
// level package, create a logger as per normal in your func main, and wrap it
// with level.NewFilter.
//
// var logger log.Logger
// logger = log.NewLogfmtLogger(os.Stderr)
// logger = level.NewFilter(logger, level.AllowInfoAndAbove()) // <--
// logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
//
// Then, at the callsites, use one of the level.Debug, Info, Warn, or Error
// helper methods to emit leveled log events.
//
// logger.Log("foo", "bar") // as normal, no level
// level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get())
// if value > 100 {
// level.Error(logger).Log("value", value)
// }
//
// NewFilter allows precise control over what happens when a log event is
// emitted without a level key, or if a squelched level is used. Check the
// Option functions for details.
package level

View File

@@ -1,25 +0,0 @@
package level_test
import (
"errors"
"os"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
)
func Example_basic() {
// setup logger with level filter
logger := log.NewLogfmtLogger(os.Stdout)
logger = level.NewFilter(logger, level.AllowInfo())
logger = log.With(logger, "caller", log.DefaultCaller)
// use level helpers to log at different levels
level.Error(logger).Log("err", errors.New("bad data"))
level.Info(logger).Log("event", "data saved")
level.Debug(logger).Log("next item", 17) // filtered
// Output:
// level=error caller=example_test.go:18 err="bad data"
// level=info caller=example_test.go:19 event="data saved"
}

View File

@@ -1,205 +0,0 @@
package level
import "github.com/go-kit/kit/log"
// Error returns a logger that includes a Key/ErrorValue pair.
func Error(logger log.Logger) log.Logger {
return log.WithPrefix(logger, Key(), ErrorValue())
}
// Warn returns a logger that includes a Key/WarnValue pair.
func Warn(logger log.Logger) log.Logger {
return log.WithPrefix(logger, Key(), WarnValue())
}
// Info returns a logger that includes a Key/InfoValue pair.
func Info(logger log.Logger) log.Logger {
return log.WithPrefix(logger, Key(), InfoValue())
}
// Debug returns a logger that includes a Key/DebugValue pair.
func Debug(logger log.Logger) log.Logger {
return log.WithPrefix(logger, Key(), DebugValue())
}
// NewFilter wraps next and implements level filtering. See the commentary on
// the Option functions for a detailed description of how to configure levels.
// If no options are provided, all leveled log events created with Debug,
// Info, Warn or Error helper methods are squelched and non-leveled log
// events are passed to next unmodified.
func NewFilter(next log.Logger, options ...Option) log.Logger {
l := &logger{
next: next,
}
for _, option := range options {
option(l)
}
return l
}
type logger struct {
next log.Logger
allowed level
squelchNoLevel bool
errNotAllowed error
errNoLevel error
}
func (l *logger) Log(keyvals ...interface{}) error {
var hasLevel, levelAllowed bool
for i := 1; i < len(keyvals); i += 2 {
if v, ok := keyvals[i].(*levelValue); ok {
hasLevel = true
levelAllowed = l.allowed&v.level != 0
break
}
}
if !hasLevel && l.squelchNoLevel {
return l.errNoLevel
}
if hasLevel && !levelAllowed {
return l.errNotAllowed
}
return l.next.Log(keyvals...)
}
// Option sets a parameter for the leveled logger.
type Option func(*logger)
// AllowAll is an alias for AllowDebug.
func AllowAll() Option {
return AllowDebug()
}
// AllowDebug allows error, warn, info and debug level log events to pass.
func AllowDebug() Option {
return allowed(levelError | levelWarn | levelInfo | levelDebug)
}
// AllowInfo allows error, warn and info level log events to pass.
func AllowInfo() Option {
return allowed(levelError | levelWarn | levelInfo)
}
// AllowWarn allows error and warn level log events to pass.
func AllowWarn() Option {
return allowed(levelError | levelWarn)
}
// AllowError allows only error level log events to pass.
func AllowError() Option {
return allowed(levelError)
}
// AllowNone allows no leveled log events to pass.
func AllowNone() Option {
return allowed(0)
}
func allowed(allowed level) Option {
return func(l *logger) { l.allowed = allowed }
}
// ErrNotAllowed sets the error to return from Log when it squelches a log
// event disallowed by the configured Allow[Level] option. By default,
// ErrNotAllowed is nil; in this case the log event is squelched with no
// error.
func ErrNotAllowed(err error) Option {
return func(l *logger) { l.errNotAllowed = err }
}
// SquelchNoLevel instructs Log to squelch log events with no level, so that
// they don't proceed through to the wrapped logger. If SquelchNoLevel is set
// to true and a log event is squelched in this way, the error value
// configured with ErrNoLevel is returned to the caller.
func SquelchNoLevel(squelch bool) Option {
return func(l *logger) { l.squelchNoLevel = squelch }
}
// ErrNoLevel sets the error to return from Log when it squelches a log event
// with no level. By default, ErrNoLevel is nil; in this case the log event is
// squelched with no error.
func ErrNoLevel(err error) Option {
return func(l *logger) { l.errNoLevel = err }
}
// NewInjector wraps next and returns a logger that adds a Key/level pair to
// the beginning of log events that don't already contain a level. In effect,
// this gives a default level to logs without a level.
func NewInjector(next log.Logger, level Value) log.Logger {
return &injector{
next: next,
level: level,
}
}
type injector struct {
next log.Logger
level interface{}
}
func (l *injector) Log(keyvals ...interface{}) error {
for i := 1; i < len(keyvals); i += 2 {
if _, ok := keyvals[i].(*levelValue); ok {
return l.next.Log(keyvals...)
}
}
kvs := make([]interface{}, len(keyvals)+2)
kvs[0], kvs[1] = key, l.level
copy(kvs[2:], keyvals)
return l.next.Log(kvs...)
}
// Value is the interface that each of the canonical level values implement.
// It contains unexported methods that prevent types from other packages from
// implementing it and guaranteeing that NewFilter can distinguish the levels
// defined in this package from all other values.
type Value interface {
String() string
levelVal()
}
// Key returns the unique key added to log events by the loggers in this
// package.
func Key() interface{} { return key }
// ErrorValue returns the unique value added to log events by Error.
func ErrorValue() Value { return errorValue }
// WarnValue returns the unique value added to log events by Warn.
func WarnValue() Value { return warnValue }
// InfoValue returns the unique value added to log events by Info.
func InfoValue() Value { return infoValue }
// DebugValue returns the unique value added to log events by Warn.
func DebugValue() Value { return debugValue }
var (
// key is of type interfae{} so that it allocates once during package
// initialization and avoids allocating every type the value is added to a
// []interface{} later.
key interface{} = "level"
errorValue = &levelValue{level: levelError, name: "error"}
warnValue = &levelValue{level: levelWarn, name: "warn"}
infoValue = &levelValue{level: levelInfo, name: "info"}
debugValue = &levelValue{level: levelDebug, name: "debug"}
)
type level byte
const (
levelDebug level = 1 << iota
levelInfo
levelWarn
levelError
)
type levelValue struct {
name string
level
}
func (v *levelValue) String() string { return v.name }
func (v *levelValue) levelVal() {}

View File

@@ -1,235 +0,0 @@
package level_test
import (
"bytes"
"errors"
"io"
"strings"
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
)
func TestVariousLevels(t *testing.T) {
testCases := []struct {
name string
allowed level.Option
want string
}{
{
"AllowAll",
level.AllowAll(),
strings.Join([]string{
`{"level":"debug","this is":"debug log"}`,
`{"level":"info","this is":"info log"}`,
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
"AllowDebug",
level.AllowDebug(),
strings.Join([]string{
`{"level":"debug","this is":"debug log"}`,
`{"level":"info","this is":"info log"}`,
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
"AllowDebug",
level.AllowInfo(),
strings.Join([]string{
`{"level":"info","this is":"info log"}`,
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
"AllowWarn",
level.AllowWarn(),
strings.Join([]string{
`{"level":"warn","this is":"warn log"}`,
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
"AllowError",
level.AllowError(),
strings.Join([]string{
`{"level":"error","this is":"error log"}`,
}, "\n"),
},
{
"AllowNone",
level.AllowNone(),
``,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
logger := level.NewFilter(log.NewJSONLogger(&buf), tc.allowed)
level.Debug(logger).Log("this is", "debug log")
level.Info(logger).Log("this is", "info log")
level.Warn(logger).Log("this is", "warn log")
level.Error(logger).Log("this is", "error log")
if want, have := tc.want, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant:\n%s\nhave:\n%s", want, have)
}
})
}
}
func TestErrNotAllowed(t *testing.T) {
myError := errors.New("squelched!")
opts := []level.Option{
level.AllowWarn(),
level.ErrNotAllowed(myError),
}
logger := level.NewFilter(log.NewNopLogger(), opts...)
if want, have := myError, level.Info(logger).Log("foo", "bar"); want != have {
t.Errorf("want %#+v, have %#+v", want, have)
}
if want, have := error(nil), level.Warn(logger).Log("foo", "bar"); want != have {
t.Errorf("want %#+v, have %#+v", want, have)
}
}
func TestErrNoLevel(t *testing.T) {
myError := errors.New("no level specified")
var buf bytes.Buffer
opts := []level.Option{
level.SquelchNoLevel(true),
level.ErrNoLevel(myError),
}
logger := level.NewFilter(log.NewJSONLogger(&buf), opts...)
if want, have := myError, logger.Log("foo", "bar"); want != have {
t.Errorf("want %v, have %v", want, have)
}
if want, have := ``, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant '%s'\nhave '%s'", want, have)
}
}
func TestAllowNoLevel(t *testing.T) {
var buf bytes.Buffer
opts := []level.Option{
level.SquelchNoLevel(false),
level.ErrNoLevel(errors.New("I should never be returned!")),
}
logger := level.NewFilter(log.NewJSONLogger(&buf), opts...)
if want, have := error(nil), logger.Log("foo", "bar"); want != have {
t.Errorf("want %v, have %v", want, have)
}
if want, have := `{"foo":"bar"}`, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant '%s'\nhave '%s'", want, have)
}
}
func TestLevelContext(t *testing.T) {
var buf bytes.Buffer
// Wrapping the level logger with a context allows users to use
// log.DefaultCaller as per normal.
var logger log.Logger
logger = log.NewLogfmtLogger(&buf)
logger = level.NewFilter(logger, level.AllowAll())
logger = log.With(logger, "caller", log.DefaultCaller)
level.Info(logger).Log("foo", "bar")
if want, have := `level=info caller=level_test.go:149 foo=bar`, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant '%s'\nhave '%s'", want, have)
}
}
func TestContextLevel(t *testing.T) {
var buf bytes.Buffer
// Wrapping a context with the level logger still works, but requires users
// to specify a higher callstack depth value.
var logger log.Logger
logger = log.NewLogfmtLogger(&buf)
logger = log.With(logger, "caller", log.Caller(5))
logger = level.NewFilter(logger, level.AllowAll())
level.Info(logger).Log("foo", "bar")
if want, have := `caller=level_test.go:165 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant '%s'\nhave '%s'", want, have)
}
}
func TestLevelFormatting(t *testing.T) {
testCases := []struct {
name string
format func(io.Writer) log.Logger
output string
}{
{
name: "logfmt",
format: log.NewLogfmtLogger,
output: `level=info foo=bar`,
},
{
name: "JSON",
format: log.NewJSONLogger,
output: `{"foo":"bar","level":"info"}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
logger := tc.format(&buf)
level.Info(logger).Log("foo", "bar")
if want, have := tc.output, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant: '%s'\nhave '%s'", want, have)
}
})
}
}
func TestInjector(t *testing.T) {
var (
output []interface{}
logger log.Logger
)
logger = log.LoggerFunc(func(keyvals ...interface{}) error {
output = keyvals
return nil
})
logger = level.NewInjector(logger, level.InfoValue())
logger.Log("foo", "bar")
if got, want := len(output), 4; got != want {
t.Errorf("missing level not injected: got len==%d, want len==%d", got, want)
}
if got, want := output[0], level.Key(); got != want {
t.Errorf("wrong level key: got %#v, want %#v", got, want)
}
if got, want := output[1], level.InfoValue(); got != want {
t.Errorf("wrong level value: got %#v, want %#v", got, want)
}
level.Error(logger).Log("foo", "bar")
if got, want := len(output), 4; got != want {
t.Errorf("leveled record modified: got len==%d, want len==%d", got, want)
}
if got, want := output[0], level.Key(); got != want {
t.Errorf("wrong level key: got %#v, want %#v", got, want)
}
if got, want := output[1], level.ErrorValue(); got != want {
t.Errorf("wrong level value: got %#v, want %#v", got, want)
}
}

View File

@@ -7,7 +7,7 @@ import "github.com/go-kit/kit/log"
// want a different set of levels, you can create your own levels type very
// easily, and you can elide the configuration.
type Levels struct {
logger log.Logger
ctx *log.Context
levelKey string
// We have a choice between storing level values in string fields or
@@ -34,7 +34,7 @@ type Levels struct {
// New creates a new leveled logger, wrapping the passed logger.
func New(logger log.Logger, options ...Option) Levels {
l := Levels{
logger: logger,
ctx: log.NewContext(logger),
levelKey: "level",
debugValue: "debug",
@@ -52,7 +52,7 @@ func New(logger log.Logger, options ...Option) Levels {
// With returns a new leveled logger that includes keyvals in all log events.
func (l Levels) With(keyvals ...interface{}) Levels {
return Levels{
logger: log.With(l.logger, keyvals...),
ctx: l.ctx.With(keyvals...),
levelKey: l.levelKey,
debugValue: l.debugValue,
infoValue: l.infoValue,
@@ -64,27 +64,27 @@ func (l Levels) With(keyvals ...interface{}) Levels {
// Debug returns a debug level logger.
func (l Levels) Debug() log.Logger {
return log.WithPrefix(l.logger, l.levelKey, l.debugValue)
return l.ctx.WithPrefix(l.levelKey, l.debugValue)
}
// Info returns an info level logger.
func (l Levels) Info() log.Logger {
return log.WithPrefix(l.logger, l.levelKey, l.infoValue)
return l.ctx.WithPrefix(l.levelKey, l.infoValue)
}
// Warn returns a warning level logger.
func (l Levels) Warn() log.Logger {
return log.WithPrefix(l.logger, l.levelKey, l.warnValue)
return l.ctx.WithPrefix(l.levelKey, l.warnValue)
}
// Error returns an error level logger.
func (l Levels) Error() log.Logger {
return log.WithPrefix(l.logger, l.levelKey, l.errorValue)
return l.ctx.WithPrefix(l.levelKey, l.errorValue)
}
// Crit returns a critical level logger.
func (l Levels) Crit() log.Logger {
return log.WithPrefix(l.logger, l.levelKey, l.critValue)
return l.ctx.WithPrefix(l.levelKey, l.critValue)
}
// Option sets a parameter for leveled loggers.

View File

@@ -6,7 +6,7 @@ import (
"testing"
"github.com/go-kit/kit/log"
levels "github.com/go-kit/kit/log/deprecated_levels"
"github.com/go-kit/kit/log/levels"
)
func TestDefaultLevels(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import "errors"
// log event from keyvals, a variadic sequence of alternating keys and values.
// Implementations must be safe for concurrent use by multiple goroutines. In
// particular, any implementation of Logger that appends to keyvals or
// modifies or retains any of its elements must make a copy first.
// modifies any of its elements must make a copy first.
type Logger interface {
Log(keyvals ...interface{}) error
}
@@ -15,100 +15,62 @@ type Logger interface {
// the missing value.
var ErrMissingValue = errors.New("(MISSING)")
// With returns a new contextual logger with keyvals prepended to those passed
// to calls to Log. If logger is also a contextual logger created by With or
// WithPrefix, keyvals is appended to the existing context.
//
// The returned Logger replaces all value elements (odd indexes) containing a
// Valuer with their generated value for each call to its Log method.
func With(logger Logger, keyvals ...interface{}) Logger {
if len(keyvals) == 0 {
return logger
}
l := newContext(logger)
kvs := append(l.keyvals, keyvals...)
if len(kvs)%2 != 0 {
kvs = append(kvs, ErrMissingValue)
}
return &context{
logger: l.logger,
// Limiting the capacity of the stored keyvals ensures that a new
// backing array is created if the slice must grow in Log or With.
// Using the extra capacity without copying risks a data race that
// would violate the Logger interface contract.
keyvals: kvs[:len(kvs):len(kvs)],
hasValuer: l.hasValuer || containsValuer(keyvals),
// NewContext returns a new Context that logs to logger.
func NewContext(logger Logger) *Context {
if c, ok := logger.(*Context); ok {
return c
}
return &Context{logger: logger}
}
// WithPrefix returns a new contextual logger with keyvals prepended to those
// passed to calls to Log. If logger is also a contextual logger created by
// With or WithPrefix, keyvals is prepended to the existing context.
//
// The returned Logger replaces all value elements (odd indexes) containing a
// Valuer with their generated value for each call to its Log method.
func WithPrefix(logger Logger, keyvals ...interface{}) Logger {
if len(keyvals) == 0 {
return logger
}
l := newContext(logger)
// Limiting the capacity of the stored keyvals ensures that a new
// backing array is created if the slice must grow in Log or With.
// Using the extra capacity without copying risks a data race that
// would violate the Logger interface contract.
n := len(l.keyvals) + len(keyvals)
if len(keyvals)%2 != 0 {
n++
}
kvs := make([]interface{}, 0, n)
kvs = append(kvs, keyvals...)
if len(kvs)%2 != 0 {
kvs = append(kvs, ErrMissingValue)
}
kvs = append(kvs, l.keyvals...)
return &context{
logger: l.logger,
keyvals: kvs,
hasValuer: l.hasValuer || containsValuer(keyvals),
}
}
// context is the Logger implementation returned by With and WithPrefix. It
// wraps a Logger and holds keyvals that it includes in all log events. Its
// Log method calls bindValues to generate values for each Valuer in the
// context keyvals.
//
// A context must always have the same number of stack frames between calls to
// Context must always have the same number of stack frames between calls to
// its Log method and the eventual binding of Valuers to their value. This
// requirement comes from the functional requirement to allow a context to
// resolve application call site information for a Caller stored in the
// resolve application call site information for a log.Caller stored in the
// context. To do this we must be able to predict the number of logging
// functions on the stack when bindValues is called.
//
// Two implementation details provide the needed stack depth consistency.
// Three implementation details provide the needed stack depth consistency.
// The first two of these details also result in better amortized performance,
// and thus make sense even without the requirements regarding stack depth.
// The third detail, however, is subtle and tied to the implementation of the
// Go compiler.
//
// 1. newContext avoids introducing an additional layer when asked to
// wrap another context.
// 2. With and WithPrefix avoid introducing an additional layer by
// returning a newly constructed context with a merged keyvals rather
// than simply wrapping the existing context.
type context struct {
// 1. NewContext avoids introducing an additional layer when asked to
// wrap another Context.
// 2. With avoids introducing an additional layer by returning a newly
// constructed Context with a merged keyvals rather than simply
// wrapping the existing Context.
// 3. All of Context's methods take pointer receivers even though they
// do not mutate the Context.
//
// Before explaining the last detail, first some background. The Go compiler
// generates wrapper methods to implement the auto dereferencing behavior when
// calling a value method through a pointer variable. These wrapper methods
// are also used when calling a value method through an interface variable
// because interfaces store a pointer to the underlying concrete value.
// Calling a pointer receiver through an interface does not require generating
// an additional function.
//
// If Context had value methods then calling Context.Log through a variable
// with type Logger would have an extra stack frame compared to calling
// Context.Log through a variable with type Context. Using pointer receivers
// avoids this problem.
// A Context wraps a Logger and holds keyvals that it includes in all log
// events. When logging, a Context replaces all value elements (odd indexes)
// containing a Valuer with their generated value for each call to its Log
// method.
type Context struct {
logger Logger
keyvals []interface{}
hasValuer bool
}
func newContext(logger Logger) *context {
if c, ok := logger.(*context); ok {
return c
}
return &context{logger: logger}
}
// Log replaces all value elements (odd indexes) containing a Valuer in the
// stored context with their generated value, appends keyvals, and passes the
// result to the wrapped Logger.
func (l *context) Log(keyvals ...interface{}) error {
func (l *Context) Log(keyvals ...interface{}) error {
kvs := append(l.keyvals, keyvals...)
if len(kvs)%2 != 0 {
kvs = append(kvs, ErrMissingValue)
@@ -124,6 +86,53 @@ func (l *context) Log(keyvals ...interface{}) error {
return l.logger.Log(kvs...)
}
// With returns a new Context with keyvals appended to those of the receiver.
func (l *Context) With(keyvals ...interface{}) *Context {
if len(keyvals) == 0 {
return l
}
kvs := append(l.keyvals, keyvals...)
if len(kvs)%2 != 0 {
kvs = append(kvs, ErrMissingValue)
}
return &Context{
logger: l.logger,
// Limiting the capacity of the stored keyvals ensures that a new
// backing array is created if the slice must grow in Log or With.
// Using the extra capacity without copying risks a data race that
// would violate the Logger interface contract.
keyvals: kvs[:len(kvs):len(kvs)],
hasValuer: l.hasValuer || containsValuer(keyvals),
}
}
// WithPrefix returns a new Context with keyvals prepended to those of the
// receiver.
func (l *Context) WithPrefix(keyvals ...interface{}) *Context {
if len(keyvals) == 0 {
return l
}
// Limiting the capacity of the stored keyvals ensures that a new
// backing array is created if the slice must grow in Log or With.
// Using the extra capacity without copying risks a data race that
// would violate the Logger interface contract.
n := len(l.keyvals) + len(keyvals)
if len(keyvals)%2 != 0 {
n++
}
kvs := make([]interface{}, 0, n)
kvs = append(kvs, keyvals...)
if len(kvs)%2 != 0 {
kvs = append(kvs, ErrMissingValue)
}
kvs = append(kvs, l.keyvals...)
return &Context{
logger: l.logger,
keyvals: kvs,
hasValuer: l.hasValuer || containsValuer(keyvals),
}
}
// LoggerFunc is an adapter to allow use of ordinary functions as Loggers. If
// f is a function with the appropriate signature, LoggerFunc(f) is a Logger
// object that calls f.

View File

@@ -16,10 +16,10 @@ func TestContext(t *testing.T) {
logger := log.NewLogfmtLogger(buf)
kvs := []interface{}{"a", 123}
lc := log.With(logger, kvs...)
lc := log.NewContext(logger).With(kvs...)
kvs[1] = 0 // With should copy its key values
lc = log.With(lc, "b", "c") // With should stack
lc = lc.With("b", "c") // With should stack
if err := lc.Log("msg", "message"); err != nil {
t.Fatal(err)
}
@@ -28,7 +28,7 @@ func TestContext(t *testing.T) {
}
buf.Reset()
lc = log.WithPrefix(lc, "p", "first")
lc = lc.WithPrefix("p", "first")
if err := lc.Log("msg", "message"); err != nil {
t.Fatal(err)
}
@@ -45,7 +45,17 @@ func TestContextMissingValue(t *testing.T) {
return nil
}))
log.WithPrefix(log.With(logger, "k1"), "k0").Log("k2")
lc := log.NewContext(logger)
lc.Log("k")
if want, have := 2, len(output); want != have {
t.Errorf("want len(output) == %v, have %v", want, have)
}
if want, have := log.ErrMissingValue, output[1]; want != have {
t.Errorf("want %#v, have %#v", want, have)
}
lc.With("k1").WithPrefix("k0").Log("k2")
if want, have := 6, len(output); want != have {
t.Errorf("want len(output) == %v, have %v", want, have)
}
@@ -56,8 +66,10 @@ func TestContextMissingValue(t *testing.T) {
}
}
// Test that context.Log has a consistent function stack depth when binding
// Valuers, regardless of how many times With has been called.
// Test that Context.Log has a consistent function stack depth when binding
// log.Valuers, regardless of how many times Context.With has been called or
// whether Context.Log is called via an interface typed variable or a concrete
// typed variable.
func TestContextStackDepth(t *testing.T) {
t.Parallel()
fn := fmt.Sprintf("%n", stack.Caller(0))
@@ -79,25 +91,32 @@ func TestContextStackDepth(t *testing.T) {
return nil
})
logger = log.With(logger, "stack", stackValuer)
concrete := log.NewContext(logger).With("stack", stackValuer)
var iface log.Logger = concrete
// Call through interface to get baseline.
logger.Log("k", "v")
iface.Log("k", "v")
want := output[1].(int)
for len(output) < 10 {
logger.Log("k", "v")
concrete.Log("k", "v")
if have := output[1]; have != want {
t.Errorf("%d Withs: have %v, want %v", len(output)/2-1, have, want)
}
wrapped := log.With(logger)
iface.Log("k", "v")
if have := output[1]; have != want {
t.Errorf("%d Withs: have %v, want %v", len(output)/2-1, have, want)
}
wrapped := log.NewContext(concrete)
wrapped.Log("k", "v")
if have := output[1]; have != want {
t.Errorf("%d Withs: have %v, want %v", len(output)/2-1, have, want)
}
logger = log.With(logger, "k", "v")
concrete = concrete.With("k", "v")
iface = concrete
}
}
@@ -121,7 +140,7 @@ func TestWithConcurrent(t *testing.T) {
// With must be careful about handling slices that can grow without
// copying the underlying array, so give it a challenge.
l := log.With(logger, make([]interface{}, 0, 2)...)
l := log.NewContext(logger).With(make([]interface{}, 0, 2)...)
// Start logging concurrently. Each goroutine logs its id so the logger
// can bucket the event counts.
@@ -156,7 +175,7 @@ func BenchmarkDiscard(b *testing.B) {
func BenchmarkOneWith(b *testing.B) {
logger := log.NewNopLogger()
lc := log.With(logger, "k", "v")
lc := log.NewContext(logger).With("k", "v")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -166,9 +185,9 @@ func BenchmarkOneWith(b *testing.B) {
func BenchmarkTwoWith(b *testing.B) {
logger := log.NewNopLogger()
lc := log.With(logger, "k", "v")
lc := log.NewContext(logger).With("k", "v")
for i := 1; i < 2; i++ {
lc = log.With(lc, "k", "v")
lc = lc.With("k", "v")
}
b.ReportAllocs()
b.ResetTimer()
@@ -179,9 +198,9 @@ func BenchmarkTwoWith(b *testing.B) {
func BenchmarkTenWith(b *testing.B) {
logger := log.NewNopLogger()
lc := log.With(logger, "k", "v")
lc := log.NewContext(logger).With("k", "v")
for i := 1; i < 10; i++ {
lc = log.With(lc, "k", "v")
lc = lc.With("k", "v")
}
b.ReportAllocs()
b.ResetTimer()

View File

@@ -12,7 +12,7 @@ func TestNopLogger(t *testing.T) {
if err := logger.Log("abc", 123); err != nil {
t.Error(err)
}
if err := log.With(logger, "def", "ghi").Log(); err != nil {
if err := log.NewContext(logger).With("def", "ghi").Log(); err != nil {
t.Error(err)
}
}

View File

@@ -39,7 +39,7 @@ func TimestampKey(key string) StdlibAdapterOption {
return func(a *StdlibAdapter) { a.timestampKey = key }
}
// FileKey sets the key for the file and line field. By default, it's "caller".
// FileKey sets the key for the file and line field. By default, it's "file".
func FileKey(key string) StdlibAdapterOption {
return func(a *StdlibAdapter) { a.fileKey = key }
}
@@ -55,7 +55,7 @@ func NewStdlibAdapter(logger Logger, options ...StdlibAdapterOption) io.Writer {
a := StdlibAdapter{
Logger: logger,
timestampKey: "ts",
fileKey: "caller",
fileKey: "file",
messageKey: "msg",
}
for _, option := range options {

View File

@@ -35,9 +35,9 @@ func TestStdlibAdapterUsage(t *testing.T) {
log.Ldate: "ts=" + date + " msg=hello\n",
log.Ltime: "ts=" + time + " msg=hello\n",
log.Ldate | log.Ltime: "ts=\"" + date + " " + time + "\" msg=hello\n",
log.Lshortfile: "caller=stdlib_test.go:44 msg=hello\n",
log.Lshortfile | log.Ldate: "ts=" + date + " caller=stdlib_test.go:44 msg=hello\n",
log.Lshortfile | log.Ldate | log.Ltime: "ts=\"" + date + " " + time + "\" caller=stdlib_test.go:44 msg=hello\n",
log.Lshortfile: "file=stdlib_test.go:44 msg=hello\n",
log.Lshortfile | log.Ldate: "ts=" + date + " file=stdlib_test.go:44 msg=hello\n",
log.Lshortfile | log.Ldate | log.Ltime: "ts=\"" + date + " " + time + "\" file=stdlib_test.go:44 msg=hello\n",
} {
buf.Reset()
stdlog.SetFlags(flag)
@@ -58,11 +58,11 @@ func TestStdLibAdapterExtraction(t *testing.T) {
"2009/01/23 01:23:23: hello": "ts=\"2009/01/23 01:23:23\" msg=hello\n",
"01:23:23: hello": "ts=01:23:23 msg=hello\n",
"2009/01/23 01:23:23.123123: hello": "ts=\"2009/01/23 01:23:23.123123\" msg=hello\n",
"2009/01/23 01:23:23.123123 /a/b/c/d.go:23: hello": "ts=\"2009/01/23 01:23:23.123123\" caller=/a/b/c/d.go:23 msg=hello\n",
"01:23:23.123123 /a/b/c/d.go:23: hello": "ts=01:23:23.123123 caller=/a/b/c/d.go:23 msg=hello\n",
"2009/01/23 01:23:23 /a/b/c/d.go:23: hello": "ts=\"2009/01/23 01:23:23\" caller=/a/b/c/d.go:23 msg=hello\n",
"2009/01/23 /a/b/c/d.go:23: hello": "ts=2009/01/23 caller=/a/b/c/d.go:23 msg=hello\n",
"/a/b/c/d.go:23: hello": "caller=/a/b/c/d.go:23 msg=hello\n",
"2009/01/23 01:23:23.123123 /a/b/c/d.go:23: hello": "ts=\"2009/01/23 01:23:23.123123\" file=/a/b/c/d.go:23 msg=hello\n",
"01:23:23.123123 /a/b/c/d.go:23: hello": "ts=01:23:23.123123 file=/a/b/c/d.go:23 msg=hello\n",
"2009/01/23 01:23:23 /a/b/c/d.go:23: hello": "ts=\"2009/01/23 01:23:23\" file=/a/b/c/d.go:23 msg=hello\n",
"2009/01/23 /a/b/c/d.go:23: hello": "ts=2009/01/23 file=/a/b/c/d.go:23 msg=hello\n",
"/a/b/c/d.go:23: hello": "file=/a/b/c/d.go:23 msg=hello\n",
} {
buf.Reset()
fmt.Fprint(writer, input)

View File

@@ -42,7 +42,7 @@ const (
// https://en.wikipedia.org/wiki/ANSI_escape_code#Colors.
var (
resetColorBytes = []byte("\x1b[39;49;22m")
resetColorBytes = []byte("\x1b[39;49m")
fgColorBytes [][]byte
bgColorBytes [][]byte
)

View File

@@ -27,7 +27,7 @@ func TestColorLogger(t *testing.T) {
if err := logger.Log("a", 1); err != nil {
t.Fatal(err)
}
if want, have := "\x1b[32;1m\x1b[47;1ma=1\n\x1b[39;49;22m", buf.String(); want != have {
if want, have := "\x1b[32;1m\x1b[47;1ma=1\n\x1b[39;49m", buf.String(); want != have {
t.Errorf("\nwant %#v\nhave %#v", want, have)
}
}
@@ -56,7 +56,7 @@ func TestColorLoggerConcurrency(t *testing.T) {
// copied from log/benchmark_test.go
func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) {
lc := log.With(logger, "common_key", "common_value")
lc := log.NewContext(logger).With("common_key", "common_value")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -66,7 +66,7 @@ func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) {
var (
baseMessage = func(logger log.Logger) { logger.Log("foo_key", "foo_value") }
withMessage = func(logger log.Logger) { log.With(logger, "a", "b").Log("c", "d") }
withMessage = func(logger log.Logger) { log.NewContext(logger).With("a", "b").Log("c", "d") }
)
// copied from log/concurrency_test.go

View File

@@ -6,9 +6,9 @@ import (
"github.com/go-stack/stack"
)
// A Valuer generates a log value. When passed to With or WithPrefix in a
// value element (odd indexes), it represents a dynamic value which is re-
// evaluated with each log event.
// A Valuer generates a log value. When passed to Context.With in a value
// element (odd indexes), it represents a dynamic value which is re-evaluated
// with each log event.
type Valuer func() interface{}
// bindValues replaces all value elements (odd indexes) containing a Valuer
@@ -39,6 +39,16 @@ func Timestamp(t func() time.Time) Valuer {
return func() interface{} { return t() }
}
var (
// DefaultTimestamp is a Valuer that returns the current wallclock time,
// respecting time zones, when bound.
DefaultTimestamp Valuer = func() interface{} { return time.Now().Format(time.RFC3339) }
// DefaultTimestampUTC is a Valuer that returns the current time in UTC
// when bound.
DefaultTimestampUTC Valuer = func() interface{} { return time.Now().UTC().Format(time.RFC3339) }
)
// Caller returns a Valuer that returns a file and line from a specified depth
// in the callstack. Users will probably want to use DefaultCaller.
func Caller(depth int) Valuer {
@@ -46,18 +56,6 @@ func Caller(depth int) Valuer {
}
var (
// DefaultTimestamp is a Valuer that returns the current wallclock time,
// respecting time zones, when bound.
DefaultTimestamp = Valuer(func() interface{} {
return time.Now().Format(time.RFC3339Nano)
})
// DefaultTimestampUTC is a Valuer that returns the current time in UTC
// when bound.
DefaultTimestampUTC = Valuer(func() interface{} {
return time.Now().UTC().Format(time.RFC3339Nano)
})
// DefaultCaller is a Valuer that returns the file and line where the Log
// method was invoked. It can only be used with log.With.
DefaultCaller = Caller(3)

View File

@@ -24,7 +24,7 @@ func TestValueBinding(t *testing.T) {
return now
}
lc := log.With(logger, "ts", log.Timestamp(mocktime), "caller", log.DefaultCaller)
lc := log.NewContext(logger).With("ts", log.Timestamp(mocktime), "caller", log.DefaultCaller)
lc.Log("foo", "bar")
timestamp, ok := output[1].(time.Time)
@@ -68,7 +68,7 @@ func TestValueBinding_loggingZeroKeyvals(t *testing.T) {
return now
}
logger = log.With(logger, "ts", log.Timestamp(mocktime))
logger = log.NewContext(logger).With("ts", log.Timestamp(mocktime))
logger.Log()
timestamp, ok := output[1].(time.Time)
@@ -92,7 +92,7 @@ func TestValueBinding_loggingZeroKeyvals(t *testing.T) {
func BenchmarkValueBindingTimestamp(b *testing.B) {
logger := log.NewNopLogger()
lc := log.With(logger, "ts", log.DefaultTimestamp)
lc := log.NewContext(logger).With("ts", log.DefaultTimestamp)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -102,7 +102,7 @@ func BenchmarkValueBindingTimestamp(b *testing.B) {
func BenchmarkValueBindingCaller(b *testing.B) {
logger := log.NewNopLogger()
lc := log.With(logger, "caller", log.DefaultCaller)
lc := log.NewContext(logger).With("caller", log.DefaultCaller)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@@ -49,7 +49,7 @@ import (
)
func main() {
var dur metrics.Histogram = prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
var dur metrics.Histogram = prometheus.NewSummary(stdprometheus.SummaryOpts{
Namespace: "myservice",
Subsystem: "api",
Name: "request_duration_seconds",
@@ -93,5 +93,3 @@ func exportGoroutines(g metrics.Gauge) {
}
}
```
For more information, see [the package documentation](https://godoc.org/github.com/go-kit/kit/metrics).

View File

@@ -0,0 +1,85 @@
// Package circonus provides a Circonus backend for metrics.
package circonus
import (
"github.com/circonus-labs/circonus-gometrics"
"github.com/go-kit/kit/metrics"
)
// Circonus wraps a CirconusMetrics object and provides constructors for each of
// the Go kit metrics. The CirconusMetrics object manages aggregation of
// observations and emission to the Circonus server.
type Circonus struct {
m *circonusgometrics.CirconusMetrics
}
// New creates a new Circonus object wrapping the passed CirconusMetrics, which
// the caller should create and set in motion. The Circonus object can be used
// to construct individual Go kit metrics.
func New(m *circonusgometrics.CirconusMetrics) *Circonus {
return &Circonus{
m: m,
}
}
// NewCounter returns a counter metric with the given name.
func (c *Circonus) NewCounter(name string) *Counter {
return &Counter{
name: name,
m: c.m,
}
}
// NewGauge returns a gauge metric with the given name.
func (c *Circonus) NewGauge(name string) *Gauge {
return &Gauge{
name: name,
m: c.m,
}
}
// NewHistogram returns a histogram metric with the given name.
func (c *Circonus) NewHistogram(name string) *Histogram {
return &Histogram{
h: c.m.NewHistogram(name),
}
}
// Counter is a Circonus implementation of a counter metric.
type Counter struct {
name string
m *circonusgometrics.CirconusMetrics
}
// With implements Counter, but is a no-op, because Circonus metrics have no
// concept of per-observation label values.
func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
// Add implements Counter. Delta is converted to uint64; precision will be lost.
func (c *Counter) Add(delta float64) { c.m.Add(c.name, uint64(delta)) }
// Gauge is a Circonus implementation of a gauge metric.
type Gauge struct {
name string
m *circonusgometrics.CirconusMetrics
}
// With implements Gauge, but is a no-op, because Circonus metrics have no
// concept of per-observation label values.
func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set implements Gauge.
func (g *Gauge) Set(value float64) { g.m.SetGauge(g.name, value) }
// Histogram is a Circonus implementation of a histogram metric.
type Histogram struct {
h *circonusgometrics.Histogram
}
// With implements Histogram, but is a no-op, because Circonus metrics have no
// concept of per-observation label values.
func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe implements Histogram. No precision is lost.
func (h *Histogram) Observe(value float64) { h.h.RecordValue(value) }

View File

@@ -0,0 +1,120 @@
package circonus
import (
"encoding/json"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"testing"
"github.com/circonus-labs/circonus-gometrics"
"github.com/circonus-labs/circonus-gometrics/checkmgr"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
// The only way to extract values from Circonus is to pose as a Circonus
// server and receive real HTTP writes.
const name = "abc"
var val int64
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var res map[string]struct {
Value int64 `json:"_value"` // reverse-engineered :\
}
json.NewDecoder(r.Body).Decode(&res)
val = res[name].Value
}))
defer s.Close()
// Set up a Circonus object, submitting to our HTTP server.
m := newCirconusMetrics(s.URL)
counter := New(m).NewCounter(name).With("label values", "not supported")
value := func() float64 { m.Flush(); return float64(val) }
// Engage.
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
const name = "def"
var val float64
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var res map[string]struct {
Value string `json:"_value"`
}
json.NewDecoder(r.Body).Decode(&res)
val, _ = strconv.ParseFloat(res[name].Value, 64)
}))
defer s.Close()
m := newCirconusMetrics(s.URL)
gauge := New(m).NewGauge(name).With("label values", "not supported")
value := func() float64 { m.Flush(); return val }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
const name = "ghi"
// Circonus just emits bucketed counts. We'll dump them into a generic
// histogram (losing some precision) and take statistics from there. Note
// this does assume that the generic histogram computes statistics properly,
// but we have another test for that :)
re := regexp.MustCompile(`^H\[([0-9\.e\+]+)\]=([0-9]+)$`) // H[1.2e+03]=456
var p50, p90, p95, p99 float64
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var res map[string]struct {
Values []string `json:"_value"` // reverse-engineered :\
}
json.NewDecoder(r.Body).Decode(&res)
h := generic.NewHistogram("dummy", len(res[name].Values)) // match tbe bucket counts
for _, v := range res[name].Values {
match := re.FindStringSubmatch(v)
f, _ := strconv.ParseFloat(match[1], 64)
n, _ := strconv.ParseInt(match[2], 10, 64)
for i := int64(0); i < n; i++ {
h.Observe(f)
}
}
p50 = h.Quantile(0.50)
p90 = h.Quantile(0.90)
p95 = h.Quantile(0.95)
p99 = h.Quantile(0.99)
}))
defer s.Close()
m := newCirconusMetrics(s.URL)
histogram := New(m).NewHistogram(name).With("label values", "not supported")
quantiles := func() (float64, float64, float64, float64) { m.Flush(); return p50, p90, p95, p99 }
// Circonus metrics, because they do their own bucketing, are less precise
// than other systems. So, we bump the tolerance to 5 percent.
if err := teststat.TestHistogram(histogram, quantiles, 0.05); err != nil {
t.Fatal(err)
}
}
func newCirconusMetrics(url string) *circonusgometrics.CirconusMetrics {
m, err := circonusgometrics.NewCirconusMetrics(&circonusgometrics.Config{
CheckManager: checkmgr.Config{
Check: checkmgr.CheckConfig{
SubmissionURL: url,
},
},
})
if err != nil {
panic(err)
}
return m
}

View File

@@ -25,9 +25,6 @@ func (g gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set implements Gauge.
func (g gauge) Set(value float64) {}
// Add implements metrics.Gauge.
func (g gauge) Add(delta float64) {}
type histogram struct{}
// NewHistogram returns a new no-op histogram.

View File

@@ -1,48 +1,17 @@
// Package metrics provides a framework for application instrumentation. It's
// primarily designed to help you get started with good and robust
// instrumentation, and to help you migrate from a less-capable system like
// Graphite to a more-capable system like Prometheus. If your organization has
// already standardized on an instrumentation system like Prometheus, and has no
// plans to change, it may make sense to use that system's instrumentation
// library directly.
// Package metrics provides a framework for application instrumentation. All
// metrics are safe for concurrent use. Considerable design influence has been
// taken from https://github.com/codahale/metrics and https://prometheus.io.
//
// This package provides three core metric abstractions (Counter, Gauge, and
// Histogram) and implementations for almost all common instrumentation
// backends. Each metric has an observation method (Add, Set, or Observe,
// respectively) used to record values, and a With method to "scope" the
// observation by various parameters. For example, you might have a Histogram to
// record request durations, parameterized by the method that's being called.
//
// var requestDuration metrics.Histogram
// // ...
// requestDuration.With("method", "MyMethod").Observe(time.Since(begin))
//
// This allows a single high-level metrics object (requestDuration) to work with
// many code paths somewhat dynamically. The concept of With is fully supported
// in some backends like Prometheus, and not supported in other backends like
// Graphite. So, With may be a no-op, depending on the concrete implementation
// you choose. Please check the implementation to know for sure. For
// implementations that don't provide With, it's necessary to fully parameterize
// each metric in the metric name, e.g.
//
// // Statsd
// c := statsd.NewCounter("request_duration_MyMethod_200")
// c.Add(1)
//
// // Prometheus
// c := prometheus.NewCounter(stdprometheus.CounterOpts{
// Name: "request_duration",
// ...
// }, []string{"method", "status_code"})
// c.With("method", "MyMethod", "status_code", strconv.Itoa(code)).Add(1)
// This package contains the common interfaces. Your code should take these
// interfaces as parameters. Implementations are provided for different
// instrumentation systems in the various subdirectories.
//
// Usage
//
// Metrics are dependencies, and should be passed to the components that need
// Metrics are dependencies and should be passed to the components that need
// them in the same way you'd construct and pass a database handle, or reference
// to another component. Metrics should *not* be created in the global scope.
// Instead, instantiate metrics in your func main, using whichever concrete
// implementation is appropriate for your organization.
// to another component. So, create metrics in your func main, using whichever
// concrete implementation is appropriate for your organization.
//
// latency := prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
// Namespace: "myteam",
@@ -71,14 +40,8 @@
// api := NewAPI(store, logger, latency)
// http.ListenAndServe("/", api)
//
// Note that metrics are "write-only" interfaces.
//
// Implementation details
//
// All metrics are safe for concurrent use. Considerable design influence has
// been taken from https://github.com/codahale/metrics and
// https://prometheus.io.
//
// Each telemetry system has different semantics for label values, push vs.
// pull, support for histograms, etc. These properties influence the design of
// their respective packages. This table attempts to summarize the key points of
@@ -91,6 +54,7 @@
// expvar 1 atomic atomic synthetic, batch, in-place expose
// influx n custom custom custom
// prometheus n native native native
// circonus 1 native native native
// pcp 1 native native native
//
package metrics

View File

@@ -72,7 +72,6 @@ func (d *Dogstatsd) NewGauge(name string) *Gauge {
return &Gauge{
name: d.prefix + name,
obs: d.gauges.Observe,
add: d.gauges.Add,
}
}
@@ -245,7 +244,6 @@ type Gauge struct {
name string
lvs lv.LabelValues
obs observeFunc
add observeFunc
}
// With implements metrics.Gauge.
@@ -254,7 +252,6 @@ func (g *Gauge) With(labelValues ...string) metrics.Gauge {
name: g.name,
lvs: g.lvs.With(labelValues...),
obs: g.obs,
add: g.add,
}
}
@@ -263,11 +260,6 @@ func (g *Gauge) Set(value float64) {
g.obs(g.name, g.lvs, value)
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.add(g.name, g.lvs, delta)
}
// Timing is a DogStatsD timing, or metrics.Histogram. Observations are
// forwarded to a Dogstatsd object, and collected (but not aggregated) per
// timeseries.

View File

@@ -50,9 +50,6 @@ func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set implements Gauge.
func (g *Gauge) Set(value float64) { g.f.Set(value) }
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) { g.f.Add(delta) }
// Histogram implements the histogram metric with a combination of the generic
// Histogram object and several expvar Floats, one for each of the 50th, 90th,
// 95th, and 99th quantiles of observed values, with the quantile attached to

View File

@@ -33,7 +33,6 @@ func NewCounter(name string) *Counter {
// With implements Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
Name: c.Name,
bits: atomic.LoadUint64(&c.bits),
lvs: c.lvs.With(labelValues...),
}
@@ -96,7 +95,6 @@ func NewGauge(name string) *Gauge {
// With implements Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
Name: g.Name,
bits: atomic.LoadUint64(&g.bits),
lvs: g.lvs.With(labelValues...),
}
@@ -107,20 +105,6 @@ func (g *Gauge) Set(value float64) {
atomic.StoreUint64(&g.bits, math.Float64bits(value))
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
for {
var (
old = atomic.LoadUint64(&g.bits)
newf = math.Float64frombits(old) + delta
new = math.Float64bits(newf)
)
if atomic.CompareAndSwapUint64(&g.bits, old, new) {
break
}
}
}
// Value returns the current value of the gauge.
func (g *Gauge) Value() float64 {
return math.Float64frombits(atomic.LoadUint64(&g.bits))
@@ -137,7 +121,7 @@ func (g *Gauge) LabelValues() []string {
type Histogram struct {
Name string
lvs lv.LabelValues
h *safeHistogram
h gohistogram.Histogram
}
// NewHistogram returns a numeric histogram based on VividCortex/gohistogram. A
@@ -145,30 +129,25 @@ type Histogram struct {
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{
Name: name,
h: &safeHistogram{Histogram: gohistogram.NewHistogram(buckets)},
h: gohistogram.NewHistogram(buckets),
}
}
// With implements Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
Name: h.Name,
lvs: h.lvs.With(labelValues...),
h: h.h,
lvs: h.lvs.With(labelValues...),
h: h.h,
}
}
// Observe implements Histogram.
func (h *Histogram) Observe(value float64) {
h.h.Lock()
defer h.h.Unlock()
h.h.Add(value)
}
// Quantile returns the value of the quantile q, 0.0 < q < 1.0.
func (h *Histogram) Quantile(q float64) float64 {
h.h.RLock()
defer h.h.RUnlock()
return h.h.Quantile(q)
}
@@ -180,17 +159,9 @@ func (h *Histogram) LabelValues() []string {
// Print writes a string representation of the histogram to the passed writer.
// Useful for printing to a terminal.
func (h *Histogram) Print(w io.Writer) {
h.h.RLock()
defer h.h.RUnlock()
fmt.Fprintf(w, h.h.String())
}
// safeHistogram exists as gohistogram.Histogram is not goroutine-safe.
type safeHistogram struct {
sync.RWMutex
gohistogram.Histogram
}
// Bucket is a range in a histogram which aggregates observations.
type Bucket struct {
From, To, Count int64
@@ -237,7 +208,7 @@ func (h *SimpleHistogram) Observe(value float64) {
// ApproximateMovingAverage returns the approximate moving average of observations.
func (h *SimpleHistogram) ApproximateMovingAverage() float64 {
h.mtx.RLock()
defer h.mtx.RUnlock()
h.mtx.RUnlock()
return h.avg
}

View File

@@ -7,7 +7,6 @@ package generic_test
import (
"math"
"math/rand"
"sync"
"testing"
"github.com/go-kit/kit/metrics/generic"
@@ -15,11 +14,7 @@ import (
)
func TestCounter(t *testing.T) {
name := "my_counter"
counter := generic.NewCounter(name).With("label", "counter").(*generic.Counter)
if want, have := name, counter.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
counter := generic.NewCounter("my_counter").With("label", "counter").(*generic.Counter)
value := func() float64 { return counter.Value() }
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
@@ -40,11 +35,7 @@ func TestValueReset(t *testing.T) {
}
func TestGauge(t *testing.T) {
name := "my_gauge"
gauge := generic.NewGauge(name).With("label", "gauge").(*generic.Gauge)
if want, have := name, gauge.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
gauge := generic.NewGauge("my_gauge").With("label", "gauge").(*generic.Gauge)
value := func() float64 { return gauge.Value() }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
@@ -52,11 +43,7 @@ func TestGauge(t *testing.T) {
}
func TestHistogram(t *testing.T) {
name := "my_histogram"
histogram := generic.NewHistogram(name, 50).With("label", "histogram").(*generic.Histogram)
if want, have := name, histogram.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
histogram := generic.NewHistogram("my_histogram", 50).With("label", "histogram").(*generic.Histogram)
quantiles := func() (float64, float64, float64, float64) {
return histogram.Quantile(0.50), histogram.Quantile(0.90), histogram.Quantile(0.95), histogram.Quantile(0.99)
}
@@ -65,27 +52,6 @@ func TestHistogram(t *testing.T) {
}
}
func TestIssue424(t *testing.T) {
var (
histogram = generic.NewHistogram("dont_panic", 50)
concurrency = 100
operations = 1000
wg sync.WaitGroup
)
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func() {
defer wg.Done()
for j := 0; j < operations; j++ {
histogram.Observe(float64(j))
histogram.Observe(histogram.Quantile(0.5))
}
}()
}
wg.Wait()
}
func TestSimpleHistogram(t *testing.T) {
histogram := generic.NewSimpleHistogram().With("label", "simple_histogram").(*generic.SimpleHistogram)
var (

View File

@@ -38,7 +38,7 @@ type Graphite struct {
logger log.Logger
}
// New returns a Graphite object that may be used to create metrics. Prefix is
// New returns a Statsd object that may be used to create metrics. Prefix is
// applied to all created metrics. Callers must ensure that regular calls to
// WriteTo are performed, either manually or with one of the helper methods.
func New(prefix string, logger log.Logger) *Graphite {
@@ -182,9 +182,6 @@ func (g *Gauge) With(...string) metrics.Gauge { return g }
// Set implements gauge.
func (g *Gauge) Set(value float64) { g.g.Set(value) }
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) { g.g.Add(delta) }
// Histogram is a Graphite histogram metric. Observations are bucketed into
// per-quantile gauges.
type Histogram struct {

View File

@@ -36,7 +36,7 @@ func TestGauge(t *testing.T) {
func TestHistogram(t *testing.T) {
// The histogram test is actually like 4 gauge tests.
prefix, name := "graphite.", "histogram_test"
prefix, name := "statsd.", "histogram_test"
label, value := "abc", "def" // ignored for Graphite
re50 := regexp.MustCompile(prefix + name + `.p50 ([0-9\.]+) [0-9]+`)
re90 := regexp.MustCompile(prefix + name + `.p90 ([0-9\.]+) [0-9]+`)

View File

@@ -44,14 +44,11 @@ func ExampleGauge() {
gauge.With("error", "true").Set(1)
gauge.With("error", "false").Set(2)
gauge.Set(50)
gauge.With("test", "true").Set(1)
gauge.With("test", "true").Add(1)
client := &bufWriter{}
in.WriteTo(client)
expectedLines := []string{
`(influx_gauge,a=b,test=true value=2) [0-9]{19}`,
`(influx_gauge,a=b value=50) [0-9]{19}`,
`(influx_gauge,a=b,error=true value=1) [0-9]{19}`,
`(influx_gauge,a=b,error=false value=2) [0-9]{19}`,
@@ -62,7 +59,6 @@ func ExampleGauge() {
}
// Output:
// influx_gauge,a=b,test=true value=2
// influx_gauge,a=b value=50
// influx_gauge,a=b,error=true value=1
// influx_gauge,a=b,error=false value=2

View File

@@ -66,7 +66,6 @@ func (in *Influx) NewGauge(name string) *Gauge {
return &Gauge{
name: name,
obs: in.gauges.Observe,
add: in.gauges.Add,
}
}
@@ -169,14 +168,10 @@ func mergeTags(tags map[string]string, labelValues []string) map[string]string {
if len(labelValues)%2 != 0 {
panic("mergeTags received a labelValues with an odd number of strings")
}
ret := make(map[string]string, len(tags)+len(labelValues)/2)
for k, v := range tags {
ret[k] = v
}
for i := 0; i < len(labelValues); i += 2 {
ret[labelValues[i]] = labelValues[i+1]
tags[labelValues[i]] = labelValues[i+1]
}
return ret
return tags
}
func sum(a []float64) float64 {
@@ -221,7 +216,6 @@ type Gauge struct {
name string
lvs lv.LabelValues
obs observeFunc
add observeFunc
}
// With implements metrics.Gauge.
@@ -230,7 +224,6 @@ func (g *Gauge) With(labelValues ...string) metrics.Gauge {
name: g.name,
lvs: g.lvs.With(labelValues...),
obs: g.obs,
add: g.add,
}
}
@@ -239,11 +232,6 @@ func (g *Gauge) Set(value float64) {
g.obs(g.name, g.lvs, value)
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.add(g.name, g.lvs, delta)
}
// Histogram is an Influx histrogram. Observations are aggregated into a
// generic.Histogram and emitted as per-quantile gauges to the Influx server.
type Histogram struct {

View File

@@ -82,37 +82,6 @@ func TestHistogramLabels(t *testing.T) {
}
}
func TestIssue404(t *testing.T) {
in := New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
counterOne := in.NewCounter("influx_counter_one").With("a", "b")
counterOne.Add(123)
counterTwo := in.NewCounter("influx_counter_two").With("c", "d")
counterTwo.Add(456)
w := &bufWriter{}
in.WriteTo(w)
lines := strings.Split(strings.TrimSpace(w.buf.String()), "\n")
if want, have := 2, len(lines); want != have {
t.Fatalf("want %d, have %d", want, have)
}
for _, line := range lines {
if strings.HasPrefix(line, "influx_counter_one") {
if !strings.HasPrefix(line, "influx_counter_one,a=b count=123 ") {
t.Errorf("invalid influx_counter_one: %s", line)
}
} else if strings.HasPrefix(line, "influx_counter_two") {
if !strings.HasPrefix(line, "influx_counter_two,c=d count=456 ") {
t.Errorf("invalid influx_counter_two: %s", line)
}
} else {
t.Errorf("unexpected line: %s", line)
}
}
}
type bufWriter struct {
buf bytes.Buffer
}

View File

@@ -21,13 +21,6 @@ func (s *Space) Observe(name string, lvs LabelValues, value float64) {
s.nodeFor(name).observe(lvs, value)
}
// Add locates the time series identified by the name and label values in
// the vector space, and appends the delta to the last value in the list of
// observations.
func (s *Space) Add(name string, lvs LabelValues, delta float64) {
s.nodeFor(name).add(lvs, delta)
}
// Walk traverses the vector space and invokes fn for each non-empty time series
// which is encountered. Return false to abort the traversal.
func (s *Space) Walk(fn func(name string, lvs LabelValues, observations []float64) bool) {
@@ -98,34 +91,6 @@ func (n *node) observe(lvs LabelValues, value float64) {
child.observe(tail, value)
}
func (n *node) add(lvs LabelValues, delta float64) {
n.mtx.Lock()
defer n.mtx.Unlock()
if len(lvs) == 0 {
var value float64
if len(n.observations) > 0 {
value = last(n.observations) + delta
} else {
value = delta
}
n.observations = append(n.observations, value)
return
}
if len(lvs) < 2 {
panic("too few LabelValues; programmer error!")
}
head, tail := pair{lvs[0], lvs[1]}, lvs[2:]
if n.children == nil {
n.children = map[pair]*node{}
}
child, ok := n.children[head]
if !ok {
child = &node{}
n.children[head] = child
}
child.add(tail, delta)
}
func (n *node) walk(lvs LabelValues, fn func(LabelValues, []float64) bool) bool {
n.mtx.RLock()
defer n.mtx.RUnlock()
@@ -139,7 +104,3 @@ func (n *node) walk(lvs LabelValues, fn func(LabelValues, []float64) bool) bool
}
return true
}
func last(a []float64) float64 {
return a[len(a)-1]
}

View File

@@ -12,7 +12,6 @@ type Counter interface {
type Gauge interface {
With(labelValues ...string) Gauge
Set(value float64)
Add(delta float64)
}
// Histogram describes a metric that takes repeated observations of the same

View File

@@ -54,13 +54,6 @@ func (g Gauge) With(labelValues ...string) metrics.Gauge {
return next
}
// Add implements metrics.Gauge.
func (g Gauge) Add(delta float64) {
for _, gauge := range g {
gauge.Add(delta)
}
}
// Histogram collects multiple individual histograms and treats them as a unit.
type Histogram []metrics.Histogram

View File

@@ -33,9 +33,8 @@ func TestMultiGauge(t *testing.T) {
mg.Set(9)
mg.Set(8)
mg.Set(7)
mg.Add(3)
want := "[9 8 7 10]"
want := "[9 8 7]"
for i, m := range []fmt.Stringer{g1, g2, g3} {
if have := m.String(); want != have {
t.Errorf("g%d: want %q, have %q", i+1, want, have)
@@ -77,15 +76,6 @@ type mockGauge struct {
func (g *mockGauge) Set(value float64) { g.obs = append(g.obs, value) }
func (g *mockGauge) With(...string) metrics.Gauge { return g }
func (g *mockGauge) String() string { return fmt.Sprintf("%v", g.obs) }
func (g *mockGauge) Add(delta float64) {
var value float64
if len(g.obs) > 0 {
value = g.obs[len(g.obs)-1] + delta
} else {
value = delta
}
g.obs = append(g.obs, value)
}
type mockHistogram struct {
obs []float64

View File

@@ -82,7 +82,7 @@ func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
func (g *Gauge) Set(value float64) { g.g.Set(value) }
// Add adds a value to the gauge.
func (g *Gauge) Add(delta float64) { g.g.Inc(delta) }
func (g *Gauge) Add(value float64) { g.g.Inc(value) }
// Histogram wraps a speed Histogram.
type Histogram struct {

View File

@@ -0,0 +1,36 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/circonus"
)
type circonusProvider struct {
c *circonus.Circonus
}
// NewCirconusProvider takes the given Circonnus object and returns a Provider
// that produces Circonus metrics.
func NewCirconusProvider(c *circonus.Circonus) Provider {
return &circonusProvider{
c: c,
}
}
// NewCounter implements Provider.
func (p *circonusProvider) NewCounter(name string) metrics.Counter {
return p.c.NewCounter(name)
}
// NewGauge implements Provider.
func (p *circonusProvider) NewGauge(name string) metrics.Gauge {
return p.c.NewGauge(name)
}
// NewHistogram implements Provider. The buckets parameter is ignored.
func (p *circonusProvider) NewHistogram(name string, _ int) metrics.Histogram {
return p.c.NewHistogram(name)
}
// Stop implements Provider, but is a no-op.
func (p *circonusProvider) Stop() {}

View File

@@ -74,7 +74,6 @@ func (s *Statsd) NewGauge(name string) *Gauge {
return &Gauge{
name: s.prefix + name,
obs: s.gauges.Observe,
add: s.gauges.Add,
}
}
@@ -202,7 +201,6 @@ func (c *Counter) Add(delta float64) {
type Gauge struct {
name string
obs observeFunc
add observeFunc
}
// With is a no-op.
@@ -215,11 +213,6 @@ func (g *Gauge) Set(value float64) {
g.obs(g.name, lv.LabelValues{}, value)
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.add(g.name, lv.LabelValues{}, delta)
}
// Timing is a StatsD timing, or metrics.Histogram. Observations are
// forwarded to a Statsd object, and collected (but not aggregated) per
// timeseries.

View File

@@ -44,12 +44,6 @@ func TestGauge(gauge metrics.Gauge, value func() float64) error {
want = f
}
for i := 0; i < n; i++ {
f := float64(a[i])
gauge.Add(f)
want += f
}
if have := value(); want != have {
return fmt.Errorf("want %f, have %f", want, have)
}

View File

@@ -1,11 +1,11 @@
package ratelimit
import (
"context"
"errors"
"time"
"github.com/juju/ratelimit"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -1,12 +1,12 @@
package ratelimit_test
import (
"context"
"math"
"testing"
"time"
jujuratelimit "github.com/juju/ratelimit"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/ratelimit"

View File

@@ -1,8 +1,6 @@
package consul
import (
consul "github.com/hashicorp/consul/api"
)
import consul "github.com/hashicorp/consul/api"
// Client is a wrapper around the Consul API.
type Client interface {

View File

@@ -1,13 +1,13 @@
package consul
import (
"context"
"errors"
"io"
"reflect"
"testing"
stdconsul "github.com/hashicorp/consul/api"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)

View File

@@ -14,9 +14,11 @@ import (
)
func TestIntegration(t *testing.T) {
consulAddr := os.Getenv("CONSUL_ADDR")
// Connect to Consul.
// docker run -p 8500:8500 progrium/consul -server -bootstrap
consulAddr := os.Getenv("CONSUL_ADDRESS")
if consulAddr == "" {
t.Fatal("CONSUL_ADDR is not set")
t.Fatal("CONSUL_ADDRESS is not set")
}
stdClient, err := stdconsul.NewClient(&stdconsul.Config{
Address: consulAddr,
@@ -46,7 +48,7 @@ func TestIntegration(t *testing.T) {
subscriber := NewSubscriber(
client,
factory,
log.With(logger, "component", "subscriber"),
log.NewContext(logger).With("component", "subscriber"),
r.Name,
r.Tags,
true,
@@ -64,7 +66,7 @@ func TestIntegration(t *testing.T) {
}
// Build a registrar for r.
registrar := NewRegistrar(client, r, log.With(logger, "component", "registrar"))
registrar := NewRegistrar(client, r, log.NewContext(logger).With("component", "registrar"))
registrar.Register()
defer registrar.Deregister()

View File

@@ -21,7 +21,7 @@ func NewRegistrar(client Client, r *stdconsul.AgentServiceRegistration, logger l
return &Registrar{
client: client,
registration: r,
logger: log.With(logger, "service", r.Name, "tags", fmt.Sprint(r.Tags), "address", r.Address),
logger: log.NewContext(logger).With("service", r.Name, "tags", fmt.Sprint(r.Tags), "address", r.Address),
}
}

Some files were not shown because too many files have changed in this diff Show More