Repository: go-kit/kit
Branch: master
Commit: 78fbbceece7b
Files: 288
Total size: 869.1 KB
Directory structure:
gitextract_llwttmje/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yaml
│ │ ├── config.yml
│ │ └── feature_request.yaml
│ └── workflows/
│ ├── .editorconfig
│ └── ci.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── auth/
│ ├── basic/
│ │ ├── README.md
│ │ ├── middleware.go
│ │ └── middleware_test.go
│ ├── casbin/
│ │ ├── middleware.go
│ │ ├── middleware_test.go
│ │ └── testdata/
│ │ ├── basic_model.conf
│ │ ├── basic_policy.csv
│ │ └── keymatch_policy.csv
│ └── jwt/
│ ├── README.md
│ ├── middleware.go
│ ├── middleware_test.go
│ ├── transport.go
│ └── transport_test.go
├── circuitbreaker/
│ ├── doc.go
│ ├── gobreaker.go
│ ├── gobreaker_test.go
│ ├── handy_breaker.go
│ ├── handy_breaker_test.go
│ ├── hystrix.go
│ ├── hystrix_test.go
│ └── util_test.go
├── codecov.yml
├── docker-compose-integration.yml
├── endpoint/
│ ├── doc.go
│ ├── endpoint.go
│ └── endpoint_example_test.go
├── examples/
│ └── README.md
├── go.mod
├── go.sum
├── lint
├── log/
│ ├── README.md
│ ├── deprecated_levels/
│ │ ├── levels.go
│ │ └── levels_test.go
│ ├── doc.go
│ ├── example_test.go
│ ├── json_logger.go
│ ├── level/
│ │ ├── doc.go
│ │ ├── example_test.go
│ │ └── level.go
│ ├── log.go
│ ├── logfmt_logger.go
│ ├── logrus/
│ │ ├── logrus_logger.go
│ │ └── logrus_logger_test.go
│ ├── nop_logger.go
│ ├── stdlib.go
│ ├── sync.go
│ ├── syslog/
│ │ ├── example_test.go
│ │ └── syslog.go
│ ├── term/
│ │ ├── colorlogger.go
│ │ ├── colorwriter.go
│ │ ├── example_test.go
│ │ └── term.go
│ ├── value.go
│ └── zap/
│ ├── zap_sugar_logger.go
│ └── zap_sugar_logger_test.go
├── metrics/
│ ├── README.md
│ ├── cloudwatch/
│ │ ├── cloudwatch.go
│ │ └── cloudwatch_test.go
│ ├── cloudwatch2/
│ │ ├── cloudwatch2.go
│ │ └── cloudwatch2_test.go
│ ├── discard/
│ │ └── discard.go
│ ├── doc.go
│ ├── dogstatsd/
│ │ ├── dogstatsd.go
│ │ └── dogstatsd_test.go
│ ├── expvar/
│ │ ├── expvar.go
│ │ └── expvar_test.go
│ ├── generic/
│ │ ├── generic.go
│ │ └── generic_test.go
│ ├── graphite/
│ │ ├── graphite.go
│ │ └── graphite_test.go
│ ├── influx/
│ │ ├── example_test.go
│ │ ├── influx.go
│ │ └── influx_test.go
│ ├── influxstatsd/
│ │ ├── influxstatsd.go
│ │ └── influxstatsd_test.go
│ ├── internal/
│ │ ├── convert/
│ │ │ ├── convert.go
│ │ │ └── convert_test.go
│ │ ├── lv/
│ │ │ ├── labelvalues.go
│ │ │ ├── labelvalues_test.go
│ │ │ ├── space.go
│ │ │ └── space_test.go
│ │ └── ratemap/
│ │ └── ratemap.go
│ ├── metrics.go
│ ├── multi/
│ │ ├── multi.go
│ │ └── multi_test.go
│ ├── pcp/
│ │ ├── pcp.go
│ │ └── pcp_test.go
│ ├── prometheus/
│ │ ├── prometheus.go
│ │ └── prometheus_test.go
│ ├── provider/
│ │ ├── discard.go
│ │ ├── dogstatsd.go
│ │ ├── expvar.go
│ │ ├── graphite.go
│ │ ├── influx.go
│ │ ├── prometheus.go
│ │ ├── provider.go
│ │ └── statsd.go
│ ├── statsd/
│ │ ├── statsd.go
│ │ └── statsd_test.go
│ ├── teststat/
│ │ ├── buffers.go
│ │ ├── populate.go
│ │ └── teststat.go
│ ├── timer.go
│ └── timer_test.go
├── ratelimit/
│ ├── token_bucket.go
│ └── token_bucket_test.go
├── sd/
│ ├── benchmark_test.go
│ ├── consul/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── doc.go
│ │ ├── instancer.go
│ │ ├── instancer_test.go
│ │ ├── integration_test.go
│ │ ├── registrar.go
│ │ └── registrar_test.go
│ ├── dnssrv/
│ │ ├── doc.go
│ │ ├── instancer.go
│ │ ├── instancer_test.go
│ │ └── lookup.go
│ ├── doc.go
│ ├── endpoint_cache.go
│ ├── endpoint_cache_test.go
│ ├── endpointer.go
│ ├── endpointer_test.go
│ ├── etcd/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── doc.go
│ │ ├── example_test.go
│ │ ├── instancer.go
│ │ ├── instancer_test.go
│ │ ├── integration_test.go
│ │ ├── registrar.go
│ │ └── registrar_test.go
│ ├── etcdv3/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── doc.go
│ │ ├── example_test.go
│ │ ├── instancer.go
│ │ ├── instancer_test.go
│ │ ├── integration_test.go
│ │ ├── registrar.go
│ │ └── registrar_test.go
│ ├── eureka/
│ │ ├── doc.go
│ │ ├── instancer.go
│ │ ├── instancer_test.go
│ │ ├── integration_test.go
│ │ ├── registrar.go
│ │ ├── registrar_test.go
│ │ └── util_test.go
│ ├── factory.go
│ ├── instancer.go
│ ├── internal/
│ │ └── instance/
│ │ ├── cache.go
│ │ └── cache_test.go
│ ├── lb/
│ │ ├── balancer.go
│ │ ├── doc.go
│ │ ├── random.go
│ │ ├── random_test.go
│ │ ├── retry.go
│ │ ├── retry_test.go
│ │ ├── round_robin.go
│ │ └── round_robin_test.go
│ ├── registrar.go
│ └── zk/
│ ├── client.go
│ ├── client_test.go
│ ├── doc.go
│ ├── instancer.go
│ ├── instancer_test.go
│ ├── integration_test.go
│ ├── logwrapper.go
│ ├── registrar.go
│ └── util_test.go
├── tracing/
│ ├── README.md
│ ├── doc.go
│ ├── opencensus/
│ │ ├── doc.go
│ │ ├── endpoint.go
│ │ ├── endpoint_options.go
│ │ ├── endpoint_test.go
│ │ ├── grpc.go
│ │ ├── grpc_test.go
│ │ ├── http.go
│ │ ├── http_test.go
│ │ ├── jsonrpc.go
│ │ ├── jsonrpc_test.go
│ │ ├── opencensus_test.go
│ │ └── tracer_options.go
│ ├── opentracing/
│ │ ├── doc.go
│ │ ├── endpoint.go
│ │ ├── endpoint_options.go
│ │ ├── endpoint_test.go
│ │ ├── grpc.go
│ │ ├── grpc_test.go
│ │ ├── http.go
│ │ └── http_test.go
│ └── zipkin/
│ ├── README.md
│ ├── doc.go
│ ├── endpoint.go
│ ├── endpoint_test.go
│ ├── grpc.go
│ ├── grpc_test.go
│ ├── http.go
│ ├── http_test.go
│ └── options.go
├── transport/
│ ├── amqp/
│ │ ├── doc.go
│ │ ├── encode_decode.go
│ │ ├── publisher.go
│ │ ├── publisher_test.go
│ │ ├── request_response_func.go
│ │ ├── subscriber.go
│ │ ├── subscriber_test.go
│ │ └── util.go
│ ├── awslambda/
│ │ ├── doc.go
│ │ ├── encode_decode.go
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ └── request_response_funcs.go
│ ├── doc.go
│ ├── error_handler.go
│ ├── error_handler_test.go
│ ├── grpc/
│ │ ├── README.md
│ │ ├── _grpc_test/
│ │ │ ├── client.go
│ │ │ ├── context_metadata.go
│ │ │ ├── pb/
│ │ │ │ ├── generate.go
│ │ │ │ ├── test.pb.go
│ │ │ │ ├── test.proto
│ │ │ │ └── test_grpc.pb.go
│ │ │ ├── request_response.go
│ │ │ ├── server.go
│ │ │ └── service.go
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── doc.go
│ │ ├── encode_decode.go
│ │ ├── request_response_funcs.go
│ │ └── server.go
│ ├── http/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── doc.go
│ │ ├── encode_decode.go
│ │ ├── example_test.go
│ │ ├── intercepting_writer.go
│ │ ├── intercepting_writer_test.go
│ │ ├── jsonrpc/
│ │ │ ├── README.md
│ │ │ ├── client.go
│ │ │ ├── client_test.go
│ │ │ ├── doc.go
│ │ │ ├── encode_decode.go
│ │ │ ├── error.go
│ │ │ ├── error_test.go
│ │ │ ├── request_response_types.go
│ │ │ ├── request_response_types_test.go
│ │ │ ├── server.go
│ │ │ └── server_test.go
│ │ ├── proto/
│ │ │ ├── client.go
│ │ │ ├── generate.go
│ │ │ ├── proto_pb_test.go
│ │ │ ├── proto_test.go
│ │ │ ├── proto_test.proto
│ │ │ └── server.go
│ │ ├── request_response_funcs.go
│ │ ├── request_response_funcs_test.go
│ │ ├── server.go
│ │ └── server_test.go
│ ├── httprp/
│ │ ├── doc.go
│ │ ├── server.go
│ │ └── server_test.go
│ ├── nats/
│ │ ├── doc.go
│ │ ├── encode_decode.go
│ │ ├── publisher.go
│ │ ├── publisher_test.go
│ │ ├── request_response_funcs.go
│ │ ├── subscriber.go
│ │ └── subscriber_test.go
│ ├── netrpc/
│ │ └── README.md
│ └── thrift/
│ └── README.md
└── util/
├── README.md
└── conn/
├── doc.go
├── manager.go
└── manager_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [peterbourgon]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug report
description: Report a bug
labels: [bug]
body:
- type: textarea
attributes:
label: What did you do?
validations:
required: true
- type: textarea
attributes:
label: What did you expect?
validations:
required: true
- type: textarea
attributes:
label: What happened instead?
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/go-kit/kit/discussions/new?category=q-a
about: Questions and discussions with the Go kit community
- name: Website
url: https://gokit.io/
about: Project overview, examples, frequently asked questions, etc.
- name: Reference
url: https://pkg.go.dev/github.com/go-kit/kit
about: Go kit package documentation
- name: Slack channel
url: https://gophers.slack.com/messages/go-kit
about: Real-time discussions and Q&A
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: Feature request
description: Suggest new functionality or an enhancement
body:
- type: textarea
attributes:
label: What would you like?
validations:
required: true
================================================
FILE: .github/workflows/.editorconfig
================================================
[*.yml]
indent_size = 2
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
matrix: # Support latest and one minor back
go: ["1.17", "1.18", "1.19"]
env:
GOFLAGS: -mod=readonly
services:
etcd:
image: gcr.io/etcd-development/etcd:v3.5.0
ports:
- 2379
env:
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
options: --health-cmd "ETCDCTL_API=3 etcdctl --endpoints http://localhost:2379 endpoint health" --health-interval 10s --health-timeout 5s --health-retries 5
consul:
image: consul:1.10
ports:
- 8500
zk:
image: zookeeper:3.5
ports:
- 2181
eureka:
image: springcloud/eureka
ports:
- 8761
env:
eureka.server.responseCacheUpdateIntervalMs: 1000
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.3
with:
stable: "false"
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
env:
ETCD_ADDR: http://localhost:${{ job.services.etcd.ports[2379] }}
CONSUL_ADDR: localhost:${{ job.services.consul.ports[8500] }}
ZK_ADDR: localhost:${{ job.services.zk.ports[2181] }}
EUREKA_ADDR: http://localhost:${{ job.services.eureka.ports[8761] }}/eureka
run: go test -v -race -coverprofile=coverage.coverprofile -covermode=atomic -tags integration ./...
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: coverage.coverprofile
================================================
FILE: .gitignore
================================================
*.coverprofile
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
_old*
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
# https://github.com/github/gitignore/blob/master/Global/Vim.gitignore
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
First, thank you for contributing! We love and encourage pull requests from everyone.
Before submitting major changes, here are a few guidelines to follow:
1. Check the [open issues][issues] and [pull requests][prs] for existing discussions.
1. Open an [issue][issues] first, to discuss a new feature or enhancement.
1. Write tests, and make sure the test suite passes locally and on CI.
1. Open a pull request, and reference the relevant issue(s).
1. After receiving feedback, [squash your commits][squash] and add a [great commit message][message].
1. Have fun!
[issues]: https://github.com/go-kit/kit/issues
[prs]: https://github.com/go-kit/kit/pulls
[squash]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html
[message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Peter Bourgon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Go kit

[](https://pkg.go.dev/github.com/go-kit/kit?tab=doc)
[](https://codecov.io/gh/go-kit/kit)
[](https://goreportcard.com/report/go-kit/kit)
[](https://sourcegraph.com/github.com/go-kit/kit?badge)
**Go kit** is a **programming toolkit** for building microservices
(or elegant monoliths) in Go. We solve common problems in distributed
systems and application architecture so you can focus on delivering
business value.
- Website: [gokit.io](https://gokit.io)
- Mailing list: [go-kit](https://groups.google.com/forum/#!forum/go-kit)
- Slack: [gophers.slack.com](https://gophers.slack.com) **#go-kit** ([invite](https://gophersinvite.herokuapp.com/))
## Sponsors
Click [here](https://github.com/sponsors/peterbourgon) or Sponsor, above, for more information on sponsorship.
## Motivation
Go has emerged as the language of the server, but it remains underrepresented
in so-called "modern enterprise" companies like Facebook, Twitter, Netflix, and
SoundCloud. Many of these organizations have turned to JVM-based stacks for
their business logic, owing in large part to libraries and ecosystems that
directly support their microservice architectures.
To reach its next level of success, Go needs more than simple primitives and
idioms. It needs a comprehensive toolkit, for coherent distributed programming
in the large. Go kit is a set of packages and best practices, which provide a
comprehensive, robust, and trustable way of building microservices for
organizations of any size.
For more details, see
[the website](https://gokit.io),
[the motivating blog post](http://peter.bourgon.org/go-kit/) and
[the video of the talk](https://www.youtube.com/watch?v=iFR_7AKkJFU).
See also the
[Go kit talk at GopherCon 2015](https://www.youtube.com/watch?v=1AjaZi4QuGo).
## Goals
- Operate in a heterogeneous SOA — expect to interact with mostly non-Go-kit services
- RPC as the primary messaging pattern
- Pluggable serialization and transport — not just JSON over HTTP
- Operate within existing infrastructures — no mandates for specific tools or technologies
## Non-goals
- Supporting messaging patterns other than RPC (for now) — e.g. MPI, pub/sub, CQRS, etc.
- Re-implementing functionality that can be provided by adapting existing software
- Having opinions on operational concerns: deployment, configuration, process supervision, orchestration, etc.
## Contributing
Please see [CONTRIBUTING.md](/CONTRIBUTING.md).
Thank you, [contributors](https://github.com/go-kit/kit/graphs/contributors)!
## Dependency management
Go kit is [modules](https://github.com/golang/go/wiki/Modules) aware, and we
encourage users to use the standard modules tooling. But Go kit is at major
version 0, so it should be compatible with non-modules environments.
## Code generators
There are several third-party tools that can generate Go kit code based on
different starting assumptions.
- [RecoLabs/microgen](https://github.com/RecoLabs/microgen)
- [GrantZheng/kit](https://github.com/GrantZheng/kit)
- [kujtimiihoxha/kit](https://github.com/kujtimiihoxha/kit) (unmaintained)
- [nytimes/marvin](https://github.com/nytimes/marvin)
- [sagikazarmark/mga](https://github.com/sagikazarmark/mga)
- [sagikazarmark/protoc-gen-go-kit](https://github.com/sagikazarmark/protoc-gen-go-kit)
- [metaverse/truss](https://github.com/metaverse/truss)
- [goadesign/goakit](https://github.com/goadesign/plugins/tree/v3/goakit)
## Related projects
Projects with a ★ have had particular influence on Go kit's design (or vice-versa).
### Service frameworks
- [gizmo](https://github.com/nytimes/gizmo), a microservice toolkit from The New York Times ★
- [go-micro](https://github.com/micro/go-micro), a distributed systems development framework ★
- [gotalk](https://github.com/rsms/gotalk), async peer communication protocol & library
- [Kite](https://github.com/koding/kite), a micro-service framework
- [gocircuit](https://github.com/gocircuit/circuit), dynamic cloud orchestration
### Individual components
- [afex/hystrix-go](https://github.com/afex/hystrix-go), client-side latency and fault tolerance library
- [armon/go-metrics](https://github.com/armon/go-metrics), library for exporting performance and runtime metrics to external metrics systems
- [codahale/lunk](https://github.com/codahale/lunk), structured logging in the style of Google's Dapper or Twitter's Zipkin
- [eapache/go-resiliency](https://github.com/eapache/go-resiliency), resiliency patterns
- [sasbury/logging](https://github.com/sasbury/logging), a tagged style of logging
- [grpc/grpc-go](https://github.com/grpc/grpc-go), HTTP/2 based RPC
- [inconshreveable/log15](https://github.com/inconshreveable/log15), simple, powerful logging for Go ★
- [mailgun/vulcand](https://github.com/vulcand/vulcand), programmatic load balancer backed by etcd
- [mattheath/phosphor](https://github.com/mondough/phosphor), distributed system tracing
- [pivotal-golang/lager](https://github.com/pivotal-golang/lager), an opinionated logging library
- [rubyist/circuitbreaker](https://github.com/rubyist/circuitbreaker), circuit breaker library
- [sirupsen/logrus](https://github.com/sirupsen/logrus), structured, pluggable logging for Go ★
- [sourcegraph/appdash](https://github.com/sourcegraph/appdash), application tracing system based on Google's Dapper
- [spacemonkeygo/monitor](https://github.com/spacemonkeygo/monitor), data collection, monitoring, instrumentation, and Zipkin client library
- [streadway/handy](https://github.com/streadway/handy), net/http handler filters
- [vitess/rpcplus](https://godoc.org/github.com/youtube/vitess/go/rpcplus), package rpc + context.Context
- [gdamore/mangos](https://github.com/gdamore/mangos), nanomsg implementation in pure Go
### Web frameworks
- [Gorilla](http://www.gorillatoolkit.org)
- [Gin](https://gin-gonic.com/)
- [Negroni](https://github.com/codegangsta/negroni)
- [Goji](https://github.com/zenazn/goji)
- [Martini](https://github.com/go-martini/martini)
- [Beego](https://beego.vip/)
- [Revel](https://revel.github.io/) (considered [harmful](https://github.com/go-kit/kit/issues/350))
- [GoBuffalo](https://gobuffalo.io/)
## Additional reading
- [Architecting for the Cloud](https://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
================================================
FILE: auth/basic/README.md
================================================
This package provides a Basic Authentication middleware.
It'll try to compare credentials from Authentication request header to a username/password pair in middleware constructor.
More details about this type of authentication can be found in [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
## Usage
```go
import httptransport "github.com/go-kit/kit/transport/http"
httptransport.NewServer(
AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm")(makeUppercaseEndpoint()),
decodeMappingsRequest,
httptransport.EncodeJSONResponse,
httptransport.ServerBefore(httptransport.PopulateRequestContext),
)
```
For AuthMiddleware to be able to pick up the Authentication header from an HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```.
================================================
FILE: auth/basic/middleware.go
================================================
package basic
import (
"bytes"
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http"
"strings"
"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
)
// AuthError represents an authorization error.
type AuthError struct {
Realm string
}
// StatusCode is an implementation of the StatusCoder interface in go-kit/http.
func (AuthError) StatusCode() int {
return http.StatusUnauthorized
}
// Error is an implementation of the Error interface.
func (AuthError) Error() string {
return http.StatusText(http.StatusUnauthorized)
}
// Headers is an implementation of the Headerer interface in go-kit/http.
func (e AuthError) Headers() http.Header {
return http.Header{
"Content-Type": []string{"text/plain; charset=utf-8"},
"X-Content-Type-Options": []string{"nosniff"},
"WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)},
}
}
// parseBasicAuth parses an HTTP Basic Authentication string.
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true).
func parseBasicAuth(auth string) (username, password []byte, ok bool) {
const prefix = "Basic "
if !strings.HasPrefix(auth, prefix) {
return
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return
}
s := bytes.IndexByte(c, ':')
if s < 0 {
return
}
return c[:s], c[s+1:], true
}
// Returns a hash of a given slice.
func toHashSlice(s []byte) []byte {
hash := sha256.Sum256(s)
return hash[:]
}
// AuthMiddleware returns a Basic Authentication middleware for a particular user and password.
func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware {
requiredUserBytes := toHashSlice([]byte(requiredUser))
requiredPasswordBytes := toHashSlice([]byte(requiredPassword))
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
auth, ok := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string)
if !ok {
return nil, AuthError{realm}
}
givenUser, givenPassword, ok := parseBasicAuth(auth)
if !ok {
return nil, AuthError{realm}
}
givenUserBytes := toHashSlice(givenUser)
givenPasswordBytes := toHashSlice(givenPassword)
if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 ||
subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 {
return nil, AuthError{realm}
}
return next(ctx, request)
}
}
}
================================================
FILE: auth/basic/middleware_test.go
================================================
package basic
import (
"context"
"encoding/base64"
"fmt"
"testing"
httptransport "github.com/go-kit/kit/transport/http"
)
func TestWithBasicAuth(t *testing.T) {
requiredUser := "test-user"
requiredPassword := "test-pass"
realm := "test realm"
type want struct {
result interface{}
err error
}
tests := []struct {
name string
authHeader interface{}
want want
}{
{"Isn't valid with nil header", nil, want{nil, AuthError{realm}}},
{"Isn't valid with non-string header", 42, want{nil, AuthError{realm}}},
{"Isn't valid without authHeader", "", want{nil, AuthError{realm}}},
{"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}},
{"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}},
{"Is valid for correct creds", makeAuthString(requiredUser, requiredPassword), want{true, nil}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.WithValue(context.TODO(), httptransport.ContextKeyRequestAuthorization, tt.authHeader)
result, err := AuthMiddleware(requiredUser, requiredPassword, realm)(passedValidation)(ctx, nil)
if result != tt.want.result || err != tt.want.err {
t.Errorf("WithBasicAuth() = result: %v, err: %v, want result: %v, want error: %v", result, err, tt.want.result, tt.want.err)
}
})
}
}
func makeAuthString(user string, password string) string {
data := []byte(fmt.Sprintf("%s:%s", user, password))
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(data))
}
func passedValidation(ctx context.Context, request interface{}) (response interface{}, err error) {
return true, nil
}
================================================
FILE: auth/casbin/middleware.go
================================================
package casbin
import (
"context"
"errors"
stdcasbin "github.com/casbin/casbin/v2"
"github.com/go-kit/kit/endpoint"
)
type contextKey string
const (
// CasbinModelContextKey holds the key to store the access control model
// in context, it can be a path to configuration file or a casbin/model
// Model.
CasbinModelContextKey contextKey = "CasbinModel"
// CasbinPolicyContextKey holds the key to store the access control policy
// in context, it can be a path to policy file or an implementation of
// casbin/persist Adapter interface.
CasbinPolicyContextKey contextKey = "CasbinPolicy"
// CasbinEnforcerContextKey holds the key to retrieve the active casbin
// Enforcer.
CasbinEnforcerContextKey contextKey = "CasbinEnforcer"
)
var (
// ErrModelContextMissing denotes a casbin model was not passed into
// the parsing of middleware's context.
ErrModelContextMissing = errors.New("CasbinModel is required in context")
// ErrPolicyContextMissing denotes a casbin policy was not passed into
// the parsing of middleware's context.
ErrPolicyContextMissing = errors.New("CasbinPolicy is required in context")
// ErrUnauthorized denotes the subject is not authorized to do the action
// intended on the given object, based on the context model and policy.
ErrUnauthorized = errors.New("Unauthorized Access")
)
// NewEnforcer checks whether the subject is authorized to do the specified
// action on the given object. If a valid access control model and policy
// is given, then the generated casbin Enforcer is stored in the context
// with CasbinEnforcer as the key.
func NewEnforcer(
subject string, object interface{}, action string,
) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
casbinModel := ctx.Value(CasbinModelContextKey)
casbinPolicy := ctx.Value(CasbinPolicyContextKey)
enforcer, err := stdcasbin.NewEnforcer(casbinModel, casbinPolicy)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, CasbinEnforcerContextKey, enforcer)
ok, err := enforcer.Enforce(subject, object, action)
if err != nil {
return nil, err
}
if !ok {
return nil, ErrUnauthorized
}
return next(ctx, request)
}
}
}
================================================
FILE: auth/casbin/middleware_test.go
================================================
package casbin
import (
"context"
"testing"
stdcasbin "github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
fileadapter "github.com/casbin/casbin/v2/persist/file-adapter"
)
func TestStructBaseContext(t *testing.T) {
e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil }
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)")
a := fileadapter.NewAdapter("testdata/keymatch_policy.csv")
ctx := context.WithValue(context.Background(), CasbinModelContextKey, m)
ctx = context.WithValue(ctx, CasbinPolicyContextKey, a)
// positive case
middleware := NewEnforcer("alice", "/alice_data/resource1", "GET")(e)
ctx1, err := middleware(ctx, struct{}{})
if err != nil {
t.Fatalf("Enforcer returned error: %s", err)
}
_, ok := ctx1.(context.Context).Value(CasbinEnforcerContextKey).(*stdcasbin.Enforcer)
if !ok {
t.Fatalf("context should contains the active enforcer")
}
// negative case
middleware = NewEnforcer("alice", "/alice_data/resource2", "POST")(e)
_, err = middleware(ctx, struct{}{})
if err == nil {
t.Fatalf("Enforcer should return error")
}
}
func TestFileBaseContext(t *testing.T) {
e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil }
ctx := context.WithValue(context.Background(), CasbinModelContextKey, "testdata/basic_model.conf")
ctx = context.WithValue(ctx, CasbinPolicyContextKey, "testdata/basic_policy.csv")
// positive case
middleware := NewEnforcer("alice", "data1", "read")(e)
_, err := middleware(ctx, struct{}{})
if err != nil {
t.Fatalf("Enforcer returned error: %s", err)
}
}
================================================
FILE: auth/casbin/testdata/basic_model.conf
================================================
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
================================================
FILE: auth/casbin/testdata/basic_policy.csv
================================================
p, alice, data1, read
p, bob, data2, write
================================================
FILE: auth/casbin/testdata/keymatch_policy.csv
================================================
p, alice, /alice_data/*, GET
p, alice, /alice_data/resource1, POST
p, bob, /alice_data/resource2, GET
p, bob, /bob_data/*, POST
p, cathy, /cathy_data, (GET)|(POST)
================================================
FILE: auth/jwt/README.md
================================================
# package auth/jwt
`package auth/jwt` provides a set of interfaces for service authorization
through [JSON Web Tokens](https://jwt.io/).
## Usage
NewParser takes a key function and an expected signing method and returns an
`endpoint.Middleware`. The middleware will parse a token passed into the
context via the `jwt.JWTContextKey`. If the token is valid, any claims
will be added to the context via the `jwt.JWTClaimsContextKey`.
```go
import (
stdjwt "github.com/golang-jwt/jwt/v4"
"github.com/go-kit/kit/auth/jwt"
"github.com/go-kit/kit/endpoint"
)
func main() {
var exampleEndpoint endpoint.Endpoint
{
kf := func(token *stdjwt.Token) (interface{}, error) { return []byte("SigningString"), nil }
exampleEndpoint = MakeExampleEndpoint(service)
exampleEndpoint = jwt.NewParser(kf, stdjwt.SigningMethodHS256, jwt.StandardClaimsFactory)(exampleEndpoint)
}
}
```
NewSigner takes a JWT key ID header, the signing key, signing method, and a
claims object. It returns an `endpoint.Middleware`. The middleware will build
the token string and add it to the context via the `jwt.JWTContextKey`.
```go
import (
stdjwt "github.com/golang-jwt/jwt/v4"
"github.com/go-kit/kit/auth/jwt"
"github.com/go-kit/kit/endpoint"
)
func main() {
var exampleEndpoint endpoint.Endpoint
{
exampleEndpoint = grpctransport.NewClient(...).Endpoint()
exampleEndpoint = jwt.NewSigner(
"kid-header",
[]byte("SigningString"),
stdjwt.SigningMethodHS256,
jwt.Claims{},
)(exampleEndpoint)
}
}
```
In order for the parser and the signer to work, the authorization headers need
to be passed between the request and the context. `HTTPToContext()`,
`ContextToHTTP()`, `GRPCToContext()`, and `ContextToGRPC()` are given as
helpers to do this. These functions implement the correlating transport's
RequestFunc interface and can be passed as ClientBefore or ServerBefore
options.
Example of use in a client:
```go
import (
stdjwt "github.com/golang-jwt/jwt/v4"
grpctransport "github.com/go-kit/kit/transport/grpc"
"github.com/go-kit/kit/auth/jwt"
"github.com/go-kit/kit/endpoint"
)
func main() {
options := []httptransport.ClientOption{}
var exampleEndpoint endpoint.Endpoint
{
exampleEndpoint = grpctransport.NewClient(..., grpctransport.ClientBefore(jwt.ContextToGRPC())).Endpoint()
exampleEndpoint = jwt.NewSigner(
"kid-header",
[]byte("SigningString"),
stdjwt.SigningMethodHS256,
jwt.Claims{},
)(exampleEndpoint)
}
}
```
Example of use in a server:
```go
import (
"context"
"github.com/go-kit/kit/auth/jwt"
"github.com/go-kit/log"
grpctransport "github.com/go-kit/kit/transport/grpc"
)
func MakeGRPCServer(ctx context.Context, endpoints Endpoints, logger log.Logger) pb.ExampleServer {
options := []grpctransport.ServerOption{grpctransport.ServerErrorLogger(logger)}
return &grpcServer{
createUser: grpctransport.NewServer(
ctx,
endpoints.CreateUserEndpoint,
DecodeGRPCCreateUserRequest,
EncodeGRPCCreateUserResponse,
append(options, grpctransport.ServerBefore(jwt.GRPCToContext()))...,
),
getUser: grpctransport.NewServer(
ctx,
endpoints.GetUserEndpoint,
DecodeGRPCGetUserRequest,
EncodeGRPCGetUserResponse,
options...,
),
}
}
```
================================================
FILE: auth/jwt/middleware.go
================================================
package jwt
import (
"context"
"errors"
"github.com/go-kit/kit/endpoint"
"github.com/golang-jwt/jwt/v4"
)
type contextKey string
const (
// JWTContextKey holds the key used to store a JWT in the context.
JWTContextKey contextKey = "JWTToken"
// JWTTokenContextKey is an alias for JWTContextKey.
//
// Deprecated: prefer JWTContextKey.
JWTTokenContextKey = JWTContextKey
// JWTClaimsContextKey holds the key used to store the JWT Claims in the
// context.
JWTClaimsContextKey contextKey = "JWTClaims"
)
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 was invalid")
// ErrTokenExpired denotes a token's expire header (exp) has since passed.
ErrTokenExpired = errors.New("JWT is expired")
// ErrTokenMalformed denotes a token was not formatted as a JWT.
ErrTokenMalformed = errors.New("JWT 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
// signing method.
ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
)
// NewSigner creates a new JWT generating middleware, specifying key ID,
// signing string, signing method and the claims you would like it to contain.
// Tokens are signed with a Key ID header (kid) which is useful for determining
// the key to use for parsing. Particularly useful for clients.
func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
token := jwt.NewWithClaims(method, claims)
token.Header["kid"] = kid
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(key)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, JWTContextKey, tokenString)
return next(ctx, request)
}
}
}
// ClaimsFactory is a factory for jwt.Claims.
// Useful in NewParser middleware.
type ClaimsFactory func() jwt.Claims
// MapClaimsFactory is a ClaimsFactory that returns
// an empty jwt.MapClaims.
func MapClaimsFactory() jwt.Claims {
return jwt.MapClaims{}
}
// StandardClaimsFactory is a ClaimsFactory that returns
// an empty jwt.StandardClaims.
func StandardClaimsFactory() jwt.Claims {
return &jwt.StandardClaims{}
}
// NewParser creates a new JWT parsing middleware, specifying a
// jwt.Keyfunc interface, the signing method and the claims type to be used. NewParser
// adds the resulting claims to endpoint context or returns error on invalid token.
// Particularly useful for servers.
func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod, newClaims ClaimsFactory) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
// tokenString is stored in the context from the transport handlers.
tokenString, ok := ctx.Value(JWTContextKey).(string)
if !ok {
return nil, ErrTokenContextMissing
}
// Parse takes the token string and a function for looking up the
// key. The latter is especially useful if you use multiple keys
// for your application. The standard is to use 'kid' in the head
// of the token to identify which key to use, but the parsed token
// (head and claims) is provided to the callback, providing
// flexibility.
token, err := jwt.ParseWithClaims(tokenString, newClaims(), func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if token.Method != method {
return nil, ErrUnexpectedSigningMethod
}
return keyFunc(token)
})
if err != nil {
if e, ok := err.(*jwt.ValidationError); ok {
switch {
case e.Errors&jwt.ValidationErrorMalformed != 0:
// Token is malformed
return nil, ErrTokenMalformed
case e.Errors&jwt.ValidationErrorExpired != 0:
// Token is expired
return nil, ErrTokenExpired
case e.Errors&jwt.ValidationErrorNotValidYet != 0:
// Token is not active yet
return nil, ErrTokenNotActive
case e.Inner != nil:
// report e.Inner
return nil, e.Inner
}
// We have a ValidationError but have no specific Go kit error for it.
// Fall through to return original error.
}
return nil, err
}
if !token.Valid {
return nil, ErrTokenInvalid
}
ctx = context.WithValue(ctx, JWTClaimsContextKey, token.Claims)
return next(ctx, request)
}
}
}
================================================
FILE: auth/jwt/middleware_test.go
================================================
package jwt
import (
"context"
"sync"
"testing"
"time"
"crypto/subtle"
"github.com/go-kit/kit/endpoint"
"github.com/golang-jwt/jwt/v4"
)
type customClaims struct {
MyProperty string `json:"my_property"`
jwt.StandardClaims
}
func (c customClaims) VerifyMyProperty(p string) bool {
return subtle.ConstantTimeCompare([]byte(c.MyProperty), []byte(p)) != 0
}
var (
kid = "kid"
key = []byte("test_signing_key")
myProperty = "some value"
method = jwt.SigningMethodHS256
invalidMethod = jwt.SigningMethodRS256
mapClaims = jwt.MapClaims{"user": "go-kit"}
standardClaims = jwt.StandardClaims{Audience: "go-kit"}
myCustomClaims = customClaims{MyProperty: myProperty, StandardClaims: standardClaims}
// Signed tokens generated at https://jwt.io/
signedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E"
standardSignedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJnby1raXQifQ.L5ypIJjCOOv3jJ8G5SelaHvR04UJuxmcBN5QW3m_aoY"
customSignedKey = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJteV9wcm9wZXJ0eSI6InNvbWUgdmFsdWUiLCJhdWQiOiJnby1raXQifQ.s8F-IDrV4WPJUsqr7qfDi-3GRlcKR0SRnkTeUT_U-i0"
invalidKey = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.vKVCKto-Wn6rgz3vBdaZaCBGfCBDTXOENSo_X2Gq7qA"
malformedKey = "malformed.jwt.token"
)
func signingValidator(t *testing.T, signer endpoint.Endpoint, expectedKey string) {
ctx, err := signer(context.Background(), struct{}{})
if err != nil {
t.Fatalf("Signer returned error: %s", err)
}
token, ok := ctx.(context.Context).Value(JWTContextKey).(string)
if !ok {
t.Fatal("Token did not exist in context")
}
if token != expectedKey {
t.Fatalf("JWTs did not match: expecting %s got %s", expectedKey, token)
}
}
func TestNewSigner(t *testing.T) {
e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil }
signer := NewSigner(kid, key, method, mapClaims)(e)
signingValidator(t, signer, signedKey)
signer = NewSigner(kid, key, method, standardClaims)(e)
signingValidator(t, signer, standardSignedKey)
signer = NewSigner(kid, key, method, myCustomClaims)(e)
signingValidator(t, signer, customSignedKey)
}
func TestJWTParser(t *testing.T) {
e := func(ctx context.Context, i interface{}) (interface{}, error) { return ctx, nil }
keys := func(token *jwt.Token) (interface{}, error) {
return key, nil
}
parser := NewParser(keys, method, MapClaimsFactory)(e)
// No Token is passed into the parser
_, err := parser(context.Background(), struct{}{})
if err == nil {
t.Error("Parser should have returned an error")
}
if err != ErrTokenContextMissing {
t.Errorf("unexpected error returned, expected: %s got: %s", ErrTokenContextMissing, err)
}
// Invalid Token is passed into the parser
ctx := context.WithValue(context.Background(), JWTContextKey, invalidKey)
_, err = parser(ctx, struct{}{})
if err == nil {
t.Error("Parser should have returned an error")
}
// Invalid Method is used in the parser
badParser := NewParser(keys, invalidMethod, MapClaimsFactory)(e)
ctx = context.WithValue(context.Background(), JWTContextKey, signedKey)
_, err = badParser(ctx, struct{}{})
if err == nil {
t.Error("Parser should have returned an error")
}
if err != ErrUnexpectedSigningMethod {
t.Errorf("unexpected error returned, expected: %s got: %s", ErrUnexpectedSigningMethod, err)
}
// Invalid key is used in the parser
invalidKeys := func(token *jwt.Token) (interface{}, error) {
return []byte("bad"), nil
}
badParser = NewParser(invalidKeys, method, MapClaimsFactory)(e)
ctx = context.WithValue(context.Background(), JWTContextKey, signedKey)
_, err = badParser(ctx, struct{}{})
if err == nil {
t.Error("Parser should have returned an error")
}
// Correct token is passed into the parser
ctx = context.WithValue(context.Background(), JWTContextKey, signedKey)
ctx1, err := parser(ctx, struct{}{})
if err != nil {
t.Fatalf("Parser returned error: %s", err)
}
cl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(jwt.MapClaims)
if !ok {
t.Fatal("Claims were not passed into context correctly")
}
if cl["user"] != mapClaims["user"] {
t.Fatalf("JWT Claims.user did not match: expecting %s got %s", mapClaims["user"], cl["user"])
}
// Test for malformed token error response
parser = NewParser(keys, method, StandardClaimsFactory)(e)
ctx = context.WithValue(context.Background(), JWTContextKey, malformedKey)
ctx1, err = parser(ctx, struct{}{})
if want, have := ErrTokenMalformed, err; want != have {
t.Fatalf("Expected %+v, got %+v", want, have)
}
// Test for expired token error response
parser = NewParser(keys, method, StandardClaimsFactory)(e)
expired := jwt.NewWithClaims(method, jwt.StandardClaims{ExpiresAt: time.Now().Unix() - 100})
token, err := expired.SignedString(key)
if err != nil {
t.Fatalf("Unable to Sign Token: %+v", err)
}
ctx = context.WithValue(context.Background(), JWTContextKey, token)
ctx1, err = parser(ctx, struct{}{})
if want, have := ErrTokenExpired, err; want != have {
t.Fatalf("Expected %+v, got %+v", want, have)
}
// Test for not activated token error response
parser = NewParser(keys, method, StandardClaimsFactory)(e)
notactive := jwt.NewWithClaims(method, jwt.StandardClaims{NotBefore: time.Now().Unix() + 100})
token, err = notactive.SignedString(key)
if err != nil {
t.Fatalf("Unable to Sign Token: %+v", err)
}
ctx = context.WithValue(context.Background(), JWTContextKey, token)
ctx1, err = parser(ctx, struct{}{})
if want, have := ErrTokenNotActive, err; want != have {
t.Fatalf("Expected %+v, got %+v", want, have)
}
// test valid standard claims token
parser = NewParser(keys, method, StandardClaimsFactory)(e)
ctx = context.WithValue(context.Background(), JWTContextKey, standardSignedKey)
ctx1, err = parser(ctx, struct{}{})
if err != nil {
t.Fatalf("Parser returned error: %s", err)
}
stdCl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(*jwt.StandardClaims)
if !ok {
t.Fatal("Claims were not passed into context correctly")
}
if !stdCl.VerifyAudience("go-kit", true) {
t.Fatalf("JWT jwt.StandardClaims.Audience did not match: expecting %s got %s", standardClaims.Audience, stdCl.Audience)
}
// test valid customized claims token
parser = NewParser(keys, method, func() jwt.Claims { return &customClaims{} })(e)
ctx = context.WithValue(context.Background(), JWTContextKey, customSignedKey)
ctx1, err = parser(ctx, struct{}{})
if err != nil {
t.Fatalf("Parser returned error: %s", err)
}
custCl, ok := ctx1.(context.Context).Value(JWTClaimsContextKey).(*customClaims)
if !ok {
t.Fatal("Claims were not passed into context correctly")
}
if !custCl.VerifyAudience("go-kit", true) {
t.Fatalf("JWT customClaims.Audience did not match: expecting %s got %s", standardClaims.Audience, custCl.Audience)
}
if !custCl.VerifyMyProperty(myProperty) {
t.Fatalf("JWT customClaims.MyProperty did not match: expecting %s got %s", myProperty, custCl.MyProperty)
}
}
func TestIssue562(t *testing.T) {
var (
kf = func(token *jwt.Token) (interface{}, error) { return []byte("secret"), nil }
e = NewParser(kf, jwt.SigningMethodHS256, MapClaimsFactory)(endpoint.Nop)
key = JWTContextKey
val = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtpZCIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ28ta2l0In0.14M2VmYyApdSlV_LZ88ajjwuaLeIFplB8JpyNy0A19E"
ctx = context.WithValue(context.Background(), key, val)
)
wg := sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
e(ctx, struct{}{}) // fatal error: concurrent map read and map write
}()
}
wg.Wait()
}
================================================
FILE: auth/jwt/transport.go
================================================
package jwt
import (
"context"
"fmt"
stdhttp "net/http"
"strings"
"google.golang.org/grpc/metadata"
"github.com/go-kit/kit/transport/grpc"
"github.com/go-kit/kit/transport/http"
)
const (
bearer string = "bearer"
bearerFormat string = "Bearer %s"
)
// HTTPToContext moves a JWT from request header to context. Particularly
// useful for servers.
func HTTPToContext() http.RequestFunc {
return func(ctx context.Context, r *stdhttp.Request) context.Context {
token, ok := extractTokenFromAuthHeader(r.Header.Get("Authorization"))
if !ok {
return ctx
}
return context.WithValue(ctx, JWTContextKey, token)
}
}
// ContextToHTTP moves a JWT from context to request header. Particularly
// useful for clients.
func ContextToHTTP() http.RequestFunc {
return func(ctx context.Context, r *stdhttp.Request) context.Context {
token, ok := ctx.Value(JWTContextKey).(string)
if ok {
r.Header.Add("Authorization", generateAuthHeaderFromToken(token))
}
return ctx
}
}
// GRPCToContext moves a JWT from grpc metadata to context. Particularly
// userful for servers.
func GRPCToContext() grpc.ServerRequestFunc {
return func(ctx context.Context, md metadata.MD) context.Context {
// capital "Key" is illegal in HTTP/2.
authHeader, ok := md["authorization"]
if !ok {
return ctx
}
token, ok := extractTokenFromAuthHeader(authHeader[0])
if ok {
ctx = context.WithValue(ctx, JWTContextKey, token)
}
return ctx
}
}
// ContextToGRPC moves a JWT from context to grpc metadata. Particularly
// useful for clients.
func ContextToGRPC() grpc.ClientRequestFunc {
return func(ctx context.Context, md *metadata.MD) context.Context {
token, ok := ctx.Value(JWTContextKey).(string)
if ok {
// capital "Key" is illegal in HTTP/2.
(*md)["authorization"] = []string{generateAuthHeaderFromToken(token)}
}
return ctx
}
}
func extractTokenFromAuthHeader(val string) (token string, ok bool) {
authHeaderParts := strings.Split(val, " ")
if len(authHeaderParts) != 2 || !strings.EqualFold(authHeaderParts[0], bearer) {
return "", false
}
return authHeaderParts[1], true
}
func generateAuthHeaderFromToken(token string) string {
return fmt.Sprintf(bearerFormat, token)
}
================================================
FILE: auth/jwt/transport_test.go
================================================
package jwt
import (
"context"
"fmt"
"net/http"
"testing"
"google.golang.org/grpc/metadata"
)
func TestHTTPToContext(t *testing.T) {
reqFunc := HTTPToContext()
// When the header doesn't exist
ctx := reqFunc(context.Background(), &http.Request{})
if ctx.Value(JWTContextKey) != nil {
t.Error("Context shouldn't contain the encoded JWT")
}
// Authorization header value has invalid format
header := http.Header{}
header.Set("Authorization", "no expected auth header format value")
ctx = reqFunc(context.Background(), &http.Request{Header: header})
if ctx.Value(JWTContextKey) != nil {
t.Error("Context shouldn't contain the encoded JWT")
}
// Authorization header is correct
header.Set("Authorization", generateAuthHeaderFromToken(signedKey))
ctx = reqFunc(context.Background(), &http.Request{Header: header})
token := ctx.Value(JWTContextKey).(string)
if token != signedKey {
t.Errorf("Context doesn't contain the expected encoded token value; expected: %s, got: %s", signedKey, token)
}
}
func TestContextToHTTP(t *testing.T) {
reqFunc := ContextToHTTP()
// No JWT is passed in the context
ctx := context.Background()
r := http.Request{}
reqFunc(ctx, &r)
token := r.Header.Get("Authorization")
if token != "" {
t.Error("authorization key should not exist in metadata")
}
// Correct JWT is passed in the context
ctx = context.WithValue(context.Background(), JWTContextKey, signedKey)
r = http.Request{Header: http.Header{}}
reqFunc(ctx, &r)
token = r.Header.Get("Authorization")
expected := generateAuthHeaderFromToken(signedKey)
if token != expected {
t.Errorf("Authorization header does not contain the expected JWT; expected %s, got %s", expected, token)
}
}
func TestGRPCToContext(t *testing.T) {
md := metadata.MD{}
reqFunc := GRPCToContext()
// No Authorization header is passed
ctx := reqFunc(context.Background(), md)
token := ctx.Value(JWTContextKey)
if token != nil {
t.Error("Context should not contain a JWT")
}
// Invalid Authorization header is passed
md["authorization"] = []string{signedKey}
ctx = reqFunc(context.Background(), md)
token = ctx.Value(JWTContextKey)
if token != nil {
t.Error("Context should not contain a JWT")
}
// Authorization header is correct
md["authorization"] = []string{fmt.Sprintf("Bearer %s", signedKey)}
ctx = reqFunc(context.Background(), md)
token, ok := ctx.Value(JWTContextKey).(string)
if !ok {
t.Fatal("JWT not passed to context correctly")
}
if token != signedKey {
t.Errorf("JWTs did not match: expecting %s got %s", signedKey, token)
}
}
func TestContextToGRPC(t *testing.T) {
reqFunc := ContextToGRPC()
// No JWT is passed in the context
ctx := context.Background()
md := metadata.MD{}
reqFunc(ctx, &md)
_, ok := md["authorization"]
if ok {
t.Error("authorization key should not exist in metadata")
}
// Correct JWT is passed in the context
ctx = context.WithValue(context.Background(), JWTContextKey, signedKey)
md = metadata.MD{}
reqFunc(ctx, &md)
token, ok := md["authorization"]
if !ok {
t.Fatal("JWT not passed to metadata correctly")
}
if token[0] != generateAuthHeaderFromToken(signedKey) {
t.Errorf("JWTs did not match: expecting %s got %s", signedKey, token[0])
}
}
================================================
FILE: circuitbreaker/doc.go
================================================
// Package circuitbreaker implements the circuit breaker pattern.
//
// Circuit breakers prevent thundering herds, and improve resiliency against
// intermittent errors. Every client-side endpoint should be wrapped in a
// circuit breaker.
//
// We provide several implementations in this package, but if you're looking
// for guidance, Gobreaker is probably the best place to start. It has a
// simple and intuitive API, and is well-tested.
package circuitbreaker
================================================
FILE: circuitbreaker/gobreaker.go
================================================
package circuitbreaker
import (
"context"
"github.com/sony/gobreaker"
"github.com/go-kit/kit/endpoint"
)
// Gobreaker returns an endpoint.Middleware that implements the circuit
// breaker pattern using the sony/gobreaker package. Only errors returned by
// the wrapped endpoint count against the circuit breaker's error count.
//
// See http://godoc.org/github.com/sony/gobreaker for more information.
func Gobreaker(cb *gobreaker.CircuitBreaker) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
return cb.Execute(func() (interface{}, error) { return next(ctx, request) })
}
}
}
================================================
FILE: circuitbreaker/gobreaker_test.go
================================================
package circuitbreaker_test
import (
"testing"
"github.com/sony/gobreaker"
"github.com/go-kit/kit/circuitbreaker"
)
func TestGobreaker(t *testing.T) {
var (
breaker = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{}))
primeWith = 100
shouldPass = func(n int) bool { return n <= 5 } // https://github.com/sony/gobreaker/blob/bfa846d/gobreaker.go#L76
circuitOpenError = "circuit breaker is open"
)
testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, circuitOpenError)
}
================================================
FILE: circuitbreaker/handy_breaker.go
================================================
package circuitbreaker
import (
"context"
"time"
"github.com/streadway/handy/breaker"
"github.com/go-kit/kit/endpoint"
)
// HandyBreaker returns an endpoint.Middleware that implements the circuit
// breaker pattern using the streadway/handy/breaker package. Only errors
// returned by the wrapped endpoint count against the circuit breaker's error
// count.
//
// See http://godoc.org/github.com/streadway/handy/breaker for more
// information.
func HandyBreaker(cb breaker.Breaker) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
if !cb.Allow() {
return nil, breaker.ErrCircuitOpen
}
defer func(begin time.Time) {
if err == nil {
cb.Success(time.Since(begin))
} else {
cb.Failure(time.Since(begin))
}
}(time.Now())
response, err = next(ctx, request)
return
}
}
}
================================================
FILE: circuitbreaker/handy_breaker_test.go
================================================
package circuitbreaker_test
import (
"testing"
handybreaker "github.com/streadway/handy/breaker"
"github.com/go-kit/kit/circuitbreaker"
)
func TestHandyBreaker(t *testing.T) {
var (
failureRatio = 0.05
breaker = circuitbreaker.HandyBreaker(handybreaker.NewBreaker(failureRatio))
primeWith = handybreaker.DefaultMinObservations * 10
shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= failureRatio }
openCircuitError = handybreaker.ErrCircuitOpen.Error()
)
testFailingEndpoint(t, breaker, primeWith, shouldPass, 0, openCircuitError)
}
================================================
FILE: circuitbreaker/hystrix.go
================================================
package circuitbreaker
import (
"context"
"github.com/afex/hystrix-go/hystrix"
"github.com/go-kit/kit/endpoint"
)
// Hystrix returns an endpoint.Middleware that implements the circuit
// breaker pattern using the afex/hystrix-go package.
//
// When using this circuit breaker, please configure your commands separately.
//
// See https://godoc.org/github.com/afex/hystrix-go/hystrix for more
// information.
func Hystrix(commandName string) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
var resp interface{}
if err := hystrix.Do(commandName, func() (err error) {
resp, err = next(ctx, request)
return err
}, nil); err != nil {
return nil, err
}
return resp, nil
}
}
}
================================================
FILE: circuitbreaker/hystrix_test.go
================================================
package circuitbreaker_test
import (
"io/ioutil"
stdlog "log"
"testing"
"time"
"github.com/afex/hystrix-go/hystrix"
"github.com/go-kit/kit/circuitbreaker"
)
func TestHystrix(t *testing.T) {
stdlog.SetOutput(ioutil.Discard)
const (
commandName = "my-endpoint"
errorPercent = 5
maxConcurrent = 1000
)
hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{
ErrorPercentThreshold: errorPercent,
MaxConcurrentRequests: maxConcurrent,
})
var (
breaker = circuitbreaker.Hystrix(commandName)
primeWith = hystrix.DefaultVolumeThreshold * 2
shouldPass = func(n int) bool { return (float64(n) / float64(primeWith+n)) <= (float64(errorPercent-1) / 100.0) }
openCircuitError = hystrix.ErrCircuitOpen.Error()
)
// hystrix-go uses buffered channels to receive reports on request success/failure,
// and so is basically impossible to test deterministically. We have to make sure
// the report buffer is emptied, by injecting a sleep between each invocation.
requestDelay := 5 * time.Millisecond
testFailingEndpoint(t, breaker, primeWith, shouldPass, requestDelay, openCircuitError)
}
================================================
FILE: circuitbreaker/util_test.go
================================================
package circuitbreaker_test
import (
"context"
"errors"
"fmt"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
)
func testFailingEndpoint(
t *testing.T,
breaker endpoint.Middleware,
primeWith int,
shouldPass func(int) bool,
requestDelay time.Duration,
openCircuitError string,
) {
_, file, line, _ := runtime.Caller(1)
caller := fmt.Sprintf("%s:%d", filepath.Base(file), line)
// Create a mock endpoint and wrap it with the breaker.
m := mock{}
var e endpoint.Endpoint
e = m.endpoint
e = breaker(e)
// Prime the endpoint with successful requests.
for i := 0; i < primeWith; i++ {
if _, err := e(context.Background(), struct{}{}); err != nil {
t.Fatalf("%s: during priming, got error: %v", caller, err)
}
time.Sleep(requestDelay)
}
// Switch the endpoint to start throwing errors.
m.err = errors.New("tragedy+disaster")
m.through = 0
// The first several should be allowed through and yield our error.
for i := 0; shouldPass(i); i++ {
if _, err := e(context.Background(), struct{}{}); err != m.err {
t.Fatalf("%s: want %v, have %v", caller, m.err, err)
}
time.Sleep(requestDelay)
}
through := m.through
// But the rest should be blocked by an open circuit.
for i := 0; i < 10; i++ {
if _, err := e(context.Background(), struct{}{}); err.Error() != openCircuitError {
t.Fatalf("%s: want %q, have %q", caller, openCircuitError, err.Error())
}
time.Sleep(requestDelay)
}
// Make sure none of those got through.
if want, have := through, m.through; want != have {
t.Errorf("%s: want %d, have %d", caller, want, have)
}
}
type mock struct {
through int
err error
}
func (m *mock) endpoint(context.Context, interface{}) (interface{}, error) {
m.through++
return struct{}{}, m.err
}
================================================
FILE: codecov.yml
================================================
comment: false
================================================
FILE: docker-compose-integration.yml
================================================
version: '2'
services:
etcd:
image: gcr.io/etcd-development/etcd:v3.5.0
ports:
- "2379:2379"
environment:
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
consul:
image: consul:1.7
ports:
- "8500:8500"
zk:
image: zookeeper:3.5
ports:
- "2181:2181"
eureka:
image: springcloud/eureka
environment:
eureka.server.responseCacheUpdateIntervalMs: 1000
ports:
- "8761:8761"
================================================
FILE: endpoint/doc.go
================================================
// Package endpoint defines an abstraction for RPCs.
//
// Endpoints are a fundamental building block for many Go kit components.
// Endpoints are implemented by servers, and called by clients.
package endpoint
================================================
FILE: endpoint/endpoint.go
================================================
package endpoint
import (
"context"
)
// Endpoint is the fundamental building block of servers and clients.
// It represents a single RPC method.
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
// Nop is an endpoint that does nothing and returns a nil error.
// Useful for tests.
func Nop(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil }
// Middleware is a chainable behavior modifier for endpoints.
type Middleware func(Endpoint) Endpoint
// Chain is a helper function for composing middlewares. Requests will
// traverse them in the order they're declared. That is, the first middleware
// is treated as the outermost middleware.
func Chain(outer Middleware, others ...Middleware) Middleware {
return func(next Endpoint) Endpoint {
for i := len(others) - 1; i >= 0; i-- { // reverse
next = others[i](next)
}
return outer(next)
}
}
// Failer may be implemented by Go kit response types that contain business
// logic error details. If Failed returns a non-nil error, the Go kit transport
// layer may interpret this as a business logic error, and may encode it
// differently than a regular, successful response.
//
// It's not necessary for your response types to implement Failer, but it may
// help for more sophisticated use cases. The addsvc example shows how Failer
// should be used by a complete application.
type Failer interface {
Failed() error
}
================================================
FILE: endpoint/endpoint_example_test.go
================================================
package endpoint_test
import (
"context"
"fmt"
"github.com/go-kit/kit/endpoint"
)
func ExampleChain() {
e := endpoint.Chain(
annotate("first"),
annotate("second"),
annotate("third"),
)(myEndpoint)
if _, err := e(ctx, req); err != nil {
panic(err)
}
// Output:
// first pre
// second pre
// third pre
// my endpoint!
// third post
// second post
// first post
}
var (
ctx = context.Background()
req = struct{}{}
)
func annotate(s string) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
fmt.Println(s, "pre")
defer fmt.Println(s, "post")
return next(ctx, request)
}
}
}
func myEndpoint(context.Context, interface{}) (interface{}, error) {
fmt.Println("my endpoint!")
return struct{}{}, nil
}
================================================
FILE: examples/README.md
================================================
# Examples
Examples have been relocated to a separate repository: https://github.com/go-kit/examples
================================================
FILE: go.mod
================================================
module github.com/go-kit/kit
go 1.17
require (
github.com/VividCortex/gohistogram v1.0.0
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
github.com/aws/aws-sdk-go v1.40.45
github.com/aws/aws-sdk-go-v2 v1.9.1
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1
github.com/casbin/casbin/v2 v2.37.0
github.com/go-kit/log v0.2.0
github.com/go-zookeeper/zk v1.0.2
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/hashicorp/consul/api v1.14.0
github.com/hudl/fargo v1.4.0
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab
github.com/nats-io/nats-server/v2 v2.8.4
github.com/nats-io/nats.go v1.15.0
github.com/opentracing/opentracing-go v1.2.0
github.com/openzipkin/zipkin-go v0.2.5
github.com/performancecopilot/speed/v4 v4.0.0
github.com/prometheus/client_golang v1.11.1
github.com/rabbitmq/amqp091-go v1.2.0
github.com/sirupsen/logrus v1.8.1
github.com/sony/gobreaker v0.4.1
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e
go.etcd.io/etcd/client/pkg/v3 v3.5.0
go.etcd.io/etcd/client/v2 v2.305.0
go.etcd.io/etcd/client/v3 v3.5.0
go.opencensus.io v0.23.0
go.uber.org/zap v1.19.1
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
google.golang.org/grpc v1.40.0
google.golang.org/protobuf v1.27.1
)
require (
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/armon/go-metrics v0.4.0 // indirect
github.com/aws/smithy-go v1.8.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/edsrzf/mmap-go v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.2.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/serf v0.10.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.14.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
gopkg.in/gcfg.v1 v1.2.3 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q=
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI=
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v1.9.1 h1:ZbovGV/qo40nrOJ4q8G33AGICzaPI45FHQWJ9650pF4=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw=
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw=
github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-zookeeper/zk v1.0.2 h1:4mx0EYENAdX/B/rbunjlt5+4RTA/a9SMHBRuSKdGxPM=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.14.0 h1:Y64GIJ8hYTu+tuGekwO4G4ardXoiCivX9wv1iP/kihk=
github.com/hashicorp/consul/api v1.14.0/go.mod h1:bcaw5CSZ7NE9qfOfKCI1xb7ZKjzu/MyvQkCLTfqLqxQ=
github.com/hashicorp/consul/sdk v0.10.0 h1:rGLEh2AWK4K0KCMvqWAz2EYxQqgciIfMagWZ0nVe5MI=
github.com/hashicorp/consul/sdk v0.10.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M=
github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.4.0 h1:k3uda5gZcltmafuFF+UFqNEl5PrH+yPZ4zkjp1f/H/8=
github.com/hashicorp/memberlist v0.4.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.10.0 h1:89qvvpfMQnz6c2y4pv7j2vUUmeT1+5TSZMexuTbtsPs=
github.com/hashicorp/serf v0.10.0/go.mod h1:bXN03oZc5xlH46k/K1qTrpXb9ERKyY1/i/N5mxvgrZw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig=
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=
github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a h1:lem6QCvxR0Y28gth9P+wV2K/zYUUAkJ+55U8cpS0p5I=
github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
github.com/nats-io/nats-server/v2 v2.8.4 h1:0jQzze1T9mECg8YZEl8+WYUXb9JKluJfCBriPUtluB4=
github.com/nats-io/nats-server/v2 v2.8.4/go.mod h1:8zZa+Al3WsESfmgSs98Fi06dRWLH5Bnq90m5bKD/eT4=
github.com/nats-io/nats.go v1.15.0 h1:3IXNBolWrwIUf2soxh6Rla8gPzYWEZQBUBK6RV21s+o=
github.com/nats-io/nats.go v1.15.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.2.5 h1:UwtQQx2pyPIgWYHRg+epgdx1/HnBQTgN3/oIYEJTQzU=
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY=
github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rabbitmq/amqp091-go v1.2.0 h1:1pHBxAsQh54R9eX/xo679fUEAfv3loMqi0pvRFOj2nk=
github.com/rabbitmq/amqp091-go v1.2.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8=
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek=
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 h1:TyKJRhyo17yWxOMCTHKWrc5rddHORMlnZ/j57umaUd8=
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 h1:ysnBoUyeL/H6RCvNRhWHjKoDEmguI+mPU+qHgK8qv/w=
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
================================================
FILE: lint
================================================
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
if [ ! $(command -v gometalinter) ]
then
go get github.com/alecthomas/gometalinter
gometalinter --update --install
fi
time gometalinter \
--exclude='error return value not checked.*(Close|Log|Print).*\(errcheck\)$' \
--exclude='.*_test\.go:.*error return value not checked.*\(errcheck\)$' \
--exclude='/thrift/' \
--exclude='/pb/' \
--exclude='no args in Log call \(vet\)' \
--disable=dupl \
--disable=aligncheck \
--disable=gotype \
--cyclo-over=20 \
--tests \
--concurrency=2 \
--deadline=300s \
./...
================================================
FILE: log/README.md
================================================
# package log
**Deprecation notice:** The core Go kit log packages (log, log/level, log/term, and
log/syslog) have been moved to their own repository at github.com/go-kit/log.
The corresponding packages in this directory remain for backwards compatibility.
Their types alias the types and their functions call the functions provided by
the new repository. Using either import path should be equivalent. Prefer the
new import path when practical.
______
`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.
## 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.
```go
// Unstructured
log.Printf("HTTP server listening on %s", addr)
// Structured
logger.Log("transport", "HTTP", "addr", addr, "msg", "listening")
```
## Usage
### Typical application logging
```go
w := log.NewSyncWriter(os.Stderr)
logger := log.NewLogfmtLogger(w)
logger.Log("question", "what is the meaning of life?", "answer", 42)
// Output:
// question="what is the meaning of life?" answer=42
```
### Contextual Loggers
```go
func main() {
var logger log.Logger
logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
logger = log.With(logger, "instance_id", 123)
logger.Log("msg", "starting")
NewWorker(log.With(logger, "component", "worker")).Run()
NewSlacker(log.With(logger, "component", "slacker")).Run()
}
// Output:
// instance_id=123 msg=starting
// instance_id=123 component=worker msg=running
// instance_id=123 component=slacker msg=running
```
### Interact with stdlib logger
Redirect stdlib logger to Go kit logger.
```go
import (
"os"
stdlog "log"
kitlog "github.com/go-kit/kit/log"
)
func main() {
logger := kitlog.NewJSONLogger(kitlog.NewSyncWriter(os.Stdout))
stdlog.SetOutput(kitlog.NewStdlibAdapter(logger))
stdlog.Print("I sure like pie")
}
// Output:
// {"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.
```go
logger := kitlog.NewLogfmtLogger(kitlog.StdlibWriter{})
logger.Log("legacy", true, "msg", "at least it's something")
// Output:
// 2016/01/01 12:34:56 legacy=true msg="at least it's something"
```
### Timestamps and callers
```go
var logger log.Logger
logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)
logger.Log("msg", "hello")
// Output:
// ts=2016-01-01T12:34:56Z caller=main.go:15 msg=hello
```
## Levels
Log levels are supported via the [level package](https://godoc.org/github.com/go-kit/kit/log/level).
## Supported output formats
- [Logfmt](https://brandur.org/logfmt) ([see also](https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write))
- JSON
## Enhancements
`package log` is centered on the one-method Logger interface.
```go
type Logger interface {
Log(keyvals ...interface{}) error
}
```
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).
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.
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
- 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 friendly to packages that accept only an unadorned log.Logger.
## Benchmarks & comparisons
There are a few Go logging benchmarks and comparisons that include Go kit's package log.
- [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench) includes kit/log
- [uber-common/zap](https://github.com/uber-common/zap), a zero-alloc logging library, includes a comparison with kit/log
================================================
FILE: log/deprecated_levels/levels.go
================================================
// Package levels implements leveled logging on top of Go kit's log package.
//
// Deprecated: Use github.com/go-kit/log/level instead.
package levels
import "github.com/go-kit/log"
// Levels provides a leveled logging wrapper around a logger. It has five
// levels: debug, info, warning (warn), error, and critical (crit). If you
// 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
levelKey string
// We have a choice between storing level values in string fields or
// making a separate context for each level. When using string fields the
// Log method must combine the base context, the level data, and the
// logged keyvals; but the With method only requires updating one context.
// If we instead keep a separate context for each level the Log method
// must only append the new keyvals; but the With method would have to
// update all five contexts.
// Roughly speaking, storing multiple contexts breaks even if the ratio of
// Log/With calls is more than the number of levels. We have chosen to
// make the With method cheap and the Log method a bit more costly because
// we do not expect most applications to Log more than five times for each
// call to With.
debugValue string
infoValue string
warnValue string
errorValue string
critValue string
}
// New creates a new leveled logger, wrapping the passed logger.
func New(logger log.Logger, options ...Option) Levels {
l := Levels{
logger: logger,
levelKey: "level",
debugValue: "debug",
infoValue: "info",
warnValue: "warn",
errorValue: "error",
critValue: "crit",
}
for _, option := range options {
option(&l)
}
return l
}
// 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...),
levelKey: l.levelKey,
debugValue: l.debugValue,
infoValue: l.infoValue,
warnValue: l.warnValue,
errorValue: l.errorValue,
critValue: l.critValue,
}
}
// Debug returns a debug level logger.
func (l Levels) Debug() log.Logger {
return log.WithPrefix(l.logger, 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)
}
// Warn returns a warning level logger.
func (l Levels) Warn() log.Logger {
return log.WithPrefix(l.logger, 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)
}
// Crit returns a critical level logger.
func (l Levels) Crit() log.Logger {
return log.WithPrefix(l.logger, l.levelKey, l.critValue)
}
// Option sets a parameter for leveled loggers.
type Option func(*Levels)
// Key sets the key for the field used to indicate log level. By default,
// the key is "level".
func Key(key string) Option {
return func(l *Levels) { l.levelKey = key }
}
// DebugValue sets the value for the field used to indicate the debug log
// level. By default, the value is "debug".
func DebugValue(value string) Option {
return func(l *Levels) { l.debugValue = value }
}
// InfoValue sets the value for the field used to indicate the info log level.
// By default, the value is "info".
func InfoValue(value string) Option {
return func(l *Levels) { l.infoValue = value }
}
// WarnValue sets the value for the field used to indicate the warning log
// level. By default, the value is "warn".
func WarnValue(value string) Option {
return func(l *Levels) { l.warnValue = value }
}
// ErrorValue sets the value for the field used to indicate the error log
// level. By default, the value is "error".
func ErrorValue(value string) Option {
return func(l *Levels) { l.errorValue = value }
}
// CritValue sets the value for the field used to indicate the critical log
// level. By default, the value is "crit".
func CritValue(value string) Option {
return func(l *Levels) { l.critValue = value }
}
================================================
FILE: log/deprecated_levels/levels_test.go
================================================
package levels_test
import (
"bytes"
"os"
"testing"
levels "github.com/go-kit/kit/log/deprecated_levels"
"github.com/go-kit/log"
)
func TestDefaultLevels(t *testing.T) {
buf := bytes.Buffer{}
logger := levels.New(log.NewLogfmtLogger(&buf))
logger.Debug().Log("msg", "résumé") // of course you'd want to do this
if want, have := "level=debug msg=résumé\n", buf.String(); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
buf.Reset()
logger.Info().Log("msg", "Åhus")
if want, have := "level=info msg=Åhus\n", buf.String(); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
buf.Reset()
logger.Error().Log("msg", "© violation")
if want, have := "level=error msg=\"© violation\"\n", buf.String(); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
buf.Reset()
logger.Crit().Log("msg", " ")
if want, have := "level=crit msg=\"\\t\"\n", buf.String(); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
}
func TestModifiedLevels(t *testing.T) {
buf := bytes.Buffer{}
logger := levels.New(
log.NewJSONLogger(&buf),
levels.Key("l"),
levels.DebugValue("dbg"),
levels.InfoValue("nfo"),
levels.WarnValue("wrn"),
levels.ErrorValue("err"),
levels.CritValue("crt"),
)
logger.With("easter_island", "176°").Debug().Log("msg", "moai")
if want, have := `{"easter_island":"176°","l":"dbg","msg":"moai"}`+"\n", buf.String(); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
}
func ExampleLevels() {
logger := levels.New(log.NewLogfmtLogger(os.Stdout))
logger.Debug().Log("msg", "hello")
logger.With("context", "foo").Warn().Log("err", "error")
// Output:
// level=debug msg=hello
// level=warn context=foo err=error
}
================================================
FILE: log/doc.go
================================================
// Package log provides a structured logger.
//
// Deprecated: Use github.com/go-kit/log instead.
//
// Structured logging produces logs easily consumed later by humans or
// machines. Humans might be interested in debugging errors, or tracing
// specific requests. Machines might be interested in counting interesting
// events, or aggregating information for off-line processing. In both cases,
// it is important that the log messages are structured and actionable.
// Package log is designed to encourage both of these best practices.
//
// Basic Usage
//
// The fundamental interface is Logger. Loggers create log events from
// key/value data. The Logger interface has a single method, Log, which
// accepts a sequence of alternating key/value pairs, which this package names
// keyvals.
//
// type Logger interface {
// Log(keyvals ...interface{}) error
// }
//
// Here is an example of a function using a Logger to create log events.
//
// func RunTask(task Task, logger log.Logger) string {
// logger.Log("taskID", task.ID, "event", "starting task")
// ...
// logger.Log("taskID", task.ID, "event", "task complete")
// }
//
// The keys in the above example are "taskID" and "event". The values are
// task.ID, "starting task", and "task complete". Every key is followed
// immediately by its value.
//
// Keys are usually plain strings. Values may be any type that has a sensible
// encoding in the chosen log format. With structured logging it is a good
// idea to log simple values without formatting them. This practice allows
// the chosen logger to encode values in the most appropriate way.
//
// Contextual Loggers
//
// 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, WithPrefix, and WithSuffix
// add context to a logger. We can use With to improve the RunTask example.
//
// func RunTask(task Task, logger log.Logger) string {
// logger = log.With(logger, "taskID", task.ID)
// logger.Log("event", "starting task")
// ...
// taskHelper(task.Cmd, logger)
// ...
// logger.Log("event", "task complete")
// }
//
// 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.)
//
// Dynamic Contextual 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.
//
// Valuers provide the basis for consistently logging timestamps and source
// code location. The log package defines several valuers for that purpose.
// See Timestamp, DefaultTimestamp, DefaultTimestampUTC, Caller, and
// DefaultCaller. A common logger initialization sequence that ensures all log
// 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)
//
// Concurrent Safety
//
// Applications with multiple goroutines want each log event written to the
// same logger to remain separate from other log events. Package log provides
// two simple solutions for concurrent safe logging.
//
// NewSyncWriter wraps an io.Writer and serializes each call to its Write
// method. Using a SyncWriter has the benefit that the smallest practical
// portion of the logging logic is performed within a mutex, but it requires
// the formatting Logger to make only one call to Write per log event.
//
// NewSyncLogger wraps any Logger and serializes each call to its Log method.
// Using a SyncLogger has the benefit that it guarantees each log event is
// 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
================================================
FILE: log/example_test.go
================================================
package log_test
import (
"math/rand"
"os"
"sync"
"time"
"github.com/go-kit/kit/log"
)
func Example_basic() {
logger := log.NewLogfmtLogger(os.Stdout)
type Task struct {
ID int
}
RunTask := func(task Task, logger log.Logger) {
logger.Log("taskID", task.ID, "event", "starting task")
logger.Log("taskID", task.ID, "event", "task complete")
}
RunTask(Task{ID: 1}, logger)
// Output:
// taskID=1 event="starting task"
// taskID=1 event="task complete"
}
func Example_contextual() {
logger := log.NewLogfmtLogger(os.Stdout)
type Task struct {
ID int
Cmd string
}
taskHelper := func(cmd string, logger log.Logger) {
// execute(cmd)
logger.Log("cmd", cmd, "dur", 42*time.Millisecond)
}
RunTask := func(task Task, logger log.Logger) {
logger = log.With(logger, "taskID", task.ID)
logger.Log("event", "starting task")
taskHelper(task.Cmd, logger)
logger.Log("event", "task complete")
}
RunTask(Task{ID: 1, Cmd: "echo Hello, world!"}, logger)
// Output:
// taskID=1 event="starting task"
// taskID=1 cmd="echo Hello, world!" dur=42ms
// taskID=1 event="task complete"
}
func Example_valuer() {
logger := log.NewLogfmtLogger(os.Stdout)
count := 0
counter := func() interface{} {
count++
return count
}
logger = log.With(logger, "count", log.Valuer(counter))
logger.Log("call", "first")
logger.Log("call", "second")
// Output:
// count=1 call=first
// count=2 call=second
}
func Example_debugInfo() {
logger := log.NewLogfmtLogger(os.Stdout)
// make time predictable for this test
baseTime := time.Date(2015, time.February, 3, 10, 0, 0, 0, time.UTC)
mockTime := func() time.Time {
baseTime = baseTime.Add(time.Second)
return baseTime
}
logger = log.With(logger, "time", log.Timestamp(mockTime), "caller", log.DefaultCaller)
logger.Log("call", "first")
logger.Log("call", "second")
// ...
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"
}
================================================
FILE: log/json_logger.go
================================================
package log
import (
"io"
"github.com/go-kit/log"
)
// NewJSONLogger returns a Logger that encodes keyvals to the Writer as a
// single JSON object. Each log event produces no more than one call to
// w.Write. The passed Writer must be safe for concurrent use by multiple
// goroutines if the returned Logger will be used concurrently.
func NewJSONLogger(w io.Writer) Logger {
return log.NewJSONLogger(w)
}
================================================
FILE: log/level/doc.go
================================================
// Package level implements leveled logging on top of Go kit's log package.
//
// Deprecated: Use github.com/go-kit/log/level instead.
//
// 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.AllowInfo()) // <--
// logger = log.With(logger, "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
================================================
FILE: log/level/example_test.go
================================================
package level_test
import (
"errors"
"os"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
)
func Example_basic() {
logger := log.NewLogfmtLogger(os.Stdout)
level.Debug(logger).Log("msg", "this message is at the debug level")
level.Info(logger).Log("msg", "this message is at the info level")
level.Warn(logger).Log("msg", "this message is at the warn level")
level.Error(logger).Log("msg", "this message is at the error level")
// Output:
// level=debug msg="this message is at the debug level"
// level=info msg="this message is at the info level"
// level=warn msg="this message is at the warn level"
// level=error msg="this message is at the error level"
}
func Example_filtered() {
// Set up 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:32 err="bad data"
// level=info caller=example_test.go:33 event="data saved"
}
================================================
FILE: log/level/level.go
================================================
package level
import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
)
// Error returns a logger that includes a Key/ErrorValue pair.
func Error(logger log.Logger) log.Logger {
return level.Error(logger)
}
// Warn returns a logger that includes a Key/WarnValue pair.
func Warn(logger log.Logger) log.Logger {
return level.Warn(logger)
}
// Info returns a logger that includes a Key/InfoValue pair.
func Info(logger log.Logger) log.Logger {
return level.Info(logger)
}
// Debug returns a logger that includes a Key/DebugValue pair.
func Debug(logger log.Logger) log.Logger {
return level.Debug(logger)
}
// 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 {
return level.NewFilter(next, options...)
}
// Option sets a parameter for the leveled logger.
type Option = level.Option
// AllowAll is an alias for AllowDebug.
func AllowAll() Option {
return level.AllowAll()
}
// AllowDebug allows error, warn, info and debug level log events to pass.
func AllowDebug() Option {
return level.AllowDebug()
}
// AllowInfo allows error, warn and info level log events to pass.
func AllowInfo() Option {
return level.AllowInfo()
}
// AllowWarn allows error and warn level log events to pass.
func AllowWarn() Option {
return level.AllowWarn()
}
// AllowError allows only error level log events to pass.
func AllowError() Option {
return level.AllowError()
}
// AllowNone allows no leveled log events to pass.
func AllowNone() Option {
return level.AllowNone()
}
// 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 level.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 level.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 level.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, lvl Value) log.Logger {
return level.NewInjector(next, lvl)
}
// 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 = level.Value
// Key returns the unique key added to log events by the loggers in this
// package.
func Key() interface{} { return level.Key() }
// ErrorValue returns the unique value added to log events by Error.
func ErrorValue() Value { return level.ErrorValue() }
// WarnValue returns the unique value added to log events by Warn.
func WarnValue() Value { return level.WarnValue() }
// InfoValue returns the unique value added to log events by Info.
func InfoValue() Value { return level.InfoValue() }
// DebugValue returns the unique value added to log events by Debug.
func DebugValue() Value { return level.DebugValue() }
================================================
FILE: log/log.go
================================================
package log
import (
"github.com/go-kit/log"
)
// Logger is the fundamental interface for all log operations. Log creates a
// 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.
type Logger = log.Logger
// ErrMissingValue is appended to keyvals slices with odd length to substitute
// the missing value.
var ErrMissingValue = log.ErrMissingValue
// 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,
// WithPrefix, or WithSuffix, 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 {
return log.With(logger, keyvals...)
}
// 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, WithPrefix, or WithSuffix, 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 {
return log.WithPrefix(logger, keyvals...)
}
// WithSuffix returns a new contextual logger with keyvals appended to those
// passed to calls to Log. If logger is also a contextual logger created by
// With, WithPrefix, or WithSuffix, 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 WithSuffix(logger Logger, keyvals ...interface{}) Logger {
return log.WithSuffix(logger, 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.
type LoggerFunc = log.LoggerFunc
================================================
FILE: log/logfmt_logger.go
================================================
package log
import (
"io"
"github.com/go-kit/log"
)
// NewLogfmtLogger returns a logger that encodes keyvals to the Writer in
// logfmt format. Each log event produces no more than one call to w.Write.
// The passed Writer must be safe for concurrent use by multiple goroutines if
// the returned Logger will be used concurrently.
func NewLogfmtLogger(w io.Writer) Logger {
return log.NewLogfmtLogger(w)
}
================================================
FILE: log/logrus/logrus_logger.go
================================================
// Package logrus provides an adapter to the
// go-kit log.Logger interface.
package logrus
import (
"errors"
"fmt"
"github.com/go-kit/log"
"github.com/sirupsen/logrus"
)
type Logger struct {
field logrus.FieldLogger
level logrus.Level
}
type Option func(*Logger)
var errMissingValue = errors.New("(MISSING)")
// NewLogger returns a Go kit log.Logger that sends log events to a logrus.Logger.
func NewLogger(logger logrus.FieldLogger, options ...Option) log.Logger {
l := &Logger{
field: logger,
level: logrus.InfoLevel,
}
for _, optFunc := range options {
optFunc(l)
}
return l
}
// WithLevel configures a logrus logger to log at level for all events.
func WithLevel(level logrus.Level) Option {
return func(c *Logger) {
c.level = level
}
}
func (l Logger) Log(keyvals ...interface{}) error {
fields := logrus.Fields{}
for i := 0; i < len(keyvals); i += 2 {
if i+1 < len(keyvals) {
fields[fmt.Sprint(keyvals[i])] = keyvals[i+1]
} else {
fields[fmt.Sprint(keyvals[i])] = errMissingValue
}
}
switch l.level {
case logrus.InfoLevel:
l.field.WithFields(fields).Info()
case logrus.ErrorLevel:
l.field.WithFields(fields).Error()
case logrus.DebugLevel:
l.field.WithFields(fields).Debug()
case logrus.WarnLevel:
l.field.WithFields(fields).Warn()
case logrus.TraceLevel:
l.field.WithFields(fields).Trace()
default:
l.field.WithFields(fields).Print()
}
return nil
}
================================================
FILE: log/logrus/logrus_logger_test.go
================================================
package logrus_test
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
log "github.com/go-kit/kit/log/logrus"
"github.com/sirupsen/logrus"
)
func TestLogrusLogger(t *testing.T) {
t.Parallel()
buf := &bytes.Buffer{}
logrusLogger := logrus.New()
logrusLogger.Out = buf
logrusLogger.Formatter = &logrus.TextFormatter{TimestampFormat: "02-01-2006 15:04:05", FullTimestamp: true}
logger := log.NewLogger(logrusLogger)
if err := logger.Log("hello", "world"); err != nil {
t.Fatal(err)
}
if want, have := "hello=world\n", strings.Split(buf.String(), " ")[3]; want != have {
t.Errorf("want %#v, have %#v", want, have)
}
buf.Reset()
if err := logger.Log("a", 1, "err", errors.New("error")); err != nil {
t.Fatal(err)
}
if want, have := "a=1 err=error", strings.TrimSpace(strings.SplitAfterN(buf.String(), " ", 4)[3]); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
buf.Reset()
if err := logger.Log("a", 1, "b"); err != nil {
t.Fatal(err)
}
if want, have := "a=1 b=\"(MISSING)\"", strings.TrimSpace(strings.SplitAfterN(buf.String(), " ", 4)[3]); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
buf.Reset()
if err := logger.Log("my_map", mymap{0: 0}); err != nil {
t.Fatal(err)
}
if want, have := "my_map=special_behavior", strings.TrimSpace(strings.Split(buf.String(), " ")[3]); want != have {
t.Errorf("want %#v, have %#v", want, have)
}
}
type mymap map[int]int
func (m mymap) String() string { return "special_behavior" }
func TestWithLevel(t *testing.T) {
tests := []struct {
name string
level logrus.Level
expectedLevel logrus.Level
}{
{
name: "Test Debug level",
level: logrus.DebugLevel,
expectedLevel: logrus.DebugLevel,
},
{
name: "Test Error level",
level: logrus.ErrorLevel,
expectedLevel: logrus.ErrorLevel,
},
{
name: "Test Warn level",
level: logrus.WarnLevel,
expectedLevel: logrus.WarnLevel,
},
{
name: "Test Info level",
level: logrus.InfoLevel,
expectedLevel: logrus.InfoLevel,
},
{
name: "Test Trace level",
level: logrus.TraceLevel,
expectedLevel: logrus.TraceLevel,
},
{
name: "Test not existing level",
level: 999,
expectedLevel: logrus.InfoLevel,
},
}
for _, tt := range tests {
buf := &bytes.Buffer{}
logrusLogger := logrus.New()
logrusLogger.Out = buf
logrusLogger.Level = tt.level
logrusLogger.Formatter = &logrus.JSONFormatter{}
logger := log.NewLogger(logrusLogger, log.WithLevel(tt.level))
t.Run(tt.name, func(t *testing.T) {
if err := logger.Log(); err != nil {
t.Fatal(err)
}
l := map[string]interface{}{}
if err := json.Unmarshal(buf.Bytes(), &l); err != nil {
t.Fatal(err)
}
if v, ok := l["level"].(string); !ok || v != tt.expectedLevel.String() {
t.Fatalf("Logging levels doesn't match. Expected: %s, got: %s", tt.level, v)
}
})
}
}
================================================
FILE: log/nop_logger.go
================================================
package log
import "github.com/go-kit/log"
// NewNopLogger returns a logger that doesn't do anything.
func NewNopLogger() Logger {
return log.NewNopLogger()
}
================================================
FILE: log/stdlib.go
================================================
package log
import (
"io"
"github.com/go-kit/log"
)
// StdlibWriter implements io.Writer by invoking the stdlib log.Print. It's
// designed to be passed to a Go kit logger as the writer, for cases where
// it's necessary to redirect all Go kit log output to the stdlib logger.
//
// If you have any choice in the matter, you shouldn't use this. Prefer to
// redirect the stdlib log to the Go kit logger via NewStdlibAdapter.
type StdlibWriter = log.StdlibWriter
// StdlibAdapter wraps a Logger and allows it to be passed to the stdlib
// logger's SetOutput. It will extract date/timestamps, filenames, and
// messages, and place them under relevant keys.
type StdlibAdapter = log.StdlibAdapter
// StdlibAdapterOption sets a parameter for the StdlibAdapter.
type StdlibAdapterOption = log.StdlibAdapterOption
// TimestampKey sets the key for the timestamp field. By default, it's "ts".
func TimestampKey(key string) StdlibAdapterOption {
return log.TimestampKey(key)
}
// FileKey sets the key for the file and line field. By default, it's "caller".
func FileKey(key string) StdlibAdapterOption {
return log.FileKey(key)
}
// MessageKey sets the key for the actual log message. By default, it's "msg".
func MessageKey(key string) StdlibAdapterOption {
return log.MessageKey(key)
}
// Prefix configures the adapter to parse a prefix from stdlib log events. If
// you provide a non-empty prefix to the stdlib logger, then your should provide
// that same prefix to the adapter via this option.
//
// By default, the prefix isn't included in the msg key. Set joinPrefixToMsg to
// true if you want to include the parsed prefix in the msg.
func Prefix(prefix string, joinPrefixToMsg bool) StdlibAdapterOption {
return log.Prefix(prefix, joinPrefixToMsg)
}
// NewStdlibAdapter returns a new StdlibAdapter wrapper around the passed
// logger. It's designed to be passed to log.SetOutput.
func NewStdlibAdapter(logger Logger, options ...StdlibAdapterOption) io.Writer {
return log.NewStdlibAdapter(logger, options...)
}
================================================
FILE: log/sync.go
================================================
package log
import (
"io"
"github.com/go-kit/log"
)
// SwapLogger wraps another logger that may be safely replaced while other
// goroutines use the SwapLogger concurrently. The zero value for a SwapLogger
// will discard all log events without error.
//
// SwapLogger serves well as a package global logger that can be changed by
// importers.
type SwapLogger = log.SwapLogger
// NewSyncWriter returns a new writer that is safe for concurrent use by
// multiple goroutines. Writes to the returned writer are passed on to w. If
// another write is already in progress, the calling goroutine blocks until
// the writer is available.
//
// If w implements the following interface, so does the returned writer.
//
// interface {
// Fd() uintptr
// }
func NewSyncWriter(w io.Writer) io.Writer {
return log.NewSyncWriter(w)
}
// NewSyncLogger returns a logger that synchronizes concurrent use of the
// wrapped logger. When multiple goroutines use the SyncLogger concurrently
// only one goroutine will be allowed to log to the wrapped logger at a time.
// The other goroutines will block until the logger is available.
func NewSyncLogger(logger Logger) Logger {
return log.NewSyncLogger(logger)
}
================================================
FILE: log/syslog/example_test.go
================================================
// +build !windows
// +build !plan9
// +build !nacl
package syslog_test
import (
"fmt"
gosyslog "log/syslog"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/go-kit/kit/log/syslog"
)
func ExampleNewSyslogLogger_defaultPrioritySelector() {
// Normal syslog writer
w, err := gosyslog.New(gosyslog.LOG_INFO, "experiment")
if err != nil {
fmt.Println(err)
return
}
// syslog logger with logfmt formatting
logger := syslog.NewSyslogLogger(w, log.NewLogfmtLogger)
logger.Log("msg", "info because of default")
logger.Log(level.Key(), level.DebugValue(), "msg", "debug because of explicit level")
}
================================================
FILE: log/syslog/syslog.go
================================================
//go:build !windows && !plan9 && !nacl
// +build !windows,!plan9,!nacl
// Deprecated: Use github.com/go-kit/log/syslog instead.
package syslog
import (
"io"
"github.com/go-kit/log"
"github.com/go-kit/log/syslog"
)
// SyslogWriter is an interface wrapping stdlib syslog Writer.
type SyslogWriter = syslog.SyslogWriter
// NewSyslogLogger returns a new Logger which writes to syslog in syslog format.
// The body of the log message is the formatted output from the Logger returned
// by newLogger.
func NewSyslogLogger(w SyslogWriter, newLogger func(io.Writer) log.Logger, options ...Option) log.Logger {
return syslog.NewSyslogLogger(w, newLogger, options...)
}
// Option sets a parameter for syslog loggers.
type Option = syslog.Option
// PrioritySelector inspects the list of keyvals and selects a syslog priority.
type PrioritySelector = syslog.PrioritySelector
// PrioritySelectorOption sets priority selector function to choose syslog
// priority.
func PrioritySelectorOption(selector PrioritySelector) Option {
return syslog.PrioritySelectorOption(selector)
}
================================================
FILE: log/term/colorlogger.go
================================================
package term
import (
"io"
"github.com/go-kit/log"
"github.com/go-kit/log/term"
)
// Color represents an ANSI color. The zero value is Default.
type Color = term.Color
// ANSI colors.
const (
Default = term.Default
Black = term.Black
DarkRed = term.DarkRed
DarkGreen = term.DarkGreen
Brown = term.Brown
DarkBlue = term.DarkBlue
DarkMagenta = term.DarkMagenta
DarkCyan = term.DarkCyan
Gray = term.Gray
DarkGray = term.DarkGray
Red = term.Red
Green = term.Green
Yellow = term.Yellow
Blue = term.Blue
Magenta = term.Magenta
Cyan = term.Cyan
White = term.White
)
// FgBgColor represents a foreground and background color.
type FgBgColor = term.FgBgColor
// NewColorLogger returns a Logger which writes colored logs to w. ANSI color
// codes for the colors returned by color are added to the formatted output
// from the Logger returned by newLogger and the combined result written to w.
func NewColorLogger(w io.Writer, newLogger func(io.Writer) log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger {
return term.NewColorLogger(w, newLogger, color)
}
================================================
FILE: log/term/colorwriter.go
================================================
package term
import (
"io"
"github.com/go-kit/log/term"
)
// NewColorWriter returns an io.Writer that writes to w and provides cross
// platform support for ANSI color codes. If w is not a terminal it is
// returned unmodified.
func NewColorWriter(w io.Writer) io.Writer {
return term.NewColorWriter(w)
}
================================================
FILE: log/term/example_test.go
================================================
package term_test
import (
"errors"
"os"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/term"
)
func ExampleNewLogger_redErrors() {
// Color errors red
colorFn := func(keyvals ...interface{}) term.FgBgColor {
for i := 1; i < len(keyvals); i += 2 {
if _, ok := keyvals[i].(error); ok {
return term.FgBgColor{Fg: term.White, Bg: term.Red}
}
}
return term.FgBgColor{}
}
logger := term.NewLogger(os.Stdout, log.NewLogfmtLogger, colorFn)
logger.Log("msg", "default color", "err", nil)
logger.Log("msg", "colored because of error", "err", errors.New("coloring error"))
}
func ExampleNewLogger_levelColors() {
// Color by level value
colorFn := func(keyvals ...interface{}) term.FgBgColor {
for i := 0; i < len(keyvals)-1; i += 2 {
if keyvals[i] != "level" {
continue
}
switch keyvals[i+1] {
case "debug":
return term.FgBgColor{Fg: term.DarkGray}
case "info":
return term.FgBgColor{Fg: term.Gray}
case "warn":
return term.FgBgColor{Fg: term.Yellow}
case "error":
return term.FgBgColor{Fg: term.Red}
case "crit":
return term.FgBgColor{Fg: term.Gray, Bg: term.DarkRed}
default:
return term.FgBgColor{}
}
}
return term.FgBgColor{}
}
logger := term.NewLogger(os.Stdout, log.NewJSONLogger, colorFn)
logger.Log("level", "warn", "msg", "yellow")
logger.Log("level", "debug", "msg", "dark gray")
}
================================================
FILE: log/term/term.go
================================================
// Package term provides tools for logging to a terminal.
//
// Deprecated: Use github.com/go-kit/log/term instead.
package term
import (
"io"
"github.com/go-kit/log"
"github.com/go-kit/log/term"
)
// NewLogger returns a Logger that takes advantage of terminal features if
// possible. Log events are formatted by the Logger returned by newLogger. If
// w is a terminal each log event is colored according to the color function.
func NewLogger(w io.Writer, newLogger func(io.Writer) log.Logger, color func(keyvals ...interface{}) FgBgColor) log.Logger {
return term.NewLogger(w, newLogger, color)
}
// IsTerminal returns true if w writes to a terminal.
func IsTerminal(w io.Writer) bool {
return term.IsTerminal(w)
}
================================================
FILE: log/value.go
================================================
package log
import (
"time"
"github.com/go-kit/log"
)
// A Valuer generates a log value. When passed to With, WithPrefix, or
// WithSuffix in a value element (odd indexes), it represents a dynamic
// value which is re-evaluated with each log event.
type Valuer = log.Valuer
// Timestamp returns a timestamp Valuer. It invokes the t function to get the
// time; unless you are doing something tricky, pass time.Now.
//
// Most users will want to use DefaultTimestamp or DefaultTimestampUTC, which
// are TimestampFormats that use the RFC3339Nano format.
func Timestamp(t func() time.Time) Valuer {
return log.Timestamp(t)
}
// TimestampFormat returns a timestamp Valuer with a custom time format. It
// invokes the t function to get the time to format; unless you are doing
// something tricky, pass time.Now. The layout string is passed to
// Time.Format.
//
// Most users will want to use DefaultTimestamp or DefaultTimestampUTC, which
// are TimestampFormats that use the RFC3339Nano format.
func TimestampFormat(t func() time.Time, layout string) Valuer {
return log.TimestampFormat(t, layout)
}
// 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 {
return log.Caller(depth)
}
var (
// DefaultTimestamp is a Valuer that returns the current wallclock time,
// respecting time zones, when bound.
DefaultTimestamp = log.DefaultTimestamp
// DefaultTimestampUTC is a Valuer that returns the current time in UTC
// when bound.
DefaultTimestampUTC = log.DefaultTimestampUTC
// 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 = log.DefaultCaller
)
================================================
FILE: log/zap/zap_sugar_logger.go
================================================
package zap
import (
"github.com/go-kit/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type zapSugarLogger func(msg string, keysAndValues ...interface{})
func (l zapSugarLogger) Log(kv ...interface{}) error {
l("", kv...)
return nil
}
// NewZapSugarLogger returns a Go kit log.Logger that sends
// log events to a zap.Logger.
func NewZapSugarLogger(logger *zap.Logger, level zapcore.Level) log.Logger {
sugarLogger := logger.WithOptions(zap.AddCallerSkip(2)).Sugar()
var sugar zapSugarLogger
switch level {
case zapcore.DebugLevel:
sugar = sugarLogger.Debugw
case zapcore.InfoLevel:
sugar = sugarLogger.Infow
case zapcore.WarnLevel:
sugar = sugarLogger.Warnw
case zapcore.ErrorLevel:
sugar = sugarLogger.Errorw
case zapcore.DPanicLevel:
sugar = sugarLogger.DPanicw
case zapcore.PanicLevel:
sugar = sugarLogger.Panicw
case zapcore.FatalLevel:
sugar = sugarLogger.Fatalw
default:
sugar = sugarLogger.Infow
}
return sugar
}
================================================
FILE: log/zap/zap_sugar_logger_test.go
================================================
package zap_test
import (
"encoding/json"
kitzap "github.com/go-kit/kit/log/zap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"strings"
"testing"
)
func TestZapSugarLogger(t *testing.T) {
// logger config
encoderConfig := zap.NewDevelopmentEncoderConfig()
encoder := zapcore.NewJSONEncoder(encoderConfig)
levelKey := encoderConfig.LevelKey
// basic test cases
type testCase struct {
level zapcore.Level
kvs []interface{}
want map[string]string
}
testCases := []testCase{
{level: zapcore.DebugLevel, kvs: []interface{}{"key1", "value1"},
want: map[string]string{levelKey: "DEBUG", "key1": "value1"}},
{level: zapcore.InfoLevel, kvs: []interface{}{"key2", "value2"},
want: map[string]string{levelKey: "INFO", "key2": "value2"}},
{level: zapcore.WarnLevel, kvs: []interface{}{"key3", "value3"},
want: map[string]string{levelKey: "WARN", "key3": "value3"}},
{level: zapcore.ErrorLevel, kvs: []interface{}{"key4", "value4"},
want: map[string]string{levelKey: "ERROR", "key4": "value4"}},
{level: zapcore.DPanicLevel, kvs: []interface{}{"key5", "value5"},
want: map[string]string{levelKey: "DPANIC", "key5": "value5"}},
{level: zapcore.PanicLevel, kvs: []interface{}{"key6", "value6"},
want: map[string]string{levelKey: "PANIC", "key6": "value6"}},
}
// test
for _, testCase := range testCases {
t.Run(testCase.level.String(), func(t *testing.T) {
// make logger
writer := &tbWriter{tb: t}
logger := zap.New(
zapcore.NewCore(encoder, zapcore.AddSync(writer), zap.DebugLevel),
zap.Development())
// check panic
shouldPanic := testCase.level >= zapcore.DPanicLevel
kitLogger := kitzap.NewZapSugarLogger(logger, testCase.level)
defer func() {
isPanic := recover() != nil
if shouldPanic != isPanic {
t.Errorf("test level %v should panic(%v), but %v", testCase.level, shouldPanic, isPanic)
}
// check log kvs
logMap := make(map[string]string)
err := json.Unmarshal([]byte(writer.sb.String()), &logMap)
if err != nil {
t.Errorf("unmarshal error: %v", err)
} else {
for k, v := range testCase.want {
vv, ok := logMap[k]
if !ok || v != vv {
t.Error("error log")
}
}
}
}()
kitLogger.Log(testCase.kvs...)
})
}
}
type tbWriter struct {
tb testing.TB
sb strings.Builder
}
func (w *tbWriter) Write(b []byte) (n int, err error) {
w.tb.Logf(string(b))
w.sb.Write(b)
return len(b), nil
}
================================================
FILE: metrics/README.md
================================================
# package metrics
`package metrics` provides a set of uniform interfaces for service instrumentation.
It has
[counters](http://prometheus.io/docs/concepts/metric_types/#counter),
[gauges](http://prometheus.io/docs/concepts/metric_types/#gauge), and
[histograms](http://prometheus.io/docs/concepts/metric_types/#histogram),
and provides adapters to popular metrics packages, like
[expvar](https://golang.org/pkg/expvar),
[StatsD](https://github.com/etsy/statsd), and
[Prometheus](https://prometheus.io).
## Rationale
Code instrumentation is absolutely essential to achieve
[observability](https://speakerdeck.com/mattheath/observability-in-micro-service-architectures)
into a distributed system.
Metrics and instrumentation tools have coalesced around a few well-defined idioms.
`package metrics` provides a common, minimal interface those idioms for service authors.
## Usage
A simple counter, exported via expvar.
```go
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/expvar"
)
func main() {
var myCount metrics.Counter
myCount = expvar.NewCounter("my_count")
myCount.Add(1)
}
```
A histogram for request duration,
exported via a Prometheus summary with dynamically-computed quantiles.
```go
import (
"time"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/prometheus"
)
func main() {
var dur metrics.Histogram = prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "myservice",
Subsystem: "api",
Name: "request_duration_seconds",
Help: "Total time spent serving requests.",
}, []string{})
// ...
}
func handleRequest(dur metrics.Histogram) {
defer func(begin time.Time) { dur.Observe(time.Since(begin).Seconds()) }(time.Now())
// handle request
}
```
A gauge for the number of goroutines currently running, exported via StatsD.
```go
import (
"context"
"net"
"os"
"runtime"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/statsd"
)
func main() {
statsd := statsd.New("foo_svc.", log.NewNopLogger())
report := time.NewTicker(5 * time.Second)
defer report.Stop()
go statsd.SendLoop(context.Background(), report.C, "tcp", "statsd.internal:8125")
goroutines := statsd.NewGauge("goroutine_count")
go exportGoroutines(goroutines)
// ...
}
func exportGoroutines(g metrics.Gauge) {
for range time.Tick(time.Second) {
g.Set(float64(runtime.NumGoroutine()))
}
}
```
For more information, see [the package documentation](https://godoc.org/github.com/go-kit/kit/metrics).
================================================
FILE: metrics/cloudwatch/cloudwatch.go
================================================
package cloudwatch
import (
"context"
"fmt"
"os"
"strconv"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/log"
)
const (
maxConcurrentRequests = 20
maxValuesInABatch = 150
)
// CloudWatch receives metrics observations and forwards them to CloudWatch.
// Create a CloudWatch object, use it to create metrics, and pass those metrics as
// dependencies to the components that will use them.
//
// To regularly report metrics to CloudWatch, use the WriteLoop helper method.
type CloudWatch struct {
mtx sync.RWMutex
sem chan struct{}
namespace string
svc cloudwatchiface.CloudWatchAPI
counters *lv.Space
gauges *lv.Space
histograms *lv.Space
percentiles []float64 // percentiles to track
logger log.Logger
numConcurrentRequests int
}
// Option is a function adapter to change config of the CloudWatch struct
type Option func(*CloudWatch)
// WithLogger sets the Logger that will receive error messages generated
// during the WriteLoop. By default, fmt logger is used.
func WithLogger(logger log.Logger) Option {
return func(c *CloudWatch) {
c.logger = logger
}
}
// WithPercentiles registers the percentiles to track, overriding the
// existing/default values.
// Reason is that Cloudwatch makes you pay per metric, so you can save half the money
// by only using 2 metrics instead of the default 4.
func WithPercentiles(percentiles ...float64) Option {
return func(c *CloudWatch) {
c.percentiles = make([]float64, 0, len(percentiles))
for _, p := range percentiles {
if p < 0 || p > 1 {
continue // illegal entry; ignore
}
c.percentiles = append(c.percentiles, p)
}
}
}
// WithConcurrentRequests sets the upper limit on how many
// cloudwatch.PutMetricDataRequest may be under way at any
// given time. If n is greater than 20, 20 is used. By default,
// the max is set at 10 concurrent requests.
func WithConcurrentRequests(n int) Option {
return func(c *CloudWatch) {
if n > maxConcurrentRequests {
n = maxConcurrentRequests
}
c.numConcurrentRequests = n
}
}
// New returns a CloudWatch object that may be used to create metrics.
// Namespace is applied to all created metrics and maps to the CloudWatch namespace.
// Callers must ensure that regular calls to Send are performed, either
// manually or with one of the helper methods.
func New(namespace string, svc cloudwatchiface.CloudWatchAPI, options ...Option) *CloudWatch {
cw := &CloudWatch{
sem: nil, // set below
namespace: namespace,
svc: svc,
counters: lv.NewSpace(),
gauges: lv.NewSpace(),
histograms: lv.NewSpace(),
numConcurrentRequests: 10,
logger: log.NewLogfmtLogger(os.Stderr),
percentiles: []float64{0.50, 0.90, 0.95, 0.99},
}
for _, opt := range options {
opt(cw)
}
cw.sem = make(chan struct{}, cw.numConcurrentRequests)
return cw
}
// NewCounter returns a counter. Observations are aggregated and emitted once
// per write invocation.
func (cw *CloudWatch) NewCounter(name string) metrics.Counter {
return &Counter{
name: name,
obs: cw.counters.Observe,
}
}
// NewGauge returns an gauge.
func (cw *CloudWatch) NewGauge(name string) metrics.Gauge {
return &Gauge{
name: name,
obs: cw.gauges.Observe,
add: cw.gauges.Add,
}
}
// NewHistogram returns a histogram.
func (cw *CloudWatch) NewHistogram(name string) metrics.Histogram {
return &Histogram{
name: name,
obs: cw.histograms.Observe,
}
}
// WriteLoop is a helper method that invokes Send every time the passed
// channel fires. This method blocks until ctx is canceled, so clients
// probably want to run it in its own goroutine. For typical usage, create a
// time.Ticker and pass its C channel to this method.
func (cw *CloudWatch) WriteLoop(ctx context.Context, c <-chan time.Time) {
for {
select {
case <-c:
if err := cw.Send(); err != nil {
cw.logger.Log("during", "Send", "err", err)
}
case <-ctx.Done():
return
}
}
}
// Send will fire an API request to CloudWatch with the latest stats for
// all metrics. It is preferred that the WriteLoop method is used.
func (cw *CloudWatch) Send() error {
cw.mtx.RLock()
defer cw.mtx.RUnlock()
now := time.Now()
var datums []*cloudwatch.MetricDatum
cw.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
value := sum(values)
datums = append(datums, &cloudwatch.MetricDatum{
MetricName: aws.String(name),
Dimensions: makeDimensions(lvs...),
Value: aws.Float64(value),
Timestamp: aws.Time(now),
})
return true
})
cw.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
if len(values) == 0 {
return true
}
datum := &cloudwatch.MetricDatum{
MetricName: aws.String(name),
Dimensions: makeDimensions(lvs...),
Timestamp: aws.Time(now),
}
// CloudWatch Put Metrics API (https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html)
// expects batch of unique values including the array of corresponding counts
valuesCounter := make(map[float64]int)
for _, v := range values {
valuesCounter[v]++
}
for value, count := range valuesCounter {
if len(datum.Values) == maxValuesInABatch {
break
}
datum.Values = append(datum.Values, aws.Float64(value))
datum.Counts = append(datum.Counts, aws.Float64(float64(count)))
}
datums = append(datums, datum)
return true
})
// format a [0,1]-float value to a percentile value, with minimum nr of decimals
// 0.90 -> "90"
// 0.95 -> "95"
// 0.999 -> "99.9"
formatPerc := func(p float64) string {
return strconv.FormatFloat(p*100, 'f', -1, 64)
}
cw.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
histogram := generic.NewHistogram(name, 50)
for _, v := range values {
histogram.Observe(v)
}
for _, perc := range cw.percentiles {
value := histogram.Quantile(perc)
datums = append(datums, &cloudwatch.MetricDatum{
MetricName: aws.String(fmt.Sprintf("%s_%s", name, formatPerc(perc))),
Dimensions: makeDimensions(lvs...),
Value: aws.Float64(value),
Timestamp: aws.Time(now),
})
}
return true
})
var batches [][]*cloudwatch.MetricDatum
for len(datums) > 0 {
var batch []*cloudwatch.MetricDatum
lim := min(len(datums), maxConcurrentRequests)
batch, datums = datums[:lim], datums[lim:]
batches = append(batches, batch)
}
var errors = make(chan error, len(batches))
for _, batch := range batches {
go func(batch []*cloudwatch.MetricDatum) {
cw.sem <- struct{}{}
defer func() {
<-cw.sem
}()
_, err := cw.svc.PutMetricData(&cloudwatch.PutMetricDataInput{
Namespace: aws.String(cw.namespace),
MetricData: batch,
})
errors <- err
}(batch)
}
var firstErr error
for i := 0; i < cap(errors); i++ {
if err := <-errors; err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is a counter. Observations are forwarded to a node
// object, and aggregated (summed) per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
// Gauge is a gauge. Observations are forwarded to a node
// object, and aggregated (the last observation selected) per timeseries.
type Gauge struct {
name string
lvs lv.LabelValues
obs observeFunc
add observeFunc
}
// With implements metrics.Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
name: g.name,
lvs: g.lvs.With(labelValues...),
obs: g.obs,
add: g.add,
}
}
// Set implements metrics.Gauge.
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 {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
name: h.name,
lvs: h.lvs.With(labelValues...),
obs: h.obs,
}
}
// Observe implements metrics.Histogram.
func (h *Histogram) Observe(value float64) {
h.obs(h.name, h.lvs, value)
}
func makeDimensions(labelValues ...string) []*cloudwatch.Dimension {
dimensions := make([]*cloudwatch.Dimension, len(labelValues)/2)
for i, j := 0, 0; i < len(labelValues); i, j = i+2, j+1 {
dimensions[j] = &cloudwatch.Dimension{
Name: aws.String(labelValues[i]),
Value: aws.String(labelValues[i+1]),
}
}
return dimensions
}
================================================
FILE: metrics/cloudwatch/cloudwatch_test.go
================================================
package cloudwatch
import (
"errors"
"fmt"
"strconv"
"sync"
"testing"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/teststat"
"github.com/go-kit/log"
)
const metricNameToGenerateError = "metric_name_used_to_throw_an_error"
var errTest = errors.New("test error")
type mockCloudWatch struct {
cloudwatchiface.CloudWatchAPI
mtx sync.RWMutex
valuesReceived map[string][]float64
dimensionsReceived map[string][]*cloudwatch.Dimension
}
func newMockCloudWatch() *mockCloudWatch {
return &mockCloudWatch{
valuesReceived: map[string][]float64{},
dimensionsReceived: map[string][]*cloudwatch.Dimension{},
}
}
func (mcw *mockCloudWatch) PutMetricData(input *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) {
mcw.mtx.Lock()
defer mcw.mtx.Unlock()
for _, datum := range input.MetricData {
if *datum.MetricName == metricNameToGenerateError {
return nil, errTest
}
if len(datum.Values) > 0 {
for _, v := range datum.Values {
mcw.valuesReceived[*datum.MetricName] = append(mcw.valuesReceived[*datum.MetricName], *v)
}
} else {
mcw.valuesReceived[*datum.MetricName] = append(mcw.valuesReceived[*datum.MetricName], *datum.Value)
}
mcw.dimensionsReceived[*datum.MetricName] = datum.Dimensions
}
return nil, nil
}
func (mcw *mockCloudWatch) testDimensions(name string, labelValues ...string) error {
mcw.mtx.RLock()
_, hasValue := mcw.valuesReceived[name]
if !hasValue {
return nil // nothing to check; 0 samples were received
}
dimensions, ok := mcw.dimensionsReceived[name]
mcw.mtx.RUnlock()
if !ok {
if len(labelValues) > 0 {
return errors.New("Expected dimensions to be available, but none were")
}
}
LabelValues:
for i, j := 0, 0; i < len(labelValues); i, j = i+2, j+1 {
name, value := labelValues[i], labelValues[i+1]
for _, dimension := range dimensions {
if *dimension.Name == name {
if *dimension.Value == value {
break LabelValues
}
}
}
return fmt.Errorf("could not find dimension with name %s and value %s", name, value)
}
return nil
}
func TestCounter(t *testing.T) {
namespace, name := "abc", "def"
label, value := "label", "value"
svc := newMockCloudWatch()
cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
counter := cw.NewCounter(name).With(label, value)
valuef := func() float64 {
if err := cw.Send(); err != nil {
t.Fatal(err)
}
svc.mtx.RLock()
defer svc.mtx.RUnlock()
value := svc.valuesReceived[name][len(svc.valuesReceived[name])-1]
delete(svc.valuesReceived, name)
return value
}
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal("Fill and flush counter 2nd time: ", err)
}
if err := svc.testDimensions(name, label, value); err != nil {
t.Fatal(err)
}
}
func TestCounterLowSendConcurrency(t *testing.T) {
namespace := "abc"
var names, labels, values []string
for i := 1; i <= 45; i++ {
num := strconv.Itoa(i)
names = append(names, "name"+num)
labels = append(labels, "label"+num)
values = append(values, "value"+num)
}
svc := newMockCloudWatch()
cw := New(namespace, svc,
WithLogger(log.NewNopLogger()),
WithConcurrentRequests(2),
)
counters := make(map[string]metrics.Counter)
var wants []float64
for i, name := range names {
counters[name] = cw.NewCounter(name).With(labels[i], values[i])
wants = append(wants, teststat.FillCounter(counters[name]))
}
if err := cw.Send(); err != nil {
t.Fatal(err)
}
for i, name := range names {
if l := len(svc.valuesReceived[name]); l == 0 && wants[i] == 0 {
continue
} else if l != 1 {
t.Fatalf("one value expected, got %d", l)
}
if svc.valuesReceived[name][0] != wants[i] {
t.Fatalf("want %f, have %f", wants[i], svc.valuesReceived[name])
}
if err := svc.testDimensions(name, labels[i], values[i]); err != nil {
t.Fatal(err)
}
}
}
func TestGauge(t *testing.T) {
namespace, name := "abc", "def"
label, value := "label", "value"
svc := newMockCloudWatch()
cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
gauge := cw.NewGauge(name).With(label, value)
valuef := func() []float64 {
if err := cw.Send(); err != nil {
t.Fatal(err)
}
svc.mtx.RLock()
defer svc.mtx.RUnlock()
res := svc.valuesReceived[name]
delete(svc.valuesReceived, name)
return res
}
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(name, label, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
namespace, name := "abc", "def"
label, value := "label", "value"
svc := newMockCloudWatch()
cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
histogram := cw.NewHistogram(name).With(label, value)
n50 := fmt.Sprintf("%s_50", name)
n90 := fmt.Sprintf("%s_90", name)
n95 := fmt.Sprintf("%s_95", name)
n99 := fmt.Sprintf("%s_99", name)
quantiles := func() (p50, p90, p95, p99 float64) {
err := cw.Send()
if err != nil {
t.Fatal(err)
}
svc.mtx.RLock()
defer svc.mtx.RUnlock()
if len(svc.valuesReceived[n50]) > 0 {
p50 = svc.valuesReceived[n50][0]
delete(svc.valuesReceived, n50)
}
if len(svc.valuesReceived[n90]) > 0 {
p90 = svc.valuesReceived[n90][0]
delete(svc.valuesReceived, n90)
}
if len(svc.valuesReceived[n95]) > 0 {
p95 = svc.valuesReceived[n95][0]
delete(svc.valuesReceived, n95)
}
if len(svc.valuesReceived[n99]) > 0 {
p99 = svc.valuesReceived[n99][0]
delete(svc.valuesReceived, n99)
}
return
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n50, label, value); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n90, label, value); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n95, label, value); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n99, label, value); err != nil {
t.Fatal(err)
}
// now test with only 2 custom percentiles
//
svc = newMockCloudWatch()
cw = New(namespace, svc, WithLogger(log.NewNopLogger()), WithPercentiles(0.50, 0.90))
histogram = cw.NewHistogram(name).With(label, value)
customQuantiles := func() (p50, p90, p95, p99 float64) {
err := cw.Send()
if err != nil {
t.Fatal(err)
}
svc.mtx.RLock()
defer svc.mtx.RUnlock()
if len(svc.valuesReceived[n50]) > 0 {
p50 = svc.valuesReceived[n50][0]
delete(svc.valuesReceived, n50)
}
if len(svc.valuesReceived[n90]) > 0 {
p90 = svc.valuesReceived[n90][0]
delete(svc.valuesReceived, n90)
}
// our teststat.TestHistogram wants us to give p95 and p99,
// but with custom percentiles we don't have those.
// So fake them. Maybe we should make teststat.nvq() public and use that?
p95 = 541.121341
p99 = 558.158697
// but fail if they are actually set (because that would mean the
// WithPercentiles() is not respected)
if _, isSet := svc.valuesReceived[n95]; isSet {
t.Fatal("p95 should not be set")
}
if _, isSet := svc.valuesReceived[n99]; isSet {
t.Fatal("p99 should not be set")
}
return
}
if err := teststat.TestHistogram(histogram, customQuantiles, 0.01); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n50, label, value); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n90, label, value); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n95, label, value); err != nil {
t.Fatal(err)
}
if err := svc.testDimensions(n99, label, value); err != nil {
t.Fatal(err)
}
}
func TestErrorLog(t *testing.T) {
namespace := "abc"
svc := newMockCloudWatch()
cw := New(namespace, svc, WithLogger(log.NewNopLogger()))
cw.NewGauge(metricNameToGenerateError).Set(123)
if err := cw.Send(); err != errTest {
t.Fatal("Expected error, but didn't get one")
}
}
================================================
FILE: metrics/cloudwatch2/cloudwatch2.go
================================================
// Package cloudwatch2 emits all data as a StatisticsSet (rather than
// a singular Value) to CloudWatch via the aws-sdk-go-v2 SDK.
package cloudwatch2
import (
"context"
"math"
"sync"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"golang.org/x/sync/errgroup"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/convert"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/log"
)
const (
maxConcurrentRequests = 20
)
// CloudWatchAPI is an interface that defines the set of Amazon CloudWatch API operations required by CloudWatch.
type CloudWatchAPI interface {
PutMetricData(ctx context.Context, params *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error)
}
// CloudWatch receives metrics observations and forwards them to CloudWatch.
// Create a CloudWatch object, use it to create metrics, and pass those metrics as
// dependencies to the components that will use them.
//
// To regularly report metrics to CloudWatch, use the WriteLoop helper method.
type CloudWatch struct {
mtx sync.RWMutex
sem chan struct{}
namespace string
svc CloudWatchAPI
counters *lv.Space
logger log.Logger
numConcurrentRequests int
}
// Option is a function adapter to change config of the CloudWatch struct
type Option func(*CloudWatch)
// WithLogger sets the Logger that will receive error messages generated
// during the WriteLoop. By default, no logger is used.
func WithLogger(logger log.Logger) Option {
return func(cw *CloudWatch) {
cw.logger = logger
}
}
// WithConcurrentRequests sets the upper limit on how many
// cloudwatch.PutMetricDataRequest may be under way at any
// given time. If n is greater than 20, 20 is used. By default,
// the max is set at 10 concurrent requests.
func WithConcurrentRequests(n int) Option {
return func(cw *CloudWatch) {
if n > maxConcurrentRequests {
n = maxConcurrentRequests
}
cw.numConcurrentRequests = n
}
}
// New returns a CloudWatch object that may be used to create metrics.
// Namespace is applied to all created metrics and maps to the CloudWatch namespace.
// Callers must ensure that regular calls to Send are performed, either
// manually or with one of the helper methods.
func New(namespace string, svc CloudWatchAPI, options ...Option) *CloudWatch {
cw := &CloudWatch{
namespace: namespace,
svc: svc,
counters: lv.NewSpace(),
numConcurrentRequests: 10,
logger: log.NewNopLogger(),
}
for _, optFunc := range options {
optFunc(cw)
}
cw.sem = make(chan struct{}, cw.numConcurrentRequests)
return cw
}
// NewCounter returns a counter. Observations are aggregated and emitted once
// per write invocation.
func (cw *CloudWatch) NewCounter(name string) metrics.Counter {
return &Counter{
name: name,
obs: cw.counters.Observe,
}
}
// NewGauge returns an gauge. Under the covers, there is no distinctions
// in CloudWatch for how Counters/Histograms/Gauges are reported, so this
// just wraps a cloudwatch2.Counter.
func (cw *CloudWatch) NewGauge(name string) metrics.Gauge {
return convert.NewCounterAsGauge(cw.NewCounter(name))
}
// NewHistogram returns a histogram. Under the covers, there is no distinctions
// in CloudWatch for how Counters/Histograms/Gauges are reported, so this
// just wraps a cloudwatch2.Counter.
func (cw *CloudWatch) NewHistogram(name string) metrics.Histogram {
return convert.NewCounterAsHistogram(cw.NewCounter(name))
}
// WriteLoop is a helper method that invokes Send every time the passed
// channel fires. This method blocks until ctx is canceled, so clients
// probably want to run it in its own goroutine. For typical usage, create a
// time.Ticker and pass its C channel to this method.
func (cw *CloudWatch) WriteLoop(ctx context.Context, c <-chan time.Time) {
for {
select {
case <-c:
if err := cw.Send(); err != nil {
cw.logger.Log("during", "Send", "err", err)
}
case <-ctx.Done():
return
}
}
}
// Send will fire an API request to CloudWatch with the latest stats for
// all metrics. It is preferred that the WriteLoop method is used.
func (cw *CloudWatch) Send() error {
cw.mtx.RLock()
defer cw.mtx.RUnlock()
now := time.Now()
var datums []types.MetricDatum
cw.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
datums = append(datums, types.MetricDatum{
MetricName: aws.String(name),
Dimensions: makeDimensions(lvs...),
StatisticValues: stats(values),
Timestamp: aws.Time(now),
})
return true
})
var batches [][]types.MetricDatum
for len(datums) > 0 {
var batch []types.MetricDatum
lim := len(datums)
if lim > maxConcurrentRequests {
lim = maxConcurrentRequests
}
batch, datums = datums[:lim], datums[lim:]
batches = append(batches, batch)
}
var g errgroup.Group
for _, batch := range batches {
batch := batch
g.Go(func() error {
cw.sem <- struct{}{}
defer func() {
<-cw.sem
}()
_, err := cw.svc.PutMetricData(context.TODO(), &cloudwatch.PutMetricDataInput{
Namespace: aws.String(cw.namespace),
MetricData: batch,
})
return err
})
}
return g.Wait()
}
var zero = float64(0.0)
// Just build this once to reduce construction costs whenever
// someone does a Send with no aggregated values.
var zeros = types.StatisticSet{
Maximum: &zero,
Minimum: &zero,
Sum: &zero,
SampleCount: &zero,
}
func stats(a []float64) *types.StatisticSet {
count := float64(len(a))
if count == 0 {
return &zeros
}
var sum float64
var min = math.MaxFloat64
var max = math.MaxFloat64 * -1
for _, f := range a {
sum += f
if f < min {
min = f
}
if f > max {
max = f
}
}
return &types.StatisticSet{
Maximum: &max,
Minimum: &min,
Sum: &sum,
SampleCount: &count,
}
}
func makeDimensions(labelValues ...string) []types.Dimension {
dimensions := make([]types.Dimension, len(labelValues)/2)
for i, j := 0, 0; i < len(labelValues); i, j = i+2, j+1 {
dimensions[j] = types.Dimension{
Name: aws.String(labelValues[i]),
Value: aws.String(labelValues[i+1]),
}
}
return dimensions
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is a counter. Observations are forwarded to a node
// object, and aggregated per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
================================================
FILE: metrics/cloudwatch2/cloudwatch2_test.go
================================================
package cloudwatch2
import (
"context"
"strings"
"testing"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
)
func TestStats(t *testing.T) {
testCases := []struct {
name string
vals []float64
xMin float64
xMax float64
xSum float64
xCt float64
}{
{
"empty",
[]float64{},
0.0,
0.0,
0.0,
0.0,
},
{
"single",
[]float64{3.1416},
3.1416,
3.1416,
3.1416,
1.0,
},
{
"double",
[]float64{1.0, 9.0},
1.0,
9.0,
10.0,
2.0,
},
{
"multiple",
[]float64{5.0, 1.0, 9.0, 5.0},
1.0,
9.0,
20.0,
4.0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := stats(tc.vals)
if tc.xMin != *s.Minimum {
t.Errorf("expected [%f]: %f\n", tc.xMin, *s.Minimum)
}
if tc.xMax != *s.Maximum {
t.Errorf("expected [%f]: %f\n", tc.xMax, *s.Maximum)
}
if tc.xSum != *s.Sum {
t.Errorf("expected [%f]: %f\n", tc.xSum, *s.Sum)
}
if tc.xCt != *s.SampleCount {
t.Errorf("expected [%f]: %f\n", tc.xCt, *s.SampleCount)
}
})
}
}
type mockCloudWatch struct {
CloudWatchAPI
latestName string
latestData []types.MetricDatum
}
func (mcw *mockCloudWatch) PutMetricData(ctx context.Context, params *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) {
mcw.latestName = *params.Namespace
mcw.latestData = params.MetricData
return nil, nil
}
func TestSend(t *testing.T) {
ns := "example-namespace"
svc := &mockCloudWatch{}
cw := New(ns, svc)
c := cw.NewCounter("c").With("charlie", "cat")
h := cw.NewHistogram("h").With("hotel", "horse")
g := cw.NewGauge("g").With("golf", "giraffe")
c.Add(4.0)
c.Add(5.0)
c.Add(6.0)
h.Observe(3.0)
h.Observe(5.0)
h.Observe(7.0)
g.Set(2.0)
g.Set(5.0)
g.Set(8.0)
err := cw.Send()
if err != nil {
t.Fatalf("unexpected: %v\n", err)
}
if ns != svc.latestName {
t.Errorf("expected namespace %q; not %q\n", ns, svc.latestName)
}
if len(svc.latestData) != 3 {
t.Errorf("expected 3 datums: %v\n", svc.latestData)
}
for _, datum := range svc.latestData {
initial := *datum.MetricName
if len(datum.Dimensions) != 1 {
t.Errorf("expected 1 dimension: %v\n", datum)
}
if !strings.HasPrefix(*datum.Dimensions[0].Name, initial) {
t.Errorf("expected %q in Name of %v\n", initial, datum.Dimensions)
}
if !strings.HasPrefix(*datum.Dimensions[0].Value, initial) {
t.Errorf("expected %q in Value of %v\n", initial, datum.Dimensions)
}
if datum.StatisticValues == nil {
t.Errorf("expected StatisticValues in %v\n", datum)
}
if *datum.StatisticValues.Sum != 15.0 {
t.Errorf("expected 15.0 for Sum in %v\n", datum)
}
if *datum.StatisticValues.SampleCount != 3.0 {
t.Errorf("expected 3.0 for SampleCount in %v\n", datum)
}
}
}
================================================
FILE: metrics/discard/discard.go
================================================
// Package discard provides a no-op metrics backend.
package discard
import "github.com/go-kit/kit/metrics"
type counter struct{}
// NewCounter returns a new no-op counter.
func NewCounter() metrics.Counter { return counter{} }
// With implements Counter.
func (c counter) With(labelValues ...string) metrics.Counter { return c }
// Add implements Counter.
func (c counter) Add(delta float64) {}
type gauge struct{}
// NewGauge returns a new no-op gauge.
func NewGauge() metrics.Gauge { return gauge{} }
// With implements Gauge.
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.
func NewHistogram() metrics.Histogram { return histogram{} }
// With implements Histogram.
func (h histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe implements histogram.
func (h histogram) Observe(value float64) {}
================================================
FILE: metrics/doc.go
================================================
// 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.
//
// 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)
//
// Usage
//
// 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.
//
// latency := prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
// Namespace: "myteam",
// Subsystem: "foosvc",
// Name: "request_latency_seconds",
// Help: "Incoming request latency in seconds.",
// }, []string{"method", "status_code"})
//
// Write your components to take the metrics they will use as parameters to
// their constructors. Use the interface types, not the concrete types. That is,
//
// // NewAPI takes metrics.Histogram, not *prometheus.Summary
// func NewAPI(s Store, logger log.Logger, latency metrics.Histogram) *API {
// // ...
// }
//
// func (a *API) ServeFoo(w http.ResponseWriter, r *http.Request) {
// begin := time.Now()
// // ...
// a.latency.Observe(time.Since(begin).Seconds())
// }
//
// Finally, pass the metrics as dependencies when building your object graph.
// This should happen in func main, not in the global scope.
//
// 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
// distinction.
//
// SYSTEM DIM COUNTERS GAUGES HISTOGRAMS
// dogstatsd n batch, push-aggregate batch, push-aggregate native, batch, push-each
// statsd 1 batch, push-aggregate batch, push-aggregate native, batch, push-each
// graphite 1 batch, push-aggregate batch, push-aggregate synthetic, batch, push-aggregate
// expvar 1 atomic atomic synthetic, batch, in-place expose
// influx n custom custom custom
// prometheus n native native native
// pcp 1 native native native
// cloudwatch n batch push-aggregate batch push-aggregate synthetic, batch, push-aggregate
//
package metrics
================================================
FILE: metrics/dogstatsd/dogstatsd.go
================================================
// Package dogstatsd provides a DogStatsD backend for package metrics. It's very
// similar to StatsD, but supports arbitrary tags per-metric, which map to Go
// kit's label values. So, while label values are no-ops in StatsD, they are
// supported here. For more details, see the documentation at
// http://docs.datadoghq.com/guides/dogstatsd/.
//
// This package batches observations and emits them on some schedule to the
// remote server. This is useful even if you connect to your DogStatsD server
// over UDP. Emitting one network packet per observation can quickly overwhelm
// even the fastest internal network.
package dogstatsd
import (
"context"
"fmt"
"io"
"math/rand"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/kit/metrics/internal/ratemap"
"github.com/go-kit/kit/util/conn"
"github.com/go-kit/log"
)
// Dogstatsd receives metrics observations and forwards them to a DogStatsD
// server. Create a Dogstatsd object, use it to create metrics, and pass those
// metrics as dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Timings and
// histograms are buffered but not aggregated.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a DogStatsD server, use the SendLoop helper method.
type Dogstatsd struct {
mtx sync.RWMutex
prefix string
rates *ratemap.RateMap
counters *lv.Space
gauges map[string]*gaugeNode
timings *lv.Space
histograms *lv.Space
logger log.Logger
lvs lv.LabelValues
}
// New returns a Dogstatsd 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, lvs ...string) *Dogstatsd {
if len(lvs)%2 != 0 {
panic("odd number of LabelValues; programmer error!")
}
return &Dogstatsd{
prefix: prefix,
rates: ratemap.New(),
counters: lv.NewSpace(),
gauges: map[string]*gaugeNode{},
timings: lv.NewSpace(),
histograms: lv.NewSpace(),
logger: logger,
lvs: lvs,
}
}
// NewCounter returns a counter, sending observations to this Dogstatsd object.
func (d *Dogstatsd) NewCounter(name string, sampleRate float64) *Counter {
d.rates.Set(name, sampleRate)
return &Counter{
name: name,
obs: sampleObservations(d.counters.Observe, sampleRate),
}
}
// NewGauge returns a gauge, sending observations to this Dogstatsd object.
func (d *Dogstatsd) NewGauge(name string) *Gauge {
d.mtx.Lock()
n, ok := d.gauges[name]
if !ok {
n = &gaugeNode{gauge: &Gauge{g: generic.NewGauge(name), ddog: d}}
d.gauges[name] = n
}
d.mtx.Unlock()
return n.gauge
}
// NewTiming returns a histogram whose observations are interpreted as
// millisecond durations, and are forwarded to this Dogstatsd object.
func (d *Dogstatsd) NewTiming(name string, sampleRate float64) *Timing {
d.rates.Set(name, sampleRate)
return &Timing{
name: name,
obs: sampleObservations(d.timings.Observe, sampleRate),
}
}
// NewHistogram returns a histogram whose observations are of an unspecified
// unit, and are forwarded to this Dogstatsd object.
func (d *Dogstatsd) NewHistogram(name string, sampleRate float64) *Histogram {
d.rates.Set(name, sampleRate)
return &Histogram{
name: name,
obs: sampleObservations(d.histograms.Observe, sampleRate),
}
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until ctx is canceled,
// so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (d *Dogstatsd) WriteLoop(ctx context.Context, c <-chan time.Time, w io.Writer) {
for {
select {
case <-c:
if _, err := d.WriteTo(w); err != nil {
d.logger.Log("during", "WriteTo", "err", err)
}
case <-ctx.Done():
return
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until ctx is canceled, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (d *Dogstatsd) SendLoop(ctx context.Context, c <-chan time.Time, network, address string) {
d.WriteLoop(ctx, c, conn.NewDefaultManager(network, address, d.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// DogStatsD format. WriteTo abides best-effort semantics, so observations are
// lost if there is a problem with the write. Clients should be sure to call
// WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) {
var n int
d.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s%s:%f|c%s%s\n", d.prefix, name, sum(values), sampling(d.rates.Get(name)), d.tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
d.mtx.RLock()
for _, root := range d.gauges {
root.walk(func(name string, lvs lv.LabelValues, value float64) bool {
n, err = fmt.Fprintf(w, "%s%s:%f|g%s\n", d.prefix, name, value, d.tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
return true
})
}
d.mtx.RUnlock()
d.timings.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
sampleRate := d.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s%s:%f|ms%s%s\n", d.prefix, name, value, sampling(sampleRate), d.tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
d.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
sampleRate := d.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s%s:%f|h%s%s\n", d.prefix, name, value, sampling(sampleRate), d.tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
return count, err
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func sampling(r float64) string {
var sv string
if r < 1.0 {
sv = fmt.Sprintf("|@%f", r)
}
return sv
}
func (d *Dogstatsd) tagValues(labelValues []string) string {
if len(labelValues) == 0 && len(d.lvs) == 0 {
return ""
}
if len(labelValues)%2 != 0 {
panic("tagValues received a labelValues with an odd number of strings")
}
pairs := make([]string, 0, (len(d.lvs)+len(labelValues))/2)
for i := 0; i < len(d.lvs); i += 2 {
pairs = append(pairs, d.lvs[i]+":"+d.lvs[i+1])
}
for i := 0; i < len(labelValues); i += 2 {
pairs = append(pairs, labelValues[i]+":"+labelValues[i+1])
}
return "|#" + strings.Join(pairs, ",")
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// sampleObservations returns a modified observeFunc that samples observations.
func sampleObservations(obs observeFunc, sampleRate float64) observeFunc {
if sampleRate >= 1 {
return obs
}
return func(name string, lvs lv.LabelValues, value float64) {
if rand.Float64() > sampleRate {
return
}
obs(name, lvs, value)
}
}
// Counter is a DogStatsD counter. Observations are forwarded to a Dogstatsd
// object, and aggregated (summed) per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
// Gauge is a DogStatsD gauge. Observations are forwarded to a Dogstatsd
// object, and aggregated (the last observation selected) per timeseries.
type Gauge struct {
g *generic.Gauge
ddog *Dogstatsd
set int32
}
// With implements metrics.Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
g.ddog.mtx.RLock()
node := g.ddog.gauges[g.g.Name]
g.ddog.mtx.RUnlock()
ga := &Gauge{g: g.g.With(labelValues...).(*generic.Gauge), ddog: g.ddog}
return node.addGauge(ga, ga.g.LabelValues())
}
// Set implements metrics.Gauge.
func (g *Gauge) Set(value float64) {
g.g.Set(value)
g.touch()
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.g.Add(delta)
g.touch()
}
// Timing is a DogStatsD timing, or metrics.Histogram. Observations are
// forwarded to a Dogstatsd object, and collected (but not aggregated) per
// timeseries.
type Timing struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Timing.
func (t *Timing) With(labelValues ...string) metrics.Histogram {
return &Timing{
name: t.name,
lvs: t.lvs.With(labelValues...),
obs: t.obs,
}
}
// Observe implements metrics.Histogram. Value is interpreted as milliseconds.
func (t *Timing) Observe(value float64) {
t.obs(t.name, t.lvs, value)
}
// Histogram is a DogStatsD histrogram. Observations are forwarded to a
// Dogstatsd object, and collected (but not aggregated) per timeseries.
type Histogram struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
name: h.name,
lvs: h.lvs.With(labelValues...),
obs: h.obs,
}
}
// Observe implements metrics.Histogram.
func (h *Histogram) Observe(value float64) {
h.obs(h.name, h.lvs, value)
}
type pair struct{ label, value string }
type gaugeNode struct {
mtx sync.RWMutex
gauge *Gauge
children map[pair]*gaugeNode
}
func (n *gaugeNode) addGauge(g *Gauge, lvs lv.LabelValues) *Gauge {
n.mtx.Lock()
defer n.mtx.Unlock()
if len(lvs) == 0 {
if n.gauge == nil {
n.gauge = g
}
return n.gauge
}
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]*gaugeNode{}
}
child, ok := n.children[head]
if !ok {
child = &gaugeNode{}
n.children[head] = child
}
return child.addGauge(g, tail)
}
func (n *gaugeNode) walk(fn func(string, lv.LabelValues, float64) bool) bool {
n.mtx.RLock()
defer n.mtx.RUnlock()
if n.gauge != nil {
value, ok := n.gauge.read()
if ok && !fn(n.gauge.g.Name, n.gauge.g.LabelValues(), value) {
return false
}
}
for _, child := range n.children {
if !child.walk(fn) {
return false
}
}
return true
}
func (g *Gauge) touch() {
atomic.StoreInt32(&(g.set), 1)
}
func (g *Gauge) read() (float64, bool) {
set := atomic.SwapInt32(&(g.set), 0)
return g.g.Value(), set != 0
}
================================================
FILE: metrics/dogstatsd/dogstatsd_test.go
================================================
package dogstatsd
import (
"testing"
"github.com/go-kit/kit/metrics/teststat"
"github.com/go-kit/log"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value"
regex := `^` + prefix + name + `:([0-9\.]+)\|c\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
counter := d.NewCounter(name, 1.0).With(label, value)
valuef := teststat.SumLines(d, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestCounterSampled(t *testing.T) {
// This will involve multiplying the observed sum by the inverse of the
// sample rate and checking against the expected value within some
// tolerance.
t.Skip("TODO")
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc"
regex := `^` + prefix + name + `:([0-9\.]+)\|g\|#hostname:foohost,` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger(), "hostname", "foohost")
gauge := d.NewGauge(name).With(label, value)
valuef := teststat.LastLine(d, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
// DogStatsD histograms just emit all observations. So, we collect them into
// a generic histogram, and run the statistics test on that.
func TestHistogram(t *testing.T) {
prefix, name := "dogstatsd.", "histogram_test"
label, value := "abc", "def"
regex := `^` + prefix + name + `:([0-9\.]+)\|h\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewHistogram(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogramSampled(t *testing.T) {
prefix, name := "dogstatsd.", "sampled_histogram_test"
label, value := "foo", "bar"
regex := `^` + prefix + name + `:([0-9\.]+)\|h\|@0\.01[0]*\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewHistogram(name, 0.01).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50)
if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}
func TestTiming(t *testing.T) {
prefix, name := "dogstatsd.", "timing_test"
label, value := "wiggle", "bottom"
regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewTiming(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestTimingSampled(t *testing.T) {
prefix, name := "dogstatsd.", "sampled_timing_test"
label, value := "internal", "external"
regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0.03[0]*\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewTiming(name, 0.03).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50)
if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/expvar/expvar.go
================================================
// Package expvar provides expvar backends for metrics.
// Label values are not supported.
package expvar
import (
"expvar"
"sync"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
)
// Counter implements the counter metric with an expvar float.
// Label values are not supported.
type Counter struct {
f *expvar.Float
}
// NewCounter creates an expvar Float with the given name, and returns an object
// that implements the Counter interface.
func NewCounter(name string) *Counter {
return &Counter{
f: expvar.NewFloat(name),
}
}
// With is a no-op.
func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
// Add implements Counter.
func (c *Counter) Add(delta float64) { c.f.Add(delta) }
// Gauge implements the gauge metric with an expvar float.
// Label values are not supported.
type Gauge struct {
f *expvar.Float
}
// NewGauge creates an expvar Float with the given name, and returns an object
// that implements the Gauge interface.
func NewGauge(name string) *Gauge {
return &Gauge{
f: expvar.NewFloat(name),
}
}
// With is a no-op.
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
// the name as a suffix. Label values are not supported.
type Histogram struct {
mtx sync.Mutex
h *generic.Histogram
p50 *expvar.Float
p90 *expvar.Float
p95 *expvar.Float
p99 *expvar.Float
}
// NewHistogram returns a Histogram object with the given name and number of
// buckets in the underlying histogram object. 50 is a good default number of
// buckets.
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{
h: generic.NewHistogram(name, buckets),
p50: expvar.NewFloat(name + ".p50"),
p90: expvar.NewFloat(name + ".p90"),
p95: expvar.NewFloat(name + ".p95"),
p99: expvar.NewFloat(name + ".p99"),
}
}
// With is a no-op.
func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe implements Histogram.
func (h *Histogram) Observe(value float64) {
h.mtx.Lock()
defer h.mtx.Unlock()
h.h.Observe(value)
h.p50.Set(h.h.Quantile(0.50))
h.p90.Set(h.h.Quantile(0.90))
h.p95.Set(h.h.Quantile(0.95))
h.p99.Set(h.h.Quantile(0.99))
}
================================================
FILE: metrics/expvar/expvar_test.go
================================================
package expvar
import (
"strconv"
"testing"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
counter := NewCounter("expvar_counter").With("label values", "not supported").(*Counter)
value := func() float64 { f, _ := strconv.ParseFloat(counter.f.String(), 64); return f }
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
gauge := NewGauge("expvar_gauge").With("label values", "not supported").(*Gauge)
value := func() []float64 { f, _ := strconv.ParseFloat(gauge.f.String(), 64); return []float64{f} }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
histogram := NewHistogram("expvar_histogram", 50).With("label values", "not supported").(*Histogram)
quantiles := func() (float64, float64, float64, float64) {
p50, _ := strconv.ParseFloat(histogram.p50.String(), 64)
p90, _ := strconv.ParseFloat(histogram.p90.String(), 64)
p95, _ := strconv.ParseFloat(histogram.p95.String(), 64)
p99, _ := strconv.ParseFloat(histogram.p99.String(), 64)
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/generic/generic.go
================================================
// Package generic implements generic versions of each of the metric types. They
// can be embedded by other implementations, and converted to specific formats
// as necessary.
package generic
import (
"fmt"
"io"
"math"
"sync"
"sync/atomic"
"github.com/VividCortex/gohistogram"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
)
// Counter is an in-memory implementation of a Counter.
type Counter struct {
bits uint64 // bits has to be the first word in order to be 64-aligned on 32-bit
Name string
lvs lv.LabelValues
}
// NewCounter returns a new, usable Counter.
func NewCounter(name string) *Counter {
return &Counter{
Name: name,
}
}
// 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...),
}
}
// Add implements Counter.
func (c *Counter) Add(delta float64) {
for {
var (
old = atomic.LoadUint64(&c.bits)
newf = math.Float64frombits(old) + delta
new = math.Float64bits(newf)
)
if atomic.CompareAndSwapUint64(&c.bits, old, new) {
break
}
}
}
// Value returns the current value of the counter.
func (c *Counter) Value() float64 {
return math.Float64frombits(atomic.LoadUint64(&c.bits))
}
// ValueReset returns the current value of the counter, and resets it to zero.
// This is useful for metrics backends whose counter aggregations expect deltas,
// like Graphite.
func (c *Counter) ValueReset() float64 {
for {
var (
old = atomic.LoadUint64(&c.bits)
newf = 0.0
new = math.Float64bits(newf)
)
if atomic.CompareAndSwapUint64(&c.bits, old, new) {
return math.Float64frombits(old)
}
}
}
// LabelValues returns the set of label values attached to the counter.
func (c *Counter) LabelValues() []string {
return c.lvs
}
// Gauge is an in-memory implementation of a Gauge.
type Gauge struct {
bits uint64 // bits has to be the first word in order to be 64-aligned on 32-bit
Name string
lvs lv.LabelValues
}
// NewGauge returns a new, usable Gauge.
func NewGauge(name string) *Gauge {
return &Gauge{
Name: name,
}
}
// 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...),
}
}
// Set implements Gauge.
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))
}
// LabelValues returns the set of label values attached to the gauge.
func (g *Gauge) LabelValues() []string {
return g.lvs
}
// Histogram is an in-memory implementation of a streaming histogram, based on
// VividCortex/gohistogram. It dynamically computes quantiles, so it's not
// suitable for aggregation.
type Histogram struct {
Name string
lvs lv.LabelValues
h *safeHistogram
}
// NewHistogram returns a numeric histogram based on VividCortex/gohistogram. A
// good default value for buckets is 50.
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{
Name: name,
h: &safeHistogram{Histogram: 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,
}
}
// 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)
}
// LabelValues returns the set of label values attached to the histogram.
func (h *Histogram) LabelValues() []string {
return h.lvs
}
// 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.Fprint(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
}
// Quantile is a pair of a quantile (0..100) and its observed maximum value.
type Quantile struct {
Quantile int // 0..100
Value int64
}
// SimpleHistogram is an in-memory implementation of a Histogram. It only tracks
// an approximate moving average, so is likely too naïve for many use cases.
type SimpleHistogram struct {
mtx sync.RWMutex
lvs lv.LabelValues
avg float64
n uint64
}
// NewSimpleHistogram returns a SimpleHistogram, ready for observations.
func NewSimpleHistogram() *SimpleHistogram {
return &SimpleHistogram{}
}
// With implements Histogram.
func (h *SimpleHistogram) With(labelValues ...string) metrics.Histogram {
return &SimpleHistogram{
lvs: h.lvs.With(labelValues...),
avg: h.avg,
n: h.n,
}
}
// Observe implements Histogram.
func (h *SimpleHistogram) Observe(value float64) {
h.mtx.Lock()
defer h.mtx.Unlock()
h.n++
h.avg -= h.avg / float64(h.n)
h.avg += value / float64(h.n)
}
// ApproximateMovingAverage returns the approximate moving average of observations.
func (h *SimpleHistogram) ApproximateMovingAverage() float64 {
h.mtx.RLock()
defer h.mtx.RUnlock()
return h.avg
}
// LabelValues returns the set of label values attached to the histogram.
func (h *SimpleHistogram) LabelValues() []string {
return h.lvs
}
================================================
FILE: metrics/generic/generic_test.go
================================================
package generic_test
// This is package generic_test in order to get around an import cycle: this
// package imports teststat to do its testing, but package teststat imports
// generic to use its Histogram in the Quantiles helper function.
import (
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/ioutil"
"math"
"math/rand"
"sync"
"testing"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/teststat"
)
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)
}
value := counter.Value
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestValueReset(t *testing.T) {
counter := generic.NewCounter("test_value_reset")
counter.Add(123)
counter.Add(456)
counter.Add(789)
if want, have := float64(123+456+789), counter.ValueReset(); want != have {
t.Errorf("want %f, have %f", want, have)
}
if want, have := float64(0), counter.Value(); want != have {
t.Errorf("want %f, have %f", want, have)
}
}
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)
}
value := func() []float64 { return []float64{gauge.Value()} }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
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)
}
quantiles := func() (float64, float64, float64, float64) {
return histogram.Quantile(0.50), histogram.Quantile(0.90), histogram.Quantile(0.95), histogram.Quantile(0.99)
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
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 (
sum int
count = 1234 // not too big
)
for i := 0; i < count; i++ {
value := rand.Intn(1000)
sum += value
histogram.Observe(float64(value))
}
var (
want = float64(sum) / float64(count)
have = histogram.ApproximateMovingAverage()
tolerance = 0.001 // real real slim
)
if math.Abs(want-have)/want > tolerance {
t.Errorf("want %f, have %f", want, have)
}
}
// Naive atomic alignment test.
// The problem is related to the use of `atomic.*` and not directly to a structure.
// But currently works for Counter and Gauge.
// To have a more solid test, this test should be removed and the other tests should be run on a 32-bit arch.
func TestAtomicAlignment(t *testing.T) {
content, err := ioutil.ReadFile("./generic.go")
if err != nil {
t.Fatal(err)
}
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "generic.go", content, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
conf := types.Config{Importer: importer.ForCompiler(fset, "source", nil)}
pkg, err := conf.Check(".", fset, []*ast.File{file}, nil)
if err != nil {
t.Fatal(err)
}
// uses ARM as reference for 32-bit arch
sizes := types.SizesFor("gc", "arm")
names := []string{"Counter", "Gauge"}
for _, name := range names {
t.Run(name, func(t *testing.T) {
checkAtomicAlignment(t, sizes, pkg.Scope().Lookup(name), pkg)
})
}
}
func checkAtomicAlignment(t *testing.T, sizes types.Sizes, obj types.Object, pkg *types.Package) {
t.Helper()
st := obj.Type().Underlying().(*types.Struct)
posToCheck := make(map[int]types.Type)
var vars []*types.Var
for i := 0; i < st.NumFields(); i++ {
field := st.Field(i)
if v, ok := field.Type().(*types.Basic); ok {
switch v.Kind() {
case types.Uint64, types.Float64, types.Int64:
posToCheck[i] = v
}
}
vars = append(vars, types.NewVar(field.Pos(), pkg, field.Name(), field.Type()))
}
offsets := sizes.Offsetsof(vars)
for i, offset := range offsets {
if _, ok := posToCheck[i]; !ok {
continue
}
if offset%8 != 0 {
t.Errorf("misalignment detected in %s for the type %s, offset %d", obj.Name(), posToCheck[i], offset)
}
}
}
================================================
FILE: metrics/graphite/graphite.go
================================================
// Package graphite provides a Graphite backend for metrics. Metrics are batched
// and emitted in the plaintext protocol. For more information, see
// http://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol
//
// Graphite does not have a native understanding of metric parameterization, so
// label values not supported. Use distinct metrics for each unique combination
// of label values.
package graphite
import (
"context"
"fmt"
"io"
"sync"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/util/conn"
"github.com/go-kit/log"
)
// Graphite receives metrics observations and forwards them to a Graphite server.
// Create a Graphite object, use it to create metrics, and pass those metrics as
// dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Histograms are
// exploded into per-quantile gauges and reported once per write.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a Graphite server, use the SendLoop helper method.
type Graphite struct {
mtx sync.RWMutex
prefix string
counters map[string]*Counter
gauges map[string]*Gauge
histograms map[string]*Histogram
logger log.Logger
}
// New returns a Graphite 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 {
return &Graphite{
prefix: prefix,
counters: map[string]*Counter{},
gauges: map[string]*Gauge{},
histograms: map[string]*Histogram{},
logger: logger,
}
}
// NewCounter returns a counter. Observations are aggregated and emitted once
// per write invocation.
func (g *Graphite) NewCounter(name string) *Counter {
c := NewCounter(g.prefix + name)
g.mtx.Lock()
g.counters[g.prefix+name] = c
g.mtx.Unlock()
return c
}
// NewGauge returns a gauge. Observations are aggregated and emitted once per
// write invocation.
func (g *Graphite) NewGauge(name string) *Gauge {
ga := NewGauge(g.prefix + name)
g.mtx.Lock()
g.gauges[g.prefix+name] = ga
g.mtx.Unlock()
return ga
}
// NewHistogram returns a histogram. Observations are aggregated and emitted as
// per-quantile gauges, once per write invocation. 50 is a good default value
// for buckets.
func (g *Graphite) NewHistogram(name string, buckets int) *Histogram {
h := NewHistogram(g.prefix+name, buckets)
g.mtx.Lock()
g.histograms[g.prefix+name] = h
g.mtx.Unlock()
return h
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until ctx is canceled,
// so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (g *Graphite) WriteLoop(ctx context.Context, c <-chan time.Time, w io.Writer) {
for {
select {
case <-c:
if _, err := g.WriteTo(w); err != nil {
g.logger.Log("during", "WriteTo", "err", err)
}
case <-ctx.Done():
return
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until ctx is canceled, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (g *Graphite) SendLoop(ctx context.Context, c <-chan time.Time, network, address string) {
g.WriteLoop(ctx, c, conn.NewDefaultManager(network, address, g.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// Graphite plaintext format. WriteTo abides best-effort semantics, so
// observations are lost if there is a problem with the write. Clients should be
// sure to call WriteTo regularly, ideally through the WriteLoop or SendLoop
// helper methods.
func (g *Graphite) WriteTo(w io.Writer) (count int64, err error) {
g.mtx.RLock()
defer g.mtx.RUnlock()
now := time.Now().Unix()
for name, c := range g.counters {
n, err := fmt.Fprintf(w, "%s %f %d\n", name, c.c.ValueReset(), now)
if err != nil {
return count, err
}
count += int64(n)
}
for name, ga := range g.gauges {
n, err := fmt.Fprintf(w, "%s %f %d\n", name, ga.g.Value(), now)
if err != nil {
return count, err
}
count += int64(n)
}
for name, h := range g.histograms {
for _, p := range []struct {
s string
f float64
}{
{"50", 0.50},
{"90", 0.90},
{"95", 0.95},
{"99", 0.99},
} {
n, err := fmt.Fprintf(w, "%s.p%s %f %d\n", name, p.s, h.h.Quantile(p.f), now)
if err != nil {
return count, err
}
count += int64(n)
}
}
return count, err
}
// Counter is a Graphite counter metric.
type Counter struct {
c *generic.Counter
}
// NewCounter returns a new usable counter metric.
func NewCounter(name string) *Counter {
return &Counter{generic.NewCounter(name)}
}
// With is a no-op.
func (c *Counter) With(...string) metrics.Counter { return c }
// Add implements counter.
func (c *Counter) Add(delta float64) { c.c.Add(delta) }
// Gauge is a Graphite gauge metric.
type Gauge struct {
g *generic.Gauge
}
// NewGauge returns a new usable Gauge metric.
func NewGauge(name string) *Gauge {
return &Gauge{generic.NewGauge(name)}
}
// With is a no-op.
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 {
h *generic.Histogram
}
// NewHistogram returns a new usable Histogram metric.
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{generic.NewHistogram(name, buckets)}
}
// With is a no-op.
func (h *Histogram) With(...string) metrics.Histogram { return h }
// Observe implements histogram.
func (h *Histogram) Observe(value float64) { h.h.Observe(value) }
================================================
FILE: metrics/graphite/graphite_test.go
================================================
package graphite
import (
"bytes"
"regexp"
"strconv"
"testing"
"github.com/go-kit/kit/metrics/teststat"
"github.com/go-kit/log"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value" // ignored for Graphite
regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
g := New(prefix, log.NewNopLogger())
counter := g.NewCounter(name).With(label, value)
valuef := teststat.SumLines(g, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc" // ignored for Graphite
regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
g := New(prefix, log.NewNopLogger())
gauge := g.NewGauge(name).With(label, value)
valuef := teststat.LastLine(g, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
// The histogram test is actually like 4 gauge tests.
prefix, name := "graphite.", "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]+`)
re95 := regexp.MustCompile(prefix + name + `.p95 ([0-9\.]+) [0-9]+`)
re99 := regexp.MustCompile(prefix + name + `.p99 ([0-9\.]+) [0-9]+`)
g := New(prefix, log.NewNopLogger())
histogram := g.NewHistogram(name, 50).With(label, value)
quantiles := func() (float64, float64, float64, float64) {
var buf bytes.Buffer
g.WriteTo(&buf)
match50 := re50.FindStringSubmatch(buf.String())
p50, _ := strconv.ParseFloat(match50[1], 64)
match90 := re90.FindStringSubmatch(buf.String())
p90, _ := strconv.ParseFloat(match90[1], 64)
match95 := re95.FindStringSubmatch(buf.String())
p95, _ := strconv.ParseFloat(match95[1], 64)
match99 := re99.FindStringSubmatch(buf.String())
p99, _ := strconv.ParseFloat(match99[1], 64)
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/influx/example_test.go
================================================
package influx
import (
"fmt"
"regexp"
influxdb "github.com/influxdata/influxdb1-client/v2"
"github.com/go-kit/log"
)
func ExampleCounter() {
in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
counter := in.NewCounter("influx_counter")
counter.Add(10)
counter.With("error", "true").Add(1)
counter.With("error", "false").Add(2)
counter.Add(50)
client := &bufWriter{}
in.WriteTo(client)
expectedLines := []string{
`(influx_counter,a=b count=60) [0-9]{19}`,
`(influx_counter,a=b,error=true count=1) [0-9]{19}`,
`(influx_counter,a=b,error=false count=2) [0-9]{19}`,
}
if err := extractAndPrintMessage(expectedLines, client.buf.String()); err != nil {
fmt.Println(err.Error())
}
// Output:
// influx_counter,a=b count=60
// influx_counter,a=b,error=true count=1
// influx_counter,a=b,error=false count=2
}
func ExampleGauge() {
in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
gauge := in.NewGauge("influx_gauge")
gauge.Set(10)
gauge.With("error", "true").Set(2)
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}`,
}
if err := extractAndPrintMessage(expectedLines, client.buf.String()); err != nil {
fmt.Println(err.Error())
}
// 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
}
func ExampleHistogram() {
in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
histogram := in.NewHistogram("influx_histogram")
histogram.Observe(float64(10))
histogram.With("error", "true").Observe(float64(1))
histogram.With("error", "false").Observe(float64(2))
histogram.Observe(float64(50))
client := &bufWriter{}
in.WriteTo(client)
expectedLines := []string{
`(influx_histogram,foo=alpha p50=10,p90=50,p95=50,p99=50) [0-9]{19}`,
`(influx_histogram,error=true,foo=alpha p50=1,p90=1,p95=1,p99=1) [0-9]{19}`,
`(influx_histogram,error=false,foo=alpha p50=2,p90=2,p95=2,p99=2) [0-9]{19}`,
}
if err := extractAndPrintMessage(expectedLines, client.buf.String()); err != nil {
fmt.Println(err.Error())
}
// Output:
// influx_histogram,foo=alpha p50=10,p90=50,p95=50,p99=50
// influx_histogram,error=true,foo=alpha p50=1,p90=1,p95=1,p99=1
// influx_histogram,error=false,foo=alpha p50=2,p90=2,p95=2,p99=2
}
func extractAndPrintMessage(expected []string, msg string) error {
for _, pattern := range expected {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(msg)
if len(match) != 2 {
return fmt.Errorf("pattern not found! {%s} [%s]: %v\n", pattern, msg, match)
}
fmt.Println(match[1])
}
return nil
}
================================================
FILE: metrics/influx/influx.go
================================================
// Package influx provides an InfluxDB implementation for metrics. The model is
// similar to other push-based instrumentation systems. Observations are
// aggregated locally and emitted to the Influx server on regular intervals.
package influx
import (
"context"
"time"
influxdb "github.com/influxdata/influxdb1-client/v2"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/log"
)
// Influx is a store for metrics that will be emitted to an Influx database.
//
// Influx is a general purpose time-series database, and has no native concepts
// of counters, gauges, or histograms. Counters are modeled as a timeseries with
// one data point per flush, with a "count" field that reflects all adds since
// the last flush. Gauges are modeled as a timeseries with one data point per
// flush, with a "value" field that reflects the current state of the gauge.
// Histograms are modeled as a timeseries with one data point per combination of tags,
// with a set of quantile fields that reflects the p50, p90, p95 & p99.
//
// Influx tags are attached to the Influx object, can be given to each
// metric at construction and can be updated anytime via With function. Influx fields
// are mapped to Go kit label values directly by this collector. Actual metric
// values are provided as fields with specific names depending on the metric.
//
// All observations are collected in memory locally, and flushed on demand.
type Influx struct {
counters *lv.Space
gauges *lv.Space
histograms *lv.Space
tags map[string]string
conf influxdb.BatchPointsConfig
logger log.Logger
}
// New returns an Influx, ready to create metrics and collect observations. Tags
// are applied to all metrics created from this object. The BatchPointsConfig is
// used during flushing.
func New(tags map[string]string, conf influxdb.BatchPointsConfig, logger log.Logger) *Influx {
return &Influx{
counters: lv.NewSpace(),
gauges: lv.NewSpace(),
histograms: lv.NewSpace(),
tags: tags,
conf: conf,
logger: logger,
}
}
// NewCounter returns an Influx counter.
func (in *Influx) NewCounter(name string) *Counter {
return &Counter{
name: name,
obs: in.counters.Observe,
}
}
// NewGauge returns an Influx gauge.
func (in *Influx) NewGauge(name string) *Gauge {
return &Gauge{
name: name,
obs: in.gauges.Observe,
add: in.gauges.Add,
}
}
// NewHistogram returns an Influx histogram.
func (in *Influx) NewHistogram(name string) *Histogram {
return &Histogram{
name: name,
obs: in.histograms.Observe,
}
}
// BatchPointsWriter captures a subset of the influxdb.Client methods necessary
// for emitting metrics observations.
type BatchPointsWriter interface {
Write(influxdb.BatchPoints) error
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until the channel is
// closed, so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (in *Influx) WriteLoop(ctx context.Context, c <-chan time.Time, w BatchPointsWriter) {
for {
select {
case <-c:
if err := in.WriteTo(w); err != nil {
in.logger.Log("during", "WriteTo", "err", err)
}
case <-ctx.Done():
return
}
}
}
// WriteTo flushes the buffered content of the metrics to the writer, in an
// Influx BatchPoints format. WriteTo abides best-effort semantics, so
// observations are lost if there is a problem with the write. Clients should be
// sure to call WriteTo regularly, ideally through the WriteLoop helper method.
func (in *Influx) WriteTo(w BatchPointsWriter) (err error) {
bp, err := influxdb.NewBatchPoints(in.conf)
if err != nil {
return err
}
now := time.Now()
in.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
tags := mergeTags(in.tags, lvs)
var p *influxdb.Point
fields := map[string]interface{}{"count": sum(values)}
p, err = influxdb.NewPoint(name, tags, fields, now)
if err != nil {
return false
}
bp.AddPoint(p)
return true
})
if err != nil {
return err
}
in.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
tags := mergeTags(in.tags, lvs)
var p *influxdb.Point
fields := map[string]interface{}{"value": last(values)}
p, err = influxdb.NewPoint(name, tags, fields, now)
if err != nil {
return false
}
bp.AddPoint(p)
return true
})
if err != nil {
return err
}
in.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
histogram := generic.NewHistogram(name, 50)
tags := mergeTags(in.tags, lvs)
var p *influxdb.Point
for _, v := range values {
histogram.Observe(v)
}
fields := map[string]interface{}{
"p50": histogram.Quantile(0.50),
"p90": histogram.Quantile(0.90),
"p95": histogram.Quantile(0.95),
"p99": histogram.Quantile(0.99),
}
p, err = influxdb.NewPoint(name, tags, fields, now)
if err != nil {
return false
}
bp.AddPoint(p)
return true
})
if err != nil {
return err
}
return w.Write(bp)
}
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]
}
return ret
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func last(a []float64) float64 {
return a[len(a)-1]
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is an Influx counter. Observations are forwarded to an Influx
// object, and aggregated (summed) per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
// Gauge is an Influx gauge. Observations are forwarded to a Dogstatsd
// object, and aggregated (the last observation selected) per timeseries.
type Gauge struct {
name string
lvs lv.LabelValues
obs observeFunc
add observeFunc
}
// With implements metrics.Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
name: g.name,
lvs: g.lvs.With(labelValues...),
obs: g.obs,
add: g.add,
}
}
// Set implements metrics.Gauge.
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 {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
name: h.name,
lvs: h.lvs.With(labelValues...),
obs: h.obs,
}
}
// Observe implements metrics.Histogram.
func (h *Histogram) Observe(value float64) {
h.obs(h.name, h.lvs, value)
}
================================================
FILE: metrics/influx/influx_test.go
================================================
package influx
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"testing"
influxdb "github.com/influxdata/influxdb1-client/v2"
"github.com/go-kit/kit/metrics/teststat"
"github.com/go-kit/log"
)
func TestCounter(t *testing.T) {
in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
re := regexp.MustCompile(`influx_counter,a=b count=([0-9\.]+) [0-9]+`) // reverse-engineered :\
counter := in.NewCounter("influx_counter")
value := func() float64 {
client := &bufWriter{}
in.WriteTo(client)
match := re.FindStringSubmatch(client.buf.String())
f, _ := strconv.ParseFloat(match[1], 64)
return f
}
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
re := regexp.MustCompile(`influx_gauge,foo=alpha value=([0-9\.]+) [0-9]+`)
gauge := in.NewGauge("influx_gauge")
value := func() []float64 {
client := &bufWriter{}
in.WriteTo(client)
match := re.FindStringSubmatch(client.buf.String())
f, _ := strconv.ParseFloat(match[1], 64)
return []float64{f}
}
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
re := regexp.MustCompile(`influx_histogram,bar=beta,foo=alpha p50=([0-9\.]+),p90=([0-9\.]+),p95=([0-9\.]+),p99=([0-9\.]+) [0-9]+`)
histogram := in.NewHistogram("influx_histogram").With("bar", "beta")
quantiles := func() (float64, float64, float64, float64) {
w := &bufWriter{}
in.WriteTo(w)
match := re.FindStringSubmatch(w.buf.String())
if len(match) != 5 {
t.Errorf("These are not the quantiles you're looking for: %v\n", match)
}
var result [4]float64
for i, q := range match[1:] {
result[i], _ = strconv.ParseFloat(q, 64)
}
return result[0], result[1], result[2], result[3]
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogramLabels(t *testing.T) {
in := New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
h := in.NewHistogram("foo")
h.Observe(123)
h.With("abc", "xyz").Observe(456)
w := &bufWriter{}
if err := in.WriteTo(w); err != nil {
t.Fatal(err)
}
if want, have := 2, len(strings.Split(strings.TrimSpace(w.buf.String()), "\n")); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
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
}
func (w *bufWriter) Write(bp influxdb.BatchPoints) error {
for _, p := range bp.Points() {
fmt.Fprintf(&w.buf, p.String()+"\n")
}
return nil
}
================================================
FILE: metrics/influxstatsd/influxstatsd.go
================================================
// Package influxstatsd provides support for InfluxData's StatsD Telegraf plugin. It's very
// similar to StatsD, but supports arbitrary tags per-metric, which map to Go
// kit's label values. So, while label values are no-ops in StatsD, they are
// supported here. For more details, see the article at
// https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/
//
// This package batches observations and emits them on some schedule to the
// remote server. This is useful even if you connect to your service
// over UDP. Emitting one network packet per observation can quickly overwhelm
// even the fastest internal network.
package influxstatsd
import (
"context"
"fmt"
"io"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/kit/metrics/internal/ratemap"
"github.com/go-kit/kit/util/conn"
"github.com/go-kit/log"
)
// Influxstatsd receives metrics observations and forwards them to a server.
// Create a Influxstatsd object, use it to create metrics, and pass those
// metrics as dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Timings and
// histograms are buffered but not aggregated.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a InfluxStatsD server, use the SendLoop helper method.
type Influxstatsd struct {
mtx sync.RWMutex
prefix string
rates *ratemap.RateMap
counters *lv.Space
gauges map[string]*gaugeNode
timings *lv.Space
histograms *lv.Space
logger log.Logger
lvs lv.LabelValues
}
// New returns a Influxstatsd 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, lvs ...string) *Influxstatsd {
if len(lvs)%2 != 0 {
panic("odd number of LabelValues; programmer error!")
}
return &Influxstatsd{
prefix: prefix,
rates: ratemap.New(),
counters: lv.NewSpace(),
gauges: map[string]*gaugeNode{}, // https://github.com/go-kit/kit/pull/588
timings: lv.NewSpace(),
histograms: lv.NewSpace(),
logger: logger,
lvs: lvs,
}
}
// NewCounter returns a counter, sending observations to this Influxstatsd object.
func (d *Influxstatsd) NewCounter(name string, sampleRate float64) *Counter {
d.rates.Set(name, sampleRate)
return &Counter{
name: name,
obs: d.counters.Observe,
}
}
// NewGauge returns a gauge, sending observations to this Influxstatsd object.
func (d *Influxstatsd) NewGauge(name string) *Gauge {
d.mtx.Lock()
n, ok := d.gauges[name]
if !ok {
n = &gaugeNode{gauge: &Gauge{g: generic.NewGauge(name), influx: d}}
d.gauges[name] = n
}
d.mtx.Unlock()
return n.gauge
}
// NewTiming returns a histogram whose observations are interpreted as
// millisecond durations, and are forwarded to this Influxstatsd object.
func (d *Influxstatsd) NewTiming(name string, sampleRate float64) *Timing {
d.rates.Set(name, sampleRate)
return &Timing{
name: name,
obs: d.timings.Observe,
}
}
// NewHistogram returns a histogram whose observations are of an unspecified
// unit, and are forwarded to this Influxstatsd object.
func (d *Influxstatsd) NewHistogram(name string, sampleRate float64) *Histogram {
d.rates.Set(name, sampleRate)
return &Histogram{
name: name,
obs: d.histograms.Observe,
}
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until ctx is canceled,
// so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (d *Influxstatsd) WriteLoop(ctx context.Context, c <-chan time.Time, w io.Writer) {
for {
select {
case <-c:
if _, err := d.WriteTo(w); err != nil {
d.logger.Log("during", "WriteTo", "err", err)
}
case <-ctx.Done():
return
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until ctx is canceled, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (d *Influxstatsd) SendLoop(ctx context.Context, c <-chan time.Time, network, address string) {
d.WriteLoop(ctx, c, conn.NewDefaultManager(network, address, d.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// InfluxStatsD format. WriteTo abides best-effort semantics, so observations are
// lost if there is a problem with the write. Clients should be sure to call
// WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
func (d *Influxstatsd) WriteTo(w io.Writer) (count int64, err error) {
var n int
d.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s%s%s:%f|c%s\n", d.prefix, name, d.tagValues(lvs), sum(values), sampling(d.rates.Get(name)))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
d.mtx.RLock()
for _, root := range d.gauges {
root.walk(func(name string, lvs lv.LabelValues, value float64) bool {
n, err = fmt.Fprintf(w, "%s%s%s:%f|g\n", d.prefix, name, d.tagValues(lvs), value)
if err != nil {
return false
}
count += int64(n)
return true
})
}
d.mtx.RUnlock()
d.timings.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
sampleRate := d.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s%s%s:%f|ms%s\n", d.prefix, name, d.tagValues(lvs), value, sampling(sampleRate))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
d.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
sampleRate := d.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s%s%s:%f|h%s\n", d.prefix, name, d.tagValues(lvs), value, sampling(sampleRate))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
return count, err
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func sampling(r float64) string {
var sv string
if r < 1.0 {
sv = fmt.Sprintf("|@%f", r)
}
return sv
}
func (d *Influxstatsd) tagValues(labelValues []string) string {
if len(labelValues) == 0 && len(d.lvs) == 0 {
return ""
}
if len(labelValues)%2 != 0 {
panic("tagValues received a labelValues with an odd number of strings")
}
pairs := make([]string, 0, (len(d.lvs)+len(labelValues))/2)
for i := 0; i < len(d.lvs); i += 2 {
pairs = append(pairs, d.lvs[i]+"="+d.lvs[i+1])
}
for i := 0; i < len(labelValues); i += 2 {
pairs = append(pairs, labelValues[i]+"="+labelValues[i+1])
}
return "," + strings.Join(pairs, ",")
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is a InfluxStatsD counter. Observations are forwarded to a Influxstatsd
// object, and aggregated (summed) per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
// Gauge is a InfluxStatsD gauge. Observations are forwarded to a Influxstatsd
// object, and aggregated (the last observation selected) per timeseries.
type Gauge struct {
g *generic.Gauge
influx *Influxstatsd
set int32
}
// With implements metrics.Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
g.influx.mtx.RLock()
node := g.influx.gauges[g.g.Name]
g.influx.mtx.RUnlock()
ga := &Gauge{g: g.g.With(labelValues...).(*generic.Gauge), influx: g.influx}
return node.addGauge(ga, ga.g.LabelValues())
}
// Set implements metrics.Gauge.
func (g *Gauge) Set(value float64) {
g.g.Set(value)
g.touch()
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.g.Add(delta)
g.touch()
}
// Timing is a InfluxStatsD timing, or metrics.Histogram. Observations are
// forwarded to a Influxstatsd object, and collected (but not aggregated) per
// timeseries.
type Timing struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Timing.
func (t *Timing) With(labelValues ...string) metrics.Histogram {
return &Timing{
name: t.name,
lvs: t.lvs.With(labelValues...),
obs: t.obs,
}
}
// Observe implements metrics.Histogram. Value is interpreted as milliseconds.
func (t *Timing) Observe(value float64) {
t.obs(t.name, t.lvs, value)
}
// Histogram is a InfluxStatsD histrogram. Observations are forwarded to a
// Influxstatsd object, and collected (but not aggregated) per timeseries.
type Histogram struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
name: h.name,
lvs: h.lvs.With(labelValues...),
obs: h.obs,
}
}
// Observe implements metrics.Histogram.
func (h *Histogram) Observe(value float64) {
h.obs(h.name, h.lvs, value)
}
type pair struct{ label, value string }
type gaugeNode struct {
mtx sync.RWMutex
gauge *Gauge
children map[pair]*gaugeNode
}
func (n *gaugeNode) addGauge(g *Gauge, lvs lv.LabelValues) *Gauge {
n.mtx.Lock()
defer n.mtx.Unlock()
if len(lvs) == 0 {
if n.gauge == nil {
n.gauge = g
}
return n.gauge
}
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]*gaugeNode{}
}
child, ok := n.children[head]
if !ok {
child = &gaugeNode{}
n.children[head] = child
}
return child.addGauge(g, tail)
}
func (n *gaugeNode) walk(fn func(string, lv.LabelValues, float64) bool) bool {
n.mtx.RLock()
defer n.mtx.RUnlock()
if n.gauge != nil {
value, ok := n.gauge.read()
if ok && !fn(n.gauge.g.Name, n.gauge.g.LabelValues(), value) {
return false
}
}
for _, child := range n.children {
if !child.walk(fn) {
return false
}
}
return true
}
func (g *Gauge) touch() {
atomic.StoreInt32(&(g.set), 1)
}
func (g *Gauge) read() (float64, bool) {
set := atomic.SwapInt32(&(g.set), 0)
return g.g.Value(), set != 0
}
================================================
FILE: metrics/influxstatsd/influxstatsd_test.go
================================================
package influxstatsd
import (
"testing"
"github.com/go-kit/kit/metrics/teststat"
"github.com/go-kit/log"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value"
regex := `^` + prefix + name + "," + label + `=` + value + `:([0-9\.]+)\|c$`
d := New(prefix, log.NewNopLogger())
counter := d.NewCounter(name, 1.0).With(label, value)
valuef := teststat.SumLines(d, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestCounterSampled(t *testing.T) {
// This will involve multiplying the observed sum by the inverse of the
// sample rate and checking against the expected value within some
// tolerance.
t.Skip("TODO")
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc"
regex := `^` + prefix + name + `,hostname=foohost,` + label + `=` + value + `:([0-9\.]+)\|g$`
d := New(prefix, log.NewNopLogger(), "hostname", "foohost")
gauge := d.NewGauge(name).With(label, value)
valuef := teststat.LastLine(d, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
// InfluxStatsD histograms just emit all observations. So, we collect them into
// a generic histogram, and run the statistics test on that.
func TestHistogram(t *testing.T) {
prefix, name := "influxstatsd.", "histogram_test"
label, value := "abc", "def"
regex := `^` + prefix + name + "," + label + `=` + value + `:([0-9\.]+)\|h$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewHistogram(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogramSampled(t *testing.T) {
prefix, name := "influxstatsd.", "sampled_histogram_test"
label, value := "foo", "bar"
regex := `^` + prefix + name + "," + label + `=` + value + `:([0-9\.]+)\|h\|@0\.01[0]*$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewHistogram(name, 0.01).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50)
if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}
func TestTiming(t *testing.T) {
prefix, name := "influxstatsd.", "timing_test"
label, value := "wiggle", "bottom"
regex := `^` + prefix + name + "," + label + `=` + value + `:([0-9\.]+)\|ms$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewTiming(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestTimingSampled(t *testing.T) {
prefix, name := "influxstatsd.", "sampled_timing_test"
label, value := "internal", "external"
regex := `^` + prefix + name + "," + label + `=` + value + `:([0-9\.]+)\|ms\|@0.03[0]*$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewTiming(name, 0.03).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50)
if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/internal/convert/convert.go
================================================
// Package convert provides a way to use Counters, Histograms, or Gauges
// as one of the other types
package convert
import "github.com/go-kit/kit/metrics"
type counterHistogram struct {
c metrics.Counter
}
// NewCounterAsHistogram returns a Histogram that actually writes the
// value on an underlying Counter
func NewCounterAsHistogram(c metrics.Counter) metrics.Histogram {
return counterHistogram{c}
}
// With implements Histogram.
func (ch counterHistogram) With(labelValues ...string) metrics.Histogram {
return counterHistogram{ch.c.With(labelValues...)}
}
// Observe implements histogram.
func (ch counterHistogram) Observe(value float64) {
ch.c.Add(value)
}
type histogramCounter struct {
h metrics.Histogram
}
// NewHistogramAsCounter returns a Counter that actually writes the
// value on an underlying Histogram
func NewHistogramAsCounter(h metrics.Histogram) metrics.Counter {
return histogramCounter{h}
}
// With implements Counter.
func (hc histogramCounter) With(labelValues ...string) metrics.Counter {
return histogramCounter{hc.h.With(labelValues...)}
}
// Add implements Counter.
func (hc histogramCounter) Add(delta float64) {
hc.h.Observe(delta)
}
type counterGauge struct {
c metrics.Counter
}
// NewCounterAsGauge returns a Gauge that actually writes the
// value on an underlying Counter
func NewCounterAsGauge(c metrics.Counter) metrics.Gauge {
return counterGauge{c}
}
// With implements Gauge.
func (cg counterGauge) With(labelValues ...string) metrics.Gauge {
return counterGauge{cg.c.With(labelValues...)}
}
// Set implements Gauge.
func (cg counterGauge) Set(value float64) {
cg.c.Add(value)
}
// Add implements metrics.Gauge.
func (cg counterGauge) Add(delta float64) {
cg.c.Add(delta)
}
type gaugeCounter struct {
g metrics.Gauge
}
// NewGaugeAsCounter returns a Counter that actually writes the
// value on an underlying Gauge
func NewGaugeAsCounter(g metrics.Gauge) metrics.Counter {
return gaugeCounter{g}
}
// With implements Counter.
func (gc gaugeCounter) With(labelValues ...string) metrics.Counter {
return gaugeCounter{gc.g.With(labelValues...)}
}
// Add implements Counter.
func (gc gaugeCounter) Add(delta float64) {
gc.g.Set(delta)
}
type histogramGauge struct {
h metrics.Histogram
}
// NewHistogramAsGauge returns a Gauge that actually writes the
// value on an underlying Histogram
func NewHistogramAsGauge(h metrics.Histogram) metrics.Gauge {
return histogramGauge{h}
}
// With implements Gauge.
func (hg histogramGauge) With(labelValues ...string) metrics.Gauge {
return histogramGauge{hg.h.With(labelValues...)}
}
// Set implements Gauge.
func (hg histogramGauge) Set(value float64) {
hg.h.Observe(value)
}
// Add implements metrics.Gauge.
func (hg histogramGauge) Add(delta float64) {
hg.h.Observe(delta)
}
type gaugeHistogram struct {
g metrics.Gauge
}
// NewGaugeAsHistogram returns a Histogram that actually writes the
// value on an underlying Gauge
func NewGaugeAsHistogram(g metrics.Gauge) metrics.Histogram {
return gaugeHistogram{g}
}
// With implements Histogram.
func (gh gaugeHistogram) With(labelValues ...string) metrics.Histogram {
return gaugeHistogram{gh.g.With(labelValues...)}
}
// Observe implements histogram.
func (gh gaugeHistogram) Observe(value float64) {
gh.g.Set(value)
}
================================================
FILE: metrics/internal/convert/convert_test.go
================================================
package convert
import (
"testing"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounterHistogramConversion(t *testing.T) {
name := "my_counter"
c := generic.NewCounter(name)
h := NewCounterAsHistogram(c)
top := NewHistogramAsCounter(h).With("label", "counter").(histogramCounter)
mid := top.h.(counterHistogram)
low := mid.c.(*generic.Counter)
if want, have := name, low.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
if err := teststat.TestCounter(top, low.Value); err != nil {
t.Fatal(err)
}
}
func TestCounterGaugeConversion(t *testing.T) {
name := "my_counter"
c := generic.NewCounter(name)
g := NewCounterAsGauge(c)
top := NewGaugeAsCounter(g).With("label", "counter").(gaugeCounter)
mid := top.g.(counterGauge)
low := mid.c.(*generic.Counter)
if want, have := name, low.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
if err := teststat.TestCounter(top, low.Value); err != nil {
t.Fatal(err)
}
}
func TestHistogramGaugeConversion(t *testing.T) {
name := "my_histogram"
h := generic.NewHistogram(name, 50)
g := NewHistogramAsGauge(h)
top := NewGaugeAsHistogram(g).With("label", "histogram").(gaugeHistogram)
mid := top.g.(histogramGauge)
low := mid.h.(*generic.Histogram)
if want, have := name, low.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
quantiles := func() (float64, float64, float64, float64) {
return low.Quantile(0.50), low.Quantile(0.90), low.Quantile(0.95), low.Quantile(0.99)
}
if err := teststat.TestHistogram(top, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/internal/lv/labelvalues.go
================================================
package lv
// LabelValues is a type alias that provides validation on its With method.
// Metrics may include it as a member to help them satisfy With semantics and
// save some code duplication.
type LabelValues []string
// With validates the input, and returns a new aggregate labelValues.
func (lvs LabelValues) With(labelValues ...string) LabelValues {
if len(labelValues)%2 != 0 {
labelValues = append(labelValues, "unknown")
}
return append(lvs, labelValues...)
}
================================================
FILE: metrics/internal/lv/labelvalues_test.go
================================================
package lv
import (
"strings"
"testing"
)
func TestWith(t *testing.T) {
var a LabelValues
b := a.With("a", "1")
c := a.With("b", "2", "c", "3")
if want, have := "", strings.Join(a, ""); want != have {
t.Errorf("With appears to mutate the original LabelValues: want %q, have %q", want, have)
}
if want, have := "a1", strings.Join(b, ""); want != have {
t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
}
if want, have := "b2c3", strings.Join(c, ""); want != have {
t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
}
}
================================================
FILE: metrics/internal/lv/space.go
================================================
package lv
import "sync"
// NewSpace returns an N-dimensional vector space.
func NewSpace() *Space {
return &Space{}
}
// Space represents an N-dimensional vector space. Each name and unique label
// value pair establishes a new dimension and point within that dimension. Order
// matters, i.e. [a=1 b=2] identifies a different timeseries than [b=2 a=1].
type Space struct {
mtx sync.RWMutex
nodes map[string]*node
}
// Observe locates the time series identified by the name and label values in
// the vector space, and appends the value to the list of observations.
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) {
s.mtx.RLock()
defer s.mtx.RUnlock()
for name, node := range s.nodes {
f := func(lvs LabelValues, observations []float64) bool { return fn(name, lvs, observations) }
if !node.walk(LabelValues{}, f) {
return
}
}
}
// Reset empties the current space and returns a new Space with the old
// contents. Reset a Space to get an immutable copy suitable for walking.
func (s *Space) Reset() *Space {
s.mtx.Lock()
defer s.mtx.Unlock()
n := NewSpace()
n.nodes, s.nodes = s.nodes, n.nodes
return n
}
func (s *Space) nodeFor(name string) *node {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.nodes == nil {
s.nodes = map[string]*node{}
}
n, ok := s.nodes[name]
if !ok {
n = &node{}
s.nodes[name] = n
}
return n
}
// node exists at a specific point in the N-dimensional vector space of all
// possible label values. The node collects observations and has child nodes
// with greater specificity.
type node struct {
mtx sync.RWMutex
observations []float64
children map[pair]*node
}
type pair struct{ label, value string }
func (n *node) observe(lvs LabelValues, value float64) {
n.mtx.Lock()
defer n.mtx.Unlock()
if len(lvs) <= 0 {
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.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()
if len(n.observations) > 0 && !fn(lvs, n.observations) {
return false
}
for p, child := range n.children {
if !child.walk(append(lvs, p.label, p.value), fn) {
return false
}
}
return true
}
func last(a []float64) float64 {
return a[len(a)-1]
}
================================================
FILE: metrics/internal/lv/space_test.go
================================================
package lv
import (
"strings"
"testing"
)
func TestSpaceWalkAbort(t *testing.T) {
s := NewSpace()
s.Observe("a", LabelValues{"a", "b"}, 1)
s.Observe("a", LabelValues{"c", "d"}, 2)
s.Observe("a", LabelValues{"e", "f"}, 4)
s.Observe("a", LabelValues{"g", "h"}, 8)
s.Observe("b", LabelValues{"a", "b"}, 16)
s.Observe("b", LabelValues{"c", "d"}, 32)
s.Observe("b", LabelValues{"e", "f"}, 64)
s.Observe("b", LabelValues{"g", "h"}, 128)
var count int
s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
count++
return false
})
if want, have := 1, count; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestSpaceWalkSums(t *testing.T) {
s := NewSpace()
s.Observe("metric_one", LabelValues{}, 1)
s.Observe("metric_one", LabelValues{}, 2)
s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 4)
s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 8)
s.Observe("metric_one", LabelValues{}, 16)
s.Observe("metric_one", LabelValues{"a", "1", "b", "3"}, 32)
s.Observe("metric_two", LabelValues{}, 64)
s.Observe("metric_two", LabelValues{}, 128)
s.Observe("metric_two", LabelValues{"a", "1", "b", "2"}, 256)
have := map[string]float64{}
s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
have[name+" ["+strings.Join(lvs, "")+"]"] += sum(obs)
return true
})
want := map[string]float64{
"metric_one []": 1 + 2 + 16,
"metric_one [a1b2]": 4 + 8,
"metric_one [a1b3]": 32,
"metric_two []": 64 + 128,
"metric_two [a1b2]": 256,
}
for keystr, wantsum := range want {
if havesum := have[keystr]; wantsum != havesum {
t.Errorf("%q: want %.1f, have %.1f", keystr, wantsum, havesum)
}
delete(want, keystr)
delete(have, keystr)
}
for keystr, havesum := range have {
t.Errorf("%q: unexpected observations recorded: %.1f", keystr, havesum)
}
}
func TestSpaceWalkSkipsEmptyDimensions(t *testing.T) {
s := NewSpace()
s.Observe("foo", LabelValues{"bar", "1", "baz", "2"}, 123)
var count int
s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
count++
return true
})
if want, have := 1, count; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func sum(a []float64) (v float64) {
for _, f := range a {
v += f
}
return
}
================================================
FILE: metrics/internal/ratemap/ratemap.go
================================================
// Package ratemap implements a goroutine-safe map of string to float64. It can
// be embedded in implementations whose metrics support fixed sample rates, so
// that an additional parameter doesn't have to be tracked through the e.g.
// lv.Space object.
package ratemap
import "sync"
// RateMap is a simple goroutine-safe map of string to float64.
type RateMap struct {
mtx sync.RWMutex
m map[string]float64
}
// New returns a new RateMap.
func New() *RateMap {
return &RateMap{
m: map[string]float64{},
}
}
// Set writes the given name/rate pair to the map.
// Set is safe for concurrent access by multiple goroutines.
func (m *RateMap) Set(name string, rate float64) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.m[name] = rate
}
// Get retrieves the rate for the given name, or 1.0 if none is set.
// Get is safe for concurrent access by multiple goroutines.
func (m *RateMap) Get(name string) float64 {
m.mtx.RLock()
defer m.mtx.RUnlock()
f, ok := m.m[name]
if !ok {
f = 1.0
}
return f
}
================================================
FILE: metrics/metrics.go
================================================
package metrics
// Counter describes a metric that accumulates values monotonically.
// An example of a counter is the number of received HTTP requests.
type Counter interface {
With(labelValues ...string) Counter
Add(delta float64)
}
// Gauge describes a metric that takes specific values over time.
// An example of a gauge is the current depth of a job queue.
type Gauge interface {
With(labelValues ...string) Gauge
Set(value float64)
Add(delta float64)
}
// Histogram describes a metric that takes repeated observations of the same
// kind of thing, and produces a statistical summary of those observations,
// typically expressed as quantiles or buckets. An example of a histogram is
// HTTP request latencies.
type Histogram interface {
With(labelValues ...string) Histogram
Observe(value float64)
}
================================================
FILE: metrics/multi/multi.go
================================================
// Package multi provides adapters that send observations to multiple metrics
// simultaneously. This is useful if your service needs to emit to multiple
// instrumentation systems at the same time, for example if your organization is
// transitioning from one system to another.
package multi
import "github.com/go-kit/kit/metrics"
// Counter collects multiple individual counters and treats them as a unit.
type Counter []metrics.Counter
// NewCounter returns a multi-counter, wrapping the passed counters.
func NewCounter(c ...metrics.Counter) Counter {
return Counter(c)
}
// Add implements counter.
func (c Counter) Add(delta float64) {
for _, counter := range c {
counter.Add(delta)
}
}
// With implements counter.
func (c Counter) With(labelValues ...string) metrics.Counter {
next := make(Counter, len(c))
for i := range c {
next[i] = c[i].With(labelValues...)
}
return next
}
// Gauge collects multiple individual gauges and treats them as a unit.
type Gauge []metrics.Gauge
// NewGauge returns a multi-gauge, wrapping the passed gauges.
func NewGauge(g ...metrics.Gauge) Gauge {
return Gauge(g)
}
// Set implements Gauge.
func (g Gauge) Set(value float64) {
for _, gauge := range g {
gauge.Set(value)
}
}
// With implements gauge.
func (g Gauge) With(labelValues ...string) metrics.Gauge {
next := make(Gauge, len(g))
for i := range g {
next[i] = g[i].With(labelValues...)
}
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
// NewHistogram returns a multi-histogram, wrapping the passed histograms.
func NewHistogram(h ...metrics.Histogram) Histogram {
return Histogram(h)
}
// Observe implements Histogram.
func (h Histogram) Observe(value float64) {
for _, histogram := range h {
histogram.Observe(value)
}
}
// With implements histogram.
func (h Histogram) With(labelValues ...string) metrics.Histogram {
next := make(Histogram, len(h))
for i := range h {
next[i] = h[i].With(labelValues...)
}
return next
}
================================================
FILE: metrics/multi/multi_test.go
================================================
package multi
import (
"fmt"
"testing"
"github.com/go-kit/kit/metrics"
)
func TestMultiCounter(t *testing.T) {
c1 := &mockCounter{}
c2 := &mockCounter{}
c3 := &mockCounter{}
mc := NewCounter(c1, c2, c3)
mc.Add(123)
mc.Add(456)
want := "[123 456]"
for i, m := range []fmt.Stringer{c1, c2, c3} {
if have := m.String(); want != have {
t.Errorf("c%d: want %q, have %q", i+1, want, have)
}
}
}
func TestMultiGauge(t *testing.T) {
g1 := &mockGauge{}
g2 := &mockGauge{}
g3 := &mockGauge{}
mg := NewGauge(g1, g2, g3)
mg.Set(9)
mg.Set(8)
mg.Set(7)
mg.Add(3)
want := "[9 8 7 10]"
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)
}
}
}
func TestMultiHistogram(t *testing.T) {
h1 := &mockHistogram{}
h2 := &mockHistogram{}
h3 := &mockHistogram{}
mh := NewHistogram(h1, h2, h3)
mh.Observe(1)
mh.Observe(2)
mh.Observe(4)
mh.Observe(8)
want := "[1 2 4 8]"
for i, m := range []fmt.Stringer{h1, h2, h3} {
if have := m.String(); want != have {
t.Errorf("g%d: want %q, have %q", i+1, want, have)
}
}
}
type mockCounter struct {
obs []float64
}
func (c *mockCounter) Add(delta float64) { c.obs = append(c.obs, delta) }
func (c *mockCounter) With(...string) metrics.Counter { return c }
func (c *mockCounter) String() string { return fmt.Sprintf("%v", c.obs) }
type mockGauge struct {
obs []float64
}
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
}
func (h *mockHistogram) Observe(value float64) { h.obs = append(h.obs, value) }
func (h *mockHistogram) With(...string) metrics.Histogram { return h }
func (h *mockHistogram) String() string { return fmt.Sprintf("%v", h.obs) }
================================================
FILE: metrics/pcp/pcp.go
================================================
package pcp
import (
"github.com/performancecopilot/speed/v4"
"github.com/go-kit/kit/metrics"
)
// Reporter encapsulates a speed client.
type Reporter struct {
c *speed.PCPClient
}
// NewReporter creates a new Reporter instance. The first parameter is the
// application name and is used to create the speed client. Hence it should be a
// valid speed parameter name and should not contain spaces or the path
// separator for your operating system.
func NewReporter(appname string) (*Reporter, error) {
c, err := speed.NewPCPClient(appname)
if err != nil {
return nil, err
}
return &Reporter{c}, nil
}
// Start starts the underlying speed client so it can start reporting registered
// metrics to your PCP installation.
func (r *Reporter) Start() { r.c.MustStart() }
// Stop stops the underlying speed client so it can stop reporting registered
// metrics to your PCP installation.
func (r *Reporter) Stop() { r.c.MustStop() }
// Counter implements metrics.Counter via a single dimensional speed.Counter.
type Counter struct {
c speed.Counter
}
// NewCounter creates a new Counter. This requires a name parameter and can
// optionally take a couple of description strings, that are used to create the
// underlying speed.Counter and are reported by PCP.
func (r *Reporter) NewCounter(name string, desc ...string) (*Counter, error) {
c, err := speed.NewPCPCounter(0, name, desc...)
if err != nil {
return nil, err
}
r.c.MustRegister(c)
return &Counter{c}, nil
}
// With is a no-op.
func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
// Add increments Counter. speed.Counters only take int64, so delta is converted
// to int64 before observation.
func (c *Counter) Add(delta float64) { c.c.Inc(int64(delta)) }
// Gauge implements metrics.Gauge via a single dimensional speed.Gauge.
type Gauge struct {
g speed.Gauge
}
// NewGauge creates a new Gauge. This requires a name parameter and can
// optionally take a couple of description strings, that are used to create the
// underlying speed.Gauge and are reported by PCP.
func (r *Reporter) NewGauge(name string, desc ...string) (*Gauge, error) {
g, err := speed.NewPCPGauge(0, name, desc...)
if err != nil {
return nil, err
}
r.c.MustRegister(g)
return &Gauge{g}, nil
}
// With is a no-op.
func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set sets the value of the gauge.
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) }
// Histogram wraps a speed Histogram.
type Histogram struct {
h speed.Histogram
}
// NewHistogram creates a new Histogram. The minimum observeable value is 0. The
// maximum observeable value is 3600000000 (3.6e9).
//
// The required parameters are a metric name, the minimum and maximum observable
// values, and a metric unit for the units of the observed values.
//
// Optionally, it can also take a couple of description strings.
func (r *Reporter) NewHistogram(name string, min, max int64, unit speed.MetricUnit, desc ...string) (*Histogram, error) {
h, err := speed.NewPCPHistogram(name, min, max, 5, unit, desc...)
if err != nil {
return nil, err
}
r.c.MustRegister(h)
return &Histogram{h}, nil
}
// With is a no-op.
func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe observes a value.
//
// This converts float64 value to int64 before observation, as the Histogram in
// speed is backed using codahale/hdrhistogram, which only observes int64
// values. Additionally, the value is interpreted in the metric unit used to
// construct the histogram.
func (h *Histogram) Observe(value float64) { h.h.MustRecord(int64(value)) }
// Mean returns the mean of the values observed so far by the Histogram.
func (h *Histogram) Mean() float64 { return h.h.Mean() }
// Percentile returns a percentile value for the given percentile
// between 0 and 100 for all values observed by the histogram.
func (h *Histogram) Percentile(p float64) int64 { return h.h.Percentile(p) }
================================================
FILE: metrics/pcp/pcp_test.go
================================================
package pcp
import (
"testing"
"github.com/performancecopilot/speed/v4"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
r, err := NewReporter("test_counter")
if err != nil {
t.Fatal(err)
}
counter, err := r.NewCounter("speed_counter")
if err != nil {
t.Fatal(err)
}
counter = counter.With("label values", "not supported").(*Counter)
value := func() float64 { f := counter.c.Val(); return float64(f) }
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
r, err := NewReporter("test_gauge")
if err != nil {
t.Fatal(err)
}
gauge, err := r.NewGauge("speed_gauge")
if err != nil {
t.Fatal(err)
}
gauge = gauge.With("label values", "not supported").(*Gauge)
value := func() []float64 { f := gauge.g.Val(); return []float64{f} }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
r, err := NewReporter("test_histogram")
if err != nil {
t.Fatal(err)
}
histogram, err := r.NewHistogram("speed_histogram", 0, 3600000000, speed.OneUnit)
if err != nil {
t.Fatal(err)
}
histogram = histogram.With("label values", "not supported").(*Histogram)
quantiles := func() (float64, float64, float64, float64) {
p50 := float64(histogram.Percentile(50))
p90 := float64(histogram.Percentile(90))
p95 := float64(histogram.Percentile(95))
p99 := float64(histogram.Percentile(99))
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/prometheus/prometheus.go
================================================
// Package prometheus provides Prometheus implementations for metrics.
// Individual metrics are mapped to their Prometheus counterparts, and
// (depending on the constructor used) may be automatically registered in the
// global Prometheus metrics registry.
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
)
// Counter implements Counter, via a Prometheus CounterVec.
type Counter struct {
cv *prometheus.CounterVec
lvs lv.LabelValues
}
// NewCounterFrom constructs and registers a Prometheus CounterVec,
// and returns a usable Counter object.
func NewCounterFrom(opts prometheus.CounterOpts, labelNames []string) *Counter {
cv := prometheus.NewCounterVec(opts, labelNames)
prometheus.MustRegister(cv)
return NewCounter(cv)
}
// NewCounter wraps the CounterVec and returns a usable Counter object.
func NewCounter(cv *prometheus.CounterVec) *Counter {
return &Counter{
cv: cv,
}
}
// With implements Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
cv: c.cv,
lvs: c.lvs.With(labelValues...),
}
}
// Add implements Counter.
func (c *Counter) Add(delta float64) {
c.cv.With(makeLabels(c.lvs...)).Add(delta)
}
// Gauge implements Gauge, via a Prometheus GaugeVec.
type Gauge struct {
gv *prometheus.GaugeVec
lvs lv.LabelValues
}
// NewGaugeFrom constructs and registers a Prometheus GaugeVec,
// and returns a usable Gauge object.
func NewGaugeFrom(opts prometheus.GaugeOpts, labelNames []string) *Gauge {
gv := prometheus.NewGaugeVec(opts, labelNames)
prometheus.MustRegister(gv)
return NewGauge(gv)
}
// NewGauge wraps the GaugeVec and returns a usable Gauge object.
func NewGauge(gv *prometheus.GaugeVec) *Gauge {
return &Gauge{
gv: gv,
}
}
// With implements Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
gv: g.gv,
lvs: g.lvs.With(labelValues...),
}
}
// Set implements Gauge.
func (g *Gauge) Set(value float64) {
g.gv.With(makeLabels(g.lvs...)).Set(value)
}
// Add is supported by Prometheus GaugeVecs.
func (g *Gauge) Add(delta float64) {
g.gv.With(makeLabels(g.lvs...)).Add(delta)
}
// Summary implements Histogram, via a Prometheus SummaryVec. The difference
// between a Summary and a Histogram is that Summaries don't require predefined
// quantile buckets, but cannot be statistically aggregated.
type Summary struct {
sv *prometheus.SummaryVec
lvs lv.LabelValues
}
// NewSummaryFrom constructs and registers a Prometheus SummaryVec,
// and returns a usable Summary object.
func NewSummaryFrom(opts prometheus.SummaryOpts, labelNames []string) *Summary {
sv := prometheus.NewSummaryVec(opts, labelNames)
prometheus.MustRegister(sv)
return NewSummary(sv)
}
// NewSummary wraps the SummaryVec and returns a usable Summary object.
func NewSummary(sv *prometheus.SummaryVec) *Summary {
return &Summary{
sv: sv,
}
}
// With implements Histogram.
func (s *Summary) With(labelValues ...string) metrics.Histogram {
return &Summary{
sv: s.sv,
lvs: s.lvs.With(labelValues...),
}
}
// Observe implements Histogram.
func (s *Summary) Observe(value float64) {
s.sv.With(makeLabels(s.lvs...)).Observe(value)
}
// Histogram implements Histogram via a Prometheus HistogramVec. The difference
// between a Histogram and a Summary is that Histograms require predefined
// quantile buckets, and can be statistically aggregated.
type Histogram struct {
hv *prometheus.HistogramVec
lvs lv.LabelValues
}
// NewHistogramFrom constructs and registers a Prometheus HistogramVec,
// and returns a usable Histogram object.
func NewHistogramFrom(opts prometheus.HistogramOpts, labelNames []string) *Histogram {
hv := prometheus.NewHistogramVec(opts, labelNames)
prometheus.MustRegister(hv)
return NewHistogram(hv)
}
// NewHistogram wraps the HistogramVec and returns a usable Histogram object.
func NewHistogram(hv *prometheus.HistogramVec) *Histogram {
return &Histogram{
hv: hv,
}
}
// With implements Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
hv: h.hv,
lvs: h.lvs.With(labelValues...),
}
}
// Observe implements Histogram.
func (h *Histogram) Observe(value float64) {
h.hv.With(makeLabels(h.lvs...)).Observe(value)
}
func makeLabels(labelValues ...string) prometheus.Labels {
labels := prometheus.Labels{}
for i := 0; i < len(labelValues); i += 2 {
labels[labelValues[i]] = labelValues[i+1]
}
return labels
}
================================================
FILE: metrics/prometheus/prometheus_test.go
================================================
package prometheus
import (
"io/ioutil"
"math"
"math/rand"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strconv"
"strings"
"testing"
"github.com/go-kit/kit/metrics/teststat"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func TestCounter(t *testing.T) {
s := httptest.NewServer(promhttp.HandlerFor(stdprometheus.DefaultGatherer, promhttp.HandlerOpts{}))
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "ns", "ss", "foo"
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{alpha="alpha-value",beta="beta-value"} ([0-9\.]+)`)
counter := NewCounterFrom(stdprometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is the help string.",
}, []string{"alpha", "beta"}).With("beta", "beta-value", "alpha", "alpha-value") // order shouldn't matter
value := func() float64 {
matches := re.FindStringSubmatch(scrape())
f, _ := strconv.ParseFloat(matches[1], 64)
return f
}
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
s := httptest.NewServer(promhttp.HandlerFor(stdprometheus.DefaultGatherer, promhttp.HandlerOpts{}))
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "aaa", "bbb", "ccc"
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{foo="bar"} ([0-9\.]+)`)
gauge := NewGaugeFrom(stdprometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is a different help string.",
}, []string{"foo"}).With("foo", "bar")
value := func() []float64 {
matches := re.FindStringSubmatch(scrape())
f, _ := strconv.ParseFloat(matches[1], 64)
return []float64{f}
}
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestSummary(t *testing.T) {
s := httptest.NewServer(promhttp.HandlerFor(stdprometheus.DefaultGatherer, promhttp.HandlerOpts{}))
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "test", "prometheus", "summary"
re50 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.5"} ([0-9\.]+)`)
re90 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.9"} ([0-9\.]+)`)
re99 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.99"} ([0-9\.]+)`)
summary := NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is the help string for the summary.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
}, []string{"a", "b"}).With("b", "b").With("a", "a")
quantiles := func() (float64, float64, float64, float64) {
buf := scrape()
match50 := re50.FindStringSubmatch(buf)
p50, _ := strconv.ParseFloat(match50[1], 64)
match90 := re90.FindStringSubmatch(buf)
p90, _ := strconv.ParseFloat(match90[1], 64)
match99 := re99.FindStringSubmatch(buf)
p99, _ := strconv.ParseFloat(match99[1], 64)
p95 := p90 + ((p99 - p90) / 2) // Prometheus, y u no p95??? :< #yolo
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(summary, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
// Prometheus reports histograms as a count of observations that fell into
// each predefined bucket, with the bucket value representing a global upper
// limit. That is, the count monotonically increases over the buckets. This
// requires a different strategy to test.
s := httptest.NewServer(promhttp.HandlerFor(stdprometheus.DefaultGatherer, promhttp.HandlerOpts{}))
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "test", "prometheus", "histogram"
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `_bucket{x="1",le="([0-9]+|\+Inf)"} ([0-9\.]+)`)
numStdev := 3
bucketMin := (teststat.Mean - (numStdev * teststat.Stdev))
bucketMax := (teststat.Mean + (numStdev * teststat.Stdev))
if bucketMin < 0 {
bucketMin = 0
}
bucketCount := 10
bucketDelta := (bucketMax - bucketMin) / bucketCount
buckets := []float64{}
for i := bucketMin; i <= bucketMax; i += bucketDelta {
buckets = append(buckets, float64(i))
}
histogram := NewHistogramFrom(stdprometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is the help string for the histogram.",
Buckets: buckets,
}, []string{"x"}).With("x", "1")
// Can't TestHistogram, because Prometheus Histograms don't dynamically
// compute quantiles. Instead, they fill up buckets. So, let's populate the
// histogram kind of manually.
teststat.PopulateNormalHistogram(histogram, rand.Int())
// Then, we use ExpectedObservationsLessThan to validate.
for _, line := range strings.Split(scrape(), "\n") {
match := re.FindStringSubmatch(line)
if match == nil {
continue
}
bucket, _ := strconv.ParseInt(match[1], 10, 64)
have, _ := strconv.ParseFloat(match[2], 64)
want := teststat.ExpectedObservationsLessThan(bucket)
if match[1] == "+Inf" {
want = int64(teststat.Count) // special case
}
// Unfortunately, we observe experimentally that Prometheus is quite
// imprecise at the extremes. I'm setting a very high tolerance for now.
// It would be great to dig in and figure out whether that's a problem
// with my Expected calculation, or in Prometheus.
tolerance := 0.25
if delta := math.Abs(float64(want) - float64(have)); (delta / float64(want)) > tolerance {
t.Errorf("Bucket %d: want %d, have %d (%.1f%%)", bucket, want, int(have), (100.0 * delta / float64(want)))
}
}
}
func TestInconsistentLabelCardinality(t *testing.T) {
defer func() {
x := recover()
if x == nil {
t.Fatal("expected panic, got none")
}
err, ok := x.(error)
if !ok {
t.Fatalf("expected error, got %s", reflect.TypeOf(x))
}
if want, have := "inconsistent label cardinality", err.Error(); !strings.HasPrefix(have, want) {
t.Fatalf("want %q, have %q", want, have)
}
}()
NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "test",
Subsystem: "inconsistent_label_cardinality",
Name: "foobar",
Help: "This is the help string for the metric.",
}, []string{"a", "b"}).With(
"a", "1", "b", "2", "c", "KABOOM!",
).Add(123)
}
================================================
FILE: metrics/provider/discard.go
================================================
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/discard"
)
type discardProvider struct{}
// NewDiscardProvider returns a provider that produces no-op metrics via the
// discarding backend.
func NewDiscardProvider() Provider { return discardProvider{} }
// NewCounter implements Provider.
func (discardProvider) NewCounter(string) metrics.Counter { return discard.NewCounter() }
// NewGauge implements Provider.
func (discardProvider) NewGauge(string) metrics.Gauge { return discard.NewGauge() }
// NewHistogram implements Provider.
func (discardProvider) NewHistogram(string, int) metrics.Histogram { return discard.NewHistogram() }
// Stop implements Provider.
func (discardProvider) Stop() {}
================================================
FILE: metrics/provider/dogstatsd.go
================================================
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/dogstatsd"
)
type dogstatsdProvider struct {
d *dogstatsd.Dogstatsd
stop func()
}
// NewDogstatsdProvider wraps the given Dogstatsd object and stop func and
// returns a Provider that produces Dogstatsd metrics. A typical stop function
// would be ticker.Stop from the ticker passed to the SendLoop helper method.
func NewDogstatsdProvider(d *dogstatsd.Dogstatsd, stop func()) Provider {
return &dogstatsdProvider{
d: d,
stop: stop,
}
}
// NewCounter implements Provider, returning a new Dogstatsd Counter with a
// sample rate of 1.0.
func (p *dogstatsdProvider) NewCounter(name string) metrics.Counter {
return p.d.NewCounter(name, 1.0)
}
// NewGauge implements Provider.
func (p *dogstatsdProvider) NewGauge(name string) metrics.Gauge {
return p.d.NewGauge(name)
}
// NewHistogram implements Provider, returning a new Dogstatsd Histogram (note:
// not a Timing) with a sample rate of 1.0. The buckets argument is ignored.
func (p *dogstatsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
return p.d.NewHistogram(name, 1.0)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *dogstatsdProvider) Stop() {
p.stop()
}
================================================
FILE: metrics/provider/expvar.go
================================================
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/expvar"
)
type expvarProvider struct{}
// NewExpvarProvider returns a Provider that produces expvar metrics.
func NewExpvarProvider() Provider {
return expvarProvider{}
}
// NewCounter implements Provider.
func (p expvarProvider) NewCounter(name string) metrics.Counter {
return expvar.NewCounter(name)
}
// NewGauge implements Provider.
func (p expvarProvider) NewGauge(name string) metrics.Gauge {
return expvar.NewGauge(name)
}
// NewHistogram implements Provider.
func (p expvarProvider) NewHistogram(name string, buckets int) metrics.Histogram {
return expvar.NewHistogram(name, buckets)
}
// Stop implements Provider, but is a no-op.
func (p expvarProvider) Stop() {}
================================================
FILE: metrics/provider/graphite.go
================================================
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/graphite"
)
type graphiteProvider struct {
g *graphite.Graphite
stop func()
}
// NewGraphiteProvider wraps the given Graphite object and stop func and returns
// a Provider that produces Graphite metrics. A typical stop function would be
// ticker.Stop from the ticker passed to the SendLoop helper method.
func NewGraphiteProvider(g *graphite.Graphite, stop func()) Provider {
return &graphiteProvider{
g: g,
stop: stop,
}
}
// NewCounter implements Provider.
func (p *graphiteProvider) NewCounter(name string) metrics.Counter {
return p.g.NewCounter(name)
}
// NewGauge implements Provider.
func (p *graphiteProvider) NewGauge(name string) metrics.Gauge {
return p.g.NewGauge(name)
}
// NewHistogram implements Provider.
func (p *graphiteProvider) NewHistogram(name string, buckets int) metrics.Histogram {
return p.g.NewHistogram(name, buckets)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *graphiteProvider) Stop() {
p.stop()
}
================================================
FILE: metrics/provider/influx.go
================================================
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/influx"
)
type influxProvider struct {
in *influx.Influx
stop func()
}
// NewInfluxProvider takes the given Influx object and stop func, and returns
// a Provider that produces Influx metrics.
func NewInfluxProvider(in *influx.Influx, stop func()) Provider {
return &influxProvider{
in: in,
stop: stop,
}
}
// NewCounter implements Provider. Per-metric tags are not supported.
func (p *influxProvider) NewCounter(name string) metrics.Counter {
return p.in.NewCounter(name)
}
// NewGauge implements Provider. Per-metric tags are not supported.
func (p *influxProvider) NewGauge(name string) metrics.Gauge {
return p.in.NewGauge(name)
}
// NewHistogram implements Provider. Per-metric tags are not supported.
func (p *influxProvider) NewHistogram(name string, buckets int) metrics.Histogram {
return p.in.NewHistogram(name)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *influxProvider) Stop() {
p.stop()
}
================================================
FILE: metrics/provider/prometheus.go
================================================
package provider
import (
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/prometheus"
)
type prometheusProvider struct {
namespace string
subsystem string
}
// NewPrometheusProvider returns a Provider that produces Prometheus metrics.
// Namespace and subsystem are applied to all produced metrics.
func NewPrometheusProvider(namespace, subsystem string) Provider {
return &prometheusProvider{
namespace: namespace,
subsystem: subsystem,
}
}
// NewCounter implements Provider via prometheus.NewCounterFrom, i.e. the
// counter is registered. The metric's namespace and subsystem are taken from
// the Provider. Help is set to the name of the metric, and no const label names
// are set.
func (p *prometheusProvider) NewCounter(name string) metrics.Counter {
return prometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: p.namespace,
Subsystem: p.subsystem,
Name: name,
Help: name,
}, []string{})
}
// NewGauge implements Provider via prometheus.NewGaugeFrom, i.e. the gauge is
// registered. The metric's namespace and subsystem are taken from the Provider.
// Help is set to the name of the metric, and no const label names are set.
func (p *prometheusProvider) NewGauge(name string) metrics.Gauge {
return prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
Namespace: p.namespace,
Subsystem: p.subsystem,
Name: name,
Help: name,
}, []string{})
}
// NewHistogram implements Provider via prometheus.NewSummaryFrom, i.e. the summary
// is registered. The metric's namespace and subsystem are taken from the
// Provider. Help is set to the name of the metric, and no const label names are
// set. Buckets are ignored.
func (p *prometheusProvider) NewHistogram(name string, _ int) metrics.Histogram {
return prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: p.namespace,
Subsystem: p.subsystem,
Name: name,
Help: name,
}, []string{})
}
// Stop implements Provider, but is a no-op.
func (p *prometheusProvider) Stop() {}
================================================
FILE: metrics/provider/provider.go
================================================
// Package provider provides a factory-like abstraction for metrics backends.
// This package is provided specifically for the needs of the NY Times framework
// Gizmo. Most normal Go kit users shouldn't need to use it.
//
// Normally, if your microservice needs to support different metrics backends,
// you can simply do different construction based on a flag. For example,
//
// var latency metrics.Histogram
// var requests metrics.Counter
// switch *metricsBackend {
// case "prometheus":
// latency = prometheus.NewSummaryVec(...)
// requests = prometheus.NewCounterVec(...)
// case "statsd":
// s := statsd.New(...)
// t := time.NewTicker(5*time.Second)
// go s.SendLoop(ctx, t.C, "tcp", "statsd.local:8125")
// latency = s.NewHistogram(...)
// requests = s.NewCounter(...)
// default:
// log.Fatal("unsupported metrics backend %q", *metricsBackend)
// }
//
package provider
import (
"github.com/go-kit/kit/metrics"
)
// Provider abstracts over constructors and lifecycle management functions for
// each supported metrics backend. It should only be used by those who need to
// swap out implementations dynamically.
//
// This is primarily useful for intermediating frameworks, and is likely
// unnecessary for most Go kit services. See the package-level doc comment for
// more typical usage instructions.
type Provider interface {
NewCounter(name string) metrics.Counter
NewGauge(name string) metrics.Gauge
NewHistogram(name string, buckets int) metrics.Histogram
Stop()
}
================================================
FILE: metrics/provider/statsd.go
================================================
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/statsd"
)
type statsdProvider struct {
s *statsd.Statsd
stop func()
}
// NewStatsdProvider wraps the given Statsd object and stop func and returns a
// Provider that produces Statsd metrics. A typical stop function would be
// ticker.Stop from the ticker passed to the SendLoop helper method.
func NewStatsdProvider(s *statsd.Statsd, stop func()) Provider {
return &statsdProvider{
s: s,
stop: stop,
}
}
// NewCounter implements Provider.
func (p *statsdProvider) NewCounter(name string) metrics.Counter {
return p.s.NewCounter(name, 1.0)
}
// NewGauge implements Provider.
func (p *statsdProvider) NewGauge(name string) metrics.Gauge {
return p.s.NewGauge(name)
}
// NewHistogram implements Provider, returning a StatsD Timing that accepts
// observations in milliseconds. The sample rate is fixed at 1.0. The bucket
// parameter is ignored.
func (p *statsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
return p.s.NewTiming(name, 1.0)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *statsdProvider) Stop() {
p.stop()
}
================================================
FILE: metrics/statsd/statsd.go
================================================
// Package statsd provides a StatsD backend for package metrics. StatsD has no
// concept of arbitrary key-value tagging, so label values are not supported,
// and With is a no-op on all metrics.
//
// This package batches observations and emits them on some schedule to the
// remote server. This is useful even if you connect to your StatsD server over
// UDP. Emitting one network packet per observation can quickly overwhelm even
// the fastest internal network.
package statsd
import (
"context"
"fmt"
"io"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/kit/metrics/internal/ratemap"
"github.com/go-kit/kit/util/conn"
"github.com/go-kit/log"
)
// Statsd receives metrics observations and forwards them to a StatsD server.
// Create a Statsd object, use it to create metrics, and pass those metrics as
// dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Timings are
// buffered but not aggregated.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a StatsD server, use the SendLoop helper method.
type Statsd struct {
prefix string
rates *ratemap.RateMap
// The observations are collected in an N-dimensional vector space, even
// though they only take advantage of a single dimension (name). This is an
// implementation detail born purely from convenience. It would be more
// accurate to collect them in a map[string][]float64, but we already have
// this nice data structure and helper methods.
counters *lv.Space
gauges *lv.Space
timings *lv.Space
logger log.Logger
}
// 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) *Statsd {
return &Statsd{
prefix: prefix,
rates: ratemap.New(),
counters: lv.NewSpace(),
gauges: lv.NewSpace(),
timings: lv.NewSpace(),
logger: logger,
}
}
// NewCounter returns a counter, sending observations to this Statsd object.
func (s *Statsd) NewCounter(name string, sampleRate float64) *Counter {
s.rates.Set(s.prefix+name, sampleRate)
return &Counter{
name: s.prefix + name,
obs: s.counters.Observe,
}
}
// NewGauge returns a gauge, sending observations to this Statsd object.
func (s *Statsd) NewGauge(name string) *Gauge {
return &Gauge{
name: s.prefix + name,
obs: s.gauges.Observe,
add: s.gauges.Add,
}
}
// NewTiming returns a histogram whose observations are interpreted as
// millisecond durations, and are forwarded to this Statsd object.
func (s *Statsd) NewTiming(name string, sampleRate float64) *Timing {
s.rates.Set(s.prefix+name, sampleRate)
return &Timing{
name: s.prefix + name,
obs: s.timings.Observe,
}
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until ctx is canceled,
// so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (s *Statsd) WriteLoop(ctx context.Context, c <-chan time.Time, w io.Writer) {
for {
select {
case <-c:
if _, err := s.WriteTo(w); err != nil {
s.logger.Log("during", "WriteTo", "err", err)
}
case <-ctx.Done():
return
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until ctx is canceled, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (s *Statsd) SendLoop(ctx context.Context, c <-chan time.Time, network, address string) {
s.WriteLoop(ctx, c, conn.NewDefaultManager(network, address, s.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// StatsD format. WriteTo abides best-effort semantics, so observations are
// lost if there is a problem with the write. Clients should be sure to call
// WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
func (s *Statsd) WriteTo(w io.Writer) (count int64, err error) {
var n int
s.counters.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s:%f|c%s\n", name, sum(values), sampling(s.rates.Get(name)))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
s.gauges.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s:%f|g\n", name, last(values))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
s.timings.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
sampleRate := s.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s:%f|ms%s\n", name, value, sampling(sampleRate))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
return count, err
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func last(a []float64) float64 {
return a[len(a)-1]
}
func sampling(r float64) string {
var sv string
if r < 1.0 {
sv = fmt.Sprintf("|@%f", r)
}
return sv
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is a StatsD counter. Observations are forwarded to a Statsd object,
// and aggregated (summed) per timeseries.
type Counter struct {
name string
obs observeFunc
}
// With is a no-op.
func (c *Counter) With(...string) metrics.Counter {
return c
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, lv.LabelValues{}, delta)
}
// Gauge is a StatsD gauge. Observations are forwarded to a Statsd object, and
// aggregated (the last observation selected) per timeseries.
type Gauge struct {
name string
obs observeFunc
add observeFunc
}
// With is a no-op.
func (g *Gauge) With(...string) metrics.Gauge {
return g
}
// Set implements metrics.Gauge.
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.
type Timing struct {
name string
obs observeFunc
}
// With is a no-op.
func (t *Timing) With(...string) metrics.Histogram {
return t
}
// Observe implements metrics.Histogram. Value is interpreted as milliseconds.
func (t *Timing) Observe(value float64) {
t.obs(t.name, lv.LabelValues{}, value)
}
================================================
FILE: metrics/statsd/statsd_test.go
================================================
package statsd
import (
"testing"
"github.com/go-kit/kit/metrics/teststat"
"github.com/go-kit/log"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|c$`
s := New(prefix, log.NewNopLogger())
counter := s.NewCounter(name, 1.0).With(label, value)
valuef := teststat.SumLines(s, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestCounterSampled(t *testing.T) {
// This will involve multiplying the observed sum by the inverse of the
// sample rate and checking against the expected value within some
// tolerance.
t.Skip("TODO")
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|g$`
s := New(prefix, log.NewNopLogger())
gauge := s.NewGauge(name).With(label, value)
valuef := teststat.LastLine(s, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
// StatsD timings just emit all observations. So, we collect them into a generic
// histogram, and run the statistics test on that.
func TestTiming(t *testing.T) {
prefix, name := "statsd.", "timing_test"
label, value := "abc", "def" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|ms$`
s := New(prefix, log.NewNopLogger())
timing := s.NewTiming(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(s, regex, 50) // no |@0.X
if err := teststat.TestHistogram(timing, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestTimingSampled(t *testing.T) {
prefix, name := "statsd.", "sampled_timing_test"
label, value := "foo", "bar" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0\.01[0]*$`
s := New(prefix, log.NewNopLogger())
timing := s.NewTiming(name, 0.01).With(label, value)
quantiles := teststat.Quantiles(s, regex, 50)
if err := teststat.TestHistogram(timing, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}
================================================
FILE: metrics/teststat/buffers.go
================================================
package teststat
import (
"bufio"
"bytes"
"io"
"regexp"
"strconv"
"github.com/go-kit/kit/metrics/generic"
)
// SumLines expects a regex whose first capture group can be parsed as a
// float64. It will dump the WriterTo and parse each line, expecting to find a
// match. It returns the sum of all captured floats.
func SumLines(w io.WriterTo, regex string) func() float64 {
return func() float64 {
sum, _ := stats(w, regex, nil)
return sum
}
}
// LastLine expects a regex whose first capture group can be parsed as a
// float64. It will dump the WriterTo and parse each line, expecting to find a
// match. It returns the final captured float.
func LastLine(w io.WriterTo, regex string) func() []float64 {
return func() []float64 {
_, final := stats(w, regex, nil)
return []float64{final}
}
}
// Quantiles expects a regex whose first capture group can be parsed as a
// float64. It will dump the WriterTo and parse each line, expecting to find a
// match. It observes all captured floats into a generic.Histogram with the
// given number of buckets, and returns the 50th, 90th, 95th, and 99th quantiles
// from that histogram.
func Quantiles(w io.WriterTo, regex string, buckets int) func() (float64, float64, float64, float64) {
return func() (float64, float64, float64, float64) {
h := generic.NewHistogram("quantile-test", buckets)
stats(w, regex, h)
return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99)
}
}
func stats(w io.WriterTo, regex string, h *generic.Histogram) (sum, final float64) {
re := regexp.MustCompile(regex)
buf := &bytes.Buffer{}
w.WriteTo(buf)
s := bufio.NewScanner(buf)
for s.Scan() {
match := re.FindStringSubmatch(s.Text())
f, err := strconv.ParseFloat(match[1], 64)
if err != nil {
panic(err)
}
sum += f
final = f
if h != nil {
h.Observe(f)
}
}
return sum, final
}
================================================
FILE: metrics/teststat/populate.go
================================================
package teststat
import (
"math"
"math/rand"
"github.com/go-kit/kit/metrics"
)
// PopulateNormalHistogram makes a series of normal random observations into the
// histogram. The number of observations is determined by Count. The randomness
// is determined by Mean, Stdev, and the seed parameter.
//
// This is a low-level function, exported only for metrics that don't perform
// dynamic quantile computation, like a Prometheus Histogram (c.f. Summary). In
// most cases, you don't need to use this function, and can use TestHistogram
// instead.
func PopulateNormalHistogram(h metrics.Histogram, seed int) {
r := rand.New(rand.NewSource(int64(seed)))
for i := 0; i < Count; i++ {
sample := r.NormFloat64()*float64(Stdev) + float64(Mean)
if sample < 0 {
sample = 0
}
h.Observe(sample)
}
}
func normalQuantiles() (p50, p90, p95, p99 float64) {
return nvq(50), nvq(90), nvq(95), nvq(99)
}
func nvq(quantile int) float64 {
// https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
return float64(Mean) + float64(Stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)
}
func erfinv(y float64) float64 {
// https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
if y < -1.0 || y > 1.0 {
panic("invalid input")
}
var (
a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
d = [2]float64{3.543889200, 1.637067800}
)
const y0 = 0.7
var x, z float64
if math.Abs(y) == 1.0 {
x = -y * math.Log(0.0)
} else if y < -y0 {
z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
} else {
if y < y0 {
z = y * y
x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0)
} else {
z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
}
x -= (math.Erf(x) - y) / (2.0 / math.SqrtPi * math.Exp(-x*x))
x -= (math.Erf(x) - y) / (2.0 / math.SqrtPi * math.Exp(-x*x))
}
return x
}
================================================
FILE: metrics/teststat/teststat.go
================================================
// Package teststat provides helpers for testing metrics backends.
package teststat
import (
"errors"
"fmt"
"math"
"math/rand"
"reflect"
"sort"
"strings"
"github.com/go-kit/kit/metrics"
)
// TestCounter puts some deltas through the counter, and then calls the value
// func to check that the counter has the correct final value.
func TestCounter(counter metrics.Counter, value func() float64) error {
want := FillCounter(counter)
if have := value(); want != have {
return fmt.Errorf("want %f, have %f", want, have)
}
return nil
}
// FillCounter puts some deltas through the counter and returns the total value.
func FillCounter(counter metrics.Counter) float64 {
a := rand.Perm(100)
n := rand.Intn(len(a))
var want float64
for i := 0; i < n; i++ {
f := float64(a[i])
counter.Add(f)
want += f
}
return want
}
// TestGauge puts some values through the gauge, and then calls the value func
// to check that the gauge has the correct final value.
func TestGauge(gauge metrics.Gauge, value func() []float64) error {
a := rand.Perm(100)
n := rand.Intn(len(a))
var want []float64
for i := 0; i < n; i++ {
f := float64(a[i])
gauge.Set(f)
want = append(want, f)
}
for i := 0; i < n; i++ {
f := float64(a[i])
gauge.Add(f)
want = append(want, want[len(want)-1]+f)
}
have := value()
switch len(have) {
case 0:
return fmt.Errorf("got 0 values")
case 1: // provider doesn't support multi value
if have[0] != want[len(want)-1] {
return fmt.Errorf("want %f, have %f", want, have)
}
default: // provider support multi value gauges
sort.Float64s(want)
sort.Float64s(have)
if !reflect.DeepEqual(want, have) {
return fmt.Errorf("want %f, have %f", want, have)
}
}
return nil
}
// TestHistogram puts some observations through the histogram, and then calls
// the quantiles func to checks that the histogram has computed the correct
// quantiles within some tolerance
func TestHistogram(histogram metrics.Histogram, quantiles func() (p50, p90, p95, p99 float64), tolerance float64) error {
PopulateNormalHistogram(histogram, rand.Int())
want50, want90, want95, want99 := normalQuantiles()
have50, have90, have95, have99 := quantiles()
var errs []string
if want, have := want50, have50; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p50: want %f, have %f", want, have))
}
if want, have := want90, have90; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p90: want %f, have %f", want, have))
}
if want, have := want95, have95; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p95: want %f, have %f", want, have))
}
if want, have := want99, have99; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p99: want %f, have %f", want, have))
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "; "))
}
return nil
}
var (
// Count is the number of observations.
Count = 12345
// Mean is the center of the normal distribution of observations.
Mean = 500
// Stdev of the normal distribution of observations.
Stdev = 25
)
// ExpectedObservationsLessThan returns the number of observations that should
// have a value less than or equal to the given value, given a normal
// distribution of observations described by Count, Mean, and Stdev.
func ExpectedObservationsLessThan(bucket int64) int64 {
// https://code.google.com/p/gostat/source/browse/stat/normal.go
cdf := ((1.0 / 2.0) * (1 + math.Erf((float64(bucket)-float64(Mean))/(float64(Stdev)*math.Sqrt2))))
return int64(cdf * float64(Count))
}
func cmp(want, have, tol float64) bool {
if (math.Abs(want-have) / want) > tol {
return false
}
return true
}
================================================
FILE: metrics/timer.go
================================================
package metrics
import "time"
// Timer acts as a stopwatch, sending observations to a wrapped histogram.
// It's a bit of helpful syntax sugar for h.Observe(time.Since(x)).
type Timer struct {
h Histogram
t time.Time
u time.Duration
}
// NewTimer wraps the given histogram and records the current time.
func NewTimer(h Histogram) *Timer {
return &Timer{
h: h,
t: time.Now(),
u: time.Second,
}
}
// ObserveDuration captures the number of seconds since the timer was
// constructed, and forwards that observation to the histogram.
func (t *Timer) ObserveDuration() {
d := float64(time.Since(t.t).Nanoseconds()) / float64(t.u)
if d < 0 {
d = 0
}
t.h.Observe(d)
}
// Unit sets the unit of the float64 emitted by the timer.
// By default, the timer emits seconds.
func (t *Timer) Unit(u time.Duration) {
t.u = u
}
================================================
FILE: metrics/timer_test.go
================================================
package metrics_test
import (
"math"
"testing"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
)
func TestTimerFast(t *testing.T) {
h := generic.NewSimpleHistogram()
metrics.NewTimer(h).ObserveDuration()
tolerance := 0.050
if want, have := 0.000, h.ApproximateMovingAverage(); math.Abs(want-have) > tolerance {
t.Errorf("want %.3f, have %.3f", want, have)
}
}
func TestTimerSlow(t *testing.T) {
h := generic.NewSimpleHistogram()
timer := metrics.NewTimer(h)
time.Sleep(250 * time.Millisecond)
timer.ObserveDuration()
tolerance := 0.050
if want, have := 0.250, h.ApproximateMovingAverage(); math.Abs(want-have) > tolerance {
t.Errorf("want %.3f, have %.3f", want, have)
}
}
func TestTimerUnit(t *testing.T) {
for _, tc := range []struct {
name string
unit time.Duration
tolerance float64
want float64
}{
{"Seconds", time.Second, 0.010, 0.100},
{"Milliseconds", time.Millisecond, 10, 100},
{"Nanoseconds", time.Nanosecond, 10000000, 100000000},
} {
t.Run(tc.name, func(t *testing.T) {
h := generic.NewSimpleHistogram()
timer := metrics.NewTimer(h)
time.Sleep(100 * time.Millisecond)
timer.Unit(tc.unit)
timer.ObserveDuration()
if want, have := tc.want, h.ApproximateMovingAverage(); math.Abs(want-have) > tc.tolerance {
t.Errorf("want %.3f, have %.3f", want, have)
}
})
}
}
================================================
FILE: ratelimit/token_bucket.go
================================================
package ratelimit
import (
"context"
"errors"
"github.com/go-kit/kit/endpoint"
)
// ErrLimited is returned in the request path when the rate limiter is
// triggered and the request is rejected.
var ErrLimited = errors.New("rate limit exceeded")
// Allower dictates whether or not a request is acceptable to run.
// The Limiter from "golang.org/x/time/rate" already implements this interface,
// one is able to use that in NewErroringLimiter without any modifications.
type Allower interface {
Allow() bool
}
// NewErroringLimiter returns an endpoint.Middleware that acts as a rate
// limiter. Requests that would exceed the
// maximum request rate are simply rejected with an error.
func NewErroringLimiter(limit Allower) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
if !limit.Allow() {
return nil, ErrLimited
}
return next(ctx, request)
}
}
}
// Waiter dictates how long a request must be delayed.
// The Limiter from "golang.org/x/time/rate" already implements this interface,
// one is able to use that in NewDelayingLimiter without any modifications.
type Waiter interface {
Wait(ctx context.Context) error
}
// NewDelayingLimiter returns an endpoint.Middleware that acts as a
// request throttler. Requests that would
// exceed the maximum request rate are delayed via the Waiter function
func NewDelayingLimiter(limit Waiter) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
if err := limit.Wait(ctx); err != nil {
return nil, err
}
return next(ctx, request)
}
}
}
// AllowerFunc is an adapter that lets a function operate as if
// it implements Allower
type AllowerFunc func() bool
// Allow makes the adapter implement Allower
func (f AllowerFunc) Allow() bool {
return f()
}
// WaiterFunc is an adapter that lets a function operate as if
// it implements Waiter
type WaiterFunc func(ctx context.Context) error
// Wait makes the adapter implement Waiter
func (f WaiterFunc) Wait(ctx context.Context) error {
return f(ctx)
}
================================================
FILE: ratelimit/token_bucket_test.go
================================================
package ratelimit_test
import (
"context"
"strings"
"testing"
"time"
"golang.org/x/time/rate"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/ratelimit"
)
var nopEndpoint = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil }
func TestXRateErroring(t *testing.T) {
limit := rate.NewLimiter(rate.Every(time.Minute), 1)
testSuccessThenFailure(
t,
ratelimit.NewErroringLimiter(limit)(nopEndpoint),
ratelimit.ErrLimited.Error())
}
func TestXRateDelaying(t *testing.T) {
limit := rate.NewLimiter(rate.Every(time.Minute), 1)
testSuccessThenFailure(
t,
ratelimit.NewDelayingLimiter(limit)(nopEndpoint),
"exceed context deadline")
}
func testSuccessThenFailure(t *testing.T, e endpoint.Endpoint, failContains string) {
ctx, cxl := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cxl()
// First request should succeed.
if _, err := e(ctx, struct{}{}); err != nil {
t.Errorf("unexpected: %v\n", err)
}
// Next request should fail.
if _, err := e(ctx, struct{}{}); !strings.Contains(err.Error(), failContains) {
t.Errorf("expected `%s`: %v\n", failContains, err)
}
}
================================================
FILE: sd/benchmark_test.go
================================================
package sd
import (
"io"
"testing"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/log"
)
func BenchmarkEndpoints(b *testing.B) {
var (
ca = make(closer)
cb = make(closer)
cmap = map[string]io.Closer{"a": ca, "b": cb}
factory = func(instance string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, cmap[instance], nil }
c = newEndpointCache(factory, log.NewNopLogger(), endpointerOptions{})
)
b.ReportAllocs()
c.Update(Event{Instances: []string{"a", "b"}})
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Endpoints()
}
})
}
================================================
FILE: sd/consul/client.go
================================================
package consul
import (
consul "github.com/hashicorp/consul/api"
)
// Client is a wrapper around the Consul API.
type Client interface {
// Register a service with the local agent.
Register(r *consul.AgentServiceRegistration) error
// Deregister a service with the local agent.
Deregister(r *consul.AgentServiceRegistration) error
// Service
Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error)
}
type client struct {
consul *consul.Client
}
// NewClient returns an implementation of the Client interface, wrapping a
// concrete Consul client.
func NewClient(c *consul.Client) Client {
return &client{consul: c}
}
func (c *client) Register(r *consul.AgentServiceRegistration) error {
return c.consul.Agent().ServiceRegister(r)
}
func (c *client) Deregister(r *consul.AgentServiceRegistration) error {
return c.consul.Agent().ServiceDeregister(r.ID)
}
func (c *client) Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) {
return c.consul.Health().Service(service, tag, passingOnly, queryOpts)
}
================================================
FILE: sd/consul/client_test.go
================================================
package consul
import (
"context"
"errors"
"io"
"reflect"
"testing"
stdconsul "github.com/hashicorp/consul/api"
"github.com/go-kit/kit/endpoint"
)
func TestClientRegistration(t *testing.T) {
c := newTestClient(nil)
services, _, err := c.Service(testRegistration.Name, "", true, &stdconsul.QueryOptions{})
if err != nil {
t.Error(err)
}
if want, have := 0, len(services); want != have {
t.Errorf("want %d, have %d", want, have)
}
if err := c.Register(testRegistration); err != nil {
t.Error(err)
}
if err := c.Register(testRegistration); err == nil {
t.Errorf("want error, have %v", err)
}
services, _, err = c.Service(testRegistration.Name, "", true, &stdconsul.QueryOptions{})
if err != nil {
t.Error(err)
}
if want, have := 1, len(services); want != have {
t.Errorf("want %d, have %d", want, have)
}
if err := c.Deregister(testRegistration); err != nil {
t.Error(err)
}
if err := c.Deregister(testRegistration); err == nil {
t.Errorf("want error, have %v", err)
}
services, _, err = c.Service(testRegistration.Name, "", true, &stdconsul.QueryOptions{})
if err != nil {
t.Error(err)
}
if want, have := 0, len(services); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
type testClient struct {
entries []*stdconsul.ServiceEntry
}
func newTestClient(entries []*stdconsul.ServiceEntry) *testClient {
return &testClient{
entries: entries,
}
}
var _ Client = &testClient{}
func (c *testClient) Service(service, tag string, _ bool, opts *stdconsul.QueryOptions) ([]*stdconsul.ServiceEntry, *stdconsul.QueryMeta, error) {
var results []*stdconsul.ServiceEntry
for _, entry := range c.entries {
if entry.Service.Service != service {
continue
}
if tag != "" {
tagMap := map[string]struct{}{}
for _, t := range entry.Service.Tags {
tagMap[t] = struct{}{}
}
if _, ok := tagMap[tag]; !ok {
continue
}
}
results = append(results, entry)
}
return results, &stdconsul.QueryMeta{LastIndex: opts.WaitIndex}, nil
}
func (c *testClient) Register(r *stdconsul.AgentServiceRegistration) error {
toAdd := registration2entry(r)
for _, entry := range c.entries {
if reflect.DeepEqual(*entry, *toAdd) {
return errors.New("duplicate")
}
}
c.entries = append(c.entries, toAdd)
return nil
}
func (c *testClient) Deregister(r *stdconsul.AgentServiceRegistration) error {
toDelete := registration2entry(r)
var newEntries []*stdconsul.ServiceEntry
for _, entry := range c.entries {
if reflect.DeepEqual(*entry, *toDelete) {
continue
}
newEntries = append(newEntries, entry)
}
if len(newEntries) == len(c.entries) {
return errors.New("not found")
}
c.entries = newEntries
return nil
}
func registration2entry(r *stdconsul.AgentServiceRegistration) *stdconsul.ServiceEntry {
return &stdconsul.ServiceEntry{
Node: &stdconsul.Node{
Node: "some-node",
Address: r.Address,
},
Service: &stdconsul.AgentService{
ID: r.ID,
Service: r.Name,
Tags: r.Tags,
Port: r.Port,
Address: r.Address,
},
// Checks ignored
}
}
func testFactory(instance string) (endpoint.Endpoint, io.Closer, error) {
return func(context.Context, interface{}) (interface{}, error) {
return instance, nil
}, nil, nil
}
var testRegistration = &stdconsul.AgentServiceRegistration{
ID: "my-id",
Name: "my-name",
Tags: []string{"my-tag-1", "my-tag-2"},
Port: 12345,
Address: "my-address",
}
================================================
FILE: sd/consul/doc.go
================================================
// Package consul provides Instancer and Registrar implementations for Consul.
package consul
================================================
FILE: sd/consul/instancer.go
================================================
package consul
import (
"errors"
"fmt"
"time"
consul "github.com/hashicorp/consul/api"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/kit/util/conn"
"github.com/go-kit/log"
)
const defaultIndex = 0
// errStopped notifies the loop to quit. aka stopped via quitc
var errStopped = errors.New("quit and closed consul instancer")
// Instancer yields instances for a service in Consul.
type Instancer struct {
cache *instance.Cache
client Client
logger log.Logger
service string
tags []string
passingOnly bool
quitc chan struct{}
}
// NewInstancer returns a Consul instancer that publishes instances for the
// requested service. It only returns instances for which all of the passed tags
// are present.
func NewInstancer(client Client, logger log.Logger, service string, tags []string, passingOnly bool) *Instancer {
s := &Instancer{
cache: instance.NewCache(),
client: client,
logger: log.With(logger, "service", service, "tags", fmt.Sprint(tags)),
service: service,
tags: tags,
passingOnly: passingOnly,
quitc: make(chan struct{}),
}
instances, index, err := s.getInstances(defaultIndex, nil)
if err == nil {
s.logger.Log("instances", len(instances))
} else {
s.logger.Log("err", err)
}
s.cache.Update(sd.Event{Instances: instances, Err: err})
go s.loop(index)
return s
}
// Stop terminates the instancer.
func (s *Instancer) Stop() {
close(s.quitc)
}
func (s *Instancer) loop(lastIndex uint64) {
var (
instances []string
err error
d time.Duration = 10 * time.Millisecond
index uint64
)
for {
instances, index, err = s.getInstances(lastIndex, s.quitc)
switch {
case errors.Is(err, errStopped):
return // stopped via quitc
case err != nil:
s.logger.Log("err", err)
time.Sleep(d)
d = conn.Exponential(d)
s.cache.Update(sd.Event{Err: err})
case index == defaultIndex:
s.logger.Log("err", "index is not sane")
time.Sleep(d)
d = conn.Exponential(d)
case index < lastIndex:
s.logger.Log("err", "index is less than previous; resetting to default")
lastIndex = defaultIndex
time.Sleep(d)
d = conn.Exponential(d)
default:
lastIndex = index
s.cache.Update(sd.Event{Instances: instances})
d = 10 * time.Millisecond
}
}
}
func (s *Instancer) getInstances(lastIndex uint64, interruptc chan struct{}) ([]string, uint64, error) {
tag := ""
if len(s.tags) > 0 {
tag = s.tags[0]
}
// Consul doesn't support more than one tag in its service query method.
// https://github.com/hashicorp/consul/issues/294
// Hashi suggest prepared queries, but they don't support blocking.
// https://www.consul.io/docs/agent/http/query.html#execute
// If we want blocking for efficiency, we must filter tags manually.
type response struct {
instances []string
index uint64
}
var (
errc = make(chan error, 1)
resc = make(chan response, 1)
)
go func() {
entries, meta, err := s.client.Service(s.service, tag, s.passingOnly, &consul.QueryOptions{
WaitIndex: lastIndex,
})
if err != nil {
errc <- err
return
}
if len(s.tags) > 1 {
entries = filterEntries(entries, s.tags[1:]...)
}
resc <- response{
instances: makeInstances(entries),
index: meta.LastIndex,
}
}()
select {
case err := <-errc:
return nil, 0, err
case res := <-resc:
return res.instances, res.index, nil
case <-interruptc:
return nil, 0, errStopped
}
}
// Register implements Instancer.
func (s *Instancer) Register(ch chan<- sd.Event) {
s.cache.Register(ch)
}
// Deregister implements Instancer.
func (s *Instancer) Deregister(ch chan<- sd.Event) {
s.cache.Deregister(ch)
}
func filterEntries(entries []*consul.ServiceEntry, tags ...string) []*consul.ServiceEntry {
var es []*consul.ServiceEntry
ENTRIES:
for _, entry := range entries {
ts := make(map[string]struct{}, len(entry.Service.Tags))
for _, tag := range entry.Service.Tags {
ts[tag] = struct{}{}
}
for _, tag := range tags {
if _, ok := ts[tag]; !ok {
continue ENTRIES
}
}
es = append(es, entry)
}
return es
}
func makeInstances(entries []*consul.ServiceEntry) []string {
instances := make([]string, len(entries))
for i, entry := range entries {
addr := entry.Node.Address
if entry.Service.Address != "" {
addr = entry.Service.Address
}
instances[i] = fmt.Sprintf("%s:%d", addr, entry.Service.Port)
}
return instances
}
================================================
FILE: sd/consul/instancer_test.go
================================================
package consul
import (
"context"
"fmt"
"io"
"testing"
"time"
consul "github.com/hashicorp/consul/api"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
var _ sd.Instancer = (*Instancer)(nil) // API check
var consulState = []*consul.ServiceEntry{
{
Node: &consul.Node{
Address: "10.0.0.0",
Node: "app00.local",
},
Service: &consul.AgentService{
ID: "search-api-0",
Port: 8000,
Service: "search",
Tags: []string{
"api",
"v1",
},
},
},
{
Node: &consul.Node{
Address: "10.0.0.1",
Node: "app01.local",
},
Service: &consul.AgentService{
ID: "search-api-1",
Port: 8001,
Service: "search",
Tags: []string{
"api",
"v2",
},
},
},
{
Node: &consul.Node{
Address: "10.0.0.1",
Node: "app01.local",
},
Service: &consul.AgentService{
Address: "10.0.0.10",
ID: "search-db-0",
Port: 9000,
Service: "search",
Tags: []string{
"db",
},
},
},
}
func TestInstancer(t *testing.T) {
var (
logger = log.NewNopLogger()
client = newTestClient(consulState)
)
s := NewInstancer(client, logger, "search", []string{"api"}, true)
defer s.Stop()
state := s.cache.State()
if want, have := 2, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestInstancerNoService(t *testing.T) {
var (
logger = log.NewNopLogger()
client = newTestClient(consulState)
)
s := NewInstancer(client, logger, "feed", []string{}, true)
defer s.Stop()
state := s.cache.State()
if want, have := 0, len(state.Instances); want != have {
t.Fatalf("want %d, have %d", want, have)
}
}
func TestInstancerWithTags(t *testing.T) {
var (
logger = log.NewNopLogger()
client = newTestClient(consulState)
)
s := NewInstancer(client, logger, "search", []string{"api", "v2"}, true)
defer s.Stop()
state := s.cache.State()
if want, have := 1, len(state.Instances); want != have {
t.Fatalf("want %d, have %d", want, have)
}
}
func TestInstancerAddressOverride(t *testing.T) {
s := NewInstancer(newTestClient(consulState), log.NewNopLogger(), "search", []string{"db"}, true)
defer s.Stop()
state := s.cache.State()
if want, have := 1, len(state.Instances); want != have {
t.Fatalf("want %d, have %d", want, have)
}
endpoint, closer, err := testFactory(state.Instances[0])
if err != nil {
t.Fatal(err)
}
if closer != nil {
defer closer.Close()
}
response, err := endpoint(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
if want, have := "10.0.0.10:9000", response.(string); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
type eofTestClient struct {
client *testClient
eofSig chan bool
called chan struct{}
}
func neweofTestClient(client *testClient, sig chan bool, called chan struct{}) Client {
return &eofTestClient{client: client, eofSig: sig, called: called}
}
func (c *eofTestClient) Register(r *consul.AgentServiceRegistration) error {
return c.client.Register(r)
}
func (c *eofTestClient) Deregister(r *consul.AgentServiceRegistration) error {
return c.client.Deregister(r)
}
func (c *eofTestClient) Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) {
c.called <- struct{}{}
shouldEOF := <-c.eofSig
if shouldEOF {
return nil, &consul.QueryMeta{}, io.EOF
}
return c.client.Service(service, tag, passingOnly, queryOpts)
}
func TestInstancerWithEOF(t *testing.T) {
var (
sig = make(chan bool, 1)
called = make(chan struct{}, 1)
logger = log.NewNopLogger()
client = neweofTestClient(newTestClient(consulState), sig, called)
)
sig <- false
s := NewInstancer(client, logger, "search", []string{"api"}, true)
defer s.Stop()
select {
case <-called:
case <-time.Tick(time.Millisecond * 500):
t.Error("failed, to receive call")
}
state := s.cache.State()
if want, have := 2, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
// some error occurred resulting in io.EOF
sig <- true
// Service Called Once
select {
case <-called:
case <-time.Tick(time.Millisecond * 500):
t.Error("failed, to receive call in time")
}
sig <- false
// loop should continue
select {
case <-called:
case <-time.Tick(time.Millisecond * 500):
t.Error("failed, to receive call in time")
}
}
type badIndexTestClient struct {
client *testClient
called chan struct{}
}
func newBadIndexTestClient(client *testClient, called chan struct{}) Client {
return &badIndexTestClient{client: client, called: called}
}
func (c *badIndexTestClient) Register(r *consul.AgentServiceRegistration) error {
return c.client.Register(r)
}
func (c *badIndexTestClient) Deregister(r *consul.AgentServiceRegistration) error {
return c.client.Deregister(r)
}
func (c *badIndexTestClient) Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) {
switch {
case queryOpts.WaitIndex == 0:
queryOpts.WaitIndex = 100
case queryOpts.WaitIndex == 100:
queryOpts.WaitIndex = 99
default:
}
c.called <- struct{}{}
return c.client.Service(service, tag, passingOnly, queryOpts)
}
func TestInstancerWithInvalidIndex(t *testing.T) {
var (
called = make(chan struct{}, 1)
logger = log.NewNopLogger()
client = newBadIndexTestClient(newTestClient(consulState), called)
)
s := NewInstancer(client, logger, "search", []string{"api"}, true)
defer s.Stop()
select {
case <-called:
case <-time.Tick(time.Millisecond * 500):
t.Error("failed, to receive call")
}
state := s.cache.State()
if want, have := 2, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
// loop should continue
select {
case <-called:
case <-time.Tick(time.Millisecond * 500):
t.Error("failed, to receive call in time")
}
}
type indexTestClient struct {
client *testClient
index uint64
errs chan error
}
func newIndexTestClient(c *testClient, errs chan error) *indexTestClient {
return &indexTestClient{
client: c,
index: 0,
errs: errs,
}
}
func (i *indexTestClient) Register(r *consul.AgentServiceRegistration) error {
return i.client.Register(r)
}
func (i *indexTestClient) Deregister(r *consul.AgentServiceRegistration) error {
return i.client.Deregister(r)
}
func (i *indexTestClient) Service(service, tag string, passingOnly bool, queryOpts *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) {
// Assumes this is the first call Service, loop hasn't begun running yet
if i.index == 0 && queryOpts.WaitIndex == 0 {
i.index = 100
entries, meta, err := i.client.Service(service, tag, passingOnly, queryOpts)
meta.LastIndex = i.index
return entries, meta, err
}
if queryOpts.WaitIndex < i.index {
i.errs <- fmt.Errorf("wait index %d is less than or equal to previous value", queryOpts.WaitIndex)
}
entries, meta, err := i.client.Service(service, tag, passingOnly, queryOpts)
i.index++
meta.LastIndex = i.index
return entries, meta, err
}
func TestInstancerLoopIndex(t *testing.T) {
var (
errs = make(chan error, 1)
logger = log.NewNopLogger()
client = newIndexTestClient(newTestClient(consulState), errs)
)
go func() {
for err := range errs {
t.Error(err)
t.FailNow()
}
}()
instancer := NewInstancer(client, logger, "search", []string{"api"}, true)
defer instancer.Stop()
time.Sleep(2 * time.Second)
}
================================================
FILE: sd/consul/integration_test.go
================================================
//go:build integration
// +build integration
package consul
import (
"io"
"os"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
stdconsul "github.com/hashicorp/consul/api"
)
func TestIntegration(t *testing.T) {
consulAddr := os.Getenv("CONSUL_ADDR")
if consulAddr == "" {
t.Skip("CONSUL_ADDR not set; skipping integration test")
}
stdClient, err := stdconsul.NewClient(&stdconsul.Config{
Address: consulAddr,
})
if err != nil {
t.Fatal(err)
}
client := NewClient(stdClient)
logger := log.NewLogfmtLogger(os.Stderr)
// Produce a fake service registration.
r := &stdconsul.AgentServiceRegistration{
ID: "my-service-ID",
Name: "my-service-name",
Tags: []string{"alpha", "beta"},
Port: 12345,
Address: "my-address",
EnableTagOverride: false,
// skipping check(s)
}
// Build an Instancer on r.Name + r.Tags.
factory := func(instance string) (endpoint.Endpoint, io.Closer, error) {
t.Logf("factory invoked for %q", instance)
return endpoint.Nop, nil, nil
}
instancer := NewInstancer(
client,
log.With(logger, "component", "instancer"),
r.Name,
r.Tags,
true,
)
endpointer := sd.NewEndpointer(
instancer,
factory,
log.With(logger, "component", "endpointer"),
)
time.Sleep(time.Second)
// Before we publish, we should have no endpoints.
endpoints, err := endpointer.Endpoints()
if err != nil {
t.Error(err)
}
if want, have := 0, len(endpoints); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Build a registrar for r.
registrar := NewRegistrar(client, r, log.With(logger, "component", "registrar"))
registrar.Register()
defer registrar.Deregister()
time.Sleep(time.Second)
// Now we should have one active endpoints.
endpoints, err = endpointer.Endpoints()
if err != nil {
t.Error(err)
}
if want, have := 1, len(endpoints); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
================================================
FILE: sd/consul/registrar.go
================================================
package consul
import (
"fmt"
stdconsul "github.com/hashicorp/consul/api"
"github.com/go-kit/log"
)
// Registrar registers service instance liveness information to Consul.
type Registrar struct {
client Client
registration *stdconsul.AgentServiceRegistration
logger log.Logger
}
// NewRegistrar returns a Consul Registrar acting on the provided catalog
// registration.
func NewRegistrar(client Client, r *stdconsul.AgentServiceRegistration, logger log.Logger) *Registrar {
return &Registrar{
client: client,
registration: r,
logger: log.With(logger, "service", r.Name, "tags", fmt.Sprint(r.Tags), "address", r.Address),
}
}
// Register implements sd.Registrar interface.
func (p *Registrar) Register() {
if err := p.client.Register(p.registration); err != nil {
p.logger.Log("err", err)
} else {
p.logger.Log("action", "register")
}
}
// Deregister implements sd.Registrar interface.
func (p *Registrar) Deregister() {
if err := p.client.Deregister(p.registration); err != nil {
p.logger.Log("err", err)
} else {
p.logger.Log("action", "deregister")
}
}
================================================
FILE: sd/consul/registrar_test.go
================================================
package consul
import (
"testing"
stdconsul "github.com/hashicorp/consul/api"
"github.com/go-kit/log"
)
func TestRegistrar(t *testing.T) {
client := newTestClient([]*stdconsul.ServiceEntry{})
p := NewRegistrar(client, testRegistration, log.NewNopLogger())
if want, have := 0, len(client.entries); want != have {
t.Errorf("want %d, have %d", want, have)
}
p.Register()
if want, have := 1, len(client.entries); want != have {
t.Errorf("want %d, have %d", want, have)
}
p.Deregister()
if want, have := 0, len(client.entries); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
================================================
FILE: sd/dnssrv/doc.go
================================================
// Package dnssrv provides an Instancer implementation for DNS SRV records.
package dnssrv
================================================
FILE: sd/dnssrv/instancer.go
================================================
package dnssrv
import (
"errors"
"fmt"
"net"
"time"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/log"
)
// ErrPortZero is returned by the resolve machinery
// when a DNS resolver returns an SRV record with its
// port set to zero.
var ErrPortZero = errors.New("resolver returned SRV record with port 0")
// Instancer yields instances from the named DNS SRV record. The name is
// resolved on a fixed schedule. Priorities and weights are ignored.
type Instancer struct {
cache *instance.Cache
name string
logger log.Logger
quit chan struct{}
}
// NewInstancer returns a DNS SRV instancer.
func NewInstancer(
name string,
ttl time.Duration,
logger log.Logger,
) *Instancer {
return NewInstancerDetailed(name, time.NewTicker(ttl), net.LookupSRV, logger)
}
// NewInstancerDetailed is the same as NewInstancer, but allows users to
// provide an explicit lookup refresh ticker instead of a TTL, and specify the
// lookup function instead of using net.LookupSRV.
func NewInstancerDetailed(
name string,
refresh *time.Ticker,
lookup Lookup,
logger log.Logger,
) *Instancer {
p := &Instancer{
cache: instance.NewCache(),
name: name,
logger: logger,
quit: make(chan struct{}),
}
instances, err := p.resolve(lookup)
if err == nil {
logger.Log("name", name, "instances", len(instances))
} else {
logger.Log("name", name, "err", err)
}
p.cache.Update(sd.Event{Instances: instances, Err: err})
go p.loop(refresh, lookup)
return p
}
// Stop terminates the Instancer.
func (in *Instancer) Stop() {
close(in.quit)
}
func (in *Instancer) loop(t *time.Ticker, lookup Lookup) {
defer t.Stop()
for {
select {
case <-t.C:
instances, err := in.resolve(lookup)
if err != nil {
in.logger.Log("name", in.name, "err", err)
in.cache.Update(sd.Event{Err: err})
continue // don't replace potentially-good with bad
}
in.cache.Update(sd.Event{Instances: instances})
case <-in.quit:
return
}
}
}
func (in *Instancer) resolve(lookup Lookup) ([]string, error) {
_, addrs, err := lookup("", "", in.name)
if err != nil {
return nil, err
}
instances := make([]string, len(addrs))
for i, addr := range addrs {
if addr.Port == 0 {
return nil, ErrPortZero
}
instances[i] = net.JoinHostPort(addr.Target, fmt.Sprint(addr.Port))
}
return instances, nil
}
// Register implements Instancer.
func (in *Instancer) Register(ch chan<- sd.Event) {
in.cache.Register(ch)
}
// Deregister implements Instancer.
func (in *Instancer) Deregister(ch chan<- sd.Event) {
in.cache.Deregister(ch)
}
================================================
FILE: sd/dnssrv/instancer_test.go
================================================
package dnssrv
import (
"net"
"sync/atomic"
"testing"
"time"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
var _ sd.Instancer = (*Instancer)(nil) // API check
func TestRefresh(t *testing.T) {
name := "some.service.internal"
ticker := time.NewTicker(time.Second)
ticker.Stop()
tickc := make(chan time.Time)
ticker.C = tickc
var lookups uint64
records := []*net.SRV{}
lookup := func(service, proto, name string) (string, []*net.SRV, error) {
t.Logf("lookup(%q, %q, %q)", service, proto, name)
atomic.AddUint64(&lookups, 1)
return "cname", records, nil
}
instancer := NewInstancerDetailed(name, ticker, lookup, log.NewNopLogger())
defer instancer.Stop()
// First lookup, empty
state := instancer.cache.State()
if state.Err != nil {
t.Error(state.Err)
}
if want, have := 0, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
if want, have := uint64(1), atomic.LoadUint64(&lookups); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Load some records and lookup again
records = []*net.SRV{
{Target: "1.0.0.1", Port: 1001},
{Target: "1.0.0.2", Port: 1002},
{Target: "1.0.0.3", Port: 1003},
}
tickc <- time.Now()
// There is a race condition where the instancer.State call below
// invokes the cache before it is updated by the tick above.
// TODO(pb): solve by running the read through the loop goroutine.
time.Sleep(100 * time.Millisecond)
state = instancer.cache.State()
if state.Err != nil {
t.Error(state.Err)
}
if want, have := 3, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
if want, have := uint64(2), atomic.LoadUint64(&lookups); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestIssue892(t *testing.T) {
ticker := time.NewTicker(time.Second)
ticker.Stop()
tickc := make(chan time.Time)
ticker.C = tickc
records := []*net.SRV{
{Target: "1.0.0.1", Port: 80},
{Target: "1.0.0.2", Port: 0},
{Target: "1.0.0.3", Port: 80},
}
lookup := func(service, proto, name string) (string, []*net.SRV, error) {
return "cname", records, nil
}
instancer := NewInstancerDetailed("name", ticker, lookup, log.NewNopLogger())
defer instancer.Stop()
tickc <- time.Now()
time.Sleep(100 * time.Millisecond)
if want, have := ErrPortZero, instancer.cache.State().Err; want != have {
t.Fatalf("want %v, have %v", want, have)
}
}
type nopCloser struct{}
func (nopCloser) Close() error { return nil }
================================================
FILE: sd/dnssrv/lookup.go
================================================
package dnssrv
import "net"
// Lookup is a function that resolves a DNS SRV record to multiple addresses.
// It has the same signature as net.LookupSRV.
type Lookup func(service, proto, name string) (cname string, addrs []*net.SRV, err error)
================================================
FILE: sd/doc.go
================================================
// Package sd provides utilities related to service discovery. That includes the
// client-side loadbalancer pattern, where a microservice subscribes to a
// service discovery system in order to reach remote instances; as well as the
// registrator pattern, where a microservice registers itself in a service
// discovery system. Implementations are provided for most common systems.
package sd
================================================
FILE: sd/endpoint_cache.go
================================================
package sd
import (
"io"
"sort"
"sync"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/log"
)
// endpointCache collects the most recent set of instances from a service discovery
// system, creates endpoints for them using a factory function, and makes
// them available to consumers.
type endpointCache struct {
options endpointerOptions
mtx sync.RWMutex
factory Factory
cache map[string]endpointCloser
err error
endpoints []endpoint.Endpoint
logger log.Logger
invalidateDeadline time.Time
timeNow func() time.Time
}
type endpointCloser struct {
endpoint.Endpoint
io.Closer
}
// newEndpointCache returns a new, empty endpointCache.
func newEndpointCache(factory Factory, logger log.Logger, options endpointerOptions) *endpointCache {
return &endpointCache{
options: options,
factory: factory,
cache: map[string]endpointCloser{},
logger: logger,
timeNow: time.Now,
}
}
// Update should be invoked by clients with a complete set of current instance
// strings whenever that set changes. The cache manufactures new endpoints via
// the factory, closes old endpoints when they disappear, and persists existing
// endpoints if they survive through an update.
func (c *endpointCache) Update(event Event) {
c.mtx.Lock()
defer c.mtx.Unlock()
// Happy path.
if event.Err == nil {
c.updateCache(event.Instances)
c.err = nil
return
}
// Sad path. Something's gone wrong in sd.
c.logger.Log("err", event.Err)
if !c.options.invalidateOnError {
return // keep returning the last known endpoints on error
}
if c.err != nil {
return // already in the error state, do nothing & keep original error
}
c.err = event.Err
// set new deadline to invalidate Endpoints unless non-error Event is received
c.invalidateDeadline = c.timeNow().Add(c.options.invalidateTimeout)
return
}
func (c *endpointCache) updateCache(instances []string) {
// Deterministic order (for later).
sort.Strings(instances)
// Produce the current set of services.
cache := make(map[string]endpointCloser, len(instances))
for _, instance := range instances {
// If it already exists, just copy it over.
if sc, ok := c.cache[instance]; ok {
cache[instance] = sc
delete(c.cache, instance)
continue
}
// If it doesn't exist, create it.
service, closer, err := c.factory(instance)
if err != nil {
c.logger.Log("instance", instance, "err", err)
continue
}
cache[instance] = endpointCloser{service, closer}
}
// Close any leftover endpoints.
for _, sc := range c.cache {
if sc.Closer != nil {
sc.Closer.Close()
}
}
// Populate the slice of endpoints.
endpoints := make([]endpoint.Endpoint, 0, len(cache))
for _, instance := range instances {
// A bad factory may mean an instance is not present.
if _, ok := cache[instance]; !ok {
continue
}
endpoints = append(endpoints, cache[instance].Endpoint)
}
// Swap and trigger GC for old copies.
c.endpoints = endpoints
c.cache = cache
}
// Endpoints yields the current set of (presumably identical) endpoints, ordered
// lexicographically by the corresponding instance string.
func (c *endpointCache) Endpoints() ([]endpoint.Endpoint, error) {
// in the steady state we're going to have many goroutines calling Endpoints()
// concurrently, so to minimize contention we use a shared R-lock.
c.mtx.RLock()
if c.err == nil || c.timeNow().Before(c.invalidateDeadline) {
defer c.mtx.RUnlock()
return c.endpoints, nil
}
c.mtx.RUnlock()
// in case of an error, switch to an exclusive lock.
c.mtx.Lock()
defer c.mtx.Unlock()
// re-check condition due to a race between RUnlock() and Lock().
if c.err == nil || c.timeNow().Before(c.invalidateDeadline) {
return c.endpoints, nil
}
c.updateCache(nil) // close any remaining active endpoints
return nil, c.err
}
================================================
FILE: sd/endpoint_cache_test.go
================================================
package sd
import (
"errors"
"io"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/log"
)
func TestEndpointCache(t *testing.T) {
var (
ca = make(closer)
cb = make(closer)
c = map[string]io.Closer{"a": ca, "b": cb}
f = func(instance string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, c[instance], nil }
cache = newEndpointCache(f, log.NewNopLogger(), endpointerOptions{})
)
// Populate
cache.Update(Event{Instances: []string{"a", "b"}})
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-cb:
t.Errorf("endpoint b closed, not good")
case <-time.After(time.Millisecond):
t.Logf("no closures yet, good")
}
assertEndpointsLen(t, cache, 2)
// Duplicate, should be no-op
cache.Update(Event{Instances: []string{"a", "b"}})
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-cb:
t.Errorf("endpoint b closed, not good")
case <-time.After(time.Millisecond):
t.Logf("no closures yet, good")
}
assertEndpointsLen(t, cache, 2)
// Error, should continue returning old endpoints
cache.Update(Event{Err: errors.New("sd error")})
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-cb:
t.Errorf("endpoint b closed, not good")
case <-time.After(time.Millisecond):
t.Logf("no closures yet, good")
}
assertEndpointsLen(t, cache, 2)
// Delete b
go cache.Update(Event{Instances: []string{"a"}})
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-cb:
t.Logf("endpoint b closed, good")
case <-time.After(time.Second):
t.Errorf("didn't close the deleted instance in time")
}
assertEndpointsLen(t, cache, 1)
// Delete a
go cache.Update(Event{Instances: []string{}})
select {
// case <-cb: will succeed, as it's closed
case <-ca:
t.Logf("endpoint a closed, good")
case <-time.After(time.Second):
t.Errorf("didn't close the deleted instance in time")
}
assertEndpointsLen(t, cache, 0)
}
func TestEndpointCacheErrorAndTimeout(t *testing.T) {
var (
ca = make(closer)
cb = make(closer)
c = map[string]io.Closer{"a": ca, "b": cb}
f = func(instance string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, c[instance], nil }
timeOut = 100 * time.Millisecond
cache = newEndpointCache(f, log.NewNopLogger(), endpointerOptions{
invalidateOnError: true,
invalidateTimeout: timeOut,
})
)
timeNow := time.Now()
cache.timeNow = func() time.Time { return timeNow }
// Populate
cache.Update(Event{Instances: []string{"a"}})
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-time.After(time.Millisecond):
t.Logf("no closures yet, good")
}
assertEndpointsLen(t, cache, 1)
// Send error, keep time still.
cache.Update(Event{Err: errors.New("sd error")})
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-time.After(time.Millisecond):
t.Logf("no closures yet, good")
}
assertEndpointsLen(t, cache, 1)
// Move the time, but less than the timeout
timeNow = timeNow.Add(timeOut / 2)
assertEndpointsLen(t, cache, 1)
select {
case <-ca:
t.Errorf("endpoint a closed, not good")
case <-time.After(time.Millisecond):
t.Logf("no closures yet, good")
}
// Move the time past the timeout
timeNow = timeNow.Add(timeOut)
assertEndpointsError(t, cache, "sd error")
select {
case <-ca:
t.Logf("endpoint a closed, good")
case <-time.After(time.Millisecond):
t.Errorf("didn't close the deleted instance in time")
}
// Send another error
cache.Update(Event{Err: errors.New("another sd error")})
assertEndpointsError(t, cache, "sd error") // expect original error
}
func TestBadFactory(t *testing.T) {
cache := newEndpointCache(func(string) (endpoint.Endpoint, io.Closer, error) {
return nil, nil, errors.New("bad factory")
}, log.NewNopLogger(), endpointerOptions{})
cache.Update(Event{Instances: []string{"foo:1234", "bar:5678"}})
assertEndpointsLen(t, cache, 0)
}
func assertEndpointsLen(t *testing.T, cache *endpointCache, l int) {
endpoints, err := cache.Endpoints()
if err != nil {
t.Errorf("unexpected error %v", err)
return
}
if want, have := l, len(endpoints); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func assertEndpointsError(t *testing.T, cache *endpointCache, wantErr string) {
endpoints, err := cache.Endpoints()
if err == nil {
t.Errorf("expecting error, not good")
return
}
if want, have := wantErr, err.Error(); want != have {
t.Errorf("want %s, have %s", want, have)
return
}
if want, have := 0, len(endpoints); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
type closer chan struct{}
func (c closer) Close() error { close(c); return nil }
================================================
FILE: sd/endpointer.go
================================================
package sd
import (
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/log"
)
// Endpointer listens to a service discovery system and yields a set of
// identical endpoints on demand. An error indicates a problem with connectivity
// to the service discovery system, or within the system itself; an Endpointer
// may yield no endpoints without error.
type Endpointer interface {
Endpoints() ([]endpoint.Endpoint, error)
}
// FixedEndpointer yields a fixed set of endpoints.
type FixedEndpointer []endpoint.Endpoint
// Endpoints implements Endpointer.
func (s FixedEndpointer) Endpoints() ([]endpoint.Endpoint, error) { return s, nil }
// NewEndpointer creates an Endpointer that subscribes to updates from Instancer src
// and uses factory f to create Endpoints. If src notifies of an error, the Endpointer
// keeps returning previously created Endpoints assuming they are still good, unless
// this behavior is disabled via InvalidateOnError option.
func NewEndpointer(src Instancer, f Factory, logger log.Logger, options ...EndpointerOption) *DefaultEndpointer {
opts := endpointerOptions{}
for _, opt := range options {
opt(&opts)
}
se := &DefaultEndpointer{
cache: newEndpointCache(f, logger, opts),
instancer: src,
ch: make(chan Event),
}
go se.receive()
src.Register(se.ch)
return se
}
// EndpointerOption allows control of endpointCache behavior.
type EndpointerOption func(*endpointerOptions)
// InvalidateOnError returns EndpointerOption that controls how the Endpointer
// behaves when then Instancer publishes an Event containing an error.
// Without this option the Endpointer continues returning the last known
// endpoints. With this option, the Endpointer continues returning the last
// known endpoints until the timeout elapses, then closes all active endpoints
// and starts returning an error. Once the Instancer sends a new update with
// valid resource instances, the normal operation is resumed.
func InvalidateOnError(timeout time.Duration) EndpointerOption {
return func(opts *endpointerOptions) {
opts.invalidateOnError = true
opts.invalidateTimeout = timeout
}
}
type endpointerOptions struct {
invalidateOnError bool
invalidateTimeout time.Duration
}
// DefaultEndpointer implements an Endpointer interface.
// When created with NewEndpointer function, it automatically registers
// as a subscriber to events from the Instances and maintains a list
// of active Endpoints.
type DefaultEndpointer struct {
cache *endpointCache
instancer Instancer
ch chan Event
}
func (de *DefaultEndpointer) receive() {
for event := range de.ch {
de.cache.Update(event)
}
}
// Close deregisters DefaultEndpointer from the Instancer and stops the internal go-routine.
func (de *DefaultEndpointer) Close() {
de.instancer.Deregister(de.ch)
close(de.ch)
}
// Endpoints implements Endpointer.
func (de *DefaultEndpointer) Endpoints() ([]endpoint.Endpoint, error) {
return de.cache.Endpoints()
}
================================================
FILE: sd/endpointer_test.go
================================================
package sd_test
import (
"io"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/log"
)
func TestDefaultEndpointer(t *testing.T) {
var (
ca = make(closer)
cb = make(closer)
c = map[string]io.Closer{"a": ca, "b": cb}
f = func(instance string) (endpoint.Endpoint, io.Closer, error) {
return endpoint.Nop, c[instance], nil
}
instancer = &mockInstancer{instance.NewCache()}
)
// set initial state
instancer.Update(sd.Event{Instances: []string{"a", "b"}})
endpointer := sd.NewEndpointer(instancer, f, log.NewNopLogger(), sd.InvalidateOnError(time.Minute))
var (
endpoints []endpoint.Endpoint
err error
)
if !within(time.Second, func() bool {
endpoints, err = endpointer.Endpoints()
return err == nil && len(endpoints) == 2
}) {
t.Errorf("wanted 2 endpoints, got %d (%v)", len(endpoints), err)
}
instancer.Update(sd.Event{Instances: []string{}})
select {
case <-ca:
t.Logf("endpoint a closed, good")
case <-time.After(time.Millisecond):
t.Errorf("didn't close the deleted instance in time")
}
select {
case <-cb:
t.Logf("endpoint b closed, good")
case <-time.After(time.Millisecond):
t.Errorf("didn't close the deleted instance in time")
}
if endpoints, err := endpointer.Endpoints(); err != nil {
t.Errorf("unepected error %v", err)
} else if want, have := 0, len(endpoints); want != have {
t.Errorf("want %d, have %d", want, have)
}
endpointer.Close()
instancer.Update(sd.Event{Instances: []string{"a"}})
// TODO verify that on Close the endpointer fully disconnects from the instancer.
// Unfortunately, because we use instance.Cache, this test cannot be in the sd package,
// and therefore does not have access to the endpointer's private members.
}
type mockInstancer struct{ *instance.Cache }
type closer chan struct{}
func (c closer) Close() error { close(c); return nil }
func within(d time.Duration, f func() bool) bool {
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
if f() {
return true
}
time.Sleep(d / 10)
}
return false
}
================================================
FILE: sd/etcd/client.go
================================================
package etcd
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io/ioutil"
"net"
"net/http"
"time"
etcd "go.etcd.io/etcd/client/v2"
)
var (
// ErrNoKey indicates a client method needs a key but receives none.
ErrNoKey = errors.New("no key provided")
// ErrNoValue indicates a client method needs a value but receives none.
ErrNoValue = errors.New("no value provided")
)
// Client is a wrapper around the etcd client.
type Client interface {
// GetEntries queries the given prefix in etcd and returns a slice
// containing the values of all keys found, recursively, underneath that
// prefix.
GetEntries(prefix string) ([]string, error)
// WatchPrefix watches the given prefix in etcd for changes. When a change
// is detected, it will signal on the passed channel. Clients are expected
// to call GetEntries to update themselves with the latest set of complete
// values. WatchPrefix will always send an initial sentinel value on the
// channel after establishing the watch, to ensure that clients always
// receive the latest set of values. WatchPrefix will block until the
// context passed to the NewClient constructor is terminated.
WatchPrefix(prefix string, ch chan struct{})
// Register a service with etcd.
Register(s Service) error
// Deregister a service with etcd.
Deregister(s Service) error
}
type client struct {
keysAPI etcd.KeysAPI
ctx context.Context
}
// ClientOptions defines options for the etcd client. All values are optional.
// If any duration is not specified, a default of 3 seconds will be used.
type ClientOptions struct {
Cert string
Key string
CACert string
DialTimeout time.Duration
DialKeepAlive time.Duration
HeaderTimeoutPerRequest time.Duration
}
// NewClient returns Client with a connection to the named machines. It will
// return an error if a connection to the cluster cannot be made. The parameter
// machines needs to be a full URL with schemas. e.g. "http://localhost:2379"
// will work, but "localhost:2379" will not.
func NewClient(ctx context.Context, machines []string, options ClientOptions) (Client, error) {
if options.DialTimeout == 0 {
options.DialTimeout = 3 * time.Second
}
if options.DialKeepAlive == 0 {
options.DialKeepAlive = 3 * time.Second
}
if options.HeaderTimeoutPerRequest == 0 {
options.HeaderTimeoutPerRequest = 3 * time.Second
}
transport := etcd.DefaultTransport
if options.Cert != "" && options.Key != "" {
tlsCert, err := tls.LoadX509KeyPair(options.Cert, options.Key)
if err != nil {
return nil, err
}
caCertCt, err := ioutil.ReadFile(options.CACert)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertCt)
transport = &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{tlsCert},
RootCAs: caCertPool,
},
Dial: func(network, address string) (net.Conn, error) {
return (&net.Dialer{
Timeout: options.DialTimeout,
KeepAlive: options.DialKeepAlive,
}).Dial(network, address)
},
}
}
ce, err := etcd.New(etcd.Config{
Endpoints: machines,
Transport: transport,
HeaderTimeoutPerRequest: options.HeaderTimeoutPerRequest,
})
if err != nil {
return nil, err
}
return &client{
keysAPI: etcd.NewKeysAPI(ce),
ctx: ctx,
}, nil
}
// GetEntries implements the etcd Client interface.
func (c *client) GetEntries(key string) ([]string, error) {
resp, err := c.keysAPI.Get(c.ctx, key, &etcd.GetOptions{Recursive: true})
if err != nil {
return nil, err
}
// Special case. Note that it's possible that len(resp.Node.Nodes) == 0 and
// resp.Node.Value is also empty, in which case the key is empty and we
// should not return any entries.
if len(resp.Node.Nodes) == 0 && resp.Node.Value != "" {
return []string{resp.Node.Value}, nil
}
entries := make([]string, len(resp.Node.Nodes))
for i, node := range resp.Node.Nodes {
entries[i] = node.Value
}
return entries, nil
}
// WatchPrefix implements the etcd Client interface.
func (c *client) WatchPrefix(prefix string, ch chan struct{}) {
watch := c.keysAPI.Watcher(prefix, &etcd.WatcherOptions{AfterIndex: 0, Recursive: true})
ch <- struct{}{} // make sure caller invokes GetEntries
for {
if _, err := watch.Next(c.ctx); err != nil {
return
}
ch <- struct{}{}
}
}
func (c *client) Register(s Service) error {
if s.Key == "" {
return ErrNoKey
}
if s.Value == "" {
return ErrNoValue
}
var err error
if s.TTL != nil {
_, err = c.keysAPI.Set(c.ctx, s.Key, s.Value, &etcd.SetOptions{
PrevExist: etcd.PrevIgnore,
TTL: s.TTL.ttl,
})
} else {
_, err = c.keysAPI.Create(c.ctx, s.Key, s.Value)
}
return err
}
func (c *client) Deregister(s Service) error {
if s.Key == "" {
return ErrNoKey
}
_, err := c.keysAPI.Delete(c.ctx, s.Key, s.DeleteOptions)
return err
}
================================================
FILE: sd/etcd/client_test.go
================================================
package etcd
import (
"context"
"errors"
"reflect"
"testing"
"time"
etcd "go.etcd.io/etcd/client/v2"
)
func TestNewClient(t *testing.T) {
client, err := NewClient(
context.Background(),
[]string{"http://irrelevant:12345"},
ClientOptions{
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
HeaderTimeoutPerRequest: 2 * time.Second,
},
)
if err != nil {
t.Fatalf("unexpected error creating client: %v", err)
}
if client == nil {
t.Fatal("expected new Client, got nil")
}
}
// NewClient should fail when providing invalid or missing endpoints.
func TestOptions(t *testing.T) {
a, err := NewClient(
context.Background(),
[]string{},
ClientOptions{
Cert: "",
Key: "",
CACert: "",
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
HeaderTimeoutPerRequest: 2 * time.Second,
},
)
if err == nil {
t.Errorf("expected error: %v", err)
}
if a != nil {
t.Fatalf("expected client to be nil on failure")
}
_, err = NewClient(
context.Background(),
[]string{"http://irrelevant:12345"},
ClientOptions{
Cert: "blank.crt",
Key: "blank.key",
CACert: "blank.CACert",
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
HeaderTimeoutPerRequest: 2 * time.Second,
},
)
if err == nil {
t.Errorf("expected error: %v", err)
}
}
// Mocks of the underlying etcd.KeysAPI interface that is called by the methods we want to test
// fakeKeysAPI implements etcd.KeysAPI, event and err are channels used to emulate
// an etcd event or error, getres will be returned when etcd.KeysAPI.Get is called.
type fakeKeysAPI struct {
event chan bool
err chan bool
getres *getResult
}
type getResult struct {
resp *etcd.Response
err error
}
// Get return the content of getres or nil, nil
func (fka *fakeKeysAPI) Get(ctx context.Context, key string, opts *etcd.GetOptions) (*etcd.Response, error) {
if fka.getres == nil {
return nil, nil
}
return fka.getres.resp, fka.getres.err
}
// Set is not used in the tests
func (fka *fakeKeysAPI) Set(ctx context.Context, key, value string, opts *etcd.SetOptions) (*etcd.Response, error) {
return nil, nil
}
// Delete is not used in the tests
func (fka *fakeKeysAPI) Delete(ctx context.Context, key string, opts *etcd.DeleteOptions) (*etcd.Response, error) {
return nil, nil
}
// Create is not used in the tests
func (fka *fakeKeysAPI) Create(ctx context.Context, key, value string) (*etcd.Response, error) {
return nil, nil
}
// CreateInOrder is not used in the tests
func (fka *fakeKeysAPI) CreateInOrder(ctx context.Context, dir, value string, opts *etcd.CreateInOrderOptions) (*etcd.Response, error) {
return nil, nil
}
// Update is not used in the tests
func (fka *fakeKeysAPI) Update(ctx context.Context, key, value string) (*etcd.Response, error) {
return nil, nil
}
// Watcher return a fakeWatcher that will forward event and error received on the channels
func (fka *fakeKeysAPI) Watcher(key string, opts *etcd.WatcherOptions) etcd.Watcher {
return &fakeWatcher{fka.event, fka.err}
}
// fakeWatcher implements etcd.Watcher
type fakeWatcher struct {
event chan bool
err chan bool
}
// Next blocks until an etcd event or error is emulated.
// When an event occurs it just return nil response and error.
// When an error occur it return a non nil error.
func (fw *fakeWatcher) Next(context.Context) (*etcd.Response, error) {
select {
case <-fw.event:
return nil, nil
case <-fw.err:
return nil, errors.New("error from underlying etcd watcher")
}
}
// newFakeClient return a new etcd.Client built on top of the mocked interfaces
func newFakeClient(event, err chan bool, getres *getResult) Client {
return &client{
keysAPI: &fakeKeysAPI{event, err, getres},
ctx: context.Background(),
}
}
// Register should fail when the provided service has an empty key or value
func TestRegisterClient(t *testing.T) {
client := newFakeClient(nil, nil, nil)
err := client.Register(Service{Key: "", Value: "value", DeleteOptions: nil})
if want, have := ErrNoKey, err; want != have {
t.Fatalf("want %v, have %v", want, have)
}
err = client.Register(Service{Key: "key", Value: "", DeleteOptions: nil})
if want, have := ErrNoValue, err; want != have {
t.Fatalf("want %v, have %v", want, have)
}
err = client.Register(Service{Key: "key", Value: "value", DeleteOptions: nil})
if err != nil {
t.Fatal(err)
}
}
// Deregister should fail if the input service has an empty key
func TestDeregisterClient(t *testing.T) {
client := newFakeClient(nil, nil, nil)
err := client.Deregister(Service{Key: "", Value: "value", DeleteOptions: nil})
if want, have := ErrNoKey, err; want != have {
t.Fatalf("want %v, have %v", want, have)
}
err = client.Deregister(Service{Key: "key", Value: "", DeleteOptions: nil})
if err != nil {
t.Fatal(err)
}
}
// WatchPrefix notify the caller by writing on the channel if an etcd event occurs
// or return in case of an underlying error
func TestWatchPrefix(t *testing.T) {
err := make(chan bool)
event := make(chan bool)
watchPrefixReturned := make(chan bool, 1)
client := newFakeClient(event, err, nil)
ch := make(chan struct{})
go func() {
client.WatchPrefix("prefix", ch) // block until an etcd event or error occurs
watchPrefixReturned <- true
}()
// WatchPrefix force the caller to read once from the channel before actually
// sending notification, emulate that first read.
<-ch
// Emulate an etcd event
event <- true
if want, have := struct{}{}, <-ch; want != have {
t.Fatalf("want %v, have %v", want, have)
}
// Emulate an error, WatchPrefix should return
err <- true
select {
case <-watchPrefixReturned:
break
case <-time.After(1 * time.Second):
t.Fatal("WatchPrefix not returning on errors")
}
}
var errKeyAPI = errors.New("emulate error returned by KeysAPI.Get")
// table of test cases for method GetEntries
var getEntriesTestTable = []struct {
input getResult // value returned by the underlying etcd.KeysAPI.Get
resp []string // response expected in output of GetEntries
err error //error expected in output of GetEntries
}{
// test case: an error is returned by etcd.KeysAPI.Get
{getResult{nil, errKeyAPI}, nil, errKeyAPI},
// test case: return a single leaf node, with an empty value
{getResult{&etcd.Response{
Action: "get",
Node: &etcd.Node{
Key: "nodekey",
Dir: false,
Value: "",
Nodes: nil,
CreatedIndex: 0,
ModifiedIndex: 0,
Expiration: nil,
TTL: 0,
},
PrevNode: nil,
Index: 0,
}, nil}, []string{}, nil},
// test case: return a single leaf node, with a value
{getResult{&etcd.Response{
Action: "get",
Node: &etcd.Node{
Key: "nodekey",
Dir: false,
Value: "nodevalue",
Nodes: nil,
CreatedIndex: 0,
ModifiedIndex: 0,
Expiration: nil,
TTL: 0,
},
PrevNode: nil,
Index: 0,
}, nil}, []string{"nodevalue"}, nil},
// test case: return a node with two childs
{getResult{&etcd.Response{
Action: "get",
Node: &etcd.Node{
Key: "nodekey",
Dir: true,
Value: "nodevalue",
Nodes: []*etcd.Node{
{
Key: "childnode1",
Dir: false,
Value: "childvalue1",
Nodes: nil,
CreatedIndex: 0,
ModifiedIndex: 0,
Expiration: nil,
TTL: 0,
},
{
Key: "childnode2",
Dir: false,
Value: "childvalue2",
Nodes: nil,
CreatedIndex: 0,
ModifiedIndex: 0,
Expiration: nil,
TTL: 0,
},
},
CreatedIndex: 0,
ModifiedIndex: 0,
Expiration: nil,
TTL: 0,
},
PrevNode: nil,
Index: 0,
}, nil}, []string{"childvalue1", "childvalue2"}, nil},
}
func TestGetEntries(t *testing.T) {
for _, et := range getEntriesTestTable {
client := newFakeClient(nil, nil, &et.input)
resp, err := client.GetEntries("prefix")
if want, have := et.resp, resp; !reflect.DeepEqual(want, have) {
t.Fatalf("want %v, have %v", want, have)
}
if want, have := et.err, err; want != have {
t.Fatalf("want %v, have %v", want, have)
}
}
}
================================================
FILE: sd/etcd/doc.go
================================================
// Package etcd provides an Instancer and Registrar implementation for etcd. If
// you use etcd as your service discovery system, this package will help you
// implement the registration and client-side load balancing patterns.
package etcd
================================================
FILE: sd/etcd/example_test.go
================================================
package etcd
import (
"context"
"io"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/lb"
"github.com/go-kit/log"
)
func Example() {
// Let's say this is a service that means to register itself.
// First, we will set up some context.
var (
etcdServer = "http://10.0.0.1:2379" // don't forget schema and port!
prefix = "/services/foosvc/" // known at compile time
instance = "1.2.3.4:8080" // taken from runtime or platform, somehow
key = prefix + instance // should be globally unique
value = "http://" + instance // based on our transport
ctx = context.Background()
)
// Build the client.
client, err := NewClient(ctx, []string{etcdServer}, ClientOptions{})
if err != nil {
panic(err)
}
// Build the registrar.
registrar := NewRegistrar(client, Service{
Key: key,
Value: value,
}, log.NewNopLogger())
// Register our instance.
registrar.Register()
// At the end of our service lifecycle, for example at the end of func main,
// we should make sure to deregister ourselves. This is important! Don't
// accidentally skip this step by invoking a log.Fatal or os.Exit in the
// interim, which bypasses the defer stack.
defer registrar.Deregister()
// It's likely that we'll also want to connect to other services and call
// their methods. We can build an Instancer to listen for changes from etcd,
// create Endpointer, wrap it with a load-balancer to pick a single
// endpoint, and finally wrap it with a retry strategy to get something that
// can be used as an endpoint directly.
barPrefix := "/services/barsvc"
logger := log.NewNopLogger()
instancer, err := NewInstancer(client, barPrefix, logger)
if err != nil {
panic(err)
}
endpointer := sd.NewEndpointer(instancer, barFactory, logger)
balancer := lb.NewRoundRobin(endpointer)
retry := lb.Retry(3, 3*time.Second, balancer)
// And now retry can be used like any other endpoint.
req := struct{}{}
if _, err = retry(ctx, req); err != nil {
panic(err)
}
}
func barFactory(string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, nil, nil }
================================================
FILE: sd/etcd/instancer.go
================================================
package etcd
import (
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/log"
)
// Instancer yields instances stored in a certain etcd keyspace. Any kind of
// change in that keyspace is watched and will update the Instancer's Instancers.
type Instancer struct {
cache *instance.Cache
client Client
prefix string
logger log.Logger
quitc chan struct{}
}
// NewInstancer returns an etcd instancer. It will start watching the given
// prefix for changes, and update the subscribers.
func NewInstancer(c Client, prefix string, logger log.Logger) (*Instancer, error) {
s := &Instancer{
client: c,
prefix: prefix,
cache: instance.NewCache(),
logger: logger,
quitc: make(chan struct{}),
}
instances, err := s.client.GetEntries(s.prefix)
if err == nil {
logger.Log("prefix", s.prefix, "instances", len(instances))
} else {
logger.Log("prefix", s.prefix, "err", err)
}
s.cache.Update(sd.Event{Instances: instances, Err: err})
go s.loop()
return s, nil
}
func (s *Instancer) loop() {
ch := make(chan struct{})
go s.client.WatchPrefix(s.prefix, ch)
for {
select {
case <-ch:
instances, err := s.client.GetEntries(s.prefix)
if err != nil {
s.logger.Log("msg", "failed to retrieve entries", "err", err)
s.cache.Update(sd.Event{Err: err})
continue
}
s.cache.Update(sd.Event{Instances: instances})
case <-s.quitc:
return
}
}
}
// Stop terminates the Instancer.
func (s *Instancer) Stop() {
close(s.quitc)
}
// Register implements Instancer.
func (s *Instancer) Register(ch chan<- sd.Event) {
s.cache.Register(ch)
}
// Deregister implements Instancer.
func (s *Instancer) Deregister(ch chan<- sd.Event) {
s.cache.Deregister(ch)
}
================================================
FILE: sd/etcd/instancer_test.go
================================================
package etcd
import (
"errors"
"testing"
stdetcd "go.etcd.io/etcd/client/v2"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
var _ sd.Instancer = (*Instancer)(nil) // API check
var (
node = &stdetcd.Node{
Key: "/foo",
Nodes: []*stdetcd.Node{
{Key: "/foo/1", Value: "1:1"},
{Key: "/foo/2", Value: "1:2"},
},
}
fakeResponse = &stdetcd.Response{
Node: node,
}
)
var _ sd.Instancer = &Instancer{} // API check
func TestInstancer(t *testing.T) {
client := &fakeClient{
responses: map[string]*stdetcd.Response{"/foo": fakeResponse},
}
s, err := NewInstancer(client, "/foo", log.NewNopLogger())
if err != nil {
t.Fatal(err)
}
defer s.Stop()
if state := s.cache.State(); state.Err != nil {
t.Fatal(state.Err)
}
}
type fakeClient struct {
responses map[string]*stdetcd.Response
}
func (c *fakeClient) GetEntries(prefix string) ([]string, error) {
response, ok := c.responses[prefix]
if !ok {
return nil, errors.New("key not exist")
}
entries := make([]string, len(response.Node.Nodes))
for i, node := range response.Node.Nodes {
entries[i] = node.Value
}
return entries, nil
}
func (c *fakeClient) WatchPrefix(prefix string, ch chan struct{}) {}
func (c *fakeClient) Register(Service) error {
return nil
}
func (c *fakeClient) Deregister(Service) error {
return nil
}
================================================
FILE: sd/etcd/integration_test.go
================================================
//go:build flaky_integration
// +build flaky_integration
package etcd
import (
"context"
"io"
"os"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
// Package sd/etcd provides a wrapper around the etcd key/value store. This
// example assumes the user has an instance of etcd installed and running
// locally on port 2379.
func TestIntegration(t *testing.T) {
addr := os.Getenv("ETCD_ADDR")
if addr == "" {
t.Skip("ETCD_ADDR not set; skipping integration test")
}
var (
prefix = "/services/foosvc/" // known at compile time
instance = "1.2.3.4:8080" // taken from runtime or platform, somehow
key = prefix + instance
value = "http://" + instance // based on our transport
)
client, err := NewClient(context.Background(), []string{addr}, ClientOptions{
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
HeaderTimeoutPerRequest: 2 * time.Second,
})
if err != nil {
t.Fatalf("NewClient(%q): %v", addr, err)
}
// Verify test data is initially empty.
entries, err := client.GetEntries(key)
if err == nil {
t.Fatalf("GetEntries(%q): expected error, got none", key)
}
t.Logf("GetEntries(%q): %v (OK)", key, err)
// Instantiate a new Registrar, passing in test data.
registrar := NewRegistrar(client, Service{
Key: key,
Value: value,
}, log.With(log.NewLogfmtLogger(os.Stderr), "component", "registrar"))
// Register our instance.
registrar.Register()
t.Logf("Registered")
// Retrieve entries from etcd manually.
entries, err = client.GetEntries(key)
if err != nil {
t.Fatalf("client.GetEntries(%q): %v", key, err)
}
if want, have := 1, len(entries); want != have {
t.Fatalf("client.GetEntries(%q): want %d, have %d", key, want, have)
}
if want, have := value, entries[0]; want != have {
t.Fatalf("want %q, have %q", want, have)
}
instancer, err := NewInstancer(
client,
prefix,
log.With(log.NewLogfmtLogger(os.Stderr), "component", "instancer"),
)
if err != nil {
t.Fatalf("NewInstancer: %v", err)
}
endpointer := sd.NewEndpointer(
instancer,
func(string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, nil, nil },
log.With(log.NewLogfmtLogger(os.Stderr), "component", "instancer"),
)
t.Logf("Constructed Endpointer OK")
if !within(time.Second, func() bool {
endpoints, err := endpointer.Endpoints()
return err == nil && len(endpoints) == 1
}) {
t.Fatalf("Endpointer didn't see Register in time")
}
t.Logf("Endpointer saw Register OK")
// Deregister first instance of test data.
registrar.Deregister()
t.Logf("Deregistered")
// Check it was deregistered.
if !within(time.Second, func() bool {
endpoints, err := endpointer.Endpoints()
t.Logf("Checking Deregister: len(endpoints) = %d, err = %v", len(endpoints), err)
return err == nil && len(endpoints) == 0
}) {
t.Fatalf("Endpointer didn't see Deregister in time")
}
// Verify test data no longer exists in etcd.
_, err = client.GetEntries(key)
if err == nil {
t.Fatalf("GetEntries(%q): expected error, got none", key)
}
t.Logf("GetEntries(%q): %v (OK)", key, err)
}
func within(d time.Duration, f func() bool) bool {
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
if f() {
return true
}
time.Sleep(d / 10)
}
return false
}
================================================
FILE: sd/etcd/registrar.go
================================================
package etcd
import (
"sync"
"time"
etcd "go.etcd.io/etcd/client/v2"
"github.com/go-kit/log"
)
const minHeartBeatTime = 500 * time.Millisecond
// Registrar registers service instance liveness information to etcd.
type Registrar struct {
client Client
service Service
logger log.Logger
quitmtx sync.Mutex
quit chan struct{}
}
// Service holds the instance identifying data you want to publish to etcd. Key
// must be unique, and value is the string returned to subscribers, typically
// called the "instance" string in other parts of package sd.
type Service struct {
Key string // unique key, e.g. "/service/foobar/1.2.3.4:8080"
Value string // returned to subscribers, e.g. "http://1.2.3.4:8080"
TTL *TTLOption
DeleteOptions *etcd.DeleteOptions
}
// TTLOption allow setting a key with a TTL. This option will be used by a loop
// goroutine which regularly refreshes the lease of the key.
type TTLOption struct {
heartbeat time.Duration // e.g. time.Second * 3
ttl time.Duration // e.g. time.Second * 10
}
// NewTTLOption returns a TTLOption that contains proper TTL settings. Heartbeat
// is used to refresh the lease of the key periodically; its value should be at
// least 500ms. TTL defines the lease of the key; its value should be
// significantly greater than heartbeat.
//
// Good default values might be 3s heartbeat, 10s TTL.
func NewTTLOption(heartbeat, ttl time.Duration) *TTLOption {
if heartbeat <= minHeartBeatTime {
heartbeat = minHeartBeatTime
}
if ttl <= heartbeat {
ttl = 3 * heartbeat
}
return &TTLOption{
heartbeat: heartbeat,
ttl: ttl,
}
}
// NewRegistrar returns a etcd Registrar acting on the provided catalog
// registration (service).
func NewRegistrar(client Client, service Service, logger log.Logger) *Registrar {
return &Registrar{
client: client,
service: service,
logger: log.With(logger, "key", service.Key, "value", service.Value),
}
}
// Register implements the sd.Registrar interface. Call it when you want your
// service to be registered in etcd, typically at startup.
func (r *Registrar) Register() {
if err := r.client.Register(r.service); err != nil {
r.logger.Log("err", err)
} else {
r.logger.Log("action", "register")
}
if r.service.TTL != nil {
go r.loop()
}
}
func (r *Registrar) loop() {
r.quitmtx.Lock()
if r.quit != nil {
return // already running
}
r.quit = make(chan struct{})
r.quitmtx.Unlock()
tick := time.NewTicker(r.service.TTL.heartbeat)
defer tick.Stop()
for {
select {
case <-tick.C:
if err := r.client.Register(r.service); err != nil {
r.logger.Log("err", err)
}
case <-r.quit:
return
}
}
}
// Deregister implements the sd.Registrar interface. Call it when you want your
// service to be deregistered from etcd, typically just prior to shutdown.
func (r *Registrar) Deregister() {
if err := r.client.Deregister(r.service); err != nil {
r.logger.Log("err", err)
} else {
r.logger.Log("action", "deregister")
}
r.quitmtx.Lock()
defer r.quitmtx.Unlock()
if r.quit != nil {
close(r.quit)
r.quit = nil
}
}
================================================
FILE: sd/etcd/registrar_test.go
================================================
package etcd
import (
"bytes"
"errors"
"testing"
"github.com/go-kit/log"
)
// testClient is a basic implementation of Client
type testClient struct {
registerRes error // value returned when Register or Deregister is called
}
func (tc *testClient) GetEntries(prefix string) ([]string, error) {
return nil, nil
}
func (tc *testClient) WatchPrefix(prefix string, ch chan struct{}) {
return
}
func (tc *testClient) Register(s Service) error {
return tc.registerRes
}
func (tc *testClient) Deregister(s Service) error {
return tc.registerRes
}
// default service used to build registrar in our tests
var testService = Service{"testKey", "testValue", nil, nil}
// NewRegistar should return a registar with a logger using the service key and value
func TestNewRegistar(t *testing.T) {
c := Client(&testClient{nil})
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)
r := NewRegistrar(
c,
testService,
logger,
)
if err := r.logger.Log("msg", "message"); err != nil {
t.Fatal(err)
}
if want, have := "key=testKey value=testValue msg=message\n", buf.String(); want != have {
t.Errorf("\nwant: %shave: %s", want, have)
}
}
// Register log the error returned by the client or log the successful registration action
// table of test cases for method Register
var registerTestTable = []struct {
registerRes error // value returned by the client on calls to Register
log string // expected log by the registrar
}{
// test case: an error is returned by the client
{errors.New("regError"), "key=testKey value=testValue err=regError\n"},
// test case: registration successful
{nil, "key=testKey value=testValue action=register\n"},
}
func TestRegister(t *testing.T) {
for _, tc := range registerTestTable {
c := Client(&testClient{tc.registerRes})
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)
r := NewRegistrar(
c,
testService,
logger,
)
r.Register()
if want, have := tc.log, buf.String(); want != have {
t.Fatalf("want %v, have %v", want, have)
}
}
}
// Deregister log the error returned by the client or log the successful deregistration action
// table of test cases for method Deregister
var deregisterTestTable = []struct {
deregisterRes error // value returned by the client on calls to Deregister
log string // expected log by the registrar
}{
// test case: an error is returned by the client
{errors.New("deregError"), "key=testKey value=testValue err=deregError\n"},
// test case: deregistration successful
{nil, "key=testKey value=testValue action=deregister\n"},
}
func TestDeregister(t *testing.T) {
for _, tc := range deregisterTestTable {
c := Client(&testClient{tc.deregisterRes})
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)
r := NewRegistrar(
c,
testService,
logger,
)
r.Deregister()
if want, have := tc.log, buf.String(); want != have {
t.Fatalf("want %v, have %v", want, have)
}
}
}
================================================
FILE: sd/etcdv3/client.go
================================================
package etcdv3
import (
"context"
"crypto/tls"
"errors"
"time"
"go.etcd.io/etcd/client/pkg/v3/transport"
clientv3 "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc"
)
var (
// ErrNoKey indicates a client method needs a key but receives none.
ErrNoKey = errors.New("no key provided")
// ErrNoValue indicates a client method needs a value but receives none.
ErrNoValue = errors.New("no value provided")
)
// Client is a wrapper around the etcd client.
type Client interface {
// GetEntries queries the given prefix in etcd and returns a slice
// containing the values of all keys found, recursively, underneath that
// prefix.
GetEntries(prefix string) ([]string, error)
// WatchPrefix watches the given prefix in etcd for changes. When a change
// is detected, it will signal on the passed channel. Clients are expected
// to call GetEntries to update themselves with the latest set of complete
// values. WatchPrefix will always send an initial sentinel value on the
// channel after establishing the watch, to ensure that clients always
// receive the latest set of values. WatchPrefix will block until the
// context passed to the NewClient constructor is terminated.
WatchPrefix(prefix string, ch chan struct{})
// Register a service with etcd.
Register(s Service) error
// Deregister a service with etcd.
Deregister(s Service) error
// LeaseID returns the lease id created for this service instance
LeaseID() int64
}
type client struct {
cli *clientv3.Client
ctx context.Context
kv clientv3.KV
// Watcher interface instance, used to leverage Watcher.Close()
watcher clientv3.Watcher
// watcher context
wctx context.Context
// watcher cancel func
wcf context.CancelFunc
// leaseID will be 0 (clientv3.NoLease) if a lease was not created
leaseID clientv3.LeaseID
hbch <-chan *clientv3.LeaseKeepAliveResponse
// Lease interface instance, used to leverage Lease.Close()
leaser clientv3.Lease
}
// ClientOptions defines options for the etcd client. All values are optional.
// If any duration is not specified, a default of 3 seconds will be used.
type ClientOptions struct {
Cert string
Key string
CACert string
DialTimeout time.Duration
DialKeepAlive time.Duration
// DialOptions is a list of dial options for the gRPC client (e.g., for interceptors).
// For example, pass grpc.WithBlock() to block until the underlying connection is up.
// Without this, Dial returns immediately and connecting the server happens in background.
DialOptions []grpc.DialOption
Username string
Password string
}
// NewClient returns Client with a connection to the named machines. It will
// return an error if a connection to the cluster cannot be made.
func NewClient(ctx context.Context, machines []string, options ClientOptions) (Client, error) {
if options.DialTimeout == 0 {
options.DialTimeout = 3 * time.Second
}
if options.DialKeepAlive == 0 {
options.DialKeepAlive = 3 * time.Second
}
var err error
var tlscfg *tls.Config
if options.Cert != "" && options.Key != "" {
tlsInfo := transport.TLSInfo{
CertFile: options.Cert,
KeyFile: options.Key,
TrustedCAFile: options.CACert,
}
tlscfg, err = tlsInfo.ClientConfig()
if err != nil {
return nil, err
}
}
cli, err := clientv3.New(clientv3.Config{
Context: ctx,
Endpoints: machines,
DialTimeout: options.DialTimeout,
DialKeepAliveTime: options.DialKeepAlive,
DialOptions: options.DialOptions,
TLS: tlscfg,
Username: options.Username,
Password: options.Password,
})
if err != nil {
return nil, err
}
return &client{
cli: cli,
ctx: ctx,
kv: clientv3.NewKV(cli),
}, nil
}
func (c *client) LeaseID() int64 { return int64(c.leaseID) }
// GetEntries implements the etcd Client interface.
func (c *client) GetEntries(key string) ([]string, error) {
resp, err := c.kv.Get(c.ctx, key, clientv3.WithPrefix())
if err != nil {
return nil, err
}
entries := make([]string, len(resp.Kvs))
for i, kv := range resp.Kvs {
entries[i] = string(kv.Value)
}
return entries, nil
}
// WatchPrefix implements the etcd Client interface.
func (c *client) WatchPrefix(prefix string, ch chan struct{}) {
c.wctx, c.wcf = context.WithCancel(c.ctx)
c.watcher = clientv3.NewWatcher(c.cli)
wch := c.watcher.Watch(c.wctx, prefix, clientv3.WithPrefix(), clientv3.WithRev(0))
ch <- struct{}{}
for wr := range wch {
if wr.Canceled {
return
}
ch <- struct{}{}
}
}
func (c *client) Register(s Service) error {
var err error
if s.Key == "" {
return ErrNoKey
}
if s.Value == "" {
return ErrNoValue
}
if c.leaser != nil {
c.leaser.Close()
}
c.leaser = clientv3.NewLease(c.cli)
if c.watcher != nil {
c.watcher.Close()
}
c.watcher = clientv3.NewWatcher(c.cli)
if c.kv == nil {
c.kv = clientv3.NewKV(c.cli)
}
if s.TTL == nil {
s.TTL = NewTTLOption(time.Second*3, time.Second*10)
}
grantResp, err := c.leaser.Grant(c.ctx, int64(s.TTL.ttl.Seconds()))
if err != nil {
return err
}
c.leaseID = grantResp.ID
_, err = c.kv.Put(
c.ctx,
s.Key,
s.Value,
clientv3.WithLease(c.leaseID),
)
if err != nil {
return err
}
// this will keep the key alive 'forever' or until we revoke it or
// the context is canceled
c.hbch, err = c.leaser.KeepAlive(c.ctx, c.leaseID)
if err != nil {
return err
}
// discard the keepalive response, make etcd library not to complain
// fix bug #799
go func() {
for {
select {
case r := <-c.hbch:
// avoid dead loop when channel was closed
if r == nil {
return
}
case <-c.ctx.Done():
return
}
}
}()
return nil
}
func (c *client) Deregister(s Service) error {
defer c.close()
if s.Key == "" {
return ErrNoKey
}
if _, err := c.cli.Delete(c.ctx, s.Key, clientv3.WithIgnoreLease()); err != nil {
return err
}
return nil
}
// close will close any open clients and call
// the watcher cancel func
func (c *client) close() {
if c.leaser != nil {
c.leaser.Close()
}
if c.watcher != nil {
c.watcher.Close()
}
if c.wcf != nil {
c.wcf()
}
}
================================================
FILE: sd/etcdv3/client_test.go
================================================
package etcdv3
import (
"context"
"testing"
"time"
"google.golang.org/grpc"
)
const (
// irrelevantEndpoint is an address which does not exists.
irrelevantEndpoint = "http://irrelevant:12345"
)
func TestNewClient(t *testing.T) {
client, err := NewClient(
context.Background(),
[]string{irrelevantEndpoint},
ClientOptions{
DialTimeout: 3 * time.Second,
DialKeepAlive: 3 * time.Second,
},
)
if err != nil {
t.Fatalf("unexpected error creating client: %v", err)
}
if client == nil {
t.Fatal("expected new Client, got nil")
}
}
func TestClientOptions(t *testing.T) {
client, err := NewClient(
context.Background(),
[]string{},
ClientOptions{
Cert: "",
Key: "",
CACert: "",
DialTimeout: 3 * time.Second,
DialKeepAlive: 3 * time.Second,
},
)
if err == nil {
t.Errorf("expected error: %v", err)
}
if client != nil {
t.Fatalf("expected client to be nil on failure")
}
_, err = NewClient(
context.Background(),
[]string{irrelevantEndpoint},
ClientOptions{
Cert: "does-not-exist.crt",
Key: "does-not-exist.key",
CACert: "does-not-exist.CACert",
DialTimeout: 3 * time.Second,
DialKeepAlive: 3 * time.Second,
},
)
if err == nil {
t.Errorf("expected error: %v", err)
}
client, err = NewClient(
context.Background(),
[]string{irrelevantEndpoint},
ClientOptions{
DialOptions: []grpc.DialOption{grpc.WithBlock()},
},
)
if err == nil {
t.Errorf("expected connection should fail")
}
if client != nil {
t.Errorf("expected client to be nil on failure")
}
}
================================================
FILE: sd/etcdv3/doc.go
================================================
// Package etcdv3 provides an Instancer and Registrar implementation for etcd v3. If
// you use etcd v3 as your service discovery system, this package will help you
// implement the registration and client-side load balancing patterns.
package etcdv3
================================================
FILE: sd/etcdv3/example_test.go
================================================
package etcdv3
import (
"context"
"io"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/lb"
"github.com/go-kit/log"
"google.golang.org/grpc"
)
func Example() {
// Let's say this is a service that means to register itself.
// First, we will set up some context.
var (
etcdServer = "10.0.0.1:2379" // in the change from v2 to v3, the schema is no longer necessary if connecting directly to an etcd v3 instance
prefix = "/services/foosvc/" // known at compile time
instance = "1.2.3.4:8080" // taken from runtime or platform, somehow
key = prefix + instance // should be globally unique
value = "http://" + instance // based on our transport
ctx = context.Background()
)
options := ClientOptions{
// Path to trusted ca file
CACert: "",
// Path to certificate
Cert: "",
// Path to private key
Key: "",
// Username if required
Username: "",
// Password if required
Password: "",
// If DialTimeout is 0, it defaults to 3s
DialTimeout: time.Second * 3,
// If DialKeepAlive is 0, it defaults to 3s
DialKeepAlive: time.Second * 3,
// If passing `grpc.WithBlock`, dial connection will block until success.
DialOptions: []grpc.DialOption{grpc.WithBlock()},
}
// Build the client.
client, err := NewClient(ctx, []string{etcdServer}, options)
if err != nil {
panic(err)
}
// Build the registrar.
registrar := NewRegistrar(client, Service{
Key: key,
Value: value,
}, log.NewNopLogger())
// Register our instance.
registrar.Register()
// At the end of our service lifecycle, for example at the end of func main,
// we should make sure to deregister ourselves. This is important! Don't
// accidentally skip this step by invoking a log.Fatal or os.Exit in the
// interim, which bypasses the defer stack.
defer registrar.Deregister()
// It's likely that we'll also want to connect to other services and call
// their methods. We can build an Instancer to listen for changes from etcd,
// create Endpointer, wrap it with a load-balancer to pick a single
// endpoint, and finally wrap it with a retry strategy to get something that
// can be used as an endpoint directly.
barPrefix := "/services/barsvc"
logger := log.NewNopLogger()
instancer, err := NewInstancer(client, barPrefix, logger)
if err != nil {
panic(err)
}
endpointer := sd.NewEndpointer(instancer, barFactory, logger)
balancer := lb.NewRoundRobin(endpointer)
retry := lb.Retry(3, 3*time.Second, balancer)
// And now retry can be used like any other endpoint.
req := struct{}{}
if _, err = retry(ctx, req); err != nil {
panic(err)
}
}
func barFactory(string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, nil, nil }
================================================
FILE: sd/etcdv3/instancer.go
================================================
package etcdv3
import (
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/log"
)
// Instancer yields instances stored in a certain etcd keyspace. Any kind of
// change in that keyspace is watched and will update the Instancer's Instancers.
type Instancer struct {
cache *instance.Cache
client Client
prefix string
logger log.Logger
quitc chan struct{}
}
// NewInstancer returns an etcd instancer. It will start watching the given
// prefix for changes, and update the subscribers.
func NewInstancer(c Client, prefix string, logger log.Logger) (*Instancer, error) {
s := &Instancer{
client: c,
prefix: prefix,
cache: instance.NewCache(),
logger: logger,
quitc: make(chan struct{}),
}
instances, err := s.client.GetEntries(s.prefix)
if err == nil {
logger.Log("prefix", s.prefix, "instances", len(instances))
} else {
logger.Log("prefix", s.prefix, "err", err)
}
s.cache.Update(sd.Event{Instances: instances, Err: err})
go s.loop()
return s, nil
}
func (s *Instancer) loop() {
ch := make(chan struct{})
go s.client.WatchPrefix(s.prefix, ch)
for {
select {
case <-ch:
instances, err := s.client.GetEntries(s.prefix)
if err != nil {
s.logger.Log("msg", "failed to retrieve entries", "err", err)
s.cache.Update(sd.Event{Err: err})
continue
}
s.cache.Update(sd.Event{Instances: instances})
case <-s.quitc:
return
}
}
}
// Stop terminates the Instancer.
func (s *Instancer) Stop() {
close(s.quitc)
}
// Register implements Instancer.
func (s *Instancer) Register(ch chan<- sd.Event) {
s.cache.Register(ch)
}
// Deregister implements Instancer.
func (s *Instancer) Deregister(ch chan<- sd.Event) {
s.cache.Deregister(ch)
}
================================================
FILE: sd/etcdv3/instancer_test.go
================================================
package etcdv3
import (
"errors"
"testing"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
var _ sd.Instancer = (*Instancer)(nil) // API check
type testKV struct {
Key []byte
Value []byte
}
type testResponse struct {
Kvs []testKV
}
var (
fakeResponse = testResponse{
Kvs: []testKV{
{
Key: []byte("/foo/1"),
Value: []byte("1:1"),
},
{
Key: []byte("/foo/2"),
Value: []byte("2:2"),
},
},
}
)
var _ sd.Instancer = &Instancer{} // API check
func TestInstancer(t *testing.T) {
client := &fakeClient{
responses: map[string]testResponse{"/foo": fakeResponse},
}
s, err := NewInstancer(client, "/foo", log.NewNopLogger())
if err != nil {
t.Fatal(err)
}
defer s.Stop()
if state := s.cache.State(); state.Err != nil {
t.Fatal(state.Err)
}
}
type fakeClient struct {
responses map[string]testResponse
}
func (c *fakeClient) GetEntries(prefix string) ([]string, error) {
response, ok := c.responses[prefix]
if !ok {
return nil, errors.New("key not exist")
}
entries := make([]string, len(response.Kvs))
for i, node := range response.Kvs {
entries[i] = string(node.Value)
}
return entries, nil
}
func (c *fakeClient) WatchPrefix(prefix string, ch chan struct{}) {
}
func (c *fakeClient) LeaseID() int64 {
return 0
}
func (c *fakeClient) Register(Service) error {
return nil
}
func (c *fakeClient) Deregister(Service) error {
return nil
}
================================================
FILE: sd/etcdv3/integration_test.go
================================================
//go:build flaky_integration
// +build flaky_integration
package etcdv3
import (
"context"
"io"
"os"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
func runIntegration(settings integrationSettings, client Client, service Service, t *testing.T) {
// Verify test data is initially empty.
entries, err := client.GetEntries(settings.key)
if err != nil {
t.Fatalf("GetEntries(%q): expected no error, got one: %v", settings.key, err)
}
if len(entries) > 0 {
t.Fatalf("GetEntries(%q): expected no instance entries, got %d", settings.key, len(entries))
}
t.Logf("GetEntries(%q): %v (OK)", settings.key, entries)
// Instantiate a new Registrar, passing in test data.
registrar := NewRegistrar(
client,
service,
log.With(log.NewLogfmtLogger(os.Stderr), "component", "registrar"),
)
// Register our instance.
registrar.Register()
t.Log("Registered")
// Retrieve entries from etcd manually.
entries, err = client.GetEntries(settings.key)
if err != nil {
t.Fatalf("client.GetEntries(%q): %v", settings.key, err)
}
if want, have := 1, len(entries); want != have {
t.Fatalf("client.GetEntries(%q): want %d, have %d", settings.key, want, have)
}
if want, have := settings.value, entries[0]; want != have {
t.Fatalf("want %q, have %q", want, have)
}
instancer, err := NewInstancer(
client,
settings.prefix,
log.With(log.NewLogfmtLogger(os.Stderr), "component", "instancer"),
)
if err != nil {
t.Fatalf("NewInstancer: %v", err)
}
t.Log("Constructed Instancer OK")
defer instancer.Stop()
endpointer := sd.NewEndpointer(
instancer,
func(string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, nil, nil },
log.With(log.NewLogfmtLogger(os.Stderr), "component", "instancer"),
)
t.Log("Constructed Endpointer OK")
defer endpointer.Close()
if !within(time.Second, func() bool {
endpoints, err := endpointer.Endpoints()
return err == nil && len(endpoints) == 1
}) {
t.Fatal("Endpointer didn't see Register in time")
}
t.Log("Endpointer saw Register OK")
// Deregister first instance of test data.
registrar.Deregister()
t.Log("Deregistered")
// Check it was deregistered.
if !within(time.Second, func() bool {
endpoints, err := endpointer.Endpoints()
t.Logf("Checking Deregister: len(endpoints) = %d, err = %v", len(endpoints), err)
return err == nil && len(endpoints) == 0
}) {
t.Fatalf("Endpointer didn't see Deregister in time")
}
// Verify test data no longer exists in etcd.
entries, err = client.GetEntries(settings.key)
if err != nil {
t.Fatalf("GetEntries(%q): expected no error, got one: %v", settings.key, err)
}
if len(entries) > 0 {
t.Fatalf("GetEntries(%q): expected no entries, got %v", settings.key, entries)
}
t.Logf("GetEntries(%q): %v (OK)", settings.key, entries)
}
type integrationSettings struct {
addr string
prefix string
instance string
key string
value string
}
func testIntegrationSettings(t *testing.T) integrationSettings {
var settings integrationSettings
settings.addr = os.Getenv("ETCD_ADDR")
if settings.addr == "" {
t.Skip("ETCD_ADDR not set; skipping integration test")
}
settings.prefix = "/services/foosvc/" // known at compile time
settings.instance = "1.2.3.4:8080" // taken from runtime or platform, somehow
settings.key = settings.prefix + settings.instance
settings.value = "http://" + settings.instance // based on our transport
return settings
}
// Package sd/etcd provides a wrapper around the etcd key/value store. This
// example assumes the user has an instance of etcd installed and running
// locally on port 2379.
func TestIntegration(t *testing.T) {
settings := testIntegrationSettings(t)
client, err := NewClient(context.Background(), []string{settings.addr}, ClientOptions{
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
})
if err != nil {
t.Fatalf("NewClient(%q): %v", settings.addr, err)
}
service := Service{
Key: settings.key,
Value: settings.value,
}
runIntegration(settings, client, service, t)
}
func TestIntegrationTTL(t *testing.T) {
settings := testIntegrationSettings(t)
client, err := NewClient(context.Background(), []string{settings.addr}, ClientOptions{
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
})
if err != nil {
t.Fatalf("NewClient(%q): %v", settings.addr, err)
}
service := Service{
Key: settings.key,
Value: settings.value,
TTL: NewTTLOption(time.Second*3, time.Second*10),
}
defer client.Deregister(service)
runIntegration(settings, client, service, t)
}
func TestIntegrationRegistrarOnly(t *testing.T) {
settings := testIntegrationSettings(t)
client, err := NewClient(context.Background(), []string{settings.addr}, ClientOptions{
DialTimeout: 2 * time.Second,
DialKeepAlive: 2 * time.Second,
})
if err != nil {
t.Fatalf("NewClient(%q): %v", settings.addr, err)
}
service := Service{
Key: settings.key,
Value: settings.value,
TTL: NewTTLOption(time.Second*3, time.Second*10),
}
defer client.Deregister(service)
// Verify test data is initially empty.
entries, err := client.GetEntries(settings.key)
if err != nil {
t.Fatalf("GetEntries(%q): expected no error, got one: %v", settings.key, err)
}
if len(entries) > 0 {
t.Fatalf("GetEntries(%q): expected no instance entries, got %d", settings.key, len(entries))
}
t.Logf("GetEntries(%q): %v (OK)", settings.key, entries)
// Instantiate a new Registrar, passing in test data.
registrar := NewRegistrar(
client,
service,
log.With(log.NewLogfmtLogger(os.Stderr), "component", "registrar"),
)
// Register our instance.
registrar.Register()
t.Log("Registered")
// Deregister our instance. (so we test registrar only scenario)
registrar.Deregister()
t.Log("Deregistered")
}
func within(d time.Duration, f func() bool) bool {
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
if f() {
return true
}
time.Sleep(d / 10)
}
return false
}
================================================
FILE: sd/etcdv3/registrar.go
================================================
package etcdv3
import (
"sync"
"time"
"github.com/go-kit/log"
)
const minHeartBeatTime = 500 * time.Millisecond
// Registrar registers service instance liveness information to etcd.
type Registrar struct {
client Client
service Service
logger log.Logger
quitmtx sync.Mutex
quit chan struct{}
}
// Service holds the instance identifying data you want to publish to etcd. Key
// must be unique, and value is the string returned to subscribers, typically
// called the "instance" string in other parts of package sd.
type Service struct {
Key string // unique key, e.g. "/service/foobar/1.2.3.4:8080"
Value string // returned to subscribers, e.g. "http://1.2.3.4:8080"
TTL *TTLOption
}
// TTLOption allow setting a key with a TTL. This option will be used by a loop
// goroutine which regularly refreshes the lease of the key.
type TTLOption struct {
heartbeat time.Duration // e.g. time.Second * 3
ttl time.Duration // e.g. time.Second * 10
}
// NewTTLOption returns a TTLOption that contains proper TTL settings. Heartbeat
// is used to refresh the lease of the key periodically; its value should be at
// least 500ms. TTL defines the lease of the key; its value should be
// significantly greater than heartbeat.
//
// Good default values might be 3s heartbeat, 10s TTL.
func NewTTLOption(heartbeat, ttl time.Duration) *TTLOption {
if heartbeat <= minHeartBeatTime {
heartbeat = minHeartBeatTime
}
if ttl <= heartbeat {
ttl = 3 * heartbeat
}
return &TTLOption{
heartbeat: heartbeat,
ttl: ttl,
}
}
// NewRegistrar returns a etcd Registrar acting on the provided catalog
// registration (service).
func NewRegistrar(client Client, service Service, logger log.Logger) *Registrar {
return &Registrar{
client: client,
service: service,
logger: log.With(logger, "key", service.Key, "value", service.Value),
}
}
// Register implements the sd.Registrar interface. Call it when you want your
// service to be registered in etcd, typically at startup.
func (r *Registrar) Register() {
if err := r.client.Register(r.service); err != nil {
r.logger.Log("err", err)
return
}
if r.service.TTL != nil {
r.logger.Log("action", "register", "lease", r.client.LeaseID())
} else {
r.logger.Log("action", "register")
}
}
// Deregister implements the sd.Registrar interface. Call it when you want your
// service to be deregistered from etcd, typically just prior to shutdown.
func (r *Registrar) Deregister() {
if err := r.client.Deregister(r.service); err != nil {
r.logger.Log("err", err)
} else {
r.logger.Log("action", "deregister")
}
r.quitmtx.Lock()
defer r.quitmtx.Unlock()
if r.quit != nil {
close(r.quit)
r.quit = nil
}
}
================================================
FILE: sd/etcdv3/registrar_test.go
================================================
package etcdv3
import (
"bytes"
"errors"
"testing"
"github.com/go-kit/log"
)
// testClient is a basic implementation of Client
type testClient struct {
registerRes error // value returned when Register or Deregister is called
}
func (tc *testClient) GetEntries(prefix string) ([]string, error) {
return nil, nil
}
func (tc *testClient) WatchPrefix(prefix string, ch chan struct{}) {
}
func (tc *testClient) Register(s Service) error {
return tc.registerRes
}
func (tc *testClient) Deregister(s Service) error {
return tc.registerRes
}
func (tc *testClient) LeaseID() int64 {
return 0
}
// default service used to build registrar in our tests
var testService = Service{
Key: "testKey",
Value: "testValue",
TTL: nil,
}
// NewRegistar should return a registar with a logger using the service key and value
func TestNewRegistar(t *testing.T) {
c := Client(&testClient{nil})
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)
r := NewRegistrar(
c,
testService,
logger,
)
if err := r.logger.Log("msg", "message"); err != nil {
t.Fatal(err)
}
if want, have := "key=testKey value=testValue msg=message\n", buf.String(); want != have {
t.Errorf("\nwant: %shave: %s", want, have)
}
}
func TestRegister(t *testing.T) {
// Register log the error returned by the client or log the successful registration action
// table of test cases for method Register
var registerTestTable = []struct {
registerRes error // value returned by the client on calls to Register
log string // expected log by the registrar
}{
// test case: an error is returned by the client
{errors.New("regError"), "key=testKey value=testValue err=regError\n"},
// test case: registration successful
{nil, "key=testKey value=testValue action=register\n"},
}
for _, tc := range registerTestTable {
c := Client(&testClient{tc.registerRes})
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)
r := NewRegistrar(
c,
testService,
logger,
)
r.Register()
if want, have := tc.log, buf.String(); want != have {
t.Fatalf("want %v, have %v", want, have)
}
}
}
func TestDeregister(t *testing.T) {
// Deregister log the error returned by the client or log the successful deregistration action
// table of test cases for method Deregister
var deregisterTestTable = []struct {
deregisterRes error // value returned by the client on calls to Deregister
log string // expected log by the registrar
}{
// test case: an error is returned by the client
{errors.New("deregError"), "key=testKey value=testValue err=deregError\n"},
// test case: deregistration successful
{nil, "key=testKey value=testValue action=deregister\n"},
}
for _, tc := range deregisterTestTable {
c := Client(&testClient{tc.deregisterRes})
buf := &bytes.Buffer{}
logger := log.NewLogfmtLogger(buf)
r := NewRegistrar(
c,
testService,
logger,
)
r.Deregister()
if want, have := tc.log, buf.String(); want != have {
t.Fatalf("want %v, have %v", want, have)
}
}
}
================================================
FILE: sd/eureka/doc.go
================================================
// Package eureka provides Instancer and Registrar implementations for Netflix OSS's Eureka
package eureka
================================================
FILE: sd/eureka/instancer.go
================================================
package eureka
import (
"fmt"
"github.com/hudl/fargo"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/log"
)
// Instancer yields instances stored in the Eureka registry for the given app.
// Changes in that app are watched and will update the subscribers.
type Instancer struct {
cache *instance.Cache
conn fargoConnection
app string
logger log.Logger
quitc chan chan struct{}
}
// NewInstancer returns a Eureka Instancer. It will start watching the given
// app string for changes, and update the subscribers accordingly.
func NewInstancer(conn fargoConnection, app string, logger log.Logger) *Instancer {
logger = log.With(logger, "app", app)
s := &Instancer{
cache: instance.NewCache(),
conn: conn,
app: app,
logger: logger,
quitc: make(chan chan struct{}),
}
done := make(chan struct{})
updates := conn.ScheduleAppUpdates(app, true, done)
s.consume(<-updates)
go s.loop(updates, done)
return s
}
// Stop terminates the Instancer.
func (s *Instancer) Stop() {
q := make(chan struct{})
s.quitc <- q
<-q
s.quitc = nil
}
func (s *Instancer) consume(update fargo.AppUpdate) {
if update.Err != nil {
s.logger.Log("during", "Update", "err", update.Err)
s.cache.Update(sd.Event{Err: update.Err})
return
}
instances := convertFargoAppToInstances(update.App)
s.logger.Log("instances", len(instances))
s.cache.Update(sd.Event{Instances: instances})
}
func (s *Instancer) loop(updates <-chan fargo.AppUpdate, done chan<- struct{}) {
defer close(done)
for {
select {
case update := <-updates:
s.consume(update)
case q := <-s.quitc:
close(q)
return
}
}
}
func (s *Instancer) getInstances() ([]string, error) {
app, err := s.conn.GetApp(s.app)
if err != nil {
return nil, err
}
return convertFargoAppToInstances(app), nil
}
func convertFargoAppToInstances(app *fargo.Application) []string {
instances := make([]string, len(app.Instances))
for i, inst := range app.Instances {
instances[i] = fmt.Sprintf("%s:%d", inst.IPAddr, inst.Port)
}
return instances
}
// Register implements Instancer.
func (s *Instancer) Register(ch chan<- sd.Event) {
s.cache.Register(ch)
}
// Deregister implements Instancer.
func (s *Instancer) Deregister(ch chan<- sd.Event) {
s.cache.Deregister(ch)
}
// state returns the current state of instance.Cache, only for testing
func (s *Instancer) state() sd.Event {
return s.cache.State()
}
================================================
FILE: sd/eureka/instancer_test.go
================================================
package eureka
import (
"testing"
"time"
"github.com/hudl/fargo"
"github.com/go-kit/kit/sd"
)
var _ sd.Instancer = (*Instancer)(nil) // API check
func TestInstancer(t *testing.T) {
connection := &testConnection{
instances: []*fargo.Instance{instanceTest1, instanceTest2},
errApplication: nil,
}
instancer := NewInstancer(connection, appNameTest, loggerTest)
defer instancer.Stop()
state := instancer.state()
if state.Err != nil {
t.Fatal(state.Err)
}
if want, have := 2, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestInstancerReceivesUpdates(t *testing.T) {
connection := &testConnection{
instances: []*fargo.Instance{instanceTest1},
errApplication: nil,
}
instancer := NewInstancer(connection, appNameTest, loggerTest)
defer instancer.Stop()
verifyCount := func(want int) (have int, converged bool) {
const maxPollAttempts = 5
const delayPerAttempt = 200 * time.Millisecond
for i := 1; ; i++ {
state := instancer.state()
if have := len(state.Instances); want == have {
return have, true
} else if i == maxPollAttempts {
return have, false
}
time.Sleep(delayPerAttempt)
}
}
if have, converged := verifyCount(1); !converged {
t.Fatalf("initial: want %d, have %d", 1, have)
}
if err := connection.RegisterInstance(instanceTest2); err != nil {
t.Fatalf("failed to register an instance: %v", err)
}
if have, converged := verifyCount(2); !converged {
t.Fatalf("after registration: want %d, have %d", 2, have)
}
if err := connection.DeregisterInstance(instanceTest1); err != nil {
t.Fatalf("failed to unregister an instance: %v", err)
}
if have, converged := verifyCount(1); !converged {
t.Fatalf("after deregistration: want %d, have %d", 1, have)
}
}
func TestBadInstancerScheduleUpdates(t *testing.T) {
connection := &testConnection{
instances: []*fargo.Instance{instanceTest1},
errApplication: errTest,
}
instancer := NewInstancer(connection, appNameTest, loggerTest)
defer instancer.Stop()
state := instancer.state()
if state.Err == nil {
t.Fatal("expecting error")
}
if want, have := 0, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
================================================
FILE: sd/eureka/integration_test.go
================================================
//go:build integration
// +build integration
package eureka
import (
"os"
"testing"
"time"
"github.com/hudl/fargo"
"github.com/go-kit/log"
)
// Package sd/eureka provides a wrapper around the Netflix Eureka service
// registry by way of the Fargo library. This test assumes the user has an
// instance of Eureka available at the address in the environment variable.
// Example `${EUREKA_ADDR}` format: http://localhost:8761/eureka
//
// NOTE: when starting a Eureka server for integration testing, ensure
// the response cache interval is reduced to one second. This can be
// achieved with the following Java argument:
// `-Deureka.server.responseCacheUpdateIntervalMs=1000`
func TestIntegration(t *testing.T) {
eurekaAddr := os.Getenv("EUREKA_ADDR")
if eurekaAddr == "" {
t.Skip("EUREKA_ADDR is not set")
}
logger := log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestamp)
var fargoConfig fargo.Config
// Target Eureka server(s).
fargoConfig.Eureka.ServiceUrls = []string{eurekaAddr}
// How often the subscriber should poll for updates.
fargoConfig.Eureka.PollIntervalSeconds = 1
// Create a Fargo connection and a Eureka registrar.
fargoConnection := fargo.NewConnFromConfig(fargoConfig)
registrar1 := NewRegistrar(&fargoConnection, instanceTest1, log.With(logger, "component", "registrar1"))
// Register one instance.
registrar1.Register()
defer registrar1.Deregister()
// Build a Eureka instancer.
instancer := NewInstancer(
&fargoConnection,
appNameTest,
log.With(logger, "component", "instancer"),
)
defer instancer.Stop()
// checks every 100ms (fr up to 10s) for the expected number of instances to be reported
waitForInstances := func(count int) {
for t := 0; t < 100; t++ {
state := instancer.state()
if len(state.Instances) == count {
return
}
time.Sleep(100 * time.Millisecond)
}
state := instancer.state()
if state.Err != nil {
t.Error(state.Err)
}
if want, have := 1, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
// We should have one instance immediately after subscriber instantiation.
waitForInstances(1)
// Register a second instance
registrar2 := NewRegistrar(&fargoConnection, instanceTest2, log.With(logger, "component", "registrar2"))
registrar2.Register()
defer registrar2.Deregister() // In case of exceptional circumstances.
// This should be enough time for a scheduled update assuming Eureka is
// configured with the properties mentioned in the function comments.
waitForInstances(2)
// Deregister the second instance.
registrar2.Deregister()
// Wait for another scheduled update.
// And then there was one.
waitForInstances(1)
}
================================================
FILE: sd/eureka/registrar.go
================================================
package eureka
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/hudl/fargo"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
// Matches official Netflix Java client default.
const defaultRenewalInterval = 30 * time.Second
// The methods of fargo.Connection used in this package.
type fargoConnection interface {
RegisterInstance(instance *fargo.Instance) error
DeregisterInstance(instance *fargo.Instance) error
ReregisterInstance(instance *fargo.Instance) error
HeartBeatInstance(instance *fargo.Instance) error
ScheduleAppUpdates(name string, await bool, done <-chan struct{}) <-chan fargo.AppUpdate
GetApp(name string) (*fargo.Application, error)
}
type fargoUnsuccessfulHTTPResponse struct {
statusCode int
messagePrefix string
}
func (u *fargoUnsuccessfulHTTPResponse) Error() string {
return fmt.Sprintf("err=%s code=%d", u.messagePrefix, u.statusCode)
}
// Registrar maintains service instance liveness information in Eureka.
type Registrar struct {
conn fargoConnection
instance *fargo.Instance
logger log.Logger
quitc chan chan struct{}
sync.Mutex
}
var _ sd.Registrar = (*Registrar)(nil)
// NewRegistrar returns an Eureka Registrar acting on behalf of the provided
// Fargo connection and instance. See the integration test for usage examples.
func NewRegistrar(conn fargoConnection, instance *fargo.Instance, logger log.Logger) *Registrar {
return &Registrar{
conn: conn,
instance: instance,
logger: log.With(logger, "service", instance.App, "address", fmt.Sprintf("%s:%d", instance.IPAddr, instance.Port)),
}
}
// Register implements sd.Registrar.
func (r *Registrar) Register() {
r.Lock()
defer r.Unlock()
if r.quitc != nil {
return // Already in the registration loop.
}
if err := r.conn.RegisterInstance(r.instance); err != nil {
r.logger.Log("during", "Register", "err", err)
}
r.quitc = make(chan chan struct{})
go r.loop()
}
// Deregister implements sd.Registrar.
func (r *Registrar) Deregister() {
r.Lock()
defer r.Unlock()
if r.quitc == nil {
return // Already deregistered.
}
q := make(chan struct{})
r.quitc <- q
<-q
r.quitc = nil
}
func (r *Registrar) loop() {
var renewalInterval time.Duration
if r.instance.LeaseInfo.RenewalIntervalInSecs > 0 {
renewalInterval = time.Duration(r.instance.LeaseInfo.RenewalIntervalInSecs) * time.Second
} else {
renewalInterval = defaultRenewalInterval
}
ticker := time.NewTicker(renewalInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := r.heartbeat(); err != nil {
r.logger.Log("during", "heartbeat", "err", err)
}
case q := <-r.quitc:
if err := r.conn.DeregisterInstance(r.instance); err != nil {
r.logger.Log("during", "Deregister", "err", err)
}
close(q)
return
}
}
}
func httpResponseStatusCode(err error) (code int, present bool) {
if code, ok := fargo.HTTPResponseStatusCode(err); ok {
return code, true
}
// Allow injection of errors for testing.
if u, ok := err.(*fargoUnsuccessfulHTTPResponse); ok {
return u.statusCode, true
}
return 0, false
}
func isNotFound(err error) bool {
code, ok := httpResponseStatusCode(err)
return ok && code == http.StatusNotFound
}
func (r *Registrar) heartbeat() error {
err := r.conn.HeartBeatInstance(r.instance)
if err == nil {
return nil
}
if isNotFound(err) {
// Instance expired (e.g. network partition). Re-register.
return r.conn.ReregisterInstance(r.instance)
}
return err
}
================================================
FILE: sd/eureka/registrar_test.go
================================================
package eureka
import (
"testing"
"time"
)
func TestRegistrar(t *testing.T) {
connection := &testConnection{
errHeartbeat: errTest,
}
registrar1 := NewRegistrar(connection, instanceTest1, loggerTest)
registrar2 := NewRegistrar(connection, instanceTest2, loggerTest)
// Not registered.
registrar1.Deregister()
if want, have := 0, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Register.
registrar1.Register()
if want, have := 1, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
registrar2.Register()
if want, have := 2, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Deregister.
registrar1.Deregister()
if want, have := 1, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Already registered.
registrar1.Register()
if want, have := 2, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
registrar1.Register()
if want, have := 2, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Wait for a heartbeat failure.
time.Sleep(1010 * time.Millisecond)
if want, have := 2, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
registrar1.Deregister()
if want, have := 1, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestBadRegister(t *testing.T) {
connection := &testConnection{
errRegister: errTest,
}
registrar := NewRegistrar(connection, instanceTest1, loggerTest)
registrar.Register()
if want, have := 0, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestBadDeregister(t *testing.T) {
connection := &testConnection{
errDeregister: errTest,
}
registrar := NewRegistrar(connection, instanceTest1, loggerTest)
registrar.Register()
if want, have := 1, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
registrar.Deregister()
if want, have := 1, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestExpiredInstance(t *testing.T) {
connection := &testConnection{
errHeartbeat: errNotFound,
}
registrar := NewRegistrar(connection, instanceTest1, loggerTest)
registrar.Register()
// Wait for a heartbeat failure.
time.Sleep(1010 * time.Millisecond)
if want, have := 1, len(connection.instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
================================================
FILE: sd/eureka/util_test.go
================================================
package eureka
import (
"errors"
"fmt"
"reflect"
"sync"
"time"
"github.com/go-kit/log"
"github.com/hudl/fargo"
)
type testConnection struct {
mu sync.RWMutex
instances []*fargo.Instance
errApplication error
errHeartbeat error
errRegister error
errDeregister error
}
var (
errTest = errors.New("kaboom")
errNotFound = &fargoUnsuccessfulHTTPResponse{statusCode: 404, messagePrefix: "not found"}
loggerTest = log.NewNopLogger()
appNameTest = "go-kit"
instanceTest1 = &fargo.Instance{
HostName: "serveregistrar1.acme.org",
Port: 8080,
App: appNameTest,
IPAddr: "192.168.0.1",
VipAddress: "192.168.0.1",
SecureVipAddress: "192.168.0.1",
HealthCheckUrl: "http://serveregistrar1.acme.org:8080/healthz",
StatusPageUrl: "http://serveregistrar1.acme.org:8080/status",
HomePageUrl: "http://serveregistrar1.acme.org:8080/",
Status: fargo.UP,
DataCenterInfo: fargo.DataCenterInfo{Name: fargo.MyOwn},
LeaseInfo: fargo.LeaseInfo{RenewalIntervalInSecs: 1},
}
instanceTest2 = &fargo.Instance{
HostName: "serveregistrar2.acme.org",
Port: 8080,
App: appNameTest,
IPAddr: "192.168.0.2",
VipAddress: "192.168.0.2",
SecureVipAddress: "192.168.0.2",
HealthCheckUrl: "http://serveregistrar2.acme.org:8080/healthz",
StatusPageUrl: "http://serveregistrar2.acme.org:8080/status",
HomePageUrl: "http://serveregistrar2.acme.org:8080/",
Status: fargo.UP,
DataCenterInfo: fargo.DataCenterInfo{Name: fargo.MyOwn},
}
)
var _ fargoConnection = (*testConnection)(nil)
func (c *testConnection) RegisterInstance(i *fargo.Instance) error {
if c.errRegister != nil {
return c.errRegister
}
c.mu.Lock()
defer c.mu.Unlock()
for _, instance := range c.instances {
if reflect.DeepEqual(*instance, *i) {
return errors.New("already registered")
}
}
c.instances = append(c.instances, i)
return nil
}
func (c *testConnection) HeartBeatInstance(i *fargo.Instance) error {
return c.errHeartbeat
}
func (c *testConnection) DeregisterInstance(i *fargo.Instance) error {
if c.errDeregister != nil {
return c.errDeregister
}
c.mu.Lock()
defer c.mu.Unlock()
remaining := make([]*fargo.Instance, 0, len(c.instances))
for _, instance := range c.instances {
if reflect.DeepEqual(*instance, *i) {
continue
}
remaining = append(remaining, instance)
}
if len(remaining) == len(c.instances) {
return errors.New("not registered")
}
c.instances = remaining
return nil
}
func (c *testConnection) ReregisterInstance(ins *fargo.Instance) error {
return nil
}
func (c *testConnection) instancesForApplication(name string) []*fargo.Instance {
c.mu.RLock()
defer c.mu.RUnlock()
instances := make([]*fargo.Instance, 0, len(c.instances))
for _, i := range c.instances {
if i.App == name {
instances = append(instances, i)
}
}
return instances
}
func (c *testConnection) GetApp(name string) (*fargo.Application, error) {
if err := c.errApplication; err != nil {
return nil, err
}
instances := c.instancesForApplication(name)
if len(instances) == 0 {
return nil, fmt.Errorf("application not found for name=%s", name)
}
return &fargo.Application{Name: name, Instances: instances}, nil
}
func (c *testConnection) ScheduleAppUpdates(name string, await bool, done <-chan struct{}) <-chan fargo.AppUpdate {
updatec := make(chan fargo.AppUpdate, 1)
send := func() {
app, err := c.GetApp(name)
select {
case updatec <- fargo.AppUpdate{App: app, Err: err}:
default:
}
}
if await {
send()
}
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
for {
select {
case <-ticker.C:
send()
case <-done:
ticker.Stop()
return
}
}
}()
return updatec
}
================================================
FILE: sd/factory.go
================================================
package sd
import (
"io"
"github.com/go-kit/kit/endpoint"
)
// Factory is a function that converts an instance string (e.g. host:port) to a
// specific endpoint. Instances that provide multiple endpoints require multiple
// factories. A factory also returns an io.Closer that's invoked when the
// instance goes away and needs to be cleaned up. Factories may return nil
// closers.
//
// Users are expected to provide their own factory functions that assume
// specific transports, or can deduce transports by parsing the instance string.
type Factory func(instance string) (endpoint.Endpoint, io.Closer, error)
================================================
FILE: sd/instancer.go
================================================
package sd
// Event represents a push notification generated from the underlying service discovery
// implementation. It contains either a full set of available resource instances, or
// an error indicating some issue with obtaining information from discovery backend.
// Examples of errors may include loosing connection to the discovery backend, or
// trying to look up resource instances using an incorrectly formatted key.
// After receiving an Event with an error the listenter should treat previously discovered
// resource instances as stale (although it may choose to continue using them).
// If the Instancer is able to restore connection to the discovery backend it must push
// another Event with the current set of resource instances.
type Event struct {
Instances []string
Err error
}
// Instancer listens to a service discovery system and notifies registered
// observers of changes in the resource instances. Every event sent to the channels
// contains a complete set of instances known to the Instancer. That complete set is
// sent immediately upon registering the channel, and on any future updates from
// discovery system.
type Instancer interface {
Register(chan<- Event)
Deregister(chan<- Event)
Stop()
}
// FixedInstancer yields a fixed set of instances.
type FixedInstancer []string
// Register implements Instancer.
func (d FixedInstancer) Register(ch chan<- Event) { ch <- Event{Instances: d} }
// Deregister implements Instancer.
func (d FixedInstancer) Deregister(ch chan<- Event) {}
// Stop implements Instancer.
func (d FixedInstancer) Stop() {}
================================================
FILE: sd/internal/instance/cache.go
================================================
package instance
import (
"reflect"
"sort"
"sync"
"github.com/go-kit/kit/sd"
)
// Cache keeps track of resource instances provided to it via Update method
// and implements the Instancer interface
type Cache struct {
mtx sync.RWMutex
state sd.Event
reg registry
}
// NewCache creates a new Cache.
func NewCache() *Cache {
return &Cache{
reg: registry{},
}
}
// Update receives new instances from service discovery, stores them internally,
// and notifies all registered listeners.
func (c *Cache) Update(event sd.Event) {
c.mtx.Lock()
defer c.mtx.Unlock()
sort.Strings(event.Instances)
if reflect.DeepEqual(c.state, event) {
return // no need to broadcast the same instances
}
c.state = event
c.reg.broadcast(event)
}
// State returns the current state of discovery (instances or error) as sd.Event
func (c *Cache) State() sd.Event {
c.mtx.RLock()
event := c.state
c.mtx.RUnlock()
eventCopy := copyEvent(event)
return eventCopy
}
// Stop implements Instancer. Since the cache is just a plain-old store of data,
// Stop is a no-op.
func (c *Cache) Stop() {}
// Register implements Instancer.
func (c *Cache) Register(ch chan<- sd.Event) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.reg.register(ch)
event := c.state
eventCopy := copyEvent(event)
// always push the current state to new channels
ch <- eventCopy
}
// Deregister implements Instancer.
func (c *Cache) Deregister(ch chan<- sd.Event) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.reg.deregister(ch)
}
// registry is not goroutine-safe.
type registry map[chan<- sd.Event]struct{}
func (r registry) broadcast(event sd.Event) {
for c := range r {
eventCopy := copyEvent(event)
c <- eventCopy
}
}
func (r registry) register(c chan<- sd.Event) {
r[c] = struct{}{}
}
func (r registry) deregister(c chan<- sd.Event) {
delete(r, c)
}
// copyEvent does a deep copy on sd.Event
func copyEvent(e sd.Event) sd.Event {
// observers all need their own copy of event
// because they can directly modify event.Instances
// for example, by calling sort.Strings
if e.Instances == nil {
return e
}
instances := make([]string, len(e.Instances))
copy(instances, e.Instances)
e.Instances = instances
return e
}
================================================
FILE: sd/internal/instance/cache_test.go
================================================
package instance
import (
"context"
"fmt"
"io"
"reflect"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
var _ sd.Instancer = (*Cache)(nil) // API check
// The test verifies the following:
// registering causes initial notification of the current state
// instances are sorted
// different update causes new notification
// identical notifications cause no updates
// no updates after de-registering
func TestCache(t *testing.T) {
e1 := sd.Event{Instances: []string{"y", "x"}} // not sorted
e2 := sd.Event{Instances: []string{"c", "a", "b"}}
cache := NewCache()
if want, have := 0, len(cache.State().Instances); want != have {
t.Fatalf("want %v instances, have %v", want, have)
}
cache.Update(e1) // sets initial state
if want, have := 2, len(cache.State().Instances); want != have {
t.Fatalf("want %v instances, have %v", want, have)
}
r1 := make(chan sd.Event)
go cache.Register(r1)
expectUpdate(t, r1, []string{"x", "y"})
go cache.Update(e2) // different set
expectUpdate(t, r1, []string{"a", "b", "c"})
cache.Deregister(r1)
close(r1)
}
func expectUpdate(t *testing.T, r chan sd.Event, expect []string) {
select {
case e := <-r:
if want, have := expect, e.Instances; !reflect.DeepEqual(want, have) {
t.Fatalf("want: %v, have: %v", want, have)
}
case <-time.After(time.Second):
t.Fatalf("did not receive expected update %v", expect)
}
}
func TestRegistry(t *testing.T) {
reg := make(registry)
c1 := make(chan sd.Event, 1)
c2 := make(chan sd.Event, 1)
reg.register(c1)
reg.register(c2)
// validate that both channels receive the update
reg.broadcast(sd.Event{Instances: []string{"x", "y"}})
if want, have := []string{"x", "y"}, (<-c1).Instances; !reflect.DeepEqual(want, have) {
t.Fatalf("want: %v, have: %v", want, have)
}
if want, have := []string{"x", "y"}, (<-c2).Instances; !reflect.DeepEqual(want, have) {
t.Fatalf("want: %v, have: %v", want, have)
}
reg.deregister(c1)
reg.deregister(c2)
close(c1)
close(c2)
// if deregister didn't work, broadcast would panic on closed channels
reg.broadcast(sd.Event{Instances: []string{"x", "y"}})
}
// This test is meant to be run with the race detector enabled: -race.
// It ensures that every registered observer receives a copy
// of sd.Event.Instances because observers can directly modify the field.
// For example, endpointCache calls sort.Strings() on sd.Event.Instances.
func TestDataRace(t *testing.T) {
instances := make([]string, 0)
// the number of iterations here maters because we need sort.Strings to
// perform a Swap in doPivot -> medianOfThree to cause a data race.
for i := 1; i < 1000; i++ {
instances = append(instances, fmt.Sprintf("%v", i))
}
e1 := sd.Event{Instances: instances}
cache := NewCache()
cache.Update(e1)
nullEndpoint := func(_ context.Context, _ interface{}) (interface{}, error) {
return nil, nil
}
nullFactory := func(instance string) (endpoint.Endpoint, io.Closer, error) {
return nullEndpoint, nil, nil
}
logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error {
return nil
}))
sd.NewEndpointer(cache, nullFactory, logger)
sd.NewEndpointer(cache, nullFactory, logger)
}
================================================
FILE: sd/lb/balancer.go
================================================
package lb
import (
"errors"
"github.com/go-kit/kit/endpoint"
)
// Balancer yields endpoints according to some heuristic.
type Balancer interface {
Endpoint() (endpoint.Endpoint, error)
}
// ErrNoEndpoints is returned when no qualifying endpoints are available.
var ErrNoEndpoints = errors.New("no endpoints available")
================================================
FILE: sd/lb/doc.go
================================================
// Package lb implements the client-side load balancer pattern. When combined
// with a service discovery system of record, it enables a more decentralized
// architecture, removing the need for separate load balancers like HAProxy.
package lb
================================================
FILE: sd/lb/random.go
================================================
package lb
import (
"math/rand"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
)
// NewRandom returns a load balancer that selects services randomly.
func NewRandom(s sd.Endpointer, seed int64) Balancer {
return &random{
s: s,
r: rand.New(rand.NewSource(seed)),
}
}
type random struct {
s sd.Endpointer
r *rand.Rand
}
func (r *random) Endpoint() (endpoint.Endpoint, error) {
endpoints, err := r.s.Endpoints()
if err != nil {
return nil, err
}
if len(endpoints) <= 0 {
return nil, ErrNoEndpoints
}
return endpoints[r.r.Intn(len(endpoints))], nil
}
================================================
FILE: sd/lb/random_test.go
================================================
package lb
import (
"context"
"math"
"testing"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
)
func TestRandom(t *testing.T) {
var (
n = 7
endpoints = make([]endpoint.Endpoint, n)
counts = make([]int, n)
seed = int64(12345)
iterations = 1000000
want = iterations / n
tolerance = want / 100 // 1%
)
for i := 0; i < n; i++ {
i0 := i
endpoints[i] = func(context.Context, interface{}) (interface{}, error) { counts[i0]++; return struct{}{}, nil }
}
endpointer := sd.FixedEndpointer(endpoints)
balancer := NewRandom(endpointer, seed)
for i := 0; i < iterations; i++ {
endpoint, _ := balancer.Endpoint()
endpoint(context.Background(), struct{}{})
}
for i, have := range counts {
delta := int(math.Abs(float64(want - have)))
if delta > tolerance {
t.Errorf("%d: want %d, have %d, delta %d > %d tolerance", i, want, have, delta, tolerance)
}
}
}
func TestRandomNoEndpoints(t *testing.T) {
endpointer := sd.FixedEndpointer{}
balancer := NewRandom(endpointer, 1415926)
_, err := balancer.Endpoint()
if want, have := ErrNoEndpoints, err; want != have {
t.Errorf("want %v, have %v", want, have)
}
}
================================================
FILE: sd/lb/retry.go
================================================
package lb
import (
"context"
"fmt"
"strings"
"time"
"github.com/go-kit/kit/endpoint"
)
// RetryError is an error wrapper that is used by the retry mechanism. All
// errors returned by the retry mechanism via its endpoint will be RetryErrors.
type RetryError struct {
RawErrors []error // all errors encountered from endpoints directly
Final error // the final, terminating error
}
func (e RetryError) Error() string {
var suffix string
if len(e.RawErrors) > 1 {
a := make([]string, len(e.RawErrors)-1)
for i := 0; i < len(e.RawErrors)-1; i++ { // last one is Final
a[i] = e.RawErrors[i].Error()
}
suffix = fmt.Sprintf(" (previously: %s)", strings.Join(a, "; "))
}
return fmt.Sprintf("%v%s", e.Final, suffix)
}
// Callback is a function that is given the current attempt count and the error
// received from the underlying endpoint. It should return whether the Retry
// function should continue trying to get a working endpoint, and a custom error
// if desired. The error message may be nil, but a true/false is always
// expected. In all cases, if the replacement error is supplied, the received
// error will be replaced in the calling context.
type Callback func(n int, received error) (keepTrying bool, replacement error)
// Retry wraps a service load balancer and returns an endpoint oriented load
// balancer for the specified service method. Requests to the endpoint will be
// automatically load balanced via the load balancer. Requests that return
// errors will be retried until they succeed, up to max times, or until the
// timeout is elapsed, whichever comes first.
func Retry(max int, timeout time.Duration, b Balancer) endpoint.Endpoint {
return RetryWithCallback(timeout, b, maxRetries(max))
}
func maxRetries(max int) Callback {
return func(n int, err error) (keepTrying bool, replacement error) {
return n < max, nil
}
}
func alwaysRetry(int, error) (keepTrying bool, replacement error) {
return true, nil
}
// RetryWithCallback wraps a service load balancer and returns an endpoint
// oriented load balancer for the specified service method. Requests to the
// endpoint will be automatically load balanced via the load balancer. Requests
// that return errors will be retried until they succeed, up to max times, until
// the callback returns false, or until the timeout is elapsed, whichever comes
// first.
func RetryWithCallback(timeout time.Duration, b Balancer, cb Callback) endpoint.Endpoint {
if cb == nil {
cb = alwaysRetry
}
if b == nil {
panic("nil Balancer")
}
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
var (
newctx, cancel = context.WithTimeout(ctx, timeout)
responses = make(chan interface{}, 1)
errs = make(chan error, 1)
final RetryError
)
defer cancel()
for i := 1; ; i++ {
go func() {
e, err := b.Endpoint()
if err != nil {
errs <- err
return
}
response, err := e(newctx, request)
if err != nil {
errs <- err
return
}
responses <- response
}()
select {
case <-newctx.Done():
return nil, newctx.Err()
case response := <-responses:
return response, nil
case err := <-errs:
final.RawErrors = append(final.RawErrors, err)
keepTrying, replacement := cb(i, err)
if replacement != nil {
err = replacement
}
if !keepTrying {
final.Final = err
return nil, final
}
continue
}
}
}
}
================================================
FILE: sd/lb/retry_test.go
================================================
package lb_test
import (
"context"
"errors"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/lb"
)
func TestRetryMaxTotalFail(t *testing.T) {
var (
endpoints = sd.FixedEndpointer{} // no endpoints
rr = lb.NewRoundRobin(endpoints)
retry = lb.Retry(999, time.Second, rr) // lots of retries
ctx = context.Background()
)
if _, err := retry(ctx, struct{}{}); err == nil {
t.Errorf("expected error, got none") // should fail
}
}
func TestRetryMaxPartialFail(t *testing.T) {
var (
endpoints = []endpoint.Endpoint{
func(context.Context, interface{}) (interface{}, error) { return nil, errors.New("error one") },
func(context.Context, interface{}) (interface{}, error) { return nil, errors.New("error two") },
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil /* OK */ },
}
endpointer = sd.FixedEndpointer{
0: endpoints[0],
1: endpoints[1],
2: endpoints[2],
}
retries = len(endpoints) - 1 // not quite enough retries
rr = lb.NewRoundRobin(endpointer)
ctx = context.Background()
)
if _, err := lb.Retry(retries, time.Second, rr)(ctx, struct{}{}); err == nil {
t.Errorf("expected error two, got none")
}
}
func TestRetryMaxSuccess(t *testing.T) {
var (
endpoints = []endpoint.Endpoint{
func(context.Context, interface{}) (interface{}, error) { return nil, errors.New("error one") },
func(context.Context, interface{}) (interface{}, error) { return nil, errors.New("error two") },
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil /* OK */ },
}
endpointer = sd.FixedEndpointer{
0: endpoints[0],
1: endpoints[1],
2: endpoints[2],
}
retries = len(endpoints) // exactly enough retries
rr = lb.NewRoundRobin(endpointer)
ctx = context.Background()
)
if _, err := lb.Retry(retries, time.Second, rr)(ctx, struct{}{}); err != nil {
t.Error(err)
}
}
func TestRetryTimeout(t *testing.T) {
var (
step = make(chan struct{})
e = func(context.Context, interface{}) (interface{}, error) { <-step; return struct{}{}, nil }
timeout = time.Millisecond
retry = lb.Retry(999, timeout, lb.NewRoundRobin(sd.FixedEndpointer{0: e}))
errs = make(chan error, 1)
invoke = func() { _, err := retry(context.Background(), struct{}{}); errs <- err }
)
go func() { step <- struct{}{} }() // queue up a flush of the endpoint
invoke() // invoke the endpoint and trigger the flush
if err := <-errs; err != nil { // that should succeed
t.Error(err)
}
go func() { time.Sleep(10 * timeout); step <- struct{}{} }() // a delayed flush
invoke() // invoke the endpoint
if err := <-errs; err != context.DeadlineExceeded { // that should not succeed
t.Errorf("wanted %v, got none", context.DeadlineExceeded)
}
}
func TestAbortEarlyCustomMessage(t *testing.T) {
var (
myErr = errors.New("aborting early")
cb = func(int, error) (bool, error) { return false, myErr }
endpoints = sd.FixedEndpointer{} // no endpoints
rr = lb.NewRoundRobin(endpoints)
retry = lb.RetryWithCallback(time.Second, rr, cb) // lots of retries
ctx = context.Background()
)
_, err := retry(ctx, struct{}{})
if want, have := myErr, err.(lb.RetryError).Final; want != have {
t.Errorf("want %v, have %v", want, have)
}
}
func TestErrorPassedUnchangedToCallback(t *testing.T) {
var (
myErr = errors.New("my custom error")
cb = func(_ int, err error) (bool, error) {
if want, have := myErr, err; want != have {
t.Errorf("want %v, have %v", want, have)
}
return false, nil
}
endpoint = func(ctx context.Context, request interface{}) (interface{}, error) {
return nil, myErr
}
endpoints = sd.FixedEndpointer{endpoint} // no endpoints
rr = lb.NewRoundRobin(endpoints)
retry = lb.RetryWithCallback(time.Second, rr, cb) // lots of retries
ctx = context.Background()
)
_, err := retry(ctx, struct{}{})
if want, have := myErr, err.(lb.RetryError).Final; want != have {
t.Errorf("want %v, have %v", want, have)
}
}
func TestHandleNilCallback(t *testing.T) {
var (
endpointer = sd.FixedEndpointer{
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil /* OK */ },
}
rr = lb.NewRoundRobin(endpointer)
ctx = context.Background()
)
retry := lb.RetryWithCallback(time.Second, rr, nil)
if _, err := retry(ctx, struct{}{}); err != nil {
t.Error(err)
}
}
================================================
FILE: sd/lb/round_robin.go
================================================
package lb
import (
"sync/atomic"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
)
// NewRoundRobin returns a load balancer that returns services in sequence.
func NewRoundRobin(s sd.Endpointer) Balancer {
return &roundRobin{
s: s,
c: 0,
}
}
type roundRobin struct {
s sd.Endpointer
c uint64
}
func (rr *roundRobin) Endpoint() (endpoint.Endpoint, error) {
endpoints, err := rr.s.Endpoints()
if err != nil {
return nil, err
}
if len(endpoints) <= 0 {
return nil, ErrNoEndpoints
}
old := atomic.AddUint64(&rr.c, 1) - 1
idx := old % uint64(len(endpoints))
return endpoints[idx], nil
}
================================================
FILE: sd/lb/round_robin_test.go
================================================
package lb
import (
"context"
"reflect"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
)
func TestRoundRobin(t *testing.T) {
var (
counts = []int{0, 0, 0}
endpoints = []endpoint.Endpoint{
func(context.Context, interface{}) (interface{}, error) { counts[0]++; return struct{}{}, nil },
func(context.Context, interface{}) (interface{}, error) { counts[1]++; return struct{}{}, nil },
func(context.Context, interface{}) (interface{}, error) { counts[2]++; return struct{}{}, nil },
}
)
endpointer := sd.FixedEndpointer(endpoints)
balancer := NewRoundRobin(endpointer)
for i, want := range [][]int{
{1, 0, 0},
{1, 1, 0},
{1, 1, 1},
{2, 1, 1},
{2, 2, 1},
{2, 2, 2},
{3, 2, 2},
} {
endpoint, err := balancer.Endpoint()
if err != nil {
t.Fatal(err)
}
endpoint(context.Background(), struct{}{})
if have := counts; !reflect.DeepEqual(want, have) {
t.Fatalf("%d: want %v, have %v", i, want, have)
}
}
}
func TestRoundRobinNoEndpoints(t *testing.T) {
endpointer := sd.FixedEndpointer{}
balancer := NewRoundRobin(endpointer)
_, err := balancer.Endpoint()
if want, have := ErrNoEndpoints, err; want != have {
t.Errorf("want %v, have %v", want, have)
}
}
func TestRoundRobinNoRace(t *testing.T) {
balancer := NewRoundRobin(sd.FixedEndpointer([]endpoint.Endpoint{
endpoint.Nop,
endpoint.Nop,
endpoint.Nop,
endpoint.Nop,
endpoint.Nop,
}))
var (
n = 100
done = make(chan struct{})
wg sync.WaitGroup
count uint64
)
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
_, _ = balancer.Endpoint()
atomic.AddUint64(&count, 1)
}
}
}()
}
time.Sleep(time.Second)
close(done)
wg.Wait()
t.Logf("made %d calls", atomic.LoadUint64(&count))
}
================================================
FILE: sd/registrar.go
================================================
package sd
// Registrar registers instance information to a service discovery system when
// an instance becomes alive and healthy, and deregisters that information when
// the service becomes unhealthy or goes away.
//
// Registrar implementations exist for various service discovery systems. Note
// that identifying instance information (e.g. host:port) must be given via the
// concrete constructor; this interface merely signals lifecycle changes.
type Registrar interface {
Register()
Deregister()
}
================================================
FILE: sd/zk/client.go
================================================
package zk
import (
"errors"
"net"
"strings"
"time"
"github.com/go-zookeeper/zk"
"github.com/go-kit/log"
)
// DefaultACL is the default ACL to use for creating znodes.
var (
DefaultACL = zk.WorldACL(zk.PermAll)
ErrInvalidCredentials = errors.New("invalid credentials provided")
ErrClientClosed = errors.New("client service closed")
ErrNotRegistered = errors.New("not registered")
ErrNodeNotFound = errors.New("node not found")
)
const (
// DefaultConnectTimeout is the default timeout to establish a connection to
// a ZooKeeper node.
DefaultConnectTimeout = 2 * time.Second
// DefaultSessionTimeout is the default timeout to keep the current
// ZooKeeper session alive during a temporary disconnect.
DefaultSessionTimeout = 5 * time.Second
)
// Client is a wrapper around a lower level ZooKeeper client implementation.
type Client interface {
// GetEntries should query the provided path in ZooKeeper, place a watch on
// it and retrieve data from its current child nodes.
GetEntries(path string) ([]string, <-chan zk.Event, error)
// CreateParentNodes should try to create the path in case it does not exist
// yet on ZooKeeper.
CreateParentNodes(path string) error
// Register a service with ZooKeeper.
Register(s *Service) error
// Deregister a service with ZooKeeper.
Deregister(s *Service) error
// Stop should properly shutdown the client implementation
Stop()
}
type clientConfig struct {
logger log.Logger
acl []zk.ACL
credentials []byte
connectTimeout time.Duration
sessionTimeout time.Duration
rootNodePayload [][]byte
eventHandler func(zk.Event)
}
// Option functions enable friendly APIs.
type Option func(*clientConfig) error
type client struct {
*zk.Conn
clientConfig
active bool
quit chan struct{}
}
// ACL returns an Option specifying a non-default ACL for creating parent nodes.
func ACL(acl []zk.ACL) Option {
return func(c *clientConfig) error {
c.acl = acl
return nil
}
}
// Credentials returns an Option specifying a user/password combination which
// the client will use to authenticate itself with.
func Credentials(user, pass string) Option {
return func(c *clientConfig) error {
if user == "" || pass == "" {
return ErrInvalidCredentials
}
c.credentials = []byte(user + ":" + pass)
return nil
}
}
// ConnectTimeout returns an Option specifying a non-default connection timeout
// when we try to establish a connection to a ZooKeeper server.
func ConnectTimeout(t time.Duration) Option {
return func(c *clientConfig) error {
if t.Seconds() < 1 {
return errors.New("invalid connect timeout (minimum value is 1 second)")
}
c.connectTimeout = t
return nil
}
}
// SessionTimeout returns an Option specifying a non-default session timeout.
func SessionTimeout(t time.Duration) Option {
return func(c *clientConfig) error {
if t.Seconds() < 1 {
return errors.New("invalid session timeout (minimum value is 1 second)")
}
c.sessionTimeout = t
return nil
}
}
// Payload returns an Option specifying non-default data values for each znode
// created by CreateParentNodes.
func Payload(payload [][]byte) Option {
return func(c *clientConfig) error {
c.rootNodePayload = payload
return nil
}
}
// EventHandler returns an Option specifying a callback function to handle
// incoming zk.Event payloads (ZooKeeper connection events).
func EventHandler(handler func(zk.Event)) Option {
return func(c *clientConfig) error {
c.eventHandler = handler
return nil
}
}
// NewClient returns a ZooKeeper client with a connection to the server cluster.
// It will return an error if the server cluster cannot be resolved.
func NewClient(servers []string, logger log.Logger, options ...Option) (Client, error) {
defaultEventHandler := func(event zk.Event) {
logger.Log("eventtype", event.Type.String(), "server", event.Server, "state", event.State.String(), "err", event.Err)
}
config := clientConfig{
acl: DefaultACL,
connectTimeout: DefaultConnectTimeout,
sessionTimeout: DefaultSessionTimeout,
eventHandler: defaultEventHandler,
logger: logger,
}
for _, option := range options {
if err := option(&config); err != nil {
return nil, err
}
}
// dialer overrides the default ZooKeeper library Dialer so we can configure
// the connectTimeout. The current library has a hardcoded value of 1 second
// and there are reports of race conditions, due to slow DNS resolvers and
// other network latency issues.
dialer := func(network, address string, _ time.Duration) (net.Conn, error) {
return net.DialTimeout(network, address, config.connectTimeout)
}
conn, eventc, err := zk.Connect(servers, config.sessionTimeout, withLogger(logger), zk.WithDialer(dialer))
if err != nil {
return nil, err
}
if len(config.credentials) > 0 {
err = conn.AddAuth("digest", config.credentials)
if err != nil {
return nil, err
}
}
c := &client{conn, config, true, make(chan struct{})}
// Start listening for incoming Event payloads and callback the set
// eventHandler.
go func() {
for {
select {
case event := <-eventc:
config.eventHandler(event)
case <-c.quit:
return
}
}
}()
return c, nil
}
// CreateParentNodes implements the ZooKeeper Client interface.
func (c *client) CreateParentNodes(path string) error {
if !c.active {
return ErrClientClosed
}
if path[0] != '/' {
return zk.ErrInvalidPath
}
payload := []byte("")
pathString := ""
pathNodes := strings.Split(path, "/")
for i := 1; i < len(pathNodes); i++ {
if i <= len(c.rootNodePayload) {
payload = c.rootNodePayload[i-1]
} else {
payload = []byte("")
}
pathString += "/" + pathNodes[i]
_, err := c.Create(pathString, payload, 0, c.acl)
// not being able to create the node because it exists or not having
// sufficient rights is not an issue. It is ok for the node to already
// exist and/or us to only have read rights
if err != nil && err != zk.ErrNodeExists && err != zk.ErrNoAuth {
return err
}
}
return nil
}
// GetEntries implements the ZooKeeper Client interface.
func (c *client) GetEntries(path string) ([]string, <-chan zk.Event, error) {
// retrieve list of child nodes for given path and add watch to path
znodes, _, eventc, err := c.ChildrenW(path)
if err != nil {
return nil, eventc, err
}
var resp []string
for _, znode := range znodes {
// retrieve payload for child znode and add to response array
if data, _, err := c.Get(path + "/" + znode); err == nil {
resp = append(resp, string(data))
}
}
return resp, eventc, nil
}
// Register implements the ZooKeeper Client interface.
func (c *client) Register(s *Service) error {
if s.Path[len(s.Path)-1] != '/' {
s.Path += "/"
}
path := s.Path + s.Name
if err := c.CreateParentNodes(path); err != nil {
return err
}
if path[len(path)-1] != '/' {
path += "/"
}
node, err := c.CreateProtectedEphemeralSequential(path, s.Data, c.acl)
if err != nil {
return err
}
s.node = node
return nil
}
// Deregister implements the ZooKeeper Client interface.
func (c *client) Deregister(s *Service) error {
if s.node == "" {
return ErrNotRegistered
}
path := s.Path + s.Name
found, stat, err := c.Exists(path)
if err != nil {
return err
}
if !found {
return ErrNodeNotFound
}
if err := c.Delete(path, stat.Version); err != nil {
return err
}
return nil
}
// Stop implements the ZooKeeper Client interface.
func (c *client) Stop() {
c.active = false
close(c.quit)
c.Close()
}
================================================
FILE: sd/zk/client_test.go
================================================
package zk
import (
"bytes"
"testing"
"time"
stdzk "github.com/go-zookeeper/zk"
"github.com/go-kit/log"
)
func TestNewClient(t *testing.T) {
var (
acl = stdzk.WorldACL(stdzk.PermRead)
connectTimeout = 3 * time.Second
sessionTimeout = 20 * time.Second
payload = [][]byte{[]byte("Payload"), []byte("Test")}
)
c, err := NewClient(
[]string{"FailThisInvalidHost!!!"},
log.NewNopLogger(),
)
if err == nil {
t.Errorf("expected error, got nil")
}
hasFired := false
calledEventHandler := make(chan struct{})
eventHandler := func(event stdzk.Event) {
if !hasFired {
// test is successful if this function has fired at least once
hasFired = true
close(calledEventHandler)
}
}
c, err = NewClient(
[]string{"localhost"},
log.NewNopLogger(),
ACL(acl),
ConnectTimeout(connectTimeout),
SessionTimeout(sessionTimeout),
Payload(payload),
EventHandler(eventHandler),
)
if err != nil {
t.Fatal(err)
}
defer c.Stop()
clientImpl, ok := c.(*client)
if !ok {
t.Fatal("retrieved incorrect Client implementation")
}
if want, have := acl, clientImpl.acl; want[0] != have[0] {
t.Errorf("want %+v, have %+v", want, have)
}
if want, have := connectTimeout, clientImpl.connectTimeout; want != have {
t.Errorf("want %d, have %d", want, have)
}
if want, have := sessionTimeout, clientImpl.sessionTimeout; want != have {
t.Errorf("want %d, have %d", want, have)
}
if want, have := payload, clientImpl.rootNodePayload; !bytes.Equal(want[0], have[0]) || !bytes.Equal(want[1], have[1]) {
t.Errorf("want %s, have %s", want, have)
}
select {
case <-calledEventHandler:
case <-time.After(100 * time.Millisecond):
t.Errorf("event handler never called")
}
}
func TestOptions(t *testing.T) {
_, err := NewClient([]string{"localhost"}, log.NewNopLogger(), Credentials("valid", "credentials"))
if err != nil && err != stdzk.ErrNoServer {
t.Errorf("unexpected error: %v", err)
}
_, err = NewClient([]string{"localhost"}, log.NewNopLogger(), Credentials("nopass", ""))
if want, have := err, ErrInvalidCredentials; want != have {
t.Errorf("want %v, have %v", want, have)
}
_, err = NewClient([]string{"localhost"}, log.NewNopLogger(), ConnectTimeout(0))
if err == nil {
t.Errorf("expected connect timeout error")
}
_, err = NewClient([]string{"localhost"}, log.NewNopLogger(), SessionTimeout(0))
if err == nil {
t.Errorf("expected connect timeout error")
}
}
func TestCreateParentNodes(t *testing.T) {
payload := [][]byte{[]byte("Payload"), []byte("Test")}
c, err := NewClient([]string{"localhost:65500"}, log.NewNopLogger())
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected new Client, got nil")
}
s, err := NewInstancer(c, "/validpath", log.NewNopLogger())
if err != stdzk.ErrNoServer {
t.Errorf("unexpected error: %v", err)
}
if s != nil {
t.Error("expected failed new Instancer")
}
s, err = NewInstancer(c, "invalidpath", log.NewNopLogger())
if err != stdzk.ErrInvalidPath {
t.Errorf("unexpected error: %v", err)
}
_, _, err = c.GetEntries("/validpath")
if err != stdzk.ErrNoServer {
t.Errorf("unexpected error: %v", err)
}
c.Stop()
err = c.CreateParentNodes("/validpath")
if err != ErrClientClosed {
t.Errorf("unexpected error: %v", err)
}
s, err = NewInstancer(c, "/validpath", log.NewNopLogger())
if err != ErrClientClosed {
t.Errorf("unexpected error: %v", err)
}
if s != nil {
t.Error("expected failed new Instancer")
}
c, err = NewClient([]string{"localhost:65500"}, log.NewNopLogger(), Payload(payload))
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if c == nil {
t.Fatal("expected new Client, got nil")
}
s, err = NewInstancer(c, "/validpath", log.NewNopLogger())
if err != stdzk.ErrNoServer {
t.Errorf("unexpected error: %v", err)
}
if s != nil {
t.Error("expected failed new Instancer")
}
}
================================================
FILE: sd/zk/doc.go
================================================
// Package zk provides Instancer and Registrar implementations for ZooKeeper.
package zk
================================================
FILE: sd/zk/instancer.go
================================================
package zk
import (
"github.com/go-zookeeper/zk"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/internal/instance"
"github.com/go-kit/log"
)
// Instancer yield instances stored in a certain ZooKeeper path. Any kind of
// change in that path is watched and will update the subscribers.
type Instancer struct {
cache *instance.Cache
client Client
path string
logger log.Logger
quitc chan struct{}
}
// NewInstancer returns a ZooKeeper Instancer. ZooKeeper will start watching
// the given path for changes and update the Instancer endpoints.
func NewInstancer(c Client, path string, logger log.Logger) (*Instancer, error) {
s := &Instancer{
cache: instance.NewCache(),
client: c,
path: path,
logger: logger,
quitc: make(chan struct{}),
}
err := s.client.CreateParentNodes(s.path)
if err != nil {
return nil, err
}
instances, eventc, err := s.client.GetEntries(s.path)
if err != nil {
logger.Log("path", s.path, "msg", "failed to retrieve entries", "err", err)
// other implementations continue here, but we exit because we don't know if eventc is valid
return nil, err
}
logger.Log("path", s.path, "instances", len(instances))
s.cache.Update(sd.Event{Instances: instances})
go s.loop(eventc)
return s, nil
}
func (s *Instancer) loop(eventc <-chan zk.Event) {
var (
instances []string
err error
)
for {
select {
case <-eventc:
// We received a path update notification. Call GetEntries to
// retrieve child node data, and set a new watch, as ZK watches are
// one-time triggers.
instances, eventc, err = s.client.GetEntries(s.path)
if err != nil {
s.logger.Log("path", s.path, "msg", "failed to retrieve entries", "err", err)
s.cache.Update(sd.Event{Err: err})
continue
}
s.logger.Log("path", s.path, "instances", len(instances))
s.cache.Update(sd.Event{Instances: instances})
case <-s.quitc:
return
}
}
}
// Stop terminates the Instancer.
func (s *Instancer) Stop() {
close(s.quitc)
}
// Register implements Instancer.
func (s *Instancer) Register(ch chan<- sd.Event) {
s.cache.Register(ch)
}
// Deregister implements Instancer.
func (s *Instancer) Deregister(ch chan<- sd.Event) {
s.cache.Deregister(ch)
}
// state returns the current state of instance.Cache, only for testing
func (s *Instancer) state() sd.Event {
return s.cache.State()
}
================================================
FILE: sd/zk/instancer_test.go
================================================
package zk
import (
"testing"
"time"
"github.com/go-kit/kit/sd"
)
var _ sd.Instancer = (*Instancer)(nil) // API check
func TestInstancer(t *testing.T) {
client := newFakeClient()
instancer, err := NewInstancer(client, path, logger)
if err != nil {
t.Fatalf("failed to create new Instancer: %v", err)
}
defer instancer.Stop()
endpointer := sd.NewEndpointer(instancer, newFactory(""), logger)
if _, err := endpointer.Endpoints(); err != nil {
t.Fatal(err)
}
}
func TestBadFactory(t *testing.T) {
client := newFakeClient()
instancer, err := NewInstancer(client, path, logger)
if err != nil {
t.Fatalf("failed to create new Instancer: %v", err)
}
defer instancer.Stop()
endpointer := sd.NewEndpointer(instancer, newFactory("kaboom"), logger)
// instance1 came online
client.AddService(path+"/instance1", "kaboom")
// instance2 came online
client.AddService(path+"/instance2", "zookeeper_node_data")
if err = asyncTest(100*time.Millisecond, 1, endpointer); err != nil {
t.Error(err)
}
}
func TestServiceUpdate(t *testing.T) {
client := newFakeClient()
instancer, err := NewInstancer(client, path, logger)
if err != nil {
t.Fatalf("failed to create new Instancer: %v", err)
}
defer instancer.Stop()
endpointer := sd.NewEndpointer(instancer, newFactory(""), logger)
endpoints, err := endpointer.Endpoints()
if err != nil {
t.Fatal(err)
}
if want, have := 0, len(endpoints); want != have {
t.Errorf("want %d, have %d", want, have)
}
// instance1 came online
client.AddService(path+"/instance1", "zookeeper_node_data1")
// instance2 came online
client.AddService(path+"/instance2", "zookeeper_node_data2")
// we should have 2 instances
if err = asyncTest(100*time.Millisecond, 2, endpointer); err != nil {
t.Error(err)
}
// TODO(pb): this bit is flaky
//
//// watch triggers an error...
//client.SendErrorOnWatch()
//
//// test if error was consumed
//if err = client.ErrorIsConsumedWithin(100 * time.Millisecond); err != nil {
// t.Error(err)
//}
// instance3 came online
client.AddService(path+"/instance3", "zookeeper_node_data3")
// we should have 3 instances
if err = asyncTest(100*time.Millisecond, 3, endpointer); err != nil {
t.Error(err)
}
// instance1 goes offline
client.RemoveService(path + "/instance1")
// instance2 goes offline
client.RemoveService(path + "/instance2")
// we should have 1 instance
if err = asyncTest(100*time.Millisecond, 1, endpointer); err != nil {
t.Error(err)
}
}
func TestBadInstancerCreate(t *testing.T) {
client := newFakeClient()
client.SendErrorOnWatch()
instancer, err := NewInstancer(client, path, logger)
if err == nil {
t.Error("expected error on new Instancer")
}
if instancer != nil {
t.Error("expected Instancer not to be created")
}
instancer, err = NewInstancer(client, "BadPath", logger)
if err == nil {
t.Error("expected error on new Instancer")
}
if instancer != nil {
t.Error("expected Instancer not to be created")
}
}
================================================
FILE: sd/zk/integration_test.go
================================================
// +build integration
package zk
import (
"bytes"
"os"
"testing"
"time"
stdzk "github.com/go-zookeeper/zk"
)
var (
host []string
)
func TestMain(m *testing.M) {
zkAddr := os.Getenv("ZK_ADDR")
if zkAddr != "" {
host = []string{zkAddr}
}
m.Run()
}
func TestCreateParentNodesOnServer(t *testing.T) {
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
payload := [][]byte{[]byte("Payload"), []byte("Test")}
c1, err := NewClient(host, logger, Payload(payload))
if err != nil {
t.Fatalf("Connect returned error: %v", err)
}
if c1 == nil {
t.Fatal("Expected pointer to client, got nil")
}
defer c1.Stop()
instancer, err := NewInstancer(c1, path, logger)
if err != nil {
t.Fatalf("Unable to create Subscriber: %v", err)
}
defer instancer.Stop()
state := instancer.state()
if state.Err != nil {
t.Fatal(err)
}
if want, have := 0, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
c2, err := NewClient(host, logger)
if err != nil {
t.Fatalf("Connect returned error: %v", err)
}
defer c2.Stop()
data, _, err := c2.(*client).Get(path)
if err != nil {
t.Fatal(err)
}
// test Client implementation of CreateParentNodes. It should have created
// our payload
if bytes.Compare(data, payload[1]) != 0 {
t.Errorf("want %s, have %s", payload[1], data)
}
}
func TestCreateBadParentNodesOnServer(t *testing.T) {
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
c, _ := NewClient(host, logger)
defer c.Stop()
_, err := NewInstancer(c, "invalid/path", logger)
if want, have := stdzk.ErrInvalidPath, err; want != have {
t.Errorf("want %v, have %v", want, have)
}
}
func TestCredentials1(t *testing.T) {
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
acl := stdzk.DigestACL(stdzk.PermAll, "user", "secret")
c, _ := NewClient(host, logger, ACL(acl), Credentials("user", "secret"))
defer c.Stop()
_, err := NewInstancer(c, "/acl-issue-test", logger)
if err != nil {
t.Fatal(err)
}
}
func TestCredentials2(t *testing.T) {
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
acl := stdzk.DigestACL(stdzk.PermAll, "user", "secret")
c, _ := NewClient(host, logger, ACL(acl))
defer c.Stop()
_, err := NewInstancer(c, "/acl-issue-test", logger)
if err != stdzk.ErrNoAuth {
t.Errorf("want %v, have %v", stdzk.ErrNoAuth, err)
}
}
func TestConnection(t *testing.T) {
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
c, _ := NewClient(host, logger)
c.Stop()
_, err := NewInstancer(c, "/acl-issue-test", logger)
if err != ErrClientClosed {
t.Errorf("want %v, have %v", ErrClientClosed, err)
}
}
func TestGetEntriesOnServer(t *testing.T) {
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
var instancePayload = "10.0.3.204:8002"
c1, err := NewClient(host, logger)
if err != nil {
t.Fatalf("Connect returned error: %v", err)
}
defer c1.Stop()
c2, err := NewClient(host, logger)
s, err := NewInstancer(c2, path, logger)
if err != nil {
t.Fatal(err)
}
defer c2.Stop()
instance1 := &Service{
Path: path,
Name: "instance1",
Data: []byte(instancePayload),
}
if err = c2.Register(instance1); err != nil {
t.Fatalf("Unable to create test ephemeral znode 1: %+v", err)
}
instance2 := &Service{
Path: path,
Name: "instance2",
Data: []byte(instancePayload),
}
if err = c2.Register(instance2); err != nil {
t.Fatalf("Unable to create test ephemeral znode 2: %+v", err)
}
time.Sleep(50 * time.Millisecond)
state := s.state()
if state.Err != nil {
t.Fatal(state.Err)
}
if want, have := 2, len(state.Instances); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestGetEntriesPayloadOnServer(t *testing.T) {
t.Skip("FLAKY")
if len(host) == 0 {
t.Skip("ZK_ADDR not set; skipping integration test")
}
c, err := NewClient(host, logger)
if err != nil {
t.Fatalf("Connect returned error: %v", err)
}
_, eventc, err := c.GetEntries(path)
if err != nil {
t.Fatal(err)
}
instance3 := Service{
Path: path,
Name: "instance3",
Data: []byte("just some payload"),
}
registrar := NewRegistrar(c, instance3, logger)
registrar.Register()
select {
case event := <-eventc:
if want, have := stdzk.EventNodeChildrenChanged.String(), event.Type.String(); want != have {
t.Errorf("want %s, have %s", want, have)
}
case <-time.After(10 * time.Second):
t.Errorf("expected incoming watch event, timeout occurred")
}
_, eventc, err = c.GetEntries(path)
if err != nil {
t.Fatal(err)
}
registrar.Deregister()
select {
case event := <-eventc:
if want, have := stdzk.EventNodeChildrenChanged.String(), event.Type.String(); want != have {
t.Errorf("want %s, have %s", want, have)
}
case <-time.After(100 * time.Millisecond):
t.Errorf("expected incoming watch event, timeout occurred")
}
}
================================================
FILE: sd/zk/logwrapper.go
================================================
package zk
import (
"fmt"
"github.com/go-zookeeper/zk"
"github.com/go-kit/log"
)
// wrapLogger wraps a Go kit logger so we can use it as the logging service for
// the ZooKeeper library, which expects a Printf method to be available.
type wrapLogger struct {
log.Logger
}
func (logger wrapLogger) Printf(format string, args ...interface{}) {
logger.Log("msg", fmt.Sprintf(format, args...))
}
// withLogger replaces the ZooKeeper library's default logging service with our
// own Go kit logger.
func withLogger(logger log.Logger) func(c *zk.Conn) {
return func(c *zk.Conn) {
c.SetLogger(wrapLogger{logger})
}
}
================================================
FILE: sd/zk/registrar.go
================================================
package zk
import "github.com/go-kit/log"
// Registrar registers service instance liveness information to ZooKeeper.
type Registrar struct {
client Client
service Service
logger log.Logger
}
// Service holds the root path, service name and instance identifying data you
// want to publish to ZooKeeper.
type Service struct {
Path string // discovery namespace, example: /myorganization/myplatform/
Name string // service name, example: addscv
Data []byte // instance data to store for discovery, example: 10.0.2.10:80
node string // Client will record the ephemeral node name so we can deregister
}
// NewRegistrar returns a ZooKeeper Registrar acting on the provided catalog
// registration.
func NewRegistrar(client Client, service Service, logger log.Logger) *Registrar {
return &Registrar{
client: client,
service: service,
logger: log.With(logger,
"service", service.Name,
"path", service.Path,
"data", string(service.Data),
),
}
}
// Register implements sd.Registrar interface.
func (r *Registrar) Register() {
if err := r.client.Register(&r.service); err != nil {
r.logger.Log("err", err)
} else {
r.logger.Log("action", "register")
}
}
// Deregister implements sd.Registrar interface.
func (r *Registrar) Deregister() {
if err := r.client.Deregister(&r.service); err != nil {
r.logger.Log("err", err)
} else {
r.logger.Log("action", "deregister")
}
}
================================================
FILE: sd/zk/util_test.go
================================================
package zk
import (
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/go-zookeeper/zk"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/log"
)
var (
path = "/gokit.test/service.name"
logger = log.NewNopLogger()
)
type fakeClient struct {
mtx sync.Mutex
ch chan zk.Event
responses map[string]string
result bool
}
func newFakeClient() *fakeClient {
return &fakeClient{
ch: make(chan zk.Event, 1),
responses: make(map[string]string),
result: true,
}
}
func (c *fakeClient) CreateParentNodes(path string) error {
if path == "BadPath" {
return errors.New("dummy error")
}
return nil
}
func (c *fakeClient) GetEntries(path string) ([]string, <-chan zk.Event, error) {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.result == false {
c.result = true
return []string{}, c.ch, errors.New("dummy error")
}
responses := []string{}
for _, data := range c.responses {
responses = append(responses, data)
}
return responses, c.ch, nil
}
func (c *fakeClient) AddService(node, data string) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.responses[node] = data
c.ch <- zk.Event{}
}
func (c *fakeClient) RemoveService(node string) {
c.mtx.Lock()
defer c.mtx.Unlock()
delete(c.responses, node)
c.ch <- zk.Event{}
}
func (c *fakeClient) Register(s *Service) error {
return nil
}
func (c *fakeClient) Deregister(s *Service) error {
return nil
}
func (c *fakeClient) SendErrorOnWatch() {
c.mtx.Lock()
defer c.mtx.Unlock()
c.result = false
c.ch <- zk.Event{}
}
func (c *fakeClient) ErrorIsConsumedWithin(timeout time.Duration) error {
t := time.After(timeout)
for {
select {
case <-t:
return fmt.Errorf("expected error not consumed after timeout %s", timeout)
default:
c.mtx.Lock()
if c.result == false {
c.mtx.Unlock()
return nil
}
c.mtx.Unlock()
}
}
}
func (c *fakeClient) Stop() {}
func newFactory(fakeError string) sd.Factory {
return func(instance string) (endpoint.Endpoint, io.Closer, error) {
if fakeError == instance {
return nil, nil, errors.New(fakeError)
}
return endpoint.Nop, nil, nil
}
}
func asyncTest(timeout time.Duration, want int, s sd.Endpointer) (err error) {
var endpoints []endpoint.Endpoint
have := -1 // want can never be <0
t := time.After(timeout)
for {
select {
case <-t:
return fmt.Errorf("want %d, have %d (timeout %s)", want, have, timeout.String())
default:
endpoints, err = s.Endpoints()
have = len(endpoints)
if err != nil || want == have {
return
}
time.Sleep(timeout / 10)
}
}
}
================================================
FILE: tracing/README.md
================================================
# package tracing
`package tracing` provides [Dapper]-style request tracing to services.
## Rationale
Request tracing is a fundamental building block for large distributed
applications. It's instrumental in understanding request flows, identifying
hot spots, and diagnosing errors. All microservice infrastructures will
benefit from request tracing; sufficiently large infrastructures will require
it.
## Zipkin
[Zipkin] is one of the most used OSS distributed tracing platforms available
with support for many different languages and frameworks. Go kit provides
bindings to the native Go tracing implementation [zipkin-go]. If using Zipkin
with Go kit in a polyglot microservices environment, this is the preferred
binding to use. Instrumentation exists for `kit/transport/http` and
`kit/transport/grpc`. The bindings are highlighted in the [addsvc] example. For
more information regarding Zipkin feel free to visit [Zipkin's Gitter].
## OpenCensus
Go kit supports transport and endpoint middlewares for the [OpenCensus]
instrumentation library. OpenCensus provides a cross language consistent data
model and instrumentation libraries for tracing and metrics. From this data
model it allows exports to various tracing and metrics backends including but
not limited to Zipkin, Prometheus, Stackdriver Trace & Monitoring, Jaeger,
AWS X-Ray and Datadog. Go kit uses the [opencensus-go] implementation to power
its middlewares.
## OpenTracing
Go kit supports the [OpenTracing] API and uses the [opentracing-go] package to
provide tracing middlewares for its servers and clients. Currently OpenTracing
instrumentation exists for `kit/transport/http` and `kit/transport/grpc`.
Since [OpenTracing] is an effort to provide a generic API, Go kit should support
a multitude of tracing backends. If a Tracer implementation or OpenTracing
bridge in Go for your back-end exists, it should work out of the box.
Please note that the "world view" of existing tracing systems do differ.
OpenTracing can not guarantee you that tracing alignment is perfect in a
microservice environment especially one which is not exclusively OpenTracing
enabled or switching from one tracing backend to another truly entails just a
change in configuration.
The following tracing back-ends are known to work with Go kit through the
OpenTracing interface and are highlighted in the [addsvc] example.
### AppDash
[Appdash] support is available straight from their system repository in the
[appdash/opentracing] directory.
### LightStep
[LightStep] support is available through their standard Go package
[lightstep-tracer-go].
### Zipkin
[Zipkin] support is available through the [zipkin-go-opentracing] package.
## OpenTelemetry
[OpenTelemetry] came to life as a result of merging [OpenCensus] and [OpenTracing].
Go kit instrumentation can be found in [opentelemetry-go-contrib]
which is a central repository of instrumentation libraries.
[Dapper]: http://research.google.com/pubs/pub36356.html
[addsvc]: https://github.com/go-kit/examples/tree/master/addsvc
[README]: https://github.com/go-kit/kit/blob/master/tracing/zipkin/README.md
[OpenTracing]: http://opentracing.io
[opentracing-go]: https://github.com/opentracing/opentracing-go
[Zipkin]: http://zipkin.io/
[Open Zipkin GitHub]: https://github.com/openzipkin
[zipkin-go-opentracing]: https://github.com/openzipkin-contrib/zipkin-go-opentracing
[zipkin-go]: https://github.com/openzipkin/zipkin-go
[Zipkin's Gitter]: https://gitter.im/openzipkin/zipkin
[Appdash]: https://github.com/sourcegraph/appdash
[appdash/opentracing]: https://github.com/sourcegraph/appdash/tree/master/opentracing
[LightStep]: http://lightstep.com/
[lightstep-tracer-go]: https://github.com/lightstep/lightstep-tracer-go
[OpenCensus]: https://opencensus.io/
[opencensus-go]: https://github.com/census-instrumentation/opencensus-go
[OpenTelemetry]: https://opentelemetry.io/
[opentelemetry-go-contrib]: https://github.com/open-telemetry/opentelemetry-go-contrib
================================================
FILE: tracing/doc.go
================================================
// Package tracing provides helpers and bindings for distributed tracing.
//
// As your infrastructure grows, it becomes important to be able to trace a
// request, as it travels through multiple services and back to the user.
// Package tracing provides endpoints and transport helpers and middlewares to
// capture and emit request-scoped information.
package tracing
================================================
FILE: tracing/opencensus/doc.go
================================================
// Package opencensus provides Go kit integration to the OpenCensus project.
// OpenCensus is a single distribution of libraries for metrics and distributed
// tracing with minimal overhead that allows you to export data to multiple
// backends. The Go kit OpenCencus package as provided here contains middlewares
// for tracing.
package opencensus
================================================
FILE: tracing/opencensus/endpoint.go
================================================
package opencensus
import (
"context"
"strconv"
"go.opencensus.io/trace"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd/lb"
)
// TraceEndpointDefaultName is the default endpoint span name to use.
const TraceEndpointDefaultName = "gokit/endpoint"
// TraceEndpoint returns an Endpoint middleware, tracing a Go kit endpoint.
// This endpoint tracer should be used in combination with a Go kit Transport
// tracing middleware, generic OpenCensus transport middleware or custom before
// and after transport functions as service propagation of SpanContext is not
// provided in this middleware.
func TraceEndpoint(name string, options ...EndpointOption) endpoint.Middleware {
if name == "" {
name = TraceEndpointDefaultName
}
cfg := &EndpointOptions{}
for _, o := range options {
o(cfg)
}
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
if cfg.GetName != nil {
if newName := cfg.GetName(ctx, name); newName != "" {
name = newName
}
}
ctx, span := trace.StartSpan(ctx, name)
if len(cfg.Attributes) > 0 {
span.AddAttributes(cfg.Attributes...)
}
defer span.End()
if cfg.GetAttributes != nil {
if attrs := cfg.GetAttributes(ctx); len(attrs) > 0 {
span.AddAttributes(attrs...)
}
}
defer func() {
if err != nil {
if lberr, ok := err.(lb.RetryError); ok {
// handle errors originating from lb.Retry
attrs := make([]trace.Attribute, 0, len(lberr.RawErrors))
for idx, rawErr := range lberr.RawErrors {
attrs = append(attrs, trace.StringAttribute(
"gokit.retry.error."+strconv.Itoa(idx+1), rawErr.Error(),
))
}
span.AddAttributes(attrs...)
span.SetStatus(trace.Status{
Code: trace.StatusCodeUnknown,
Message: lberr.Final.Error(),
})
return
}
// generic error
span.SetStatus(trace.Status{
Code: trace.StatusCodeUnknown,
Message: err.Error(),
})
return
}
// test for business error
if res, ok := response.(endpoint.Failer); ok && res.Failed() != nil {
span.AddAttributes(
trace.StringAttribute("gokit.business.error", res.Failed().Error()),
)
if cfg.IgnoreBusinessError {
span.SetStatus(trace.Status{Code: trace.StatusCodeOK})
return
}
// treating business error as real error in span.
span.SetStatus(trace.Status{
Code: trace.StatusCodeUnknown,
Message: res.Failed().Error(),
})
return
}
// no errors identified
span.SetStatus(trace.Status{Code: trace.StatusCodeOK})
}()
response, err = next(ctx, request)
return
}
}
}
================================================
FILE: tracing/opencensus/endpoint_options.go
================================================
package opencensus
import (
"context"
"go.opencensus.io/trace"
)
// EndpointOptions holds the options for tracing an endpoint
type EndpointOptions struct {
// IgnoreBusinessError if set to true will not treat a business error
// identified through the endpoint.Failer interface as a span error.
IgnoreBusinessError bool
// Attributes holds the default attributes which will be set on span
// creation by our Endpoint middleware.
Attributes []trace.Attribute
// GetName is an optional function that can set the span name based on the existing name
// for the endpoint and information in the context.
//
// If the function is nil, or the returned name is empty, the existing name for the endpoint is used.
GetName func(ctx context.Context, name string) string
// GetAttributes is an optional function that can extract trace attributes
// from the context and add them to the span.
GetAttributes func(ctx context.Context) []trace.Attribute
}
// EndpointOption allows for functional options to our OpenCensus endpoint
// tracing middleware.
type EndpointOption func(*EndpointOptions)
// WithEndpointConfig sets all configuration options at once by use of the
// EndpointOptions struct.
func WithEndpointConfig(options EndpointOptions) EndpointOption {
return func(o *EndpointOptions) {
*o = options
}
}
// WithEndpointAttributes sets the default attributes for the spans created by
// the Endpoint tracer.
func WithEndpointAttributes(attrs ...trace.Attribute) EndpointOption {
return func(o *EndpointOptions) {
o.Attributes = attrs
}
}
// WithIgnoreBusinessError if set to true will not treat a business error
// identified through the endpoint.Failer interface as a span error.
func WithIgnoreBusinessError(val bool) EndpointOption {
return func(o *EndpointOptions) {
o.IgnoreBusinessError = val
}
}
// WithSpanName extracts additional attributes from the request context.
func WithSpanName(fn func(ctx context.Context, name string) string) EndpointOption {
return func(o *EndpointOptions) {
o.GetName = fn
}
}
// WithSpanAttributes extracts additional attributes from the request context.
func WithSpanAttributes(fn func(ctx context.Context) []trace.Attribute) EndpointOption {
return func(o *EndpointOptions) {
o.GetAttributes = fn
}
}
================================================
FILE: tracing/opencensus/endpoint_test.go
================================================
package opencensus_test
import (
"context"
"errors"
"testing"
"time"
"go.opencensus.io/trace"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/lb"
"github.com/go-kit/kit/tracing/opencensus"
)
const (
span1 = ""
span2 = "SPAN-2"
span3 = "SPAN-3"
span4 = "SPAN-4"
span5 = "SPAN-5"
span6 = "SPAN-6"
)
var (
err1 = errors.New("some error")
err2 = errors.New("other error")
err3 = errors.New("some business error")
err4 = errors.New("other business error")
)
// compile time assertion
var _ endpoint.Failer = failedResponse{}
type failedResponse struct {
err error
}
func (r failedResponse) Failed() error { return r.err }
func passEndpoint(_ context.Context, req interface{}) (interface{}, error) {
if err, _ := req.(error); err != nil {
return nil, err
}
return req, nil
}
func TestTraceEndpoint(t *testing.T) {
ctx := context.Background()
e := &recordingExporter{}
trace.RegisterExporter(e)
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
// span 1
span1Attrs := []trace.Attribute{
trace.StringAttribute("string", "value"),
trace.Int64Attribute("int64", 42),
}
mw := opencensus.TraceEndpoint(
span1, opencensus.WithEndpointAttributes(span1Attrs...),
)
mw(endpoint.Nop)(ctx, nil)
// span 2
opts := opencensus.EndpointOptions{}
mw = opencensus.TraceEndpoint(span2, opencensus.WithEndpointConfig(opts))
mw(passEndpoint)(ctx, err1)
// span3
mw = opencensus.TraceEndpoint(span3)
ep := lb.Retry(5, 1*time.Second, lb.NewRoundRobin(sd.FixedEndpointer{passEndpoint}))
mw(ep)(ctx, err2)
// span4
mw = opencensus.TraceEndpoint(span4)
mw(passEndpoint)(ctx, failedResponse{err: err3})
// span5
mw = opencensus.TraceEndpoint(span5, opencensus.WithIgnoreBusinessError(true))
mw(passEndpoint)(ctx, failedResponse{err: err4})
// span6
span6Attrs := []trace.Attribute{
trace.StringAttribute("string", "value"),
trace.Int64Attribute("int64", 42),
}
mw = opencensus.TraceEndpoint(
"",
opencensus.WithSpanName(func(ctx context.Context, name string) string {
return span6
}),
opencensus.WithSpanAttributes(func(ctx context.Context) []trace.Attribute {
return span6Attrs
}),
)
mw(endpoint.Nop)(ctx, nil)
// check span count
spans := e.Flush()
if want, have := 6, len(spans); want != have {
t.Fatalf("incorrected number of spans, wanted %d, got %d", want, have)
}
// test span 1
span := spans[0]
if want, have := int32(trace.StatusCodeOK), span.Code; want != have {
t.Errorf("incorrect status code, wanted %d, got %d", want, have)
}
if want, have := opencensus.TraceEndpointDefaultName, span.Name; want != have {
t.Errorf("incorrect span name, wanted %q, got %q", want, have)
}
if want, have := 2, len(span.Attributes); want != have {
t.Fatalf("incorrect attribute count, wanted %d, got %d", want, have)
}
// test span 2
span = spans[1]
if want, have := int32(trace.StatusCodeUnknown), span.Code; want != have {
t.Errorf("incorrect status code, wanted %d, got %d", want, have)
}
if want, have := span2, span.Name; want != have {
t.Errorf("incorrect span name, wanted %q, got %q", want, have)
}
if want, have := 0, len(span.Attributes); want != have {
t.Fatalf("incorrect attribute count, wanted %d, got %d", want, have)
}
// test span 3
span = spans[2]
if want, have := int32(trace.StatusCodeUnknown), span.Code; want != have {
t.Errorf("incorrect status code, wanted %d, got %d", want, have)
}
if want, have := span3, span.Name; want != have {
t.Errorf("incorrect span name, wanted %q, got %q", want, have)
}
if want, have := 5, len(span.Attributes); want != have {
t.Fatalf("incorrect attribute count, wanted %d, got %d", want, have)
}
// test span 4
span = spans[3]
if want, have := int32(trace.StatusCodeUnknown), span.Code; want != have {
t.Errorf("incorrect status code, wanted %d, got %d", want, have)
}
if want, have := span4, span.Name; want != have {
t.Errorf("incorrect span name, wanted %q, got %q", want, have)
}
if want, have := 1, len(span.Attributes); want != have {
t.Fatalf("incorrect attribute count, wanted %d, got %d", want, have)
}
// test span 5
span = spans[4]
if want, have := int32(trace.StatusCodeOK), span.Code; want != have {
t.Errorf("incorrect status code, wanted %d, got %d", want, have)
}
if want, have := span5, span.Name; want != have {
t.Errorf("incorrect span name, wanted %q, got %q", want, have)
}
if want, have := 1, len(span.Attributes); want != have {
t.Fatalf("incorrect attribute count, wanted %d, got %d", want, have)
}
// test span 6
span = spans[5]
if want, have := span6, span.Name; want != have {
t.Errorf("incorrect span name, wanted %q, got %q", want, have)
}
if want, have := 2, len(span.Attributes); want != have {
t.Fatalf("incorrect attribute count, wanted %d, got %d", want, have)
}
}
================================================
FILE: tracing/opencensus/grpc.go
================================================
package opencensus
import (
"context"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
kitgrpc "github.com/go-kit/kit/transport/grpc"
)
const propagationKey = "grpc-trace-bin"
// GRPCClientTrace enables OpenCensus tracing of a Go kit gRPC transport client.
func GRPCClientTrace(options ...TracerOption) kitgrpc.ClientOption {
cfg := TracerOptions{}
for _, option := range options {
option(&cfg)
}
clientBefore := kitgrpc.ClientBefore(
func(ctx context.Context, md *metadata.MD) context.Context {
var name string
if cfg.Name != "" {
name = cfg.Name
} else {
name = ctx.Value(kitgrpc.ContextKeyRequestMethod).(string)
}
ctx, span := trace.StartSpan(
ctx,
name,
trace.WithSampler(cfg.Sampler),
trace.WithSpanKind(trace.SpanKindClient),
)
if !cfg.Public {
traceContextBinary := string(propagation.Binary(span.SpanContext()))
(*md)[propagationKey] = append((*md)[propagationKey], traceContextBinary)
}
return ctx
},
)
clientFinalizer := kitgrpc.ClientFinalizer(
func(ctx context.Context, err error) {
if span := trace.FromContext(ctx); span != nil {
if s, ok := status.FromError(err); ok {
span.SetStatus(trace.Status{Code: int32(s.Code()), Message: s.Message()})
} else {
span.SetStatus(trace.Status{Code: int32(codes.Unknown), Message: err.Error()})
}
span.End()
}
},
)
return func(c *kitgrpc.Client) {
clientBefore(c)
clientFinalizer(c)
}
}
// GRPCServerTrace enables OpenCensus tracing of a Go kit gRPC transport server.
func GRPCServerTrace(options ...TracerOption) kitgrpc.ServerOption {
cfg := TracerOptions{}
for _, option := range options {
option(&cfg)
}
serverBefore := kitgrpc.ServerBefore(
func(ctx context.Context, md metadata.MD) context.Context {
var name string
if cfg.Name != "" {
name = cfg.Name
} else {
name, _ = ctx.Value(kitgrpc.ContextKeyRequestMethod).(string)
if name == "" {
// we can't find the gRPC method. probably the
// unaryInterceptor was not wired up.
name = "unknown grpc method"
}
}
var (
parentContext trace.SpanContext
traceContext = md[propagationKey]
ok bool
)
if len(traceContext) > 0 {
traceContextBinary := []byte(traceContext[0])
parentContext, ok = propagation.FromBinary(traceContextBinary)
if ok && !cfg.Public {
ctx, _ = trace.StartSpanWithRemoteParent(
ctx,
name,
parentContext,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithSampler(cfg.Sampler),
)
return ctx
}
}
ctx, span := trace.StartSpan(
ctx,
name,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithSampler(cfg.Sampler),
)
if ok {
span.AddLink(
trace.Link{
TraceID: parentContext.TraceID,
SpanID: parentContext.SpanID,
Type: trace.LinkTypeChild,
},
)
}
return ctx
},
)
serverFinalizer := kitgrpc.ServerFinalizer(
func(ctx context.Context, err error) {
if span := trace.FromContext(ctx); span != nil {
if s, ok := status.FromError(err); ok {
span.SetStatus(trace.Status{Code: int32(s.Code()), Message: s.Message()})
} else {
span.SetStatus(trace.Status{Code: int32(codes.Internal), Message: err.Error()})
}
span.End()
}
},
)
return func(s *kitgrpc.Server) {
serverBefore(s)
serverFinalizer(s)
}
}
================================================
FILE: tracing/opencensus/grpc_test.go
================================================
package opencensus_test
import (
"context"
"errors"
"testing"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"github.com/go-kit/kit/endpoint"
ockit "github.com/go-kit/kit/tracing/opencensus"
grpctransport "github.com/go-kit/kit/transport/grpc"
)
type dummy struct{}
const traceContextKey = "grpc-trace-bin"
func unaryInterceptor(
ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
) error {
return nil
}
func TestGRPCClientTrace(t *testing.T) {
rec := &recordingExporter{}
trace.RegisterExporter(rec)
cc, err := grpc.Dial(
"",
grpc.WithUnaryInterceptor(unaryInterceptor),
grpc.WithInsecure(),
)
if err != nil {
t.Fatalf("unable to create gRPC dialer: %s", err.Error())
}
traces := []struct {
name string
err error
}{
{"", nil},
{"CustomName", nil},
{"", errors.New("dummy-error")},
}
for _, tr := range traces {
clientTracer := ockit.GRPCClientTrace(
ockit.WithName(tr.name),
ockit.WithSampler(trace.AlwaysSample()),
)
ep := grpctransport.NewClient(
cc,
"dummyService",
"dummyMethod",
func(context.Context, interface{}) (interface{}, error) {
return nil, nil
},
func(context.Context, interface{}) (interface{}, error) {
return nil, tr.err
},
dummy{},
clientTracer,
).Endpoint()
ctx, parentSpan := trace.StartSpan(context.Background(), "test")
_, err = ep(ctx, nil)
if want, have := tr.err, err; want != have {
t.Fatalf("unexpected error, want %s, have %s", tr.err.Error(), err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
span := spans[0]
if want, have := parentSpan.SpanContext().SpanID, span.ParentSpanID; want != have {
t.Errorf("incorrect parent ID, want %s, have %s", want, have)
}
if want, have := tr.name, span.Name; want != have && want != "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := "/dummyService/dummyMethod", span.Name; want != have && tr.name == "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
code := trace.StatusCodeOK
if tr.err != nil {
code = trace.StatusCodeUnknown
if want, have := err.Error(), span.Status.Message; want != have {
t.Errorf("incorrect span status msg, want %s, have %s", want, have)
}
}
if want, have := int32(code), span.Status.Code; want != have {
t.Errorf("incorrect span status code, want %d, have %d", want, have)
}
}
}
func TestGRPCServerTrace(t *testing.T) {
rec := &recordingExporter{}
trace.RegisterExporter(rec)
traces := []struct {
useParent bool
name string
err error
}{
{false, "", nil},
{true, "", nil},
{true, "CustomName", nil},
{true, "", errors.New("dummy-error")},
}
for _, tr := range traces {
var (
ctx = context.Background()
parentSpan *trace.Span
)
server := grpctransport.NewServer(
endpoint.Nop,
func(context.Context, interface{}) (interface{}, error) {
return nil, nil
},
func(context.Context, interface{}) (interface{}, error) {
return nil, tr.err
},
ockit.GRPCServerTrace(
ockit.WithName(tr.name),
ockit.WithSampler(trace.AlwaysSample()),
),
)
if tr.useParent {
_, parentSpan = trace.StartSpan(context.Background(), "test")
traceContextBinary := propagation.Binary(parentSpan.SpanContext())
md := metadata.MD{}
md.Set(traceContextKey, string(traceContextBinary))
ctx = metadata.NewIncomingContext(ctx, md)
}
server.ServeGRPC(ctx, nil)
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if tr.useParent {
if want, have := parentSpan.SpanContext().TraceID, spans[0].TraceID; want != have {
t.Errorf("incorrect trace ID, want %s, have %s", want, have)
}
if want, have := parentSpan.SpanContext().SpanID, spans[0].ParentSpanID; want != have {
t.Errorf("incorrect span ID, want %s, have %s", want, have)
}
}
if want, have := tr.name, spans[0].Name; want != have && want != "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if tr.err != nil {
if want, have := int32(codes.Internal), spans[0].Status.Code; want != have {
t.Errorf("incorrect span status code, want %d, have %d", want, have)
}
if want, have := tr.err.Error(), spans[0].Status.Message; want != have {
t.Errorf("incorrect span status message, want %s, have %s", want, have)
}
}
}
}
================================================
FILE: tracing/opencensus/http.go
================================================
package opencensus
import (
"context"
"net/http"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/trace"
kithttp "github.com/go-kit/kit/transport/http"
)
// HTTPClientTrace enables OpenCensus tracing of a Go kit HTTP transport client.
func HTTPClientTrace(options ...TracerOption) kithttp.ClientOption {
cfg := TracerOptions{}
for _, option := range options {
option(&cfg)
}
if !cfg.Public && cfg.HTTPPropagate == nil {
cfg.HTTPPropagate = &b3.HTTPFormat{}
}
clientBefore := kithttp.ClientBefore(
func(ctx context.Context, req *http.Request) context.Context {
var name string
if cfg.Name != "" {
name = cfg.Name
} else {
// OpenCensus states Path being default naming for a client span
name = req.Method + " " + req.URL.Path
}
ctx, span := trace.StartSpan(
ctx,
name,
trace.WithSampler(cfg.Sampler),
trace.WithSpanKind(trace.SpanKindClient),
)
span.AddAttributes(
trace.StringAttribute(ochttp.HostAttribute, req.URL.Host),
trace.StringAttribute(ochttp.MethodAttribute, req.Method),
trace.StringAttribute(ochttp.PathAttribute, req.URL.Path),
trace.StringAttribute(ochttp.UserAgentAttribute, req.UserAgent()),
)
if !cfg.Public {
cfg.HTTPPropagate.SpanContextToRequest(span.SpanContext(), req)
}
return ctx
},
)
clientAfter := kithttp.ClientAfter(
func(ctx context.Context, res *http.Response) context.Context {
if span := trace.FromContext(ctx); span != nil {
span.SetStatus(ochttp.TraceStatus(res.StatusCode, http.StatusText(res.StatusCode)))
span.AddAttributes(
trace.Int64Attribute(ochttp.StatusCodeAttribute, int64(res.StatusCode)),
)
}
return ctx
},
)
clientFinalizer := kithttp.ClientFinalizer(
func(ctx context.Context, err error) {
if span := trace.FromContext(ctx); span != nil {
if err != nil {
span.SetStatus(trace.Status{
Code: trace.StatusCodeUnknown,
Message: err.Error(),
})
}
span.End()
}
},
)
return func(c *kithttp.Client) {
clientBefore(c)
clientAfter(c)
clientFinalizer(c)
}
}
// HTTPServerTrace enables OpenCensus tracing of a Go kit HTTP transport server.
func HTTPServerTrace(options ...TracerOption) kithttp.ServerOption {
cfg := TracerOptions{}
for _, option := range options {
option(&cfg)
}
if !cfg.Public && cfg.HTTPPropagate == nil {
cfg.HTTPPropagate = &b3.HTTPFormat{}
}
serverBefore := kithttp.ServerBefore(
func(ctx context.Context, req *http.Request) context.Context {
var (
spanContext trace.SpanContext
span *trace.Span
name string
ok bool
)
if cfg.Name != "" {
name = cfg.Name
} else {
name = req.Method + " " + req.URL.Path
}
spanContext, ok = cfg.HTTPPropagate.SpanContextFromRequest(req)
if ok && !cfg.Public {
ctx, span = trace.StartSpanWithRemoteParent(
ctx,
name,
spanContext,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithSampler(cfg.Sampler),
)
} else {
ctx, span = trace.StartSpan(
ctx,
name,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithSampler(cfg.Sampler),
)
if ok {
span.AddLink(trace.Link{
TraceID: spanContext.TraceID,
SpanID: spanContext.SpanID,
Type: trace.LinkTypeChild,
Attributes: nil,
})
}
}
span.AddAttributes(
trace.StringAttribute(ochttp.MethodAttribute, req.Method),
trace.StringAttribute(ochttp.PathAttribute, req.URL.Path),
)
return ctx
},
)
serverFinalizer := kithttp.ServerFinalizer(
func(ctx context.Context, code int, r *http.Request) {
if span := trace.FromContext(ctx); span != nil {
span.SetStatus(ochttp.TraceStatus(code, http.StatusText(code)))
if rs, ok := ctx.Value(kithttp.ContextKeyResponseSize).(int64); ok {
span.AddAttributes(
trace.Int64Attribute("http.response_size", rs),
)
}
span.End()
}
},
)
return func(s *kithttp.Server) {
serverBefore(s)
serverFinalizer(s)
}
}
================================================
FILE: tracing/opencensus/http_test.go
================================================
package opencensus_test
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
"github.com/go-kit/kit/endpoint"
ockit "github.com/go-kit/kit/tracing/opencensus"
kithttp "github.com/go-kit/kit/transport/http"
)
func TestHTTPClientTrace(t *testing.T) {
var (
err error
rec = &recordingExporter{}
rURL, _ = url.Parse("https://httpbin.org/get")
)
trace.RegisterExporter(rec)
traces := []struct {
name string
err error
}{
{"", nil},
{"CustomName", nil},
{"", errors.New("dummy-error")},
}
for _, tr := range traces {
clientTracer := ockit.HTTPClientTrace(
ockit.WithName(tr.name),
ockit.WithSampler(trace.AlwaysSample()),
)
ep := kithttp.NewClient(
"GET",
rURL,
func(ctx context.Context, r *http.Request, i interface{}) error {
return nil
},
func(ctx context.Context, r *http.Response) (response interface{}, err error) {
return nil, tr.err
},
clientTracer,
).Endpoint()
ctx, parentSpan := trace.StartSpan(context.Background(), "test")
_, err = ep(ctx, nil)
if want, have := tr.err, err; want != have {
t.Fatalf("unexpected error, want %s, have %s", tr.err.Error(), err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
span := spans[0]
if want, have := parentSpan.SpanContext().SpanID, span.ParentSpanID; want != have {
t.Errorf("incorrect parent ID, want %s, have %s", want, have)
}
if want, have := tr.name, span.Name; want != have && want != "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := "GET /get", span.Name; want != have && tr.name == "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
code := trace.StatusCodeOK
if tr.err != nil {
code = trace.StatusCodeUnknown
if want, have := err.Error(), span.Status.Message; want != have {
t.Errorf("incorrect span status msg, want %s, have %s", want, have)
}
}
if want, have := int32(code), span.Status.Code; want != have {
t.Errorf("incorrect span status code, want %d, have %d", want, have)
}
}
}
func TestHTTPServerTrace(t *testing.T) {
rec := &recordingExporter{}
trace.RegisterExporter(rec)
traces := []struct {
useParent bool
name string
err error
propagation propagation.HTTPFormat
}{
{false, "", nil, nil},
{true, "", nil, nil},
{true, "CustomName", nil, &b3.HTTPFormat{}},
{true, "", errors.New("dummy-error"), &tracecontext.HTTPFormat{}},
}
for _, tr := range traces {
var client http.Client
handler := kithttp.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) { return nil, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return errors.New("dummy") },
ockit.HTTPServerTrace(
ockit.WithName(tr.name),
ockit.WithSampler(trace.AlwaysSample()),
ockit.WithHTTPPropagation(tr.propagation),
),
)
server := httptest.NewServer(handler)
defer server.Close()
const httpMethod = "GET"
req, err := http.NewRequest(httpMethod, server.URL, nil)
if err != nil {
t.Fatalf("unable to create HTTP request: %s", err.Error())
}
if tr.useParent {
client = http.Client{
Transport: &ochttp.Transport{
StartOptions: trace.StartOptions{
Sampler: trace.AlwaysSample(),
},
Propagation: tr.propagation,
},
}
}
resp, err := client.Do(req.WithContext(context.Background()))
if err != nil {
t.Fatalf("unable to send HTTP request: %s", err.Error())
}
resp.Body.Close()
spans := rec.Flush()
expectedSpans := 1
if tr.useParent {
expectedSpans++
}
if want, have := expectedSpans, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if tr.useParent {
if want, have := spans[1].TraceID, spans[0].TraceID; want != have {
t.Errorf("incorrect trace ID, want %s, have %s", want, have)
}
if want, have := spans[1].SpanID, spans[0].ParentSpanID; want != have {
t.Errorf("incorrect span ID, want %s, have %s", want, have)
}
}
if want, have := tr.name, spans[0].Name; want != have && want != "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := "GET /", spans[0].Name; want != have && tr.name == "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
}
}
================================================
FILE: tracing/opencensus/jsonrpc.go
================================================
package opencensus
import (
"context"
"net/http"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/trace"
kithttp "github.com/go-kit/kit/transport/http"
jsonrpc "github.com/go-kit/kit/transport/http/jsonrpc"
)
// JSONRPCClientTrace enables OpenCensus tracing of a Go kit JSONRPC transport client.
func JSONRPCClientTrace(options ...TracerOption) jsonrpc.ClientOption {
cfg := TracerOptions{}
for _, option := range options {
option(&cfg)
}
if !cfg.Public && cfg.HTTPPropagate == nil {
cfg.HTTPPropagate = &b3.HTTPFormat{}
}
clientBefore := jsonrpc.ClientBefore(
func(ctx context.Context, req *http.Request) context.Context {
var name string
if cfg.Name != "" {
name = cfg.Name
} else {
// OpenCensus states Path being default naming for a client span
name = ctx.Value(jsonrpc.ContextKeyRequestMethod).(string)
}
ctx, span := trace.StartSpan(
ctx,
name,
trace.WithSampler(cfg.Sampler),
trace.WithSpanKind(trace.SpanKindClient),
)
span.AddAttributes(
trace.StringAttribute(ochttp.HostAttribute, req.URL.Host),
trace.StringAttribute(ochttp.MethodAttribute, req.Method),
trace.StringAttribute(ochttp.PathAttribute, req.URL.Path),
trace.StringAttribute(ochttp.UserAgentAttribute, req.UserAgent()),
)
if !cfg.Public {
cfg.HTTPPropagate.SpanContextToRequest(span.SpanContext(), req)
}
return ctx
},
)
clientAfter := jsonrpc.ClientAfter(
func(ctx context.Context, res *http.Response) context.Context {
if span := trace.FromContext(ctx); span != nil {
span.SetStatus(ochttp.TraceStatus(res.StatusCode, http.StatusText(res.StatusCode)))
span.AddAttributes(
trace.Int64Attribute(ochttp.StatusCodeAttribute, int64(res.StatusCode)),
)
}
return ctx
},
)
clientFinalizer := jsonrpc.ClientFinalizer(
func(ctx context.Context, err error) {
if span := trace.FromContext(ctx); span != nil {
if err != nil {
span.SetStatus(trace.Status{
Code: trace.StatusCodeUnknown,
Message: err.Error(),
})
}
span.End()
}
},
)
return func(c *jsonrpc.Client) {
clientBefore(c)
clientAfter(c)
clientFinalizer(c)
}
}
// JSONRPCServerTrace enables OpenCensus tracing of a Go kit JSONRPC transport server.
func JSONRPCServerTrace(options ...TracerOption) jsonrpc.ServerOption {
cfg := TracerOptions{}
for _, option := range options {
option(&cfg)
}
if !cfg.Public && cfg.HTTPPropagate == nil {
cfg.HTTPPropagate = &b3.HTTPFormat{}
}
serverBeforeCodec := jsonrpc.ServerBeforeCodec(
func(ctx context.Context, httpReq *http.Request, req jsonrpc.Request) context.Context {
var (
spanContext trace.SpanContext
span *trace.Span
name string
ok bool
)
if cfg.Name != "" {
name = cfg.Name
} else {
name = ctx.Value(jsonrpc.ContextKeyRequestMethod).(string)
if name == "" {
// we can't find the rpc method. probably the
// unaryInterceptor was not wired up.
name = "unknown jsonrpc method"
}
}
spanContext, ok = cfg.HTTPPropagate.SpanContextFromRequest(httpReq)
if ok && !cfg.Public {
ctx, span = trace.StartSpanWithRemoteParent(
ctx,
name,
spanContext,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithSampler(cfg.Sampler),
)
} else {
ctx, span = trace.StartSpan(
ctx,
name,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithSampler(cfg.Sampler),
)
if ok {
span.AddLink(trace.Link{
TraceID: spanContext.TraceID,
SpanID: spanContext.SpanID,
Type: trace.LinkTypeChild,
Attributes: nil,
})
}
}
span.AddAttributes(
trace.StringAttribute(ochttp.MethodAttribute, httpReq.Method),
trace.StringAttribute(ochttp.PathAttribute, httpReq.URL.Path),
)
return ctx
},
)
serverFinalizer := jsonrpc.ServerFinalizer(
func(ctx context.Context, code int, r *http.Request) {
if span := trace.FromContext(ctx); span != nil {
span.SetStatus(ochttp.TraceStatus(code, http.StatusText(code)))
if rs, ok := ctx.Value(kithttp.ContextKeyResponseSize).(int64); ok {
span.AddAttributes(
trace.Int64Attribute("http.response_size", rs),
)
}
span.End()
}
},
)
return func(s *jsonrpc.Server) {
serverBeforeCodec(s)
serverFinalizer(s)
}
}
================================================
FILE: tracing/opencensus/jsonrpc_test.go
================================================
package opencensus_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
"github.com/go-kit/kit/endpoint"
ockit "github.com/go-kit/kit/tracing/opencensus"
jsonrpc "github.com/go-kit/kit/transport/http/jsonrpc"
)
func TestJSONRPCClientTrace(t *testing.T) {
t.Skip("FLAKY")
var (
err error
rec = &recordingExporter{}
rURL, _ = url.Parse("https://httpbin.org/anything")
endpointName = "DummyEndpoint"
)
trace.RegisterExporter(rec)
traces := []struct {
name string
err error
}{
{"", nil},
{"CustomName", nil},
{"", errors.New("dummy-error")},
}
for _, tr := range traces {
clientTracer := ockit.JSONRPCClientTrace(
ockit.WithName(tr.name),
ockit.WithSampler(trace.AlwaysSample()),
)
ep := jsonrpc.NewClient(
rURL,
endpointName,
jsonrpc.ClientRequestEncoder(func(ctx context.Context, i interface{}) (json.RawMessage, error) {
return json.RawMessage(`{}`), nil
}),
jsonrpc.ClientResponseDecoder(func(ctx context.Context, r jsonrpc.Response) (response interface{}, err error) {
return nil, tr.err
}),
clientTracer,
).Endpoint()
ctx, parentSpan := trace.StartSpan(context.Background(), "test")
_, err = ep(ctx, nil)
if want, have := tr.err, err; want != have {
t.Fatalf("unexpected error, want %v, have %v", tr.err, err)
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
span := spans[0]
if want, have := parentSpan.SpanContext().SpanID, span.ParentSpanID; want != have {
t.Errorf("incorrect parent ID, want %s, have %s", want, have)
}
if want, have := tr.name, span.Name; want != have && want != "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := endpointName, span.Name; want != have && tr.name == "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
code := trace.StatusCodeOK
if tr.err != nil {
code = trace.StatusCodeUnknown
if want, have := err.Error(), span.Status.Message; want != have {
t.Errorf("incorrect span status msg, want %s, have %s", want, have)
}
}
if want, have := int32(code), span.Status.Code; want != have {
t.Errorf("incorrect span status code, want %d, have %d", want, have)
}
}
}
func TestJSONRPCServerTrace(t *testing.T) {
var (
endpointName = "DummyEndpoint"
rec = &recordingExporter{}
)
trace.RegisterExporter(rec)
traces := []struct {
useParent bool
name string
err error
propagation propagation.HTTPFormat
}{
{false, "", nil, nil},
{true, "", nil, nil},
{true, "CustomName", nil, &b3.HTTPFormat{}},
{true, "", errors.New("dummy-error"), &tracecontext.HTTPFormat{}},
}
for _, tr := range traces {
var client http.Client
handler := jsonrpc.NewServer(
jsonrpc.EndpointCodecMap{
endpointName: jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: func(context.Context, json.RawMessage) (interface{}, error) { return nil, nil },
Encode: func(context.Context, interface{}) (json.RawMessage, error) { return nil, tr.err },
},
},
ockit.JSONRPCServerTrace(
ockit.WithName(tr.name),
ockit.WithSampler(trace.AlwaysSample()),
ockit.WithHTTPPropagation(tr.propagation),
),
)
server := httptest.NewServer(handler)
defer server.Close()
jsonStr := []byte(fmt.Sprintf(`{"method":"%s"}`, endpointName))
req, err := http.NewRequest("POST", server.URL, bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatalf("unable to create JSONRPC request: %v", err)
}
if tr.useParent {
client = http.Client{
Transport: &ochttp.Transport{
StartOptions: trace.StartOptions{
Sampler: trace.AlwaysSample(),
},
Propagation: tr.propagation,
},
}
}
resp, err := client.Do(req.WithContext(context.Background()))
if err != nil {
t.Fatalf("unable to send JSONRPC request: %v", err)
}
resp.Body.Close()
spans := rec.Flush()
expectedSpans := 1
if tr.useParent {
expectedSpans++
}
if want, have := expectedSpans, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if tr.useParent {
if want, have := spans[1].TraceID, spans[0].TraceID; want != have {
t.Errorf("incorrect trace ID, want %s, have %s", want, have)
}
if want, have := spans[1].SpanID, spans[0].ParentSpanID; want != have {
t.Errorf("incorrect span ID, want %s, have %s", want, have)
}
}
if want, have := tr.name, spans[0].Name; want != have && want != "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := endpointName, spans[0].Name; want != have && tr.name == "" {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
}
}
================================================
FILE: tracing/opencensus/opencensus_test.go
================================================
package opencensus_test
import (
"sync"
"go.opencensus.io/trace"
)
type recordingExporter struct {
mu sync.Mutex
data []*trace.SpanData
}
func (e *recordingExporter) ExportSpan(d *trace.SpanData) {
e.mu.Lock()
defer e.mu.Unlock()
e.data = append(e.data, d)
}
func (e *recordingExporter) Flush() (data []*trace.SpanData) {
e.mu.Lock()
defer e.mu.Unlock()
data = e.data
e.data = nil
return
}
================================================
FILE: tracing/opencensus/tracer_options.go
================================================
package opencensus
import (
"go.opencensus.io/plugin/ochttp/propagation/b3"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
)
// defaultHTTPPropagate holds OpenCensus' default HTTP propagation format which
// currently is Zipkin's B3.
var defaultHTTPPropagate propagation.HTTPFormat = &b3.HTTPFormat{}
// TracerOption allows for functional options to our OpenCensus tracing
// middleware.
type TracerOption func(o *TracerOptions)
// WithTracerConfig sets all configuration options at once.
func WithTracerConfig(options TracerOptions) TracerOption {
return func(o *TracerOptions) {
*o = options
}
}
// WithSampler sets the sampler to use by our OpenCensus Tracer.
func WithSampler(sampler trace.Sampler) TracerOption {
return func(o *TracerOptions) {
o.Sampler = sampler
}
}
// WithName sets the name for an instrumented transport endpoint. If name is omitted
// at tracing middleware creation, the method of the transport or transport rpc
// name is used.
func WithName(name string) TracerOption {
return func(o *TracerOptions) {
o.Name = name
}
}
// IsPublic should be set to true for publicly accessible servers and for
// clients that should not propagate their current trace metadata.
// On the server side a new trace will always be started regardless of any
// trace metadata being found in the incoming request. If any trace metadata
// is found, it will be added as a linked trace instead.
func IsPublic(isPublic bool) TracerOption {
return func(o *TracerOptions) {
o.Public = isPublic
}
}
// WithHTTPPropagation sets the propagation handlers for the HTTP transport
// middlewares. If used on a non HTTP transport this is a noop.
func WithHTTPPropagation(p propagation.HTTPFormat) TracerOption {
return func(o *TracerOptions) {
if p == nil {
// reset to default OC HTTP format
o.HTTPPropagate = defaultHTTPPropagate
return
}
o.HTTPPropagate = p
}
}
// TracerOptions holds configuration for our tracing middlewares
type TracerOptions struct {
Sampler trace.Sampler
Name string
Public bool
HTTPPropagate propagation.HTTPFormat
}
================================================
FILE: tracing/opentracing/doc.go
================================================
// Package opentracing provides Go kit integration to the OpenTracing project.
// OpenTracing implements a general purpose interface that microservices can
// program against, and which adapts to all major distributed tracing systems.
package opentracing
================================================
FILE: tracing/opentracing/endpoint.go
================================================
package opentracing
import (
"context"
"strconv"
"github.com/opentracing/opentracing-go"
otext "github.com/opentracing/opentracing-go/ext"
otlog "github.com/opentracing/opentracing-go/log"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd/lb"
)
// TraceEndpoint returns a Middleware that wraps the `next` Endpoint in an
// OpenTracing Span called `operationName`.
//
// If `ctx` already has a Span, child span is created from it.
// If `ctx` doesn't yet have a Span, the new one is created.
func TraceEndpoint(tracer opentracing.Tracer, operationName string, opts ...EndpointOption) endpoint.Middleware {
cfg := &EndpointOptions{
Tags: make(opentracing.Tags),
}
for _, opt := range opts {
opt(cfg)
}
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
if cfg.GetOperationName != nil {
if newOperationName := cfg.GetOperationName(ctx, operationName); newOperationName != "" {
operationName = newOperationName
}
}
var span opentracing.Span
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
span = tracer.StartSpan(
operationName,
opentracing.ChildOf(parentSpan.Context()),
)
} else {
span = tracer.StartSpan(operationName)
}
defer span.Finish()
applyTags(span, cfg.Tags)
if cfg.GetTags != nil {
extraTags := cfg.GetTags(ctx)
applyTags(span, extraTags)
}
ctx = opentracing.ContextWithSpan(ctx, span)
defer func() {
if err != nil {
if lbErr, ok := err.(lb.RetryError); ok {
// handle errors originating from lb.Retry
fields := make([]otlog.Field, 0, len(lbErr.RawErrors))
for idx, rawErr := range lbErr.RawErrors {
fields = append(fields, otlog.String(
"gokit.retry.error."+strconv.Itoa(idx+1), rawErr.Error(),
))
}
otext.LogError(span, lbErr, fields...)
return
}
// generic error
otext.LogError(span, err)
return
}
// test for business error
if res, ok := response.(endpoint.Failer); ok && res.Failed() != nil {
span.LogFields(
otlog.String("gokit.business.error", res.Failed().Error()),
)
if cfg.IgnoreBusinessError {
return
}
// treating business error as real error in span.
otext.LogError(span, res.Failed())
return
}
}()
return next(ctx, request)
}
}
}
// TraceServer returns a Middleware that wraps the `next` Endpoint in an
// OpenTracing Span called `operationName` with server span.kind tag..
func TraceServer(tracer opentracing.Tracer, operationName string, opts ...EndpointOption) endpoint.Middleware {
opts = append(opts, WithTags(map[string]interface{}{
otext.SpanKindRPCServer.Key: otext.SpanKindRPCServer.Value,
}))
return TraceEndpoint(tracer, operationName, opts...)
}
// TraceClient returns a Middleware that wraps the `next` Endpoint in an
// OpenTracing Span called `operationName` with client span.kind tag.
func TraceClient(tracer opentracing.Tracer, operationName string, opts ...EndpointOption) endpoint.Middleware {
opts = append(opts, WithTags(map[string]interface{}{
otext.SpanKindRPCClient.Key: otext.SpanKindRPCClient.Value,
}))
return TraceEndpoint(tracer, operationName, opts...)
}
func applyTags(span opentracing.Span, tags opentracing.Tags) {
for key, value := range tags {
span.SetTag(key, value)
}
}
================================================
FILE: tracing/opentracing/endpoint_options.go
================================================
package opentracing
import (
"context"
"github.com/opentracing/opentracing-go"
)
// EndpointOptions holds the options for tracing an endpoint
type EndpointOptions struct {
// IgnoreBusinessError if set to true will not treat a business error
// identified through the endpoint.Failer interface as a span error.
IgnoreBusinessError bool
// GetOperationName is an optional function that can set the span operation name based on the existing one
// for the endpoint and information in the context.
//
// If the function is nil, or the returned name is empty, the existing name for the endpoint is used.
GetOperationName func(ctx context.Context, name string) string
// Tags holds the default tags which will be set on span
// creation by our Endpoint middleware.
Tags opentracing.Tags
// GetTags is an optional function that can extract tags
// from the context and add them to the span.
GetTags func(ctx context.Context) opentracing.Tags
}
// EndpointOption allows for functional options to endpoint tracing middleware.
type EndpointOption func(*EndpointOptions)
// WithOptions sets all configuration options at once by use of the EndpointOptions struct.
func WithOptions(options EndpointOptions) EndpointOption {
return func(o *EndpointOptions) {
*o = options
}
}
// WithIgnoreBusinessError if set to true will not treat a business error
// identified through the endpoint.Failer interface as a span error.
func WithIgnoreBusinessError(ignoreBusinessError bool) EndpointOption {
return func(o *EndpointOptions) {
o.IgnoreBusinessError = ignoreBusinessError
}
}
// WithOperationNameFunc allows to set function that can set the span operation name based on the existing one
// for the endpoint and information in the context.
func WithOperationNameFunc(getOperationName func(ctx context.Context, name string) string) EndpointOption {
return func(o *EndpointOptions) {
o.GetOperationName = getOperationName
}
}
// WithTags adds default tags for the spans created by the Endpoint tracer.
func WithTags(tags opentracing.Tags) EndpointOption {
return func(o *EndpointOptions) {
if o.Tags == nil {
o.Tags = make(opentracing.Tags)
}
for key, value := range tags {
o.Tags[key] = value
}
}
}
// WithTagsFunc set the func to extracts additional tags from the context.
func WithTagsFunc(getTags func(ctx context.Context) opentracing.Tags) EndpointOption {
return func(o *EndpointOptions) {
o.GetTags = getTags
}
}
================================================
FILE: tracing/opentracing/endpoint_test.go
================================================
package opentracing_test
import (
"context"
"errors"
"fmt"
"reflect"
"testing"
"time"
"github.com/opentracing/opentracing-go"
otext "github.com/opentracing/opentracing-go/ext"
otlog "github.com/opentracing/opentracing-go/log"
"github.com/opentracing/opentracing-go/mocktracer"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/sd"
"github.com/go-kit/kit/sd/lb"
kitot "github.com/go-kit/kit/tracing/opentracing"
)
const (
span1 = "SPAN-1"
span2 = "SPAN-2"
span3 = "SPAN-3"
span4 = "SPAN-4"
span5 = "SPAN-5"
span6 = "SPAN-6"
span7 = "SPAN-7"
span8 = "SPAN-8"
)
var (
err1 = errors.New("some error")
err2 = errors.New("some business error")
err3 = errors.New("other business error")
)
// compile time assertion
var _ endpoint.Failer = failedResponse{}
type failedResponse struct {
err error
}
func (r failedResponse) Failed() error {
return r.err
}
func TestTraceEndpoint(t *testing.T) {
tracer := mocktracer.New()
// Initialize the ctx with a parent Span.
parentSpan := tracer.StartSpan("parent").(*mocktracer.MockSpan)
defer parentSpan.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), parentSpan)
tracedEndpoint := kitot.TraceEndpoint(tracer, "testOp")(endpoint.Nop)
if _, err := tracedEndpoint(ctx, struct{}{}); err != nil {
t.Fatal(err)
}
// tracedEndpoint created a new Span.
finishedSpans := tracer.FinishedSpans()
if want, have := 1, len(finishedSpans); want != have {
t.Fatalf("Want %v span(s), found %v", want, have)
}
endpointSpan := finishedSpans[0]
if want, have := "testOp", endpointSpan.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
parentContext := parentSpan.Context().(mocktracer.MockSpanContext)
endpointContext := parentSpan.Context().(mocktracer.MockSpanContext)
// ... and that the parent ID is set appropriately.
if want, have := parentContext.SpanID, endpointContext.SpanID; want != have {
t.Errorf("Want ParentID %q, have %q", want, have)
}
}
func TestTraceEndpointNoContextSpan(t *testing.T) {
tracer := mocktracer.New()
// Empty/background context.
tracedEndpoint := kitot.TraceEndpoint(tracer, "testOp")(endpoint.Nop)
if _, err := tracedEndpoint(context.Background(), struct{}{}); err != nil {
t.Fatal(err)
}
// tracedEndpoint created a new Span.
finishedSpans := tracer.FinishedSpans()
if want, have := 1, len(finishedSpans); want != have {
t.Fatalf("Want %v span(s), found %v", want, have)
}
endpointSpan := finishedSpans[0]
if want, have := "testOp", endpointSpan.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
}
func TestTraceEndpointWithOptions(t *testing.T) {
tracer := mocktracer.New()
// span 1 without options
mw := kitot.TraceEndpoint(tracer, span1)
tracedEndpoint := mw(endpoint.Nop)
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 2 with options
mw = kitot.TraceEndpoint(
tracer,
span2,
kitot.WithOptions(kitot.EndpointOptions{}),
)
tracedEndpoint = mw(func(context.Context, interface{}) (interface{}, error) {
return nil, err1
})
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 3 with lb error
mw = kitot.TraceEndpoint(
tracer,
span3,
kitot.WithOptions(kitot.EndpointOptions{}),
)
tracedEndpoint = mw(
lb.Retry(
5,
1*time.Second,
lb.NewRoundRobin(
sd.FixedEndpointer{
func(context.Context, interface{}) (interface{}, error) {
return nil, err1
},
},
),
),
)
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 4 with disabled IgnoreBusinessError option
mw = kitot.TraceEndpoint(
tracer,
span4,
kitot.WithIgnoreBusinessError(false),
)
tracedEndpoint = mw(func(context.Context, interface{}) (interface{}, error) {
return failedResponse{
err: err2,
}, nil
})
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 5 with enabled IgnoreBusinessError option
mw = kitot.TraceEndpoint(tracer, span5, kitot.WithIgnoreBusinessError(true))
tracedEndpoint = mw(func(context.Context, interface{}) (interface{}, error) {
return failedResponse{
err: err3,
}, nil
})
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 6 with OperationNameFunc option
mw = kitot.TraceEndpoint(
tracer,
span6,
kitot.WithOperationNameFunc(func(ctx context.Context, name string) string {
return fmt.Sprintf("%s-%s", "new", name)
}),
)
tracedEndpoint = mw(endpoint.Nop)
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 7 with Tags options
mw = kitot.TraceEndpoint(
tracer,
span7,
kitot.WithTags(map[string]interface{}{
"tag1": "tag1",
"tag2": "tag2",
}),
kitot.WithTags(map[string]interface{}{
"tag3": "tag3",
}),
)
tracedEndpoint = mw(endpoint.Nop)
_, _ = tracedEndpoint(context.Background(), struct{}{})
// span 8 with TagsFunc options
mw = kitot.TraceEndpoint(
tracer,
span8,
kitot.WithTags(map[string]interface{}{
"tag1": "tag1",
"tag2": "tag2",
}),
kitot.WithTags(map[string]interface{}{
"tag3": "tag3",
}),
kitot.WithTagsFunc(func(ctx context.Context) opentracing.Tags {
return map[string]interface{}{
"tag4": "tag4",
}
}),
)
tracedEndpoint = mw(endpoint.Nop)
_, _ = tracedEndpoint(context.Background(), struct{}{})
finishedSpans := tracer.FinishedSpans()
if want, have := 8, len(finishedSpans); want != have {
t.Fatalf("Want %v span(s), found %v", want, have)
}
// test span 1
span := finishedSpans[0]
if want, have := span1, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
// test span 2
span = finishedSpans[1]
if want, have := span2, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := true, span.Tag("error"); want != have {
t.Fatalf("Want %v, have %v", want, have)
}
// test span 3
span = finishedSpans[2]
if want, have := span3, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := true, span.Tag("error"); want != have {
t.Fatalf("Want %v, have %v", want, have)
}
if want, have := 1, len(span.Logs()); want != have {
t.Fatalf("incorrect logs count, wanted %d, got %d", want, have)
}
if want, have := []otlog.Field{
otlog.String("event", "error"),
otlog.String("error.object", "some error (previously: some error; some error; some error; some error)"),
otlog.String("gokit.retry.error.1", "some error"),
otlog.String("gokit.retry.error.2", "some error"),
otlog.String("gokit.retry.error.3", "some error"),
otlog.String("gokit.retry.error.4", "some error"),
otlog.String("gokit.retry.error.5", "some error"),
}, span.Logs()[0].Fields; reflect.DeepEqual(want, have) {
t.Fatalf("Want %q, have %q", want, have)
}
// test span 4
span = finishedSpans[3]
if want, have := span4, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := true, span.Tag("error"); want != have {
t.Fatalf("Want %v, have %v", want, have)
}
if want, have := 2, len(span.Logs()); want != have {
t.Fatalf("incorrect logs count, wanted %d, got %d", want, have)
}
if want, have := []otlog.Field{
otlog.String("gokit.business.error", "some business error"),
}, span.Logs()[0].Fields; reflect.DeepEqual(want, have) {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := []otlog.Field{
otlog.String("event", "error"),
otlog.String("error.object", "some business error"),
}, span.Logs()[1].Fields; reflect.DeepEqual(want, have) {
t.Fatalf("Want %q, have %q", want, have)
}
// test span 5
span = finishedSpans[4]
if want, have := span5, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := (interface{})(nil), span.Tag("error"); want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := 1, len(span.Logs()); want != have {
t.Fatalf("incorrect logs count, wanted %d, got %d", want, have)
}
if want, have := []otlog.Field{
otlog.String("gokit.business.error", "some business error"),
}, span.Logs()[0].Fields; reflect.DeepEqual(want, have) {
t.Fatalf("Want %q, have %q", want, have)
}
// test span 6
span = finishedSpans[5]
if want, have := fmt.Sprintf("%s-%s", "new", span6), span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
// test span 7
span = finishedSpans[6]
if want, have := span7, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := map[string]interface{}{
"tag1": "tag1",
"tag2": "tag2",
"tag3": "tag3",
}, span.Tags(); fmt.Sprint(want) != fmt.Sprint(have) {
t.Fatalf("Want %q, have %q", want, have)
}
// test span 8
span = finishedSpans[7]
if want, have := span8, span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := map[string]interface{}{
"tag1": "tag1",
"tag2": "tag2",
"tag3": "tag3",
"tag4": "tag4",
}, span.Tags(); fmt.Sprint(want) != fmt.Sprint(have) {
t.Fatalf("Want %q, have %q", want, have)
}
}
func TestTraceServer(t *testing.T) {
tracer := mocktracer.New()
// Empty/background context.
tracedEndpoint := kitot.TraceServer(tracer, "testOp")(endpoint.Nop)
if _, err := tracedEndpoint(context.Background(), struct{}{}); err != nil {
t.Fatal(err)
}
// tracedEndpoint created a new Span.
finishedSpans := tracer.FinishedSpans()
if want, have := 1, len(finishedSpans); want != have {
t.Fatalf("Want %v span(s), found %v", want, have)
}
span := finishedSpans[0]
if want, have := "testOp", span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := map[string]interface{}{
otext.SpanKindRPCServer.Key: otext.SpanKindRPCServer.Value,
}, span.Tags(); fmt.Sprint(want) != fmt.Sprint(have) {
t.Fatalf("Want %q, have %q", want, have)
}
}
func TestTraceClient(t *testing.T) {
tracer := mocktracer.New()
// Empty/background context.
tracedEndpoint := kitot.TraceClient(tracer, "testOp")(endpoint.Nop)
if _, err := tracedEndpoint(context.Background(), struct{}{}); err != nil {
t.Fatal(err)
}
// tracedEndpoint created a new Span.
finishedSpans := tracer.FinishedSpans()
if want, have := 1, len(finishedSpans); want != have {
t.Fatalf("Want %v span(s), found %v", want, have)
}
span := finishedSpans[0]
if want, have := "testOp", span.OperationName; want != have {
t.Fatalf("Want %q, have %q", want, have)
}
if want, have := map[string]interface{}{
otext.SpanKindRPCClient.Key: otext.SpanKindRPCClient.Value,
}, span.Tags(); fmt.Sprint(want) != fmt.Sprint(have) {
t.Fatalf("Want %q, have %q", want, have)
}
}
================================================
FILE: tracing/opentracing/grpc.go
================================================
package opentracing
import (
"context"
"encoding/base64"
"strings"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"google.golang.org/grpc/metadata"
"github.com/go-kit/log"
)
// ContextToGRPC returns a grpc RequestFunc that injects an OpenTracing Span
// found in `ctx` into the grpc Metadata. If no such Span can be found, the
// RequestFunc is a noop.
func ContextToGRPC(tracer opentracing.Tracer, logger log.Logger) func(ctx context.Context, md *metadata.MD) context.Context {
return func(ctx context.Context, md *metadata.MD) context.Context {
if span := opentracing.SpanFromContext(ctx); span != nil {
// There's nothing we can do with an error here.
if err := tracer.Inject(span.Context(), opentracing.TextMap, metadataReaderWriter{md}); err != nil {
logger.Log("err", err)
}
}
return ctx
}
}
// GRPCToContext returns a grpc RequestFunc that tries to join with an
// OpenTracing trace found in `req` and starts a new Span called
// `operationName` accordingly. If no trace could be found in `req`, the Span
// will be a trace root. The Span is incorporated in the returned Context and
// can be retrieved with opentracing.SpanFromContext(ctx).
func GRPCToContext(tracer opentracing.Tracer, operationName string, logger log.Logger) func(ctx context.Context, md metadata.MD) context.Context {
return func(ctx context.Context, md metadata.MD) context.Context {
var span opentracing.Span
wireContext, err := tracer.Extract(opentracing.TextMap, metadataReaderWriter{&md})
if err != nil && err != opentracing.ErrSpanContextNotFound {
logger.Log("err", err)
}
span = tracer.StartSpan(operationName, ext.RPCServerOption(wireContext))
return opentracing.ContextWithSpan(ctx, span)
}
}
// A type that conforms to opentracing.TextMapReader and
// opentracing.TextMapWriter.
type metadataReaderWriter struct {
*metadata.MD
}
func (w metadataReaderWriter) Set(key, val string) {
key = strings.ToLower(key)
if strings.HasSuffix(key, "-bin") {
val = base64.StdEncoding.EncodeToString([]byte(val))
}
(*w.MD)[key] = append((*w.MD)[key], val)
}
func (w metadataReaderWriter) ForeachKey(handler func(key, val string) error) error {
for k, vals := range *w.MD {
for _, v := range vals {
if err := handler(k, v); err != nil {
return err
}
}
}
return nil
}
================================================
FILE: tracing/opentracing/grpc_test.go
================================================
package opentracing_test
import (
"context"
"testing"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/mocktracer"
"google.golang.org/grpc/metadata"
kitot "github.com/go-kit/kit/tracing/opentracing"
"github.com/go-kit/log"
)
func TestTraceGRPCRequestRoundtrip(t *testing.T) {
logger := log.NewNopLogger()
tracer := mocktracer.New()
// Initialize the ctx with a Span to inject.
beforeSpan := tracer.StartSpan("to_inject").(*mocktracer.MockSpan)
defer beforeSpan.Finish()
beforeSpan.SetBaggageItem("baggage", "check")
beforeCtx := opentracing.ContextWithSpan(context.Background(), beforeSpan)
toGRPCFunc := kitot.ContextToGRPC(tracer, logger)
md := metadata.Pairs()
// Call the RequestFunc.
afterCtx := toGRPCFunc(beforeCtx, &md)
// The Span should not have changed.
afterSpan := opentracing.SpanFromContext(afterCtx)
if beforeSpan != afterSpan {
t.Error("Should not swap in a new span")
}
// No spans should have finished yet.
finishedSpans := tracer.FinishedSpans()
if want, have := 0, len(finishedSpans); want != have {
t.Errorf("Want %v span(s), found %v", want, have)
}
// Use GRPCToContext to verify that we can join with the trace given MD.
fromGRPCFunc := kitot.GRPCToContext(tracer, "joined", logger)
joinCtx := fromGRPCFunc(afterCtx, md)
joinedSpan := opentracing.SpanFromContext(joinCtx).(*mocktracer.MockSpan)
joinedContext := joinedSpan.Context().(mocktracer.MockSpanContext)
beforeContext := beforeSpan.Context().(mocktracer.MockSpanContext)
if joinedContext.SpanID == beforeContext.SpanID {
t.Error("SpanID should have changed", joinedContext.SpanID, beforeContext.SpanID)
}
// Check that the parent/child relationship is as expected for the joined span.
if want, have := beforeContext.SpanID, joinedSpan.ParentID; want != have {
t.Errorf("Want ParentID %q, have %q", want, have)
}
if want, have := "joined", joinedSpan.OperationName; want != have {
t.Errorf("Want %q, have %q", want, have)
}
if want, have := "check", joinedSpan.BaggageItem("baggage"); want != have {
t.Errorf("Want %q, have %q", want, have)
}
}
================================================
FILE: tracing/opentracing/http.go
================================================
package opentracing
import (
"context"
"net"
"net/http"
"strconv"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-kit/log"
)
// ContextToHTTP returns an http RequestFunc that injects an OpenTracing Span
// found in `ctx` into the http headers. If no such Span can be found, the
// RequestFunc is a noop.
func ContextToHTTP(tracer opentracing.Tracer, logger log.Logger) kithttp.RequestFunc {
return func(ctx context.Context, req *http.Request) context.Context {
// Try to find a Span in the Context.
if span := opentracing.SpanFromContext(ctx); span != nil {
// Add standard OpenTracing tags.
ext.HTTPMethod.Set(span, req.Method)
ext.HTTPUrl.Set(span, req.URL.String())
host, portString, err := net.SplitHostPort(req.URL.Host)
if err == nil {
ext.PeerHostname.Set(span, host)
if port, err := strconv.Atoi(portString); err == nil {
ext.PeerPort.Set(span, uint16(port))
}
} else {
ext.PeerHostname.Set(span, req.URL.Host)
}
// There's nothing we can do with any errors here.
if err = tracer.Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header),
); err != nil {
logger.Log("err", err)
}
}
return ctx
}
}
// HTTPToContext returns an http RequestFunc that tries to join with an
// OpenTracing trace found in `req` and starts a new Span called
// `operationName` accordingly. If no trace could be found in `req`, the Span
// will be a trace root. The Span is incorporated in the returned Context and
// can be retrieved with opentracing.SpanFromContext(ctx).
func HTTPToContext(tracer opentracing.Tracer, operationName string, logger log.Logger) kithttp.RequestFunc {
return func(ctx context.Context, req *http.Request) context.Context {
// Try to join to a trace propagated in `req`.
var span opentracing.Span
wireContext, err := tracer.Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header),
)
if err != nil && err != opentracing.ErrSpanContextNotFound {
logger.Log("err", err)
}
span = tracer.StartSpan(operationName, ext.RPCServerOption(wireContext))
ext.HTTPMethod.Set(span, req.Method)
ext.HTTPUrl.Set(span, req.URL.String())
return opentracing.ContextWithSpan(ctx, span)
}
}
================================================
FILE: tracing/opentracing/http_test.go
================================================
package opentracing_test
import (
"context"
"net/http"
"reflect"
"testing"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/mocktracer"
kitot "github.com/go-kit/kit/tracing/opentracing"
"github.com/go-kit/log"
)
func TestTraceHTTPRequestRoundtrip(t *testing.T) {
logger := log.NewNopLogger()
tracer := mocktracer.New()
// Initialize the ctx with a Span to inject.
beforeSpan := tracer.StartSpan("to_inject").(*mocktracer.MockSpan)
defer beforeSpan.Finish()
beforeSpan.SetBaggageItem("baggage", "check")
beforeCtx := opentracing.ContextWithSpan(context.Background(), beforeSpan)
toHTTPFunc := kitot.ContextToHTTP(tracer, logger)
req, _ := http.NewRequest("GET", "http://test.biz/path", nil)
// Call the RequestFunc.
afterCtx := toHTTPFunc(beforeCtx, req)
// The Span should not have changed.
afterSpan := opentracing.SpanFromContext(afterCtx)
if beforeSpan != afterSpan {
t.Error("Should not swap in a new span")
}
// No spans should have finished yet.
finishedSpans := tracer.FinishedSpans()
if want, have := 0, len(finishedSpans); want != have {
t.Errorf("Want %v span(s), found %v", want, have)
}
// Use HTTPToContext to verify that we can join with the trace given a req.
fromHTTPFunc := kitot.HTTPToContext(tracer, "joined", logger)
joinCtx := fromHTTPFunc(afterCtx, req)
joinedSpan := opentracing.SpanFromContext(joinCtx).(*mocktracer.MockSpan)
joinedContext := joinedSpan.Context().(mocktracer.MockSpanContext)
beforeContext := beforeSpan.Context().(mocktracer.MockSpanContext)
if joinedContext.SpanID == beforeContext.SpanID {
t.Error("SpanID should have changed", joinedContext.SpanID, beforeContext.SpanID)
}
// Check that the parent/child relationship is as expected for the joined span.
if want, have := beforeContext.SpanID, joinedSpan.ParentID; want != have {
t.Errorf("Want ParentID %q, have %q", want, have)
}
if want, have := "joined", joinedSpan.OperationName; want != have {
t.Errorf("Want %q, have %q", want, have)
}
if want, have := "check", joinedSpan.BaggageItem("baggage"); want != have {
t.Errorf("Want %q, have %q", want, have)
}
}
func TestContextToHTTPTags(t *testing.T) {
tracer := mocktracer.New()
span := tracer.StartSpan("to_inject").(*mocktracer.MockSpan)
defer span.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), span)
req, _ := http.NewRequest("GET", "http://test.biz/path", nil)
kitot.ContextToHTTP(tracer, log.NewNopLogger())(ctx, req)
expectedTags := map[string]interface{}{
string(ext.HTTPMethod): "GET",
string(ext.HTTPUrl): "http://test.biz/path",
string(ext.PeerHostname): "test.biz",
}
if !reflect.DeepEqual(expectedTags, span.Tags()) {
t.Errorf("Want %q, have %q", expectedTags, span.Tags())
}
}
func TestHTTPToContextTags(t *testing.T) {
tracer := mocktracer.New()
parentSpan := tracer.StartSpan("to_extract").(*mocktracer.MockSpan)
defer parentSpan.Finish()
req, _ := http.NewRequest("GET", "http://test.biz/path", nil)
tracer.Inject(parentSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
ctx := kitot.HTTPToContext(tracer, "op", log.NewNopLogger())(context.Background(), req)
opentracing.SpanFromContext(ctx).Finish()
childSpan := tracer.FinishedSpans()[0]
expectedTags := map[string]interface{}{
string(ext.HTTPMethod): "GET",
string(ext.HTTPUrl): "http://test.biz/path",
string(ext.SpanKind): ext.SpanKindRPCServerEnum,
}
if !reflect.DeepEqual(expectedTags, childSpan.Tags()) {
t.Errorf("Want %q, have %q", expectedTags, childSpan.Tags())
}
if want, have := "op", childSpan.OperationName; want != have {
t.Errorf("Want %q, have %q", want, have)
}
}
================================================
FILE: tracing/zipkin/README.md
================================================
# Zipkin
## Development and Testing Set-up
Great efforts have been made to make [Zipkin] easier to test, develop and
experiment against. [Zipkin] can now be run from a single Docker container or by
running its self-contained executable jar without extensive configuration. In
its default configuration you will run [Zipkin] with a HTTP collector, In memory
Span storage backend and web UI on port 9411.
Example:
```
docker run -d -p 9411:9411 openzipkin/zipkin
```
[zipkin]: http://zipkin.io
## Middleware Usage
Follow the [addsvc] example to check out how to wire the [Zipkin] Middleware.
The changes should be relatively minor.
The [zipkin-go] package has Reporters to send Spans to the [Zipkin] HTTP and
Kafka Collectors.
### Configuring the Zipkin HTTP Reporter
To use the HTTP Reporter with a [Zipkin] instance running on localhost you
bootstrap [zipkin-go] like this:
```go
var (
serviceName = "MyService"
serviceHostPort = "localhost:8000"
zipkinHTTPEndpoint = "http://localhost:9411/api/v2/spans"
)
// create an instance of the HTTP Reporter.
reporter := zipkin.NewReporter(zipkinHTTPEndpoint)
// create our tracer's local endpoint (how the service is identified in Zipkin).
localEndpoint, err := zipkin.NewEndpoint(serviceName, serviceHostPort)
// create our tracer instance.
tracer, err = zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(localEndpoint))
...
```
[zipkin-go]: https://github.com/openzipkin/zipkin-go
[addsvc]: https://github.com/go-kit/examples/tree/master/addsvc
[Log]: https://github.com/go-kit/kit/tree/master/log
### Tracing Resources
Here is an example of how you could trace resources and work with local spans.
```go
import (
zipkin "github.com/openzipkin/zipkin-go"
)
func (svc *Service) GetMeSomeExamples(ctx context.Context, ...) ([]Examples, error) {
// Example of annotating a database query:
var (
spanContext model.SpanContext
serviceName = "MySQL"
serviceHost = "mysql.example.com:3306"
queryLabel = "GetExamplesByParam"
query = "select * from example where param = :value"
)
// retrieve the parent span from context to use as parent if available.
if parentSpan := zipkin.SpanFromContext(ctx); parentSpan != nil {
spanContext = parentSpan.Context()
}
// create the remote Zipkin endpoint
ep, _ := zipkin.NewEndpoint(serviceName, serviceHost)
// create a new span to record the resource interaction
span := zipkin.StartSpan(
queryLabel,
zipkin.Parent(parentSpan.Context()),
zipkin.WithRemoteEndpoint(ep),
)
// add interesting key/value pair to our span
span.SetTag("query", query)
// add interesting timed event to our span
span.Annotate(time.Now(), "query:start")
// do the actual query...
// let's annotate the end...
span.Annotate(time.Now(), "query:end")
// we're done with this span.
span.Finish()
// do other stuff
...
}
```
================================================
FILE: tracing/zipkin/doc.go
================================================
// Package zipkin provides Go kit integration to the OpenZipkin project through
// the use of zipkin-go, the official OpenZipkin tracer implementation for Go.
// OpenZipkin is the most used open source distributed tracing ecosystem with
// many different libraries and interoperability options.
package zipkin
================================================
FILE: tracing/zipkin/endpoint.go
================================================
package zipkin
import (
"context"
"github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/model"
"github.com/go-kit/kit/endpoint"
)
// TraceEndpoint returns an Endpoint middleware, tracing a Go kit endpoint.
// This endpoint tracer should be used in combination with a Go kit Transport
// tracing middleware or custom before and after transport functions as
// propagation of SpanContext is not provided in this middleware.
func TraceEndpoint(tracer *zipkin.Tracer, name string) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
var sc model.SpanContext
if parentSpan := zipkin.SpanFromContext(ctx); parentSpan != nil {
sc = parentSpan.Context()
}
sp := tracer.StartSpan(name, zipkin.Parent(sc))
defer sp.Finish()
ctx = zipkin.NewContext(ctx, sp)
return next(ctx, request)
}
}
}
================================================
FILE: tracing/zipkin/endpoint_test.go
================================================
package zipkin_test
import (
"context"
"testing"
"github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/reporter/recorder"
"github.com/go-kit/kit/endpoint"
zipkinkit "github.com/go-kit/kit/tracing/zipkin"
)
const spanName = "test"
func TestTraceEndpoint(t *testing.T) {
rec := recorder.NewReporter()
tr, _ := zipkin.NewTracer(rec)
mw := zipkinkit.TraceEndpoint(tr, spanName)
mw(endpoint.Nop)(context.Background(), nil)
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, wanted %d, got %d", want, have)
}
if want, have := spanName, spans[0].Name; want != have {
t.Fatalf("incorrect span name, wanted %s, got %s", want, have)
}
}
================================================
FILE: tracing/zipkin/grpc.go
================================================
package zipkin
import (
"context"
"strconv"
zipkin "github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/model"
"github.com/openzipkin/zipkin-go/propagation/b3"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/go-kit/log"
)
// GRPCClientTrace enables native Zipkin tracing of a Go kit gRPC transport
// Client.
//
// Go kit creates gRPC transport clients per remote endpoint. This middleware
// can be set-up individually by adding the endpoint name for each of the Go kit
// transport clients using the Name() TracerOption.
// If wanting to use the gRPC FullMethod (/service/method) as Span name you can
// create a global client tracer omitting the Name() TracerOption, which you can
// then feed to each Go kit gRPC transport client.
// If instrumenting a client to an external (not on your platform) service, you
// will probably want to disallow propagation of SpanContext using the
// AllowPropagation TracerOption and setting it to false.
func GRPCClientTrace(tracer *zipkin.Tracer, options ...TracerOption) kitgrpc.ClientOption {
config := tracerOptions{
tags: make(map[string]string),
name: "",
logger: log.NewNopLogger(),
propagate: true,
}
for _, option := range options {
option(&config)
}
clientBefore := kitgrpc.ClientBefore(
func(ctx context.Context, md *metadata.MD) context.Context {
var (
spanContext model.SpanContext
name string
)
if config.name != "" {
name = config.name
} else {
name = ctx.Value(kitgrpc.ContextKeyRequestMethod).(string)
}
if parent := zipkin.SpanFromContext(ctx); parent != nil {
spanContext = parent.Context()
}
span := tracer.StartSpan(
name,
zipkin.Kind(model.Client),
zipkin.Tags(config.tags),
zipkin.Parent(spanContext),
zipkin.FlushOnFinish(false),
)
if config.propagate {
if err := b3.InjectGRPC(md)(span.Context()); err != nil {
config.logger.Log("err", err)
}
}
return zipkin.NewContext(ctx, span)
},
)
clientAfter := kitgrpc.ClientAfter(
func(ctx context.Context, _ metadata.MD, _ metadata.MD) context.Context {
if span := zipkin.SpanFromContext(ctx); span != nil {
span.Finish()
}
return ctx
},
)
clientFinalizer := kitgrpc.ClientFinalizer(
func(ctx context.Context, err error) {
if span := zipkin.SpanFromContext(ctx); span != nil {
if err != nil {
zipkin.TagError.Set(span, err.Error())
}
// calling span.Finish() a second time is a noop, if we didn't get to
// ClientAfter we can at least time the early bail out by calling it
// here.
span.Finish()
// send span to the Reporter
span.Flush()
}
},
)
return func(c *kitgrpc.Client) {
clientBefore(c)
clientAfter(c)
clientFinalizer(c)
}
}
// GRPCServerTrace enables native Zipkin tracing of a Go kit gRPC transport
// Server.
//
// Go kit creates gRPC transport servers per gRPC method. This middleware can be
// set-up individually by adding the method name for each of the Go kit method
// servers using the Name() TracerOption.
// If wanting to use the gRPC FullMethod (/service/method) as Span name you can
// create a global server tracer omitting the Name() TracerOption, which you can
// then feed to each Go kit method server. For this to work you will need to
// wire the Go kit gRPC Interceptor too.
// If instrumenting a service to external (not on your platform) clients, you
// will probably want to disallow propagation of a client SpanContext using
// the AllowPropagation TracerOption and setting it to false.
func GRPCServerTrace(tracer *zipkin.Tracer, options ...TracerOption) kitgrpc.ServerOption {
config := tracerOptions{
tags: make(map[string]string),
name: "",
logger: log.NewNopLogger(),
propagate: true,
}
for _, option := range options {
option(&config)
}
serverBefore := kitgrpc.ServerBefore(
func(ctx context.Context, md metadata.MD) context.Context {
var (
spanContext model.SpanContext
name string
tags = make(map[string]string)
)
rpcMethod, ok := ctx.Value(kitgrpc.ContextKeyRequestMethod).(string)
if !ok {
config.logger.Log("err", "unable to retrieve method name: missing gRPC interceptor hook")
} else {
tags["grpc.method"] = rpcMethod
}
if config.name != "" {
name = config.name
} else {
name = rpcMethod
}
if config.propagate {
spanContext = tracer.Extract(b3.ExtractGRPC(&md))
if spanContext.Err != nil {
config.logger.Log("err", spanContext.Err)
}
}
span := tracer.StartSpan(
name,
zipkin.Kind(model.Server),
zipkin.Tags(config.tags),
zipkin.Tags(tags),
zipkin.Parent(spanContext),
zipkin.FlushOnFinish(false),
)
return zipkin.NewContext(ctx, span)
},
)
serverAfter := kitgrpc.ServerAfter(
func(ctx context.Context, _ *metadata.MD, _ *metadata.MD) context.Context {
if span := zipkin.SpanFromContext(ctx); span != nil {
span.Finish()
}
return ctx
},
)
serverFinalizer := kitgrpc.ServerFinalizer(
func(ctx context.Context, err error) {
if span := zipkin.SpanFromContext(ctx); span != nil {
if err != nil {
if status, ok := status.FromError(err); ok {
statusCode := strconv.FormatUint(uint64(status.Code()), 10)
zipkin.TagGRPCStatusCode.Set(span, statusCode)
zipkin.TagError.Set(span, status.Message())
} else {
zipkin.TagError.Set(span, err.Error())
}
}
// calling span.Finish() a second time is a noop, if we didn't get to
// ServerAfter we can at least time the early bail out by calling it
// here.
span.Finish()
// send span to the Reporter
span.Flush()
}
},
)
return func(s *kitgrpc.Server) {
serverBefore(s)
serverAfter(s)
serverFinalizer(s)
}
}
================================================
FILE: tracing/zipkin/grpc_test.go
================================================
package zipkin_test
import (
"context"
"testing"
zipkin "github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/propagation/b3"
"github.com/openzipkin/zipkin-go/reporter/recorder"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/go-kit/kit/endpoint"
kitzipkin "github.com/go-kit/kit/tracing/zipkin"
grpctransport "github.com/go-kit/kit/transport/grpc"
)
type dummy struct{}
func unaryInterceptor(
ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
) error {
return nil
}
func TestGRPCClientTrace(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
tr, _ := zipkin.NewTracer(rec)
clientTracer := kitzipkin.GRPCClientTrace(tr)
cc, err := grpc.Dial(
"",
grpc.WithUnaryInterceptor(unaryInterceptor),
grpc.WithInsecure(),
)
if err != nil {
t.Fatalf("unable to create gRPC dialer: %s", err.Error())
}
ep := grpctransport.NewClient(
cc,
"dummyService",
"dummyMethod",
func(context.Context, interface{}) (interface{}, error) { return nil, nil },
func(context.Context, interface{}) (interface{}, error) { return nil, nil },
dummy{},
clientTracer,
).Endpoint()
parentSpan := tr.StartSpan("test")
ctx := zipkin.NewContext(context.Background(), parentSpan)
if _, err = ep(ctx, nil); err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if spans[0].SpanContext.ParentID == nil {
t.Fatalf("incorrect parent ID, want %s have nil", parentSpan.Context().ID)
}
if want, have := parentSpan.Context().ID, *spans[0].SpanContext.ParentID; want != have {
t.Fatalf("incorrect parent ID, want %s, have %s", want, have)
}
}
func TestGRPCServerTrace(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
tr, _ := zipkin.NewTracer(rec)
serverTracer := kitzipkin.GRPCServerTrace(tr)
server := grpctransport.NewServer(
endpoint.Nop,
func(context.Context, interface{}) (interface{}, error) { return nil, nil },
func(context.Context, interface{}) (interface{}, error) { return nil, nil },
serverTracer,
)
md := metadata.MD{}
parentSpan := tr.StartSpan("test")
b3.InjectGRPC(&md)(parentSpan.Context())
ctx := metadata.NewIncomingContext(context.Background(), md)
server.ServeGRPC(ctx, nil)
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if want, have := parentSpan.Context().TraceID, spans[0].SpanContext.TraceID; want != have {
t.Errorf("incorrect TraceID, want %+v, have %+v", want, have)
}
if want, have := parentSpan.Context().ID, spans[0].SpanContext.ID; want != have {
t.Errorf("incorrect span ID, want %d, have %d", want, have)
}
}
================================================
FILE: tracing/zipkin/http.go
================================================
package zipkin
import (
"context"
"net/http"
"strconv"
zipkin "github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/model"
"github.com/openzipkin/zipkin-go/propagation/b3"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-kit/log"
)
// HTTPClientTrace enables native Zipkin tracing of a Go kit HTTP transport
// Client.
//
// Go kit creates HTTP transport clients per remote endpoint. This middleware
// can be set-up individually by adding the endpoint name for each of the Go kit
// transport clients using the Name() TracerOption.
// If wanting to use the HTTP Method (Get, Post, Put, etc.) as Span name you can
// create a global client tracer omitting the Name() TracerOption, which you can
// then feed to each Go kit transport client.
// If instrumenting a client to an external (not on your platform) service, you
// will probably want to disallow propagation of SpanContext using the
// AllowPropagation TracerOption and setting it to false.
func HTTPClientTrace(tracer *zipkin.Tracer, options ...TracerOption) kithttp.ClientOption {
config := tracerOptions{
tags: make(map[string]string),
name: "",
logger: log.NewNopLogger(),
propagate: true,
}
for _, option := range options {
option(&config)
}
clientBefore := kithttp.ClientBefore(
func(ctx context.Context, req *http.Request) context.Context {
var (
spanContext model.SpanContext
name string
)
if config.name != "" {
name = config.name
} else {
name = req.Method
}
if parent := zipkin.SpanFromContext(ctx); parent != nil {
spanContext = parent.Context()
}
tags := map[string]string{
string(zipkin.TagHTTPMethod): req.Method,
string(zipkin.TagHTTPUrl): req.URL.String(),
}
span := tracer.StartSpan(
name,
zipkin.Kind(model.Client),
zipkin.Tags(config.tags),
zipkin.Tags(tags),
zipkin.Parent(spanContext),
zipkin.FlushOnFinish(false),
)
if config.propagate {
if err := b3.InjectHTTP(req)(span.Context()); err != nil {
config.logger.Log("err", err)
}
}
return zipkin.NewContext(ctx, span)
},
)
clientAfter := kithttp.ClientAfter(
func(ctx context.Context, res *http.Response) context.Context {
if span := zipkin.SpanFromContext(ctx); span != nil {
zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(res.ContentLength, 10))
zipkin.TagHTTPStatusCode.Set(span, strconv.Itoa(res.StatusCode))
if res.StatusCode > 399 {
zipkin.TagError.Set(span, strconv.Itoa(res.StatusCode))
}
span.Finish()
}
return ctx
},
)
clientFinalizer := kithttp.ClientFinalizer(
func(ctx context.Context, err error) {
if span := zipkin.SpanFromContext(ctx); span != nil {
if err != nil {
zipkin.TagError.Set(span, err.Error())
}
// calling span.Finish() a second time is a noop, if we didn't get to
// ClientAfter we can at least time the early bail out by calling it
// here.
span.Finish()
// send span to the Reporter
span.Flush()
}
},
)
return func(c *kithttp.Client) {
clientBefore(c)
clientAfter(c)
clientFinalizer(c)
}
}
// HTTPServerTrace enables native Zipkin tracing of a Go kit HTTP transport
// Server.
//
// Go kit creates HTTP transport servers per HTTP endpoint. This middleware can
// be set-up individually by adding the method name for each of the Go kit
// method servers using the Name() TracerOption.
// If wanting to use the HTTP method (Get, Post, Put, etc.) as Span name you can
// create a global server tracer omitting the Name() TracerOption, which you can
// then feed to each Go kit method server.
//
// If instrumenting a service to external (not on your platform) clients, you
// will probably want to disallow propagation of a client SpanContext using
// the AllowPropagation TracerOption and setting it to false.
func HTTPServerTrace(tracer *zipkin.Tracer, options ...TracerOption) kithttp.ServerOption {
config := tracerOptions{
tags: make(map[string]string),
name: "",
logger: log.NewNopLogger(),
propagate: true,
}
for _, option := range options {
option(&config)
}
serverBefore := kithttp.ServerBefore(
func(ctx context.Context, req *http.Request) context.Context {
var (
spanContext model.SpanContext
name string
)
if config.name != "" {
name = config.name
} else {
name = req.Method
}
if config.propagate {
spanContext = tracer.Extract(b3.ExtractHTTP(req))
if spanContext.Sampled == nil && config.requestSampler != nil {
sample := config.requestSampler(req)
spanContext.Sampled = &sample
}
if spanContext.Err != nil {
config.logger.Log("err", spanContext.Err)
}
}
tags := map[string]string{
string(zipkin.TagHTTPMethod): req.Method,
string(zipkin.TagHTTPPath): req.URL.Path,
}
span := tracer.StartSpan(
name,
zipkin.Kind(model.Server),
zipkin.Tags(config.tags),
zipkin.Tags(tags),
zipkin.Parent(spanContext),
zipkin.FlushOnFinish(false),
)
return zipkin.NewContext(ctx, span)
},
)
serverAfter := kithttp.ServerAfter(
func(ctx context.Context, _ http.ResponseWriter) context.Context {
if span := zipkin.SpanFromContext(ctx); span != nil {
span.Finish()
}
return ctx
},
)
serverFinalizer := kithttp.ServerFinalizer(
func(ctx context.Context, code int, r *http.Request) {
if span := zipkin.SpanFromContext(ctx); span != nil {
zipkin.TagHTTPStatusCode.Set(span, strconv.Itoa(code))
if code > 399 {
// set http status as error tag (if already set, this is a noop)
zipkin.TagError.Set(span, http.StatusText(code))
}
if rs, ok := ctx.Value(kithttp.ContextKeyResponseSize).(int64); ok {
zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(rs, 10))
}
// calling span.Finish() a second time is a noop, if we didn't get to
// ServerAfter we can at least time the early bail out by calling it
// here.
span.Finish()
// send span to the Reporter
span.Flush()
}
},
)
return func(s *kithttp.Server) {
serverBefore(s)
serverAfter(s)
serverFinalizer(s)
}
}
================================================
FILE: tracing/zipkin/http_test.go
================================================
package zipkin_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
zipkin "github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/model"
"github.com/openzipkin/zipkin-go/propagation/b3"
"github.com/openzipkin/zipkin-go/reporter/recorder"
"github.com/go-kit/kit/endpoint"
zipkinkit "github.com/go-kit/kit/tracing/zipkin"
kithttp "github.com/go-kit/kit/transport/http"
)
const (
testName = "test"
testBody = "test_body"
testTagKey = "test_key"
testTagValue = "test_value"
)
func TestHTTPClientTracePropagatesParentSpan(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
tr, _ := zipkin.NewTracer(rec)
rURL, _ := url.Parse("https://httpbin.org/get")
clientTracer := zipkinkit.HTTPClientTrace(tr)
ep := kithttp.NewClient(
"GET",
rURL,
func(ctx context.Context, r *http.Request, i interface{}) error {
return nil
},
func(ctx context.Context, r *http.Response) (response interface{}, err error) {
return nil, nil
},
clientTracer,
).Endpoint()
parentSpan := tr.StartSpan("test")
ctx := zipkin.NewContext(context.Background(), parentSpan)
_, err := ep(ctx, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
span := spans[0]
if span.SpanContext.ParentID == nil {
t.Fatalf("incorrect parent ID, want %s have nil", parentSpan.Context().ID)
}
if want, have := parentSpan.Context().ID, *span.SpanContext.ParentID; want != have {
t.Fatalf("incorrect parent ID, want %s, have %s", want, have)
}
}
func TestHTTPClientTraceAddsExpectedTags(t *testing.T) {
dataProvider := []struct {
ResponseStatusCode int
ErrorTagValue string
}{
{http.StatusOK, ""},
{http.StatusForbidden, fmt.Sprint(http.StatusForbidden)},
}
for _, data := range dataProvider {
testHTTPClientTraceCase(t, data.ResponseStatusCode, data.ErrorTagValue)
}
}
func testHTTPClientTraceCase(t *testing.T, responseStatusCode int, errTagValue string) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(responseStatusCode)
w.Write([]byte(testBody))
}))
defer ts.Close()
rec := recorder.NewReporter()
defer rec.Close()
tr, err := zipkin.NewTracer(rec)
if err != nil {
t.Errorf("Unwanted error: %s", err.Error())
}
rMethod := "GET"
rURL, _ := url.Parse(ts.URL)
clientTracer := zipkinkit.HTTPClientTrace(
tr,
zipkinkit.Name(testName),
zipkinkit.Tags(map[string]string{testTagKey: testTagValue}),
)
ep := kithttp.NewClient(
rMethod,
rURL,
func(ctx context.Context, r *http.Request, i interface{}) error {
return nil
},
func(ctx context.Context, r *http.Response) (response interface{}, err error) {
return nil, nil
},
clientTracer,
).Endpoint()
_, err = ep(context.Background(), nil)
if err != nil {
t.Fatalf("unwanted error: %s", err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, wanted %d, got %d", want, have)
}
span := spans[0]
if span.SpanContext.ParentID != nil {
t.Fatalf("incorrect parentID, wanted nil, got %s", span.SpanContext.ParentID)
}
if want, have := testName, span.Name; want != have {
t.Fatalf("incorrect span name, wanted %s, got %s", want, have)
}
if want, have := model.Client, span.Kind; want != have {
t.Fatalf("incorrect span kind, wanted %s, got %s", want, have)
}
tags := map[string]string{
testTagKey: testTagValue,
string(zipkin.TagHTTPStatusCode): fmt.Sprint(responseStatusCode),
string(zipkin.TagHTTPMethod): rMethod,
string(zipkin.TagHTTPUrl): rURL.String(),
string(zipkin.TagHTTPResponseSize): fmt.Sprint(len(testBody)),
}
if errTagValue != "" {
tags[string(zipkin.TagError)] = fmt.Sprint(errTagValue)
}
if !reflect.DeepEqual(span.Tags, tags) {
t.Fatalf("invalid tags set, wanted %+v, got %+v", tags, span.Tags)
}
}
func TestHTTPServerTrace(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
// explicitly show we use the default of RPC shared spans in Zipkin as it
// is idiomatic for Zipkin to share span identifiers between client and
// server side.
tr, _ := zipkin.NewTracer(rec, zipkin.WithSharedSpans(true))
handler := kithttp.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) { return nil, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return errors.New("dummy") },
zipkinkit.HTTPServerTrace(tr),
)
server := httptest.NewServer(handler)
defer server.Close()
const httpMethod = "GET"
req, err := http.NewRequest(httpMethod, server.URL, nil)
if err != nil {
t.Fatalf("unable to create HTTP request: %s", err.Error())
}
parentSpan := tr.StartSpan("Dummy")
b3.InjectHTTP(req)(parentSpan.Context())
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("unable to send HTTP request: %s", err.Error())
}
resp.Body.Close()
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if want, have := parentSpan.Context().TraceID, spans[0].SpanContext.TraceID; want != have {
t.Errorf("incorrect TraceID, want %+v, have %+v", want, have)
}
if want, have := parentSpan.Context().ID, spans[0].SpanContext.ID; want != have {
t.Errorf("incorrect span ID, want %d, have %d", want, have)
}
if want, have := httpMethod, spans[0].Name; want != have {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := http.StatusText(500), spans[0].Tags["error"]; want != have {
t.Fatalf("incorrect error tag, want %s, have %s", want, have)
}
}
func TestHTTPServerTraceIsRequestBasedSampled(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
const httpMethod = "DELETE"
tr, _ := zipkin.NewTracer(rec)
handler := kithttp.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) { return nil, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
zipkinkit.HTTPServerTrace(tr, zipkinkit.RequestSampler(func(r *http.Request) bool {
return r.Method == httpMethod
})),
)
server := httptest.NewServer(handler)
defer server.Close()
req, err := http.NewRequest(httpMethod, server.URL, nil)
if err != nil {
t.Fatalf("unable to create HTTP request: %s", err.Error())
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("unable to send HTTP request: %s", err.Error())
}
resp.Body.Close()
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
}
================================================
FILE: tracing/zipkin/options.go
================================================
package zipkin
import (
"net/http"
"github.com/go-kit/log"
)
// TracerOption allows for functional options to our Zipkin tracing middleware.
type TracerOption func(o *tracerOptions)
// Name sets the name for an instrumented transport endpoint. If name is omitted
// at tracing middleware creation, the method of the transport or transport rpc
// name is used.
func Name(name string) TracerOption {
return func(o *tracerOptions) {
o.name = name
}
}
// Tags adds default tags to our Zipkin transport spans.
func Tags(tags map[string]string) TracerOption {
return func(o *tracerOptions) {
for k, v := range tags {
o.tags[k] = v
}
}
}
// Logger adds a Go kit logger to our Zipkin Middleware to log SpanContext
// extract / inject errors if they occur. Default is Noop.
func Logger(logger log.Logger) TracerOption {
return func(o *tracerOptions) {
if logger != nil {
o.logger = logger
}
}
}
// AllowPropagation instructs the tracer to allow or deny propagation of the
// span context between this instrumented client or service and its peers. If
// the instrumented client connects to services outside its own platform or if
// the instrumented service receives requests from untrusted clients it is
// strongly advised to disallow propagation. Propagation between services inside
// your own platform benefit from propagation. Default for both TraceClient and
// TraceServer is to allow propagation.
func AllowPropagation(propagate bool) TracerOption {
return func(o *tracerOptions) {
o.propagate = propagate
}
}
// RequestSampler allows one to set the sampling decision based on the details
// found in the http.Request.
func RequestSampler(sampleFunc func(r *http.Request) bool) TracerOption {
return func(o *tracerOptions) {
o.requestSampler = sampleFunc
}
}
type tracerOptions struct {
tags map[string]string
name string
logger log.Logger
propagate bool
requestSampler func(r *http.Request) bool
}
================================================
FILE: transport/amqp/doc.go
================================================
// Package amqp implements an AMQP transport.
package amqp
================================================
FILE: transport/amqp/encode_decode.go
================================================
package amqp
import (
"context"
amqp "github.com/rabbitmq/amqp091-go"
)
// DecodeRequestFunc extracts a user-domain request object from
// an AMQP Delivery object. It is designed to be used in AMQP Subscribers.
type DecodeRequestFunc func(context.Context, *amqp.Delivery) (request interface{}, err error)
// EncodeRequestFunc encodes the passed request object into
// an AMQP Publishing object. It is designed to be used in AMQP Publishers.
type EncodeRequestFunc func(context.Context, *amqp.Publishing, interface{}) error
// EncodeResponseFunc encodes the passed response object to
// an AMQP Publishing object. It is designed to be used in AMQP Subscribers.
type EncodeResponseFunc func(context.Context, *amqp.Publishing, interface{}) error
// DecodeResponseFunc extracts a user-domain response object from
// an AMQP Delivery object. It is designed to be used in AMQP Publishers.
type DecodeResponseFunc func(context.Context, *amqp.Delivery) (response interface{}, err error)
================================================
FILE: transport/amqp/publisher.go
================================================
package amqp
import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
amqp "github.com/rabbitmq/amqp091-go"
)
// The golang AMQP implementation requires the []byte representation of
// correlation id strings to have a maximum length of 255 bytes.
const maxCorrelationIdLength = 255
// Publisher wraps an AMQP channel and queue, and provides a method that
// implements endpoint.Endpoint.
type Publisher struct {
ch Channel
q *amqp.Queue
enc EncodeRequestFunc
dec DecodeResponseFunc
before []RequestFunc
after []PublisherResponseFunc
deliverer Deliverer
timeout time.Duration
}
// NewPublisher constructs a usable Publisher for a single remote method.
func NewPublisher(
ch Channel,
q *amqp.Queue,
enc EncodeRequestFunc,
dec DecodeResponseFunc,
options ...PublisherOption,
) *Publisher {
p := &Publisher{
ch: ch,
q: q,
enc: enc,
dec: dec,
deliverer: DefaultDeliverer,
timeout: 10 * time.Second,
}
for _, option := range options {
option(p)
}
return p
}
// PublisherOption sets an optional parameter for clients.
type PublisherOption func(*Publisher)
// PublisherBefore sets the RequestFuncs that are applied to the outgoing AMQP
// request before it's invoked.
func PublisherBefore(before ...RequestFunc) PublisherOption {
return func(p *Publisher) { p.before = append(p.before, before...) }
}
// PublisherAfter sets the ClientResponseFuncs applied to the incoming AMQP
// request prior to it being decoded. This is useful for obtaining anything off
// of the response and adding onto the context prior to decoding.
func PublisherAfter(after ...PublisherResponseFunc) PublisherOption {
return func(p *Publisher) { p.after = append(p.after, after...) }
}
// PublisherDeliverer sets the deliverer function that the Publisher invokes.
func PublisherDeliverer(deliverer Deliverer) PublisherOption {
return func(p *Publisher) { p.deliverer = deliverer }
}
// PublisherTimeout sets the available timeout for an AMQP request.
func PublisherTimeout(timeout time.Duration) PublisherOption {
return func(p *Publisher) { p.timeout = timeout }
}
// Endpoint returns a usable endpoint that invokes the remote endpoint.
func (p Publisher) Endpoint() endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
pub := amqp.Publishing{
ReplyTo: p.q.Name,
CorrelationId: randomString(randInt(5, maxCorrelationIdLength)),
}
if err := p.enc(ctx, &pub, request); err != nil {
return nil, err
}
for _, f := range p.before {
// Affect only amqp.Publishing
ctx = f(ctx, &pub, nil)
}
deliv, err := p.deliverer(ctx, p, &pub)
if err != nil {
return nil, err
}
for _, f := range p.after {
ctx = f(ctx, deliv)
}
response, err := p.dec(ctx, deliv)
if err != nil {
return nil, err
}
return response, nil
}
}
// Deliverer is invoked by the Publisher to publish the specified Publishing, and to
// retrieve the appropriate response Delivery object.
type Deliverer func(
context.Context,
Publisher,
*amqp.Publishing,
) (*amqp.Delivery, error)
// DefaultDeliverer is a deliverer that publishes the specified Publishing
// and returns the first Delivery object with the matching correlationId.
// If the context times out while waiting for a reply, an error will be returned.
func DefaultDeliverer(
ctx context.Context,
p Publisher,
pub *amqp.Publishing,
) (*amqp.Delivery, error) {
err := p.ch.Publish(
getPublishExchange(ctx),
getPublishKey(ctx),
false, //mandatory
false, //immediate
*pub,
)
if err != nil {
return nil, err
}
autoAck := getConsumeAutoAck(ctx)
msg, err := p.ch.Consume(
p.q.Name,
"", //consumer
autoAck,
false, //exclusive
false, //noLocal
false, //noWait
getConsumeArgs(ctx),
)
if err != nil {
return nil, err
}
for {
select {
case d := <-msg:
if d.CorrelationId == pub.CorrelationId {
if !autoAck {
d.Ack(false) //multiple
}
return &d, nil
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// SendAndForgetDeliverer delivers the supplied publishing and
// returns a nil response.
// When using this deliverer please ensure that the supplied DecodeResponseFunc and
// PublisherResponseFunc are able to handle nil-type responses.
func SendAndForgetDeliverer(
ctx context.Context,
p Publisher,
pub *amqp.Publishing,
) (*amqp.Delivery, error) {
err := p.ch.Publish(
getPublishExchange(ctx),
getPublishKey(ctx),
false, //mandatory
false, //immediate
*pub,
)
return nil, err
}
================================================
FILE: transport/amqp/publisher_test.go
================================================
package amqp_test
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
amqptransport "github.com/go-kit/kit/transport/amqp"
amqp "github.com/rabbitmq/amqp091-go"
)
var (
defaultContentType = ""
defaultContentEncoding = ""
)
// TestBadEncode tests if encode errors are handled properly.
func TestBadEncode(t *testing.T) {
ch := &mockChannel{f: nullFunc}
q := &amqp.Queue{Name: "some queue"}
pub := amqptransport.NewPublisher(
ch,
q,
func(context.Context, *amqp.Publishing, interface{}) error { return errors.New("err!") },
func(context.Context, *amqp.Delivery) (response interface{}, err error) { return struct{}{}, nil },
)
errChan := make(chan error, 1)
var err error
go func() {
_, err := pub.Endpoint()(context.Background(), struct{}{})
errChan <- err
}()
select {
case err = <-errChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for result")
}
if err == nil {
t.Error("expected error")
}
if want, have := "err!", err.Error(); want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestBadDecode tests if decode errors are handled properly.
func TestBadDecode(t *testing.T) {
cid := "correlation"
ch := &mockChannel{
f: nullFunc,
c: make(chan amqp.Publishing, 1),
deliveries: []amqp.Delivery{
amqp.Delivery{
CorrelationId: cid,
},
},
}
q := &amqp.Queue{Name: "some queue"}
pub := amqptransport.NewPublisher(
ch,
q,
func(context.Context, *amqp.Publishing, interface{}) error { return nil },
func(context.Context, *amqp.Delivery) (response interface{}, err error) {
return struct{}{}, errors.New("err!")
},
amqptransport.PublisherBefore(
amqptransport.SetCorrelationID(cid),
),
)
var err error
errChan := make(chan error, 1)
go func() {
_, err := pub.Endpoint()(context.Background(), struct{}{})
errChan <- err
}()
select {
case err = <-errChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for result")
}
if err == nil {
t.Error("expected error")
}
if want, have := "err!", err.Error(); want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestPublisherTimeout ensures that the publisher timeout mechanism works.
func TestPublisherTimeout(t *testing.T) {
ch := &mockChannel{
f: nullFunc,
c: make(chan amqp.Publishing, 1),
deliveries: []amqp.Delivery{}, // no reply from mock subscriber
}
q := &amqp.Queue{Name: "some queue"}
pub := amqptransport.NewPublisher(
ch,
q,
func(context.Context, *amqp.Publishing, interface{}) error { return nil },
func(context.Context, *amqp.Delivery) (response interface{}, err error) {
return struct{}{}, nil
},
amqptransport.PublisherTimeout(50*time.Millisecond),
)
var err error
errChan := make(chan error, 1)
go func() {
_, err := pub.Endpoint()(context.Background(), struct{}{})
errChan <- err
}()
select {
case err = <-errChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for result")
}
if err == nil {
t.Error("expected error")
}
if want, have := context.DeadlineExceeded.Error(), err.Error(); want != have {
t.Errorf("want %s, have %s", want, have)
}
}
func TestSuccessfulPublisher(t *testing.T) {
cid := "correlation"
mockReq := testReq{437}
mockRes := testRes{
Squadron: mockReq.Squadron,
Name: names[mockReq.Squadron],
}
b, err := json.Marshal(mockRes)
if err != nil {
t.Fatal(err)
}
reqChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{
f: nullFunc,
c: reqChan,
deliveries: []amqp.Delivery{
amqp.Delivery{
CorrelationId: cid,
Body: b,
},
},
}
q := &amqp.Queue{Name: "some queue"}
pub := amqptransport.NewPublisher(
ch,
q,
testReqEncoder,
testResDeliveryDecoder,
amqptransport.PublisherBefore(
amqptransport.SetCorrelationID(cid),
),
)
var publishing amqp.Publishing
var res testRes
var ok bool
resChan := make(chan interface{}, 1)
errChan := make(chan error, 1)
go func() {
res, err := pub.Endpoint()(context.Background(), mockReq)
if err != nil {
errChan <- err
} else {
resChan <- res
}
}()
select {
case publishing = <-reqChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for request")
}
if want, have := defaultContentType, publishing.ContentType; want != have {
t.Errorf("want %s, have %s", want, have)
}
if want, have := defaultContentEncoding, publishing.ContentEncoding; want != have {
t.Errorf("want %s, have %s", want, have)
}
select {
case response := <-resChan:
res, ok = response.(testRes)
if !ok {
t.Error("failed to assert endpoint response type")
}
break
case err = <-errChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for result")
}
if err != nil {
t.Fatal(err)
}
if want, have := mockRes.Name, res.Name; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestSendAndForgetPublisher tests that the SendAndForgetDeliverer is working
func TestSendAndForgetPublisher(t *testing.T) {
ch := &mockChannel{
f: nullFunc,
c: make(chan amqp.Publishing, 1),
deliveries: []amqp.Delivery{}, // no reply from mock subscriber
}
q := &amqp.Queue{Name: "some queue"}
pub := amqptransport.NewPublisher(
ch,
q,
func(context.Context, *amqp.Publishing, interface{}) error { return nil },
func(context.Context, *amqp.Delivery) (response interface{}, err error) {
return struct{}{}, nil
},
amqptransport.PublisherDeliverer(amqptransport.SendAndForgetDeliverer),
amqptransport.PublisherTimeout(50*time.Millisecond),
)
var err error
errChan := make(chan error, 1)
finishChan := make(chan bool, 1)
go func() {
_, err := pub.Endpoint()(context.Background(), struct{}{})
if err != nil {
errChan <- err
} else {
finishChan <- true
}
}()
select {
case <-finishChan:
break
case err = <-errChan:
t.Errorf("unexpected error %s", err)
case <-time.After(100 * time.Millisecond):
t.Fatal("timed out waiting for result")
}
}
================================================
FILE: transport/amqp/request_response_func.go
================================================
package amqp
import (
"context"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
// RequestFunc may take information from a publisher request and put it into a
// request context. In Subscribers, RequestFuncs are executed prior to invoking
// the endpoint.
type RequestFunc func(context.Context, *amqp.Publishing, *amqp.Delivery) context.Context
// SubscriberResponseFunc may take information from a request context and use it to
// manipulate a Publisher. SubscriberResponseFuncs are only executed in
// subscribers, after invoking the endpoint but prior to publishing a reply.
type SubscriberResponseFunc func(context.Context,
*amqp.Delivery,
Channel,
*amqp.Publishing,
) context.Context
// PublisherResponseFunc may take information from an AMQP request and make the
// response available for consumption. PublisherResponseFunc are only executed
// in publishers, after a request has been made, but prior to it being decoded.
type PublisherResponseFunc func(context.Context, *amqp.Delivery) context.Context
// SetPublishExchange returns a RequestFunc that sets the Exchange field
// of an AMQP Publish call.
func SetPublishExchange(publishExchange string) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
return context.WithValue(ctx, ContextKeyExchange, publishExchange)
}
}
// SetPublishKey returns a RequestFunc that sets the Key field
// of an AMQP Publish call.
func SetPublishKey(publishKey string) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
return context.WithValue(ctx, ContextKeyPublishKey, publishKey)
}
}
// SetPublishDeliveryMode sets the delivery mode of a Publishing.
// Please refer to AMQP delivery mode constants in the AMQP package.
func SetPublishDeliveryMode(dmode uint8) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
pub.DeliveryMode = dmode
return ctx
}
}
// SetNackSleepDuration returns a RequestFunc that sets the amount of time
// to sleep in the event of a Nack.
// This has to be used in conjunction with an error encoder that Nack and sleeps.
// One example is the SingleNackRequeueErrorEncoder.
// It is designed to be used by Subscribers.
func SetNackSleepDuration(duration time.Duration) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
return context.WithValue(ctx, ContextKeyNackSleepDuration, duration)
}
}
// SetConsumeAutoAck returns a RequestFunc that sets whether or not to autoAck
// messages when consuming.
// When set to false, the publisher will Ack the first message it receives with
// a matching correlationId.
// It is designed to be used by Publishers.
func SetConsumeAutoAck(autoAck bool) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
return context.WithValue(ctx, ContextKeyAutoAck, autoAck)
}
}
// SetConsumeArgs returns a RequestFunc that set the arguments for amqp Consume
// function.
// It is designed to be used by Publishers.
func SetConsumeArgs(args amqp.Table) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
return context.WithValue(ctx, ContextKeyConsumeArgs, args)
}
}
// SetContentType returns a RequestFunc that sets the ContentType field of
// an AMQP Publishing.
func SetContentType(contentType string) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
pub.ContentType = contentType
return ctx
}
}
// SetContentEncoding returns a RequestFunc that sets the ContentEncoding field
// of an AMQP Publishing.
func SetContentEncoding(contentEncoding string) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
pub.ContentEncoding = contentEncoding
return ctx
}
}
// SetCorrelationID returns a RequestFunc that sets the CorrelationId field
// of an AMQP Publishing.
func SetCorrelationID(cid string) RequestFunc {
return func(ctx context.Context, pub *amqp.Publishing, _ *amqp.Delivery) context.Context {
pub.CorrelationId = cid
return ctx
}
}
// SetAckAfterEndpoint returns a SubscriberResponseFunc that prompts the service
// to Ack the Delivery object after successfully evaluating the endpoint,
// and before it encodes the response.
// It is designed to be used by Subscribers.
func SetAckAfterEndpoint(multiple bool) SubscriberResponseFunc {
return func(ctx context.Context,
deliv *amqp.Delivery,
ch Channel,
pub *amqp.Publishing,
) context.Context {
deliv.Ack(multiple)
return ctx
}
}
func getPublishExchange(ctx context.Context) string {
if exchange := ctx.Value(ContextKeyExchange); exchange != nil {
return exchange.(string)
}
return ""
}
func getPublishKey(ctx context.Context) string {
if publishKey := ctx.Value(ContextKeyPublishKey); publishKey != nil {
return publishKey.(string)
}
return ""
}
func getNackSleepDuration(ctx context.Context) time.Duration {
if duration := ctx.Value(ContextKeyNackSleepDuration); duration != nil {
return duration.(time.Duration)
}
return 0
}
func getConsumeAutoAck(ctx context.Context) bool {
if autoAck := ctx.Value(ContextKeyAutoAck); autoAck != nil {
return autoAck.(bool)
}
return false
}
func getConsumeArgs(ctx context.Context) amqp.Table {
if args := ctx.Value(ContextKeyConsumeArgs); args != nil {
return args.(amqp.Table)
}
return nil
}
type contextKey int
const (
// ContextKeyExchange is the value of the reply Exchange in
// amqp.Publish.
ContextKeyExchange contextKey = iota
// ContextKeyPublishKey is the value of the ReplyTo field in
// amqp.Publish.
ContextKeyPublishKey
// ContextKeyNackSleepDuration is the duration to sleep for if the
// service Nack and requeues a message.
// This is to prevent sporadic send-resending of message
// when a message is constantly Nack'd and requeued.
ContextKeyNackSleepDuration
// ContextKeyAutoAck is the value of autoAck field when calling
// amqp.Channel.Consume.
ContextKeyAutoAck
// ContextKeyConsumeArgs is the value of consumeArgs field when calling
// amqp.Channel.Consume.
ContextKeyConsumeArgs
)
================================================
FILE: transport/amqp/subscriber.go
================================================
package amqp
import (
"context"
"encoding/json"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
amqp "github.com/rabbitmq/amqp091-go"
)
// Subscriber wraps an endpoint and provides a handler for AMQP Delivery messages.
type Subscriber struct {
e endpoint.Endpoint
dec DecodeRequestFunc
enc EncodeResponseFunc
before []RequestFunc
after []SubscriberResponseFunc
responsePublisher ResponsePublisher
errorEncoder ErrorEncoder
errorHandler transport.ErrorHandler
}
// NewSubscriber constructs a new subscriber, which provides a handler
// for AMQP Delivery messages.
func NewSubscriber(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...SubscriberOption,
) *Subscriber {
s := &Subscriber{
e: e,
dec: dec,
enc: enc,
responsePublisher: DefaultResponsePublisher,
errorEncoder: DefaultErrorEncoder,
errorHandler: transport.NewLogErrorHandler(log.NewNopLogger()),
}
for _, option := range options {
option(s)
}
return s
}
// SubscriberOption sets an optional parameter for subscribers.
type SubscriberOption func(*Subscriber)
// SubscriberBefore functions are executed on the publisher delivery object
// before the request is decoded.
func SubscriberBefore(before ...RequestFunc) SubscriberOption {
return func(s *Subscriber) { s.before = append(s.before, before...) }
}
// SubscriberAfter functions are executed on the subscriber reply after the
// endpoint is invoked, but before anything is published to the reply.
func SubscriberAfter(after ...SubscriberResponseFunc) SubscriberOption {
return func(s *Subscriber) { s.after = append(s.after, after...) }
}
// SubscriberResponsePublisher is used by the subscriber to deliver response
// objects to the original sender.
// By default, the DefaultResponsePublisher is used.
func SubscriberResponsePublisher(rp ResponsePublisher) SubscriberOption {
return func(s *Subscriber) { s.responsePublisher = rp }
}
// SubscriberErrorEncoder is used to encode errors to the subscriber reply
// whenever they're encountered in the processing of a request. Clients can
// use this to provide custom error formatting. By default,
// errors will be published with the DefaultErrorEncoder.
func SubscriberErrorEncoder(ee ErrorEncoder) SubscriberOption {
return func(s *Subscriber) { s.errorEncoder = ee }
}
// SubscriberErrorLogger is used to log non-terminal errors. By default, no errors
// are logged. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom SubscriberErrorEncoder which has access to the context.
// Deprecated: Use SubscriberErrorHandler instead.
func SubscriberErrorLogger(logger log.Logger) SubscriberOption {
return func(s *Subscriber) { s.errorHandler = transport.NewLogErrorHandler(logger) }
}
// SubscriberErrorHandler is used to handle non-terminal errors. By default, non-terminal errors
// are ignored. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom SubscriberErrorEncoder which has access to the context.
func SubscriberErrorHandler(errorHandler transport.ErrorHandler) SubscriberOption {
return func(s *Subscriber) { s.errorHandler = errorHandler }
}
// ServeDelivery handles AMQP Delivery messages
// It is strongly recommended to use *amqp.Channel as the
// Channel interface implementation.
func (s Subscriber) ServeDelivery(ch Channel) func(deliv *amqp.Delivery) {
return func(deliv *amqp.Delivery) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
pub := amqp.Publishing{}
for _, f := range s.before {
ctx = f(ctx, &pub, deliv)
}
request, err := s.dec(ctx, deliv)
if err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, deliv, ch, &pub)
return
}
response, err := s.e(ctx, request)
if err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, deliv, ch, &pub)
return
}
for _, f := range s.after {
ctx = f(ctx, deliv, ch, &pub)
}
if err := s.enc(ctx, &pub, response); err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, deliv, ch, &pub)
return
}
if err := s.responsePublisher(ctx, deliv, ch, &pub); err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, deliv, ch, &pub)
return
}
}
}
// EncodeJSONResponse marshals the response as JSON as part of the
// payload of the AMQP Publishing object.
func EncodeJSONResponse(
ctx context.Context,
pub *amqp.Publishing,
response interface{},
) error {
b, err := json.Marshal(response)
if err != nil {
return err
}
pub.Body = b
return nil
}
// EncodeNopResponse is a response function that does nothing.
func EncodeNopResponse(
ctx context.Context,
pub *amqp.Publishing,
response interface{},
) error {
return nil
}
// ResponsePublisher functions are executed by the subscriber to
// publish response object to the original sender.
// Please note that the word "publisher" does not refer
// to the publisher of pub/sub.
// Rather, publisher is merely a function that publishes, or sends responses.
type ResponsePublisher func(
context.Context,
*amqp.Delivery,
Channel,
*amqp.Publishing,
) error
// DefaultResponsePublisher extracts the reply exchange and reply key
// from the request, and sends the response object to that destination.
func DefaultResponsePublisher(
ctx context.Context,
deliv *amqp.Delivery,
ch Channel,
pub *amqp.Publishing,
) error {
if pub.CorrelationId == "" {
pub.CorrelationId = deliv.CorrelationId
}
replyExchange := getPublishExchange(ctx)
replyTo := getPublishKey(ctx)
if replyTo == "" {
replyTo = deliv.ReplyTo
}
return ch.Publish(
replyExchange,
replyTo,
false, // mandatory
false, // immediate
*pub,
)
}
// NopResponsePublisher does not deliver a response to the original sender.
// This response publisher is used when the user wants the subscriber to
// receive and forget.
func NopResponsePublisher(
ctx context.Context,
deliv *amqp.Delivery,
ch Channel,
pub *amqp.Publishing,
) error {
return nil
}
// ErrorEncoder is responsible for encoding an error to the subscriber reply.
// Users are encouraged to use custom ErrorEncoders to encode errors to
// their replies, and will likely want to pass and check for their own error
// types.
type ErrorEncoder func(ctx context.Context,
err error, deliv *amqp.Delivery, ch Channel, pub *amqp.Publishing)
// DefaultErrorEncoder simply ignores the message. It does not reply
// nor Ack/Nack the message.
func DefaultErrorEncoder(ctx context.Context,
err error, deliv *amqp.Delivery, ch Channel, pub *amqp.Publishing) {
}
// SingleNackRequeueErrorEncoder issues a Nack to the delivery with multiple flag set as false
// and requeue flag set as true. It does not reply the message.
func SingleNackRequeueErrorEncoder(ctx context.Context,
err error, deliv *amqp.Delivery, ch Channel, pub *amqp.Publishing) {
deliv.Nack(
false, //multiple
true, //requeue
)
duration := getNackSleepDuration(ctx)
time.Sleep(duration)
}
// ReplyErrorEncoder serializes the error message as a DefaultErrorResponse
// JSON and sends the message to the ReplyTo address.
func ReplyErrorEncoder(
ctx context.Context,
err error,
deliv *amqp.Delivery,
ch Channel,
pub *amqp.Publishing,
) {
if pub.CorrelationId == "" {
pub.CorrelationId = deliv.CorrelationId
}
replyExchange := getPublishExchange(ctx)
replyTo := getPublishKey(ctx)
if replyTo == "" {
replyTo = deliv.ReplyTo
}
response := DefaultErrorResponse{err.Error()}
b, err := json.Marshal(response)
if err != nil {
return
}
pub.Body = b
ch.Publish(
replyExchange,
replyTo,
false, // mandatory
false, // immediate
*pub,
)
}
// ReplyAndAckErrorEncoder serializes the error message as a DefaultErrorResponse
// JSON and sends the message to the ReplyTo address then Acks the original
// message.
func ReplyAndAckErrorEncoder(ctx context.Context, err error, deliv *amqp.Delivery, ch Channel, pub *amqp.Publishing) {
ReplyErrorEncoder(ctx, err, deliv, ch, pub)
deliv.Ack(false)
}
// DefaultErrorResponse is the default structure of responses in the event
// of an error.
type DefaultErrorResponse struct {
Error string `json:"err"`
}
// Channel is a channel interface to make testing possible.
// It is highly recommended to use *amqp.Channel as the interface implementation.
type Channel interface {
Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error
Consume(queue, consumer string, autoAck, exclusive, noLocal, noWail bool, args amqp.Table) (<-chan amqp.Delivery, error)
}
================================================
FILE: transport/amqp/subscriber_test.go
================================================
package amqp_test
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
amqptransport "github.com/go-kit/kit/transport/amqp"
amqp "github.com/rabbitmq/amqp091-go"
)
var (
errTypeAssertion = errors.New("type assertion error")
)
// mockChannel is a mock of *amqp.Channel.
type mockChannel struct {
f func(exchange, key string, mandatory, immediate bool)
c chan<- amqp.Publishing
deliveries []amqp.Delivery
}
// Publish runs a test function f and sends resultant message to a channel.
func (ch *mockChannel) Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error {
ch.f(exchange, key, mandatory, immediate)
ch.c <- msg
return nil
}
var nullFunc = func(exchange, key string, mandatory, immediate bool) {
}
func (ch *mockChannel) Consume(queue, consumer string, autoAck, exclusive, noLocal, noWail bool, args amqp.Table) (<-chan amqp.Delivery, error) {
c := make(chan amqp.Delivery, len(ch.deliveries))
for _, d := range ch.deliveries {
c <- d
}
return c, nil
}
// TestSubscriberBadDecode checks if decoder errors are handled properly.
func TestSubscriberBadDecode(t *testing.T) {
sub := amqptransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *amqp.Delivery) (interface{}, error) { return nil, errors.New("err!") },
func(context.Context, *amqp.Publishing, interface{}) error {
return nil
},
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
)
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: nullFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{})
var msg amqp.Publishing
select {
case msg = <-outputChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for publishing")
}
res, err := decodeSubscriberError(msg)
if err != nil {
t.Fatal(err)
}
if want, have := "err!", res.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestSubscriberBadEndpoint checks if endpoint errors are handled properly.
func TestSubscriberBadEndpoint(t *testing.T) {
sub := amqptransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return nil, errors.New("err!") },
func(context.Context, *amqp.Delivery) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *amqp.Publishing, interface{}) error {
return nil
},
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
)
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: nullFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{})
var msg amqp.Publishing
select {
case msg = <-outputChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for publishing")
}
res, err := decodeSubscriberError(msg)
if err != nil {
t.Fatal(err)
}
if want, have := "err!", res.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestSubscriberBadEncoder checks if encoder errors are handled properly.
func TestSubscriberBadEncoder(t *testing.T) {
sub := amqptransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *amqp.Delivery) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *amqp.Publishing, interface{}) error {
return errors.New("err!")
},
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
)
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: nullFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{})
var msg amqp.Publishing
select {
case msg = <-outputChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for publishing")
}
res, err := decodeSubscriberError(msg)
if err != nil {
t.Fatal(err)
}
if want, have := "err!", res.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestSubscriberSuccess checks if CorrelationId and ReplyTo are set properly
// and if the payload is encoded properly.
func TestSubscriberSuccess(t *testing.T) {
cid := "correlation"
replyTo := "sender"
obj := testReq{
Squadron: 436,
}
b, err := json.Marshal(obj)
if err != nil {
t.Fatal(err)
}
sub := amqptransport.NewSubscriber(
testEndpoint,
testReqDecoder,
amqptransport.EncodeJSONResponse,
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
)
checkReplyToFunc := func(exchange, key string, mandatory, immediate bool) {
if want, have := replyTo, key; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: checkReplyToFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{
CorrelationId: cid,
ReplyTo: replyTo,
Body: b,
})
var msg amqp.Publishing
select {
case msg = <-outputChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for publishing")
}
if want, have := cid, msg.CorrelationId; want != have {
t.Errorf("want %s, have %s", want, have)
}
// check if error is not thrown
errRes, err := decodeSubscriberError(msg)
if err != nil {
t.Fatal(err)
}
if errRes.Error != "" {
t.Error("Received error from subscriber", errRes.Error)
return
}
// check obj vals
response, err := testResDecoder(msg.Body)
if err != nil {
t.Fatal(err)
}
res, ok := response.(testRes)
if !ok {
t.Error(errTypeAssertion)
}
if want, have := obj.Squadron, res.Squadron; want != have {
t.Errorf("want %d, have %d", want, have)
}
if want, have := names[obj.Squadron], res.Name; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
// TestNopResponseSubscriber checks if setting responsePublisher to
// NopResponsePublisher works properly by disabling response.
func TestNopResponseSubscriber(t *testing.T) {
cid := "correlation"
replyTo := "sender"
obj := testReq{
Squadron: 436,
}
b, err := json.Marshal(obj)
if err != nil {
t.Fatal(err)
}
sub := amqptransport.NewSubscriber(
testEndpoint,
testReqDecoder,
amqptransport.EncodeJSONResponse,
amqptransport.SubscriberResponsePublisher(amqptransport.NopResponsePublisher),
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
)
checkReplyToFunc := func(exchange, key string, mandatory, immediate bool) {}
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: checkReplyToFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{
CorrelationId: cid,
ReplyTo: replyTo,
Body: b,
})
select {
case <-outputChan:
t.Fatal("Subscriber with NopResponsePublisher replied.")
case <-time.After(100 * time.Millisecond):
break
}
}
// TestSubscriberMultipleBefore checks if options to set exchange, key, deliveryMode
// are working.
func TestSubscriberMultipleBefore(t *testing.T) {
exchange := "some exchange"
key := "some key"
deliveryMode := uint8(127)
contentType := "some content type"
contentEncoding := "some content encoding"
sub := amqptransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *amqp.Delivery) (interface{}, error) { return struct{}{}, nil },
amqptransport.EncodeJSONResponse,
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
amqptransport.SubscriberBefore(
amqptransport.SetPublishExchange(exchange),
amqptransport.SetPublishKey(key),
amqptransport.SetPublishDeliveryMode(deliveryMode),
amqptransport.SetContentType(contentType),
amqptransport.SetContentEncoding(contentEncoding),
),
)
checkReplyToFunc := func(exch, k string, mandatory, immediate bool) {
if want, have := exchange, exch; want != have {
t.Errorf("want %s, have %s", want, have)
}
if want, have := key, k; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: checkReplyToFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{})
var msg amqp.Publishing
select {
case msg = <-outputChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for publishing")
}
// check if error is not thrown
errRes, err := decodeSubscriberError(msg)
if err != nil {
t.Fatal(err)
}
if errRes.Error != "" {
t.Error("Received error from subscriber", errRes.Error)
return
}
if want, have := contentType, msg.ContentType; want != have {
t.Errorf("want %s, have %s", want, have)
}
if want, have := contentEncoding, msg.ContentEncoding; want != have {
t.Errorf("want %s, have %s", want, have)
}
if want, have := deliveryMode, msg.DeliveryMode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
// TestDefaultContentMetaData checks that default ContentType and Content-Encoding
// is not set as mentioned by AMQP specification.
func TestDefaultContentMetaData(t *testing.T) {
defaultContentType := ""
defaultContentEncoding := ""
sub := amqptransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *amqp.Delivery) (interface{}, error) { return struct{}{}, nil },
amqptransport.EncodeJSONResponse,
amqptransport.SubscriberErrorEncoder(amqptransport.ReplyErrorEncoder),
)
checkReplyToFunc := func(exch, k string, mandatory, immediate bool) {}
outputChan := make(chan amqp.Publishing, 1)
ch := &mockChannel{f: checkReplyToFunc, c: outputChan}
sub.ServeDelivery(ch)(&amqp.Delivery{})
var msg amqp.Publishing
select {
case msg = <-outputChan:
break
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for publishing")
}
// check if error is not thrown
errRes, err := decodeSubscriberError(msg)
if err != nil {
t.Fatal(err)
}
if errRes.Error != "" {
t.Error("Received error from subscriber", errRes.Error)
return
}
if want, have := defaultContentType, msg.ContentType; want != have {
t.Errorf("want %s, have %s", want, have)
}
if want, have := defaultContentEncoding, msg.ContentEncoding; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
func decodeSubscriberError(pub amqp.Publishing) (amqptransport.DefaultErrorResponse, error) {
var res amqptransport.DefaultErrorResponse
err := json.Unmarshal(pub.Body, &res)
return res, err
}
type testReq struct {
Squadron int `json:"s"`
}
type testRes struct {
Squadron int `json:"s"`
Name string `json:"n"`
}
func testEndpoint(_ context.Context, request interface{}) (interface{}, error) {
req, ok := request.(testReq)
if !ok {
return nil, errTypeAssertion
}
name, prs := names[req.Squadron]
if !prs {
return nil, errors.New("unknown squadron name")
}
res := testRes{
Squadron: req.Squadron,
Name: name,
}
return res, nil
}
func testReqDecoder(_ context.Context, d *amqp.Delivery) (interface{}, error) {
var obj testReq
err := json.Unmarshal(d.Body, &obj)
return obj, err
}
func testReqEncoder(_ context.Context, p *amqp.Publishing, request interface{}) error {
req, ok := request.(testReq)
if !ok {
return errors.New("type assertion failure")
}
b, err := json.Marshal(req)
if err != nil {
return err
}
p.Body = b
return nil
}
func testResDeliveryDecoder(_ context.Context, d *amqp.Delivery) (interface{}, error) {
return testResDecoder(d.Body)
}
func testResDecoder(b []byte) (interface{}, error) {
var obj testRes
err := json.Unmarshal(b, &obj)
return obj, err
}
var names = map[int]string{
424: "tiger",
426: "thunderbird",
429: "bison",
436: "tusker",
437: "husky",
}
================================================
FILE: transport/amqp/util.go
================================================
package amqp
import (
"math/rand"
)
func randomString(l int) string {
bytes := make([]byte, l)
for i := 0; i < l; i++ {
bytes[i] = byte(randInt(65, 90))
}
return string(bytes)
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}
================================================
FILE: transport/awslambda/doc.go
================================================
// Package awslambda provides an AWS Lambda transport layer.
package awslambda
================================================
FILE: transport/awslambda/encode_decode.go
================================================
package awslambda
import (
"context"
)
// DecodeRequestFunc extracts a user-domain request object from an
// AWS Lambda payload.
type DecodeRequestFunc func(context.Context, []byte) (interface{}, error)
// EncodeResponseFunc encodes the passed response object into []byte,
// ready to be sent as AWS Lambda response.
type EncodeResponseFunc func(context.Context, interface{}) ([]byte, error)
// ErrorEncoder is responsible for encoding an error.
type ErrorEncoder func(ctx context.Context, err error) ([]byte, error)
================================================
FILE: transport/awslambda/handler.go
================================================
package awslambda
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
)
// Handler wraps an endpoint.
type Handler struct {
e endpoint.Endpoint
dec DecodeRequestFunc
enc EncodeResponseFunc
before []HandlerRequestFunc
after []HandlerResponseFunc
errorEncoder ErrorEncoder
finalizer []HandlerFinalizerFunc
errorHandler transport.ErrorHandler
}
// NewHandler constructs a new handler, which implements
// the AWS lambda.Handler interface.
func NewHandler(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...HandlerOption,
) *Handler {
h := &Handler{
e: e,
dec: dec,
enc: enc,
errorEncoder: DefaultErrorEncoder,
errorHandler: transport.NewLogErrorHandler(log.NewNopLogger()),
}
for _, option := range options {
option(h)
}
return h
}
// HandlerOption sets an optional parameter for handlers.
type HandlerOption func(*Handler)
// HandlerBefore functions are executed on the payload byte,
// before the request is decoded.
func HandlerBefore(before ...HandlerRequestFunc) HandlerOption {
return func(h *Handler) { h.before = append(h.before, before...) }
}
// HandlerAfter functions are only executed after invoking the endpoint
// but prior to returning a response.
func HandlerAfter(after ...HandlerResponseFunc) HandlerOption {
return func(h *Handler) { h.after = append(h.after, after...) }
}
// HandlerErrorLogger is used to log non-terminal errors.
// By default, no errors are logged.
// Deprecated: Use HandlerErrorHandler instead.
func HandlerErrorLogger(logger log.Logger) HandlerOption {
return func(h *Handler) { h.errorHandler = transport.NewLogErrorHandler(logger) }
}
// HandlerErrorHandler is used to handle non-terminal errors.
// By default, non-terminal errors are ignored.
func HandlerErrorHandler(errorHandler transport.ErrorHandler) HandlerOption {
return func(h *Handler) { h.errorHandler = errorHandler }
}
// HandlerErrorEncoder is used to encode errors.
func HandlerErrorEncoder(ee ErrorEncoder) HandlerOption {
return func(h *Handler) { h.errorEncoder = ee }
}
// HandlerFinalizer sets finalizer which are called at the end of
// request. By default no finalizer is registered.
func HandlerFinalizer(f ...HandlerFinalizerFunc) HandlerOption {
return func(h *Handler) { h.finalizer = append(h.finalizer, f...) }
}
// DefaultErrorEncoder defines the default behavior of encoding an error response,
// where it returns nil, and the error itself.
func DefaultErrorEncoder(ctx context.Context, err error) ([]byte, error) {
return nil, err
}
// Invoke represents implementation of the AWS lambda.Handler interface.
func (h *Handler) Invoke(
ctx context.Context,
payload []byte,
) (resp []byte, err error) {
if len(h.finalizer) > 0 {
defer func() {
for _, f := range h.finalizer {
f(ctx, resp, err)
}
}()
}
for _, f := range h.before {
ctx = f(ctx, payload)
}
request, err := h.dec(ctx, payload)
if err != nil {
h.errorHandler.Handle(ctx, err)
return h.errorEncoder(ctx, err)
}
response, err := h.e(ctx, request)
if err != nil {
h.errorHandler.Handle(ctx, err)
return h.errorEncoder(ctx, err)
}
for _, f := range h.after {
ctx = f(ctx, response)
}
if resp, err = h.enc(ctx, response); err != nil {
h.errorHandler.Handle(ctx, err)
return h.errorEncoder(ctx, err)
}
return resp, err
}
================================================
FILE: transport/awslambda/handler_test.go
================================================
package awslambda
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
)
type key int
const (
KeyBeforeOne key = iota
KeyBeforeTwo key = iota
KeyAfterOne key = iota
KeyEncMode key = iota
)
// Created based on github.com/aws/aws-lambda-go@v1.13.3/events.APIGatewayProxyRequest for the purposes of the tests below.
type apiGatewayProxyRequest struct {
Body string `json:"body"`
}
// Created based on github.com/aws/aws-lambda-go@v1.13.3/events.APIGatewayProxyResponse for the purposes of the tests below.
type apiGatewayProxyResponse struct {
StatusCode int `json:"statusCode"`
Body string `json:"body"`
}
func TestDefaultErrorEncoder(t *testing.T) {
ctx := context.Background()
rootErr := fmt.Errorf("root")
b, err := DefaultErrorEncoder(ctx, rootErr)
if b != nil {
t.Fatalf("DefaultErrorEncoder should return nil as []byte")
}
if err != rootErr {
t.Fatalf("DefaultErrorEncoder expects return back the given error.")
}
}
func TestInvokeHappyPath(t *testing.T) {
svc := serviceTest01{}
helloHandler := NewHandler(
makeTest01HelloEndpoint(svc),
decodeHelloRequestWithTwoBefores,
encodeResponse,
HandlerErrorHandler(transport.NewLogErrorHandler(log.NewNopLogger())),
HandlerBefore(func(
ctx context.Context,
payload []byte,
) context.Context {
ctx = context.WithValue(ctx, KeyBeforeOne, "bef1")
return ctx
}),
HandlerBefore(func(
ctx context.Context,
payload []byte,
) context.Context {
ctx = context.WithValue(ctx, KeyBeforeTwo, "bef2")
return ctx
}),
HandlerAfter(func(
ctx context.Context,
response interface{},
) context.Context {
ctx = context.WithValue(ctx, KeyAfterOne, "af1")
return ctx
}),
HandlerAfter(func(
ctx context.Context,
response interface{},
) context.Context {
if _, ok := ctx.Value(KeyAfterOne).(string); !ok {
t.Fatalf("Value was not set properly during multi HandlerAfter")
}
return ctx
}),
HandlerFinalizer(func(
_ context.Context,
resp []byte,
_ error,
) {
apigwResp := apiGatewayProxyResponse{}
err := json.Unmarshal(resp, &apigwResp)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
response := helloResponse{}
err = json.Unmarshal([]byte(apigwResp.Body), &response)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
expectedGreeting := "hello john doe bef1 bef2"
if response.Greeting != expectedGreeting {
t.Fatalf(
"Expect: %s, Actual: %s", expectedGreeting, response.Greeting)
}
}),
)
ctx := context.Background()
req, _ := json.Marshal(apiGatewayProxyRequest{
Body: `{"name":"john doe"}`,
})
resp, err := helloHandler.Invoke(ctx, req)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
apigwResp := apiGatewayProxyResponse{}
err = json.Unmarshal(resp, &apigwResp)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
response := helloResponse{}
err = json.Unmarshal([]byte(apigwResp.Body), &response)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
expectedGreeting := "hello john doe bef1 bef2"
if response.Greeting != expectedGreeting {
t.Fatalf(
"Expect: %s, Actual: %s", expectedGreeting, response.Greeting)
}
}
func TestInvokeFailDecode(t *testing.T) {
svc := serviceTest01{}
helloHandler := NewHandler(
makeTest01HelloEndpoint(svc),
decodeHelloRequestWithTwoBefores,
encodeResponse,
HandlerErrorEncoder(func(
ctx context.Context,
err error,
) ([]byte, error) {
apigwResp := apiGatewayProxyResponse{}
apigwResp.Body = `{"error":"yes"}`
apigwResp.StatusCode = 500
resp, err := json.Marshal(apigwResp)
return resp, err
}),
)
ctx := context.Background()
req, _ := json.Marshal(apiGatewayProxyRequest{
Body: `{"name":"john doe"}`,
})
resp, err := helloHandler.Invoke(ctx, req)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
apigwResp := apiGatewayProxyResponse{}
json.Unmarshal(resp, &apigwResp)
if apigwResp.StatusCode != 500 {
t.Fatalf("Expect status code of 500, instead of %d", apigwResp.StatusCode)
}
}
func TestInvokeFailEndpoint(t *testing.T) {
svc := serviceTest01{}
helloHandler := NewHandler(
makeTest01FailEndpoint(svc),
decodeHelloRequestWithTwoBefores,
encodeResponse,
HandlerBefore(func(
ctx context.Context,
payload []byte,
) context.Context {
ctx = context.WithValue(ctx, KeyBeforeOne, "bef1")
return ctx
}),
HandlerBefore(func(
ctx context.Context,
payload []byte,
) context.Context {
ctx = context.WithValue(ctx, KeyBeforeTwo, "bef2")
return ctx
}),
HandlerErrorEncoder(func(
ctx context.Context,
err error,
) ([]byte, error) {
apigwResp := apiGatewayProxyResponse{}
apigwResp.Body = `{"error":"yes"}`
apigwResp.StatusCode = 500
resp, err := json.Marshal(apigwResp)
return resp, err
}),
)
ctx := context.Background()
req, _ := json.Marshal(apiGatewayProxyRequest{
Body: `{"name":"john doe"}`,
})
resp, err := helloHandler.Invoke(ctx, req)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
apigwResp := apiGatewayProxyResponse{}
json.Unmarshal(resp, &apigwResp)
if apigwResp.StatusCode != 500 {
t.Fatalf("Expect status code of 500, instead of %d", apigwResp.StatusCode)
}
}
func TestInvokeFailEncode(t *testing.T) {
svc := serviceTest01{}
helloHandler := NewHandler(
makeTest01HelloEndpoint(svc),
decodeHelloRequestWithTwoBefores,
encodeResponse,
HandlerBefore(func(
ctx context.Context,
payload []byte,
) context.Context {
ctx = context.WithValue(ctx, KeyBeforeOne, "bef1")
return ctx
}),
HandlerBefore(func(
ctx context.Context,
payload []byte,
) context.Context {
ctx = context.WithValue(ctx, KeyBeforeTwo, "bef2")
return ctx
}),
HandlerAfter(func(
ctx context.Context,
response interface{},
) context.Context {
ctx = context.WithValue(ctx, KeyEncMode, "fail_encode")
return ctx
}),
HandlerErrorEncoder(func(
ctx context.Context,
err error,
) ([]byte, error) {
// convert error into proper APIGateway response.
apigwResp := apiGatewayProxyResponse{}
apigwResp.Body = `{"error":"yes"}`
apigwResp.StatusCode = 500
resp, err := json.Marshal(apigwResp)
return resp, err
}),
)
ctx := context.Background()
req, _ := json.Marshal(apiGatewayProxyRequest{
Body: `{"name":"john doe"}`,
})
resp, err := helloHandler.Invoke(ctx, req)
if err != nil {
t.Fatalf("Should have no error, but got: %+v", err)
}
apigwResp := apiGatewayProxyResponse{}
json.Unmarshal(resp, &apigwResp)
if apigwResp.StatusCode != 500 {
t.Fatalf("Expect status code of 500, instead of %d", apigwResp.StatusCode)
}
}
func decodeHelloRequestWithTwoBefores(
ctx context.Context, req []byte,
) (interface{}, error) {
apigwReq := apiGatewayProxyRequest{}
err := json.Unmarshal([]byte(req), &apigwReq)
if err != nil {
return apigwReq, err
}
request := helloRequest{}
err = json.Unmarshal([]byte(apigwReq.Body), &request)
if err != nil {
return request, err
}
valOne, ok := ctx.Value(KeyBeforeOne).(string)
if !ok {
return request, fmt.Errorf(
"Value was not set properly when multiple HandlerBefores are used")
}
valTwo, ok := ctx.Value(KeyBeforeTwo).(string)
if !ok {
return request, fmt.Errorf(
"Value was not set properly when multiple HandlerBefores are used")
}
request.Name += " " + valOne + " " + valTwo
return request, err
}
func encodeResponse(
ctx context.Context, response interface{},
) ([]byte, error) {
apigwResp := apiGatewayProxyResponse{}
mode, ok := ctx.Value(KeyEncMode).(string)
if ok && mode == "fail_encode" {
return nil, fmt.Errorf("fail encoding")
}
respByte, err := json.Marshal(response)
if err != nil {
return nil, err
}
apigwResp.Body = string(respByte)
apigwResp.StatusCode = 200
resp, err := json.Marshal(apigwResp)
return resp, err
}
type helloRequest struct {
Name string `json:"name"`
}
type helloResponse struct {
Greeting string `json:"greeting"`
}
func makeTest01HelloEndpoint(svc serviceTest01) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(helloRequest)
greeting := svc.hello(req.Name)
return helloResponse{greeting}, nil
}
}
func makeTest01FailEndpoint(_ serviceTest01) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
return nil, fmt.Errorf("test error endpoint")
}
}
type serviceTest01 struct{}
func (ts *serviceTest01) hello(name string) string {
return fmt.Sprintf("hello %s", name)
}
================================================
FILE: transport/awslambda/request_response_funcs.go
================================================
package awslambda
import (
"context"
)
// HandlerRequestFunc may take information from the received
// payload and use it to place items in the request scoped context.
// HandlerRequestFuncs are executed prior to invoking the endpoint and
// decoding of the payload.
type HandlerRequestFunc func(ctx context.Context, payload []byte) context.Context
// HandlerResponseFunc may take information from a request context
// and use it to manipulate the response before it's marshaled.
// HandlerResponseFunc are executed after invoking the endpoint
// but prior to returning a response.
type HandlerResponseFunc func(ctx context.Context, response interface{}) context.Context
// HandlerFinalizerFunc is executed at the end of Invoke.
// This can be used for logging purposes.
type HandlerFinalizerFunc func(ctx context.Context, resp []byte, err error)
================================================
FILE: transport/doc.go
================================================
// Package transport contains helpers applicable to all supported transports.
package transport
================================================
FILE: transport/error_handler.go
================================================
package transport
import (
"context"
"github.com/go-kit/log"
)
// ErrorHandler receives a transport error to be processed for diagnostic purposes.
// Usually this means logging the error.
type ErrorHandler interface {
Handle(ctx context.Context, err error)
}
// LogErrorHandler is a transport error handler implementation which logs an error.
type LogErrorHandler struct {
logger log.Logger
}
func NewLogErrorHandler(logger log.Logger) *LogErrorHandler {
return &LogErrorHandler{
logger: logger,
}
}
func (h *LogErrorHandler) Handle(ctx context.Context, err error) {
h.logger.Log("err", err)
}
// The ErrorHandlerFunc type is an adapter to allow the use of
// ordinary function as ErrorHandler. If f is a function
// with the appropriate signature, ErrorHandlerFunc(f) is a
// ErrorHandler that calls f.
type ErrorHandlerFunc func(ctx context.Context, err error)
// Handle calls f(ctx, err).
func (f ErrorHandlerFunc) Handle(ctx context.Context, err error) {
f(ctx, err)
}
================================================
FILE: transport/error_handler_test.go
================================================
package transport_test
import (
"context"
"errors"
"testing"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
)
func TestLogErrorHandler(t *testing.T) {
var output []interface{}
logger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error {
output = append(output, keyvals...)
return nil
}))
errorHandler := transport.NewLogErrorHandler(logger)
err := errors.New("error")
errorHandler.Handle(context.Background(), err)
if output[1] != err {
t.Errorf("expected an error log event: have %v, want %v", output[1], err)
}
}
================================================
FILE: transport/grpc/README.md
================================================
# grpc
[gRPC](http://www.grpc.io/) is an excellent, modern IDL and transport for
microservices. If you're starting a greenfield project, go-kit strongly
recommends gRPC as your default transport.
One important note is that while gRPC supports streaming requests and replies,
go-kit does not. You can still use streams in your service, but their
implementation will not be able to take advantage of many go-kit features like middleware.
Using gRPC and go-kit together is very simple.
First, define your service using protobuf3. This is explained
[in gRPC documentation](http://www.grpc.io/docs/#defining-a-service).
See
[addsvc.proto](https://github.com/go-kit/examples/blob/master/addsvc/pb/addsvc.proto)
for an example. Make sure the proto definition matches your service's go-kit
(interface) definition.
Next, get the protoc compiler.
You can download pre-compiled binaries from the
[protobuf release page](https://github.com/google/protobuf/releases).
You will unzip a folder called `protoc3` with a subdirectory `bin` containing
an executable. Move that executable somewhere in your `$PATH` and you're good
to go!
It can also be built from source.
```sh
brew install autoconf automake libtool
git clone https://github.com/google/protobuf
cd protobuf
./autogen.sh ; ./configure ; make ; make install
```
Then, compile your service definition, from .proto to .go.
```sh
protoc add.proto --go_out=plugins=grpc:.
```
Finally, write a tiny binding from your service definition to the gRPC
definition. It's a simple conversion from one domain to another.
See
[grpc.go](https://github.com/go-kit/examples/blob/master/addsvc/pkg/addtransport/grpc.go)
for an example.
That's it!
The gRPC binding can be bound to a listener and serve normal gRPC requests.
And within your service, you can use standard go-kit components and idioms.
See [addsvc](https://github.com/go-kit/examples/tree/master/addsvc/) for
a complete working example with gRPC support. And remember: go-kit services
can support multiple transports simultaneously.
================================================
FILE: transport/grpc/_grpc_test/client.go
================================================
package test
import (
"context"
"google.golang.org/grpc"
"github.com/go-kit/kit/endpoint"
grpctransport "github.com/go-kit/kit/transport/grpc"
"github.com/go-kit/kit/transport/grpc/_grpc_test/pb"
)
type clientBinding struct {
test endpoint.Endpoint
}
func (c *clientBinding) Test(ctx context.Context, a string, b int64) (context.Context, string, error) {
response, err := c.test(ctx, TestRequest{A: a, B: b})
if err != nil {
return nil, "", err
}
r := response.(*TestResponse)
return r.Ctx, r.V, nil
}
func NewClient(cc *grpc.ClientConn) Service {
return &clientBinding{
test: grpctransport.NewClient(
cc,
"pb.Test",
"Test",
encodeRequest,
decodeResponse,
&pb.TestResponse{},
grpctransport.ClientBefore(
injectCorrelationID,
),
grpctransport.ClientBefore(
displayClientRequestHeaders,
),
grpctransport.ClientAfter(
displayClientResponseHeaders,
displayClientResponseTrailers,
),
grpctransport.ClientAfter(
extractConsumedCorrelationID,
),
).Endpoint(),
}
}
================================================
FILE: transport/grpc/_grpc_test/context_metadata.go
================================================
package test
import (
"context"
"fmt"
"google.golang.org/grpc/metadata"
)
type metaContext string
const (
correlationID metaContext = "correlation-id"
responseHDR metaContext = "my-response-header"
responseTRLR metaContext = "my-response-trailer"
correlationIDTRLR metaContext = "correlation-id-consumed"
)
/* client before functions */
func injectCorrelationID(ctx context.Context, md *metadata.MD) context.Context {
if hdr, ok := ctx.Value(correlationID).(string); ok {
fmt.Printf("\tClient found correlationID %q in context, set metadata header\n", hdr)
(*md)[string(correlationID)] = append((*md)[string(correlationID)], hdr)
}
return ctx
}
func displayClientRequestHeaders(ctx context.Context, md *metadata.MD) context.Context {
if len(*md) > 0 {
fmt.Println("\tClient >> Request Headers:")
for key, val := range *md {
fmt.Printf("\t\t%s: %s\n", key, val[len(val)-1])
}
}
return ctx
}
/* server before functions */
func extractCorrelationID(ctx context.Context, md metadata.MD) context.Context {
if hdr, ok := md[string(correlationID)]; ok {
cID := hdr[len(hdr)-1]
ctx = context.WithValue(ctx, correlationID, cID)
fmt.Printf("\tServer received correlationID %q in metadata header, set context\n", cID)
}
return ctx
}
func displayServerRequestHeaders(ctx context.Context, md metadata.MD) context.Context {
if len(md) > 0 {
fmt.Println("\tServer << Request Headers:")
for key, val := range md {
fmt.Printf("\t\t%s: %s\n", key, val[len(val)-1])
}
}
return ctx
}
/* server after functions */
func injectResponseHeader(ctx context.Context, md *metadata.MD, _ *metadata.MD) context.Context {
*md = metadata.Join(*md, metadata.Pairs(string(responseHDR), "has-a-value"))
return ctx
}
func displayServerResponseHeaders(ctx context.Context, md *metadata.MD, _ *metadata.MD) context.Context {
if len(*md) > 0 {
fmt.Println("\tServer >> Response Headers:")
for key, val := range *md {
fmt.Printf("\t\t%s: %s\n", key, val[len(val)-1])
}
}
return ctx
}
func injectResponseTrailer(ctx context.Context, _ *metadata.MD, md *metadata.MD) context.Context {
*md = metadata.Join(*md, metadata.Pairs(string(responseTRLR), "has-a-value-too"))
return ctx
}
func injectConsumedCorrelationID(ctx context.Context, _ *metadata.MD, md *metadata.MD) context.Context {
if hdr, ok := ctx.Value(correlationID).(string); ok {
fmt.Printf("\tServer found correlationID %q in context, set consumed trailer\n", hdr)
*md = metadata.Join(*md, metadata.Pairs(string(correlationIDTRLR), hdr))
}
return ctx
}
func displayServerResponseTrailers(ctx context.Context, _ *metadata.MD, md *metadata.MD) context.Context {
if len(*md) > 0 {
fmt.Println("\tServer >> Response Trailers:")
for key, val := range *md {
fmt.Printf("\t\t%s: %s\n", key, val[len(val)-1])
}
}
return ctx
}
/* client after functions */
func displayClientResponseHeaders(ctx context.Context, md metadata.MD, _ metadata.MD) context.Context {
if len(md) > 0 {
fmt.Println("\tClient << Response Headers:")
for key, val := range md {
fmt.Printf("\t\t%s: %s\n", key, val[len(val)-1])
}
}
return ctx
}
func displayClientResponseTrailers(ctx context.Context, _ metadata.MD, md metadata.MD) context.Context {
if len(md) > 0 {
fmt.Println("\tClient << Response Trailers:")
for key, val := range md {
fmt.Printf("\t\t%s: %s\n", key, val[len(val)-1])
}
}
return ctx
}
func extractConsumedCorrelationID(ctx context.Context, _ metadata.MD, md metadata.MD) context.Context {
if hdr, ok := md[string(correlationIDTRLR)]; ok {
fmt.Printf("\tClient received consumed correlationID %q in metadata trailer, set context\n", hdr[len(hdr)-1])
ctx = context.WithValue(ctx, correlationIDTRLR, hdr[len(hdr)-1])
}
return ctx
}
/* CorrelationID context handlers */
func SetCorrelationID(ctx context.Context, v string) context.Context {
return context.WithValue(ctx, correlationID, v)
}
func GetConsumedCorrelationID(ctx context.Context) string {
if trlr, ok := ctx.Value(correlationIDTRLR).(string); ok {
return trlr
}
return ""
}
================================================
FILE: transport/grpc/_grpc_test/pb/generate.go
================================================
package pb
//go:generate protoc test.proto --go_out=. --go-grpc_out=. --go_opt=Mtest.proto=github.com/go-kit/kit/transport/grpc/_grpc_test/pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --go-grpc_opt=Mtest.proto=github.com/go-kit/kit/transport/grpc/_grpc_test/pb
================================================
FILE: transport/grpc/_grpc_test/pb/test.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.16.0
// source: test.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type TestRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
A string `protobuf:"bytes,1,opt,name=a,proto3" json:"a,omitempty"`
B int64 `protobuf:"varint,2,opt,name=b,proto3" json:"b,omitempty"`
}
func (x *TestRequest) Reset() {
*x = TestRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestRequest) ProtoMessage() {}
func (x *TestRequest) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestRequest.ProtoReflect.Descriptor instead.
func (*TestRequest) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0}
}
func (x *TestRequest) GetA() string {
if x != nil {
return x.A
}
return ""
}
func (x *TestRequest) GetB() int64 {
if x != nil {
return x.B
}
return 0
}
type TestResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
V string `protobuf:"bytes,1,opt,name=v,proto3" json:"v,omitempty"`
}
func (x *TestResponse) Reset() {
*x = TestResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TestResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TestResponse) ProtoMessage() {}
func (x *TestResponse) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TestResponse.ProtoReflect.Descriptor instead.
func (*TestResponse) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{1}
}
func (x *TestResponse) GetV() string {
if x != nil {
return x.V
}
return ""
}
var File_test_proto protoreflect.FileDescriptor
var file_test_proto_rawDesc = []byte{
0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62,
0x22, 0x29, 0x0a, 0x0b, 0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x61, 0x12, 0x0c, 0x0a,
0x01, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x62, 0x22, 0x1c, 0x0a, 0x0c, 0x54,
0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0c, 0x0a, 0x01, 0x76,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x76, 0x32, 0x33, 0x0a, 0x04, 0x54, 0x65, 0x73,
0x74, 0x12, 0x2b, 0x0a, 0x04, 0x54, 0x65, 0x73, 0x74, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x54,
0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x62, 0x2e,
0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_test_proto_rawDescOnce sync.Once
file_test_proto_rawDescData = file_test_proto_rawDesc
)
func file_test_proto_rawDescGZIP() []byte {
file_test_proto_rawDescOnce.Do(func() {
file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData)
})
return file_test_proto_rawDescData
}
var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_test_proto_goTypes = []interface{}{
(*TestRequest)(nil), // 0: pb.TestRequest
(*TestResponse)(nil), // 1: pb.TestResponse
}
var file_test_proto_depIdxs = []int32{
0, // 0: pb.Test.Test:input_type -> pb.TestRequest
1, // 1: pb.Test.Test:output_type -> pb.TestResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_test_proto_init() }
func file_test_proto_init() {
if File_test_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TestResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_test_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_test_proto_goTypes,
DependencyIndexes: file_test_proto_depIdxs,
MessageInfos: file_test_proto_msgTypes,
}.Build()
File_test_proto = out.File
file_test_proto_rawDesc = nil
file_test_proto_goTypes = nil
file_test_proto_depIdxs = nil
}
================================================
FILE: transport/grpc/_grpc_test/pb/test.proto
================================================
syntax = "proto3";
package pb;
service Test {
rpc Test (TestRequest) returns (TestResponse) {}
}
message TestRequest {
string a = 1;
int64 b = 2;
}
message TestResponse {
string v = 1;
}
================================================
FILE: transport/grpc/_grpc_test/pb/test_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// TestClient is the client API for Test service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TestClient interface {
Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error)
}
type testClient struct {
cc grpc.ClientConnInterface
}
func NewTestClient(cc grpc.ClientConnInterface) TestClient {
return &testClient{cc}
}
func (c *testClient) Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) {
out := new(TestResponse)
err := c.cc.Invoke(ctx, "/pb.Test/Test", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// TestServer is the server API for Test service.
// All implementations must embed UnimplementedTestServer
// for forward compatibility
type TestServer interface {
Test(context.Context, *TestRequest) (*TestResponse, error)
mustEmbedUnimplementedTestServer()
}
// UnimplementedTestServer must be embedded to have forward compatible implementations.
type UnimplementedTestServer struct {
}
func (UnimplementedTestServer) Test(context.Context, *TestRequest) (*TestResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Test not implemented")
}
func (UnimplementedTestServer) mustEmbedUnimplementedTestServer() {}
// UnsafeTestServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TestServer will
// result in compilation errors.
type UnsafeTestServer interface {
mustEmbedUnimplementedTestServer()
}
func RegisterTestServer(s grpc.ServiceRegistrar, srv TestServer) {
s.RegisterService(&Test_ServiceDesc, srv)
}
func _Test_Test_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestServer).Test(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pb.Test/Test",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestServer).Test(ctx, req.(*TestRequest))
}
return interceptor(ctx, in, info, handler)
}
// Test_ServiceDesc is the grpc.ServiceDesc for Test service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Test_ServiceDesc = grpc.ServiceDesc{
ServiceName: "pb.Test",
HandlerType: (*TestServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Test",
Handler: _Test_Test_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "test.proto",
}
================================================
FILE: transport/grpc/_grpc_test/request_response.go
================================================
package test
import (
"context"
"github.com/go-kit/kit/transport/grpc/_grpc_test/pb"
)
func encodeRequest(ctx context.Context, req interface{}) (interface{}, error) {
r := req.(TestRequest)
return &pb.TestRequest{A: r.A, B: r.B}, nil
}
func decodeRequest(ctx context.Context, req interface{}) (interface{}, error) {
r := req.(*pb.TestRequest)
return TestRequest{A: r.A, B: r.B}, nil
}
func encodeResponse(ctx context.Context, resp interface{}) (interface{}, error) {
r := resp.(*TestResponse)
return &pb.TestResponse{V: r.V}, nil
}
func decodeResponse(ctx context.Context, resp interface{}) (interface{}, error) {
r := resp.(*pb.TestResponse)
return &TestResponse{V: r.V, Ctx: ctx}, nil
}
================================================
FILE: transport/grpc/_grpc_test/server.go
================================================
package test
import (
"context"
"fmt"
"github.com/go-kit/kit/endpoint"
grpctransport "github.com/go-kit/kit/transport/grpc"
"github.com/go-kit/kit/transport/grpc/_grpc_test/pb"
)
type service struct{}
func (service) Test(ctx context.Context, a string, b int64) (context.Context, string, error) {
return nil, fmt.Sprintf("%s = %d", a, b), nil
}
func NewService() Service {
return service{}
}
func makeTestEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(TestRequest)
newCtx, v, err := svc.Test(ctx, req.A, req.B)
return &TestResponse{
V: v,
Ctx: newCtx,
}, err
}
}
type serverBinding struct {
pb.UnimplementedTestServer
test grpctransport.Handler
}
func (b *serverBinding) Test(ctx context.Context, req *pb.TestRequest) (*pb.TestResponse, error) {
_, response, err := b.test.ServeGRPC(ctx, req)
if err != nil {
return nil, err
}
return response.(*pb.TestResponse), nil
}
func NewBinding(svc Service) *serverBinding {
return &serverBinding{
test: grpctransport.NewServer(
makeTestEndpoint(svc),
decodeRequest,
encodeResponse,
grpctransport.ServerBefore(
extractCorrelationID,
),
grpctransport.ServerBefore(
displayServerRequestHeaders,
),
grpctransport.ServerAfter(
injectResponseHeader,
injectResponseTrailer,
injectConsumedCorrelationID,
),
grpctransport.ServerAfter(
displayServerResponseHeaders,
displayServerResponseTrailers,
),
),
}
}
================================================
FILE: transport/grpc/_grpc_test/service.go
================================================
package test
import "context"
type Service interface {
Test(ctx context.Context, a string, b int64) (context.Context, string, error)
}
type TestRequest struct {
A string
B int64
}
type TestResponse struct {
Ctx context.Context
V string
}
================================================
FILE: transport/grpc/client.go
================================================
package grpc
import (
"context"
"fmt"
"reflect"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/go-kit/kit/endpoint"
)
// Client wraps a gRPC connection and provides a method that implements
// endpoint.Endpoint.
type Client struct {
client *grpc.ClientConn
serviceName string
method string
enc EncodeRequestFunc
dec DecodeResponseFunc
grpcReply reflect.Type
before []ClientRequestFunc
after []ClientResponseFunc
finalizer []ClientFinalizerFunc
}
// NewClient constructs a usable Client for a single remote endpoint.
// Pass an zero-value protobuf message of the RPC response type as
// the grpcReply argument.
func NewClient(
cc *grpc.ClientConn,
serviceName string,
method string,
enc EncodeRequestFunc,
dec DecodeResponseFunc,
grpcReply interface{},
options ...ClientOption,
) *Client {
c := &Client{
client: cc,
method: fmt.Sprintf("/%s/%s", serviceName, method),
enc: enc,
dec: dec,
// We are using reflect.Indirect here to allow both reply structs and
// pointers to these reply structs. New consumers of the client should
// use structs directly, while existing consumers will not break if they
// remain to use pointers to structs.
grpcReply: reflect.TypeOf(
reflect.Indirect(
reflect.ValueOf(grpcReply),
).Interface(),
),
before: []ClientRequestFunc{},
after: []ClientResponseFunc{},
}
for _, option := range options {
option(c)
}
return c
}
// ClientOption sets an optional parameter for clients.
type ClientOption func(*Client)
// ClientBefore sets the RequestFuncs that are applied to the outgoing gRPC
// request before it's invoked.
func ClientBefore(before ...ClientRequestFunc) ClientOption {
return func(c *Client) { c.before = append(c.before, before...) }
}
// ClientAfter sets the ClientResponseFuncs that are applied to the incoming
// gRPC response prior to it being decoded. This is useful for obtaining
// response metadata and adding onto the context prior to decoding.
func ClientAfter(after ...ClientResponseFunc) ClientOption {
return func(c *Client) { c.after = append(c.after, after...) }
}
// ClientFinalizer is executed at the end of every gRPC request.
// By default, no finalizer is registered.
func ClientFinalizer(f ...ClientFinalizerFunc) ClientOption {
return func(s *Client) { s.finalizer = append(s.finalizer, f...) }
}
// Endpoint returns a usable endpoint that will invoke the gRPC specified by the
// client.
func (c Client) Endpoint() endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if c.finalizer != nil {
defer func() {
for _, f := range c.finalizer {
f(ctx, err)
}
}()
}
ctx = context.WithValue(ctx, ContextKeyRequestMethod, c.method)
req, err := c.enc(ctx, request)
if err != nil {
return nil, err
}
md := &metadata.MD{}
for _, f := range c.before {
ctx = f(ctx, md)
}
ctx = metadata.NewOutgoingContext(ctx, *md)
var header, trailer metadata.MD
grpcReply := reflect.New(c.grpcReply).Interface()
if err = c.client.Invoke(
ctx, c.method, req, grpcReply, grpc.Header(&header),
grpc.Trailer(&trailer),
); err != nil {
return nil, err
}
for _, f := range c.after {
ctx = f(ctx, header, trailer)
}
response, err = c.dec(ctx, grpcReply)
if err != nil {
return nil, err
}
return response, nil
}
}
// ClientFinalizerFunc can be used to perform work at the end of a client gRPC
// request, after the response is returned. The principal
// intended use is for error logging. Additional response parameters are
// provided in the context under keys with the ContextKeyResponse prefix.
// Note: err may be nil. There maybe also no additional response parameters depending on
// when an error occurs.
type ClientFinalizerFunc func(ctx context.Context, err error)
================================================
FILE: transport/grpc/client_test.go
================================================
package grpc_test
import (
"context"
"fmt"
"net"
"testing"
"google.golang.org/grpc"
test "github.com/go-kit/kit/transport/grpc/_grpc_test"
"github.com/go-kit/kit/transport/grpc/_grpc_test/pb"
)
const (
hostPort string = "localhost:8002"
)
func TestGRPCClient(t *testing.T) {
var (
server = grpc.NewServer()
service = test.NewService()
)
sc, err := net.Listen("tcp", hostPort)
if err != nil {
t.Fatalf("unable to listen: %+v", err)
}
defer server.GracefulStop()
go func() {
pb.RegisterTestServer(server, test.NewBinding(service))
_ = server.Serve(sc)
}()
cc, err := grpc.Dial(hostPort, grpc.WithInsecure())
if err != nil {
t.Fatalf("unable to Dial: %+v", err)
}
client := test.NewClient(cc)
var (
a = "the answer to life the universe and everything"
b = int64(42)
cID = "request-1"
ctx = test.SetCorrelationID(context.Background(), cID)
)
responseCTX, v, err := client.Test(ctx, a, b)
if err != nil {
t.Fatalf("unable to Test: %+v", err)
}
if want, have := fmt.Sprintf("%s = %d", a, b), v; want != have {
t.Fatalf("want %q, have %q", want, have)
}
if want, have := cID, test.GetConsumedCorrelationID(responseCTX); want != have {
t.Fatalf("want %q, have %q", want, have)
}
}
================================================
FILE: transport/grpc/doc.go
================================================
// Package grpc provides a gRPC binding for endpoints.
package grpc
================================================
FILE: transport/grpc/encode_decode.go
================================================
package grpc
import (
"context"
)
// DecodeRequestFunc extracts a user-domain request object from a gRPC request.
// It's designed to be used in gRPC servers, for server-side endpoints. One
// straightforward DecodeRequestFunc could be something that decodes from the
// gRPC request message to the concrete request type.
type DecodeRequestFunc func(context.Context, interface{}) (request interface{}, err error)
// EncodeRequestFunc encodes the passed request object into the gRPC request
// object. It's designed to be used in gRPC clients, for client-side endpoints.
// One straightforward EncodeRequestFunc could something that encodes the object
// directly to the gRPC request message.
type EncodeRequestFunc func(context.Context, interface{}) (request interface{}, err error)
// EncodeResponseFunc encodes the passed response object to the gRPC response
// message. It's designed to be used in gRPC servers, for server-side endpoints.
// One straightforward EncodeResponseFunc could be something that encodes the
// object directly to the gRPC response message.
type EncodeResponseFunc func(context.Context, interface{}) (response interface{}, err error)
// DecodeResponseFunc extracts a user-domain response object from a gRPC
// response object. It's designed to be used in gRPC clients, for client-side
// endpoints. One straightforward DecodeResponseFunc could be something that
// decodes from the gRPC response message to the concrete response type.
type DecodeResponseFunc func(context.Context, interface{}) (response interface{}, err error)
================================================
FILE: transport/grpc/request_response_funcs.go
================================================
package grpc
import (
"context"
"encoding/base64"
"strings"
"google.golang.org/grpc/metadata"
)
const (
binHdrSuffix = "-bin"
)
// ClientRequestFunc may take information from context and use it to construct
// metadata headers to be transported to the server. ClientRequestFuncs are
// executed after creating the request but prior to sending the gRPC request to
// the server.
type ClientRequestFunc func(context.Context, *metadata.MD) context.Context
// ServerRequestFunc may take information from the received metadata header and
// use it to place items in the request scoped context. ServerRequestFuncs are
// executed prior to invoking the endpoint.
type ServerRequestFunc func(context.Context, metadata.MD) context.Context
// ServerResponseFunc may take information from a request context and use it to
// manipulate the gRPC response metadata headers and trailers. ResponseFuncs are
// only executed in servers, after invoking the endpoint but prior to writing a
// response.
type ServerResponseFunc func(ctx context.Context, header *metadata.MD, trailer *metadata.MD) context.Context
// ClientResponseFunc may take information from a gRPC metadata header and/or
// trailer and make the responses available for consumption. ClientResponseFuncs
// are only executed in clients, after a request has been made, but prior to it
// being decoded.
type ClientResponseFunc func(ctx context.Context, header metadata.MD, trailer metadata.MD) context.Context
// SetRequestHeader returns a ClientRequestFunc that sets the specified metadata
// key-value pair.
func SetRequestHeader(key, val string) ClientRequestFunc {
return func(ctx context.Context, md *metadata.MD) context.Context {
key, val := EncodeKeyValue(key, val)
(*md)[key] = append((*md)[key], val)
return ctx
}
}
// SetResponseHeader returns a ResponseFunc that sets the specified metadata
// key-value pair.
func SetResponseHeader(key, val string) ServerResponseFunc {
return func(ctx context.Context, md *metadata.MD, _ *metadata.MD) context.Context {
key, val := EncodeKeyValue(key, val)
(*md)[key] = append((*md)[key], val)
return ctx
}
}
// SetResponseTrailer returns a ResponseFunc that sets the specified metadata
// key-value pair.
func SetResponseTrailer(key, val string) ServerResponseFunc {
return func(ctx context.Context, _ *metadata.MD, md *metadata.MD) context.Context {
key, val := EncodeKeyValue(key, val)
(*md)[key] = append((*md)[key], val)
return ctx
}
}
// EncodeKeyValue sanitizes a key-value pair for use in gRPC metadata headers.
func EncodeKeyValue(key, val string) (string, string) {
key = strings.ToLower(key)
if strings.HasSuffix(key, binHdrSuffix) {
val = base64.StdEncoding.EncodeToString([]byte(val))
}
return key, val
}
type contextKey int
const (
ContextKeyRequestMethod contextKey = iota
)
================================================
FILE: transport/grpc/server.go
================================================
package grpc
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
)
// Handler which should be called from the gRPC binding of the service
// implementation. The incoming request parameter, and returned response
// parameter, are both gRPC types, not user-domain.
type Handler interface {
ServeGRPC(ctx context.Context, request interface{}) (context.Context, interface{}, error)
}
// Server wraps an endpoint and implements grpc.Handler.
type Server struct {
e endpoint.Endpoint
dec DecodeRequestFunc
enc EncodeResponseFunc
before []ServerRequestFunc
after []ServerResponseFunc
finalizer []ServerFinalizerFunc
errorHandler transport.ErrorHandler
}
// NewServer constructs a new server, which implements wraps the provided
// endpoint and implements the Handler interface. Consumers should write
// bindings that adapt the concrete gRPC methods from their compiled protobuf
// definitions to individual handlers. Request and response objects are from the
// caller business domain, not gRPC request and reply types.
func NewServer(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...ServerOption,
) *Server {
s := &Server{
e: e,
dec: dec,
enc: enc,
errorHandler: transport.NewLogErrorHandler(log.NewNopLogger()),
}
for _, option := range options {
option(s)
}
return s
}
// ServerOption sets an optional parameter for servers.
type ServerOption func(*Server)
// ServerBefore functions are executed on the gRPC request object before the
// request is decoded.
func ServerBefore(before ...ServerRequestFunc) ServerOption {
return func(s *Server) { s.before = append(s.before, before...) }
}
// ServerAfter functions are executed on the gRPC response writer after the
// endpoint is invoked, but before anything is written to the client.
func ServerAfter(after ...ServerResponseFunc) ServerOption {
return func(s *Server) { s.after = append(s.after, after...) }
}
// ServerErrorLogger is used to log non-terminal errors. By default, no errors
// are logged.
// Deprecated: Use ServerErrorHandler instead.
func ServerErrorLogger(logger log.Logger) ServerOption {
return func(s *Server) { s.errorHandler = transport.NewLogErrorHandler(logger) }
}
// ServerErrorHandler is used to handle non-terminal errors. By default, non-terminal errors
// are ignored.
func ServerErrorHandler(errorHandler transport.ErrorHandler) ServerOption {
return func(s *Server) { s.errorHandler = errorHandler }
}
// ServerFinalizer is executed at the end of every gRPC request.
// By default, no finalizer is registered.
func ServerFinalizer(f ...ServerFinalizerFunc) ServerOption {
return func(s *Server) { s.finalizer = append(s.finalizer, f...) }
}
// ServeGRPC implements the Handler interface.
func (s Server) ServeGRPC(ctx context.Context, req interface{}) (retctx context.Context, resp interface{}, err error) {
// Retrieve gRPC metadata.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
md = metadata.MD{}
}
if len(s.finalizer) > 0 {
defer func() {
for _, f := range s.finalizer {
f(ctx, err)
}
}()
}
for _, f := range s.before {
ctx = f(ctx, md)
}
var (
request interface{}
response interface{}
grpcResp interface{}
)
request, err = s.dec(ctx, req)
if err != nil {
s.errorHandler.Handle(ctx, err)
return ctx, nil, err
}
response, err = s.e(ctx, request)
if err != nil {
s.errorHandler.Handle(ctx, err)
return ctx, nil, err
}
var mdHeader, mdTrailer metadata.MD
for _, f := range s.after {
ctx = f(ctx, &mdHeader, &mdTrailer)
}
grpcResp, err = s.enc(ctx, response)
if err != nil {
s.errorHandler.Handle(ctx, err)
return ctx, nil, err
}
if len(mdHeader) > 0 {
if err = grpc.SendHeader(ctx, mdHeader); err != nil {
s.errorHandler.Handle(ctx, err)
return ctx, nil, err
}
}
if len(mdTrailer) > 0 {
if err = grpc.SetTrailer(ctx, mdTrailer); err != nil {
s.errorHandler.Handle(ctx, err)
return ctx, nil, err
}
}
return ctx, grpcResp, nil
}
// ServerFinalizerFunc can be used to perform work at the end of an gRPC
// request, after the response has been written to the client.
type ServerFinalizerFunc func(ctx context.Context, err error)
// Interceptor is a grpc UnaryInterceptor that injects the method name into
// context so it can be consumed by Go kit gRPC middlewares. The Interceptor
// typically is added at creation time of the grpc-go server.
// Like this: `grpc.NewServer(grpc.UnaryInterceptor(kitgrpc.Interceptor))`
func Interceptor(
ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (resp interface{}, err error) {
ctx = context.WithValue(ctx, ContextKeyRequestMethod, info.FullMethod)
return handler(ctx, req)
}
================================================
FILE: transport/http/client.go
================================================
package http
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/go-kit/kit/endpoint"
)
// HTTPClient is an interface that models *http.Client.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// Client wraps a URL and provides a method that implements endpoint.Endpoint.
type Client struct {
client HTTPClient
req CreateRequestFunc
dec DecodeResponseFunc
before []RequestFunc
after []ClientResponseFunc
finalizer []ClientFinalizerFunc
bufferedStream bool
}
// NewClient constructs a usable Client for a single remote method.
func NewClient(method string, tgt *url.URL, enc EncodeRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client {
return NewExplicitClient(makeCreateRequestFunc(method, tgt, enc), dec, options...)
}
// NewExplicitClient is like NewClient but uses a CreateRequestFunc instead of a
// method, target URL, and EncodeRequestFunc, which allows for more control over
// the outgoing HTTP request.
func NewExplicitClient(req CreateRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client {
c := &Client{
client: http.DefaultClient,
req: req,
dec: dec,
}
for _, option := range options {
option(c)
}
return c
}
// ClientOption sets an optional parameter for clients.
type ClientOption func(*Client)
// SetClient sets the underlying HTTP client used for requests.
// By default, http.DefaultClient is used.
func SetClient(client HTTPClient) ClientOption {
return func(c *Client) { c.client = client }
}
// ClientBefore adds one or more RequestFuncs to be applied to the outgoing HTTP
// request before it's invoked.
func ClientBefore(before ...RequestFunc) ClientOption {
return func(c *Client) { c.before = append(c.before, before...) }
}
// ClientAfter adds one or more ClientResponseFuncs, which are applied to the
// incoming HTTP response prior to it being decoded. This is useful for
// obtaining anything off of the response and adding it into the context prior
// to decoding.
func ClientAfter(after ...ClientResponseFunc) ClientOption {
return func(c *Client) { c.after = append(c.after, after...) }
}
// ClientFinalizer adds one or more ClientFinalizerFuncs to be executed at the
// end of every HTTP request. Finalizers are executed in the order in which they
// were added. By default, no finalizer is registered.
func ClientFinalizer(f ...ClientFinalizerFunc) ClientOption {
return func(s *Client) { s.finalizer = append(s.finalizer, f...) }
}
// BufferedStream sets whether the HTTP response body is left open, allowing it
// to be read from later. Useful for transporting a file as a buffered stream.
// That body has to be drained and closed to properly end the request.
func BufferedStream(buffered bool) ClientOption {
return func(c *Client) { c.bufferedStream = buffered }
}
// Endpoint returns a usable Go kit endpoint that calls the remote HTTP endpoint.
func (c Client) Endpoint() endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
ctx, cancel := context.WithCancel(ctx)
var (
resp *http.Response
err error
)
if c.finalizer != nil {
defer func() {
if resp != nil {
ctx = context.WithValue(ctx, ContextKeyResponseHeaders, resp.Header)
ctx = context.WithValue(ctx, ContextKeyResponseSize, resp.ContentLength)
}
for _, f := range c.finalizer {
f(ctx, err)
}
}()
}
req, err := c.req(ctx, request)
if err != nil {
cancel()
return nil, err
}
for _, f := range c.before {
ctx = f(ctx, req)
}
resp, err = c.client.Do(req.WithContext(ctx))
if err != nil {
cancel()
return nil, err
}
// If the caller asked for a buffered stream, we don't cancel the
// context when the endpoint returns. Instead, we should call the
// cancel func when closing the response body.
if c.bufferedStream {
resp.Body = bodyWithCancel{ReadCloser: resp.Body, cancel: cancel}
} else {
defer resp.Body.Close()
defer cancel()
}
for _, f := range c.after {
ctx = f(ctx, resp)
}
response, err := c.dec(ctx, resp)
if err != nil {
return nil, err
}
return response, nil
}
}
// bodyWithCancel is a wrapper for an io.ReadCloser with also a
// cancel function which is called when the Close is used
type bodyWithCancel struct {
io.ReadCloser
cancel context.CancelFunc
}
func (bwc bodyWithCancel) Close() error {
bwc.ReadCloser.Close()
bwc.cancel()
return nil
}
// ClientFinalizerFunc can be used to perform work at the end of a client HTTP
// request, after the response is returned. The principal
// intended use is for error logging. Additional response parameters are
// provided in the context under keys with the ContextKeyResponse prefix.
// Note: err may be nil. There maybe also no additional response parameters
// depending on when an error occurs.
type ClientFinalizerFunc func(ctx context.Context, err error)
// EncodeJSONRequest is an EncodeRequestFunc that serializes the request as a
// JSON object to the Request body. Many JSON-over-HTTP services can use it as
// a sensible default. If the request implements Headerer, the provided headers
// will be applied to the request.
func EncodeJSONRequest(c context.Context, r *http.Request, request interface{}) error {
r.Header.Set("Content-Type", "application/json; charset=utf-8")
if headerer, ok := request.(Headerer); ok {
for k := range headerer.Headers() {
r.Header.Set(k, headerer.Headers().Get(k))
}
}
var b bytes.Buffer
r.Body = ioutil.NopCloser(&b)
return json.NewEncoder(&b).Encode(request)
}
// EncodeXMLRequest is an EncodeRequestFunc that serializes the request as a
// XML object to the Request body. If the request implements Headerer,
// the provided headers will be applied to the request.
func EncodeXMLRequest(c context.Context, r *http.Request, request interface{}) error {
r.Header.Set("Content-Type", "text/xml; charset=utf-8")
if headerer, ok := request.(Headerer); ok {
for k := range headerer.Headers() {
r.Header.Set(k, headerer.Headers().Get(k))
}
}
var b bytes.Buffer
r.Body = ioutil.NopCloser(&b)
return xml.NewEncoder(&b).Encode(request)
}
//
//
//
func makeCreateRequestFunc(method string, target *url.URL, enc EncodeRequestFunc) CreateRequestFunc {
return func(ctx context.Context, request interface{}) (*http.Request, error) {
req, err := http.NewRequest(method, target.String(), nil)
if err != nil {
return nil, err
}
if err = enc(ctx, req, request); err != nil {
return nil, err
}
return req, nil
}
}
================================================
FILE: transport/http/client_test.go
================================================
package http_test
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
httptransport "github.com/go-kit/kit/transport/http"
)
type TestResponse struct {
Body io.ReadCloser
String string
}
func TestHTTPClient(t *testing.T) {
var (
testbody = "testbody"
encode = func(context.Context, *http.Request, interface{}) error { return nil }
decode = func(_ context.Context, r *http.Response) (interface{}, error) {
buffer := make([]byte, len(testbody))
r.Body.Read(buffer)
return TestResponse{r.Body, string(buffer)}, nil
}
headers = make(chan string, 1)
headerKey = "X-Foo"
headerVal = "abcde"
afterHeaderKey = "X-The-Dude"
afterHeaderVal = "Abides"
afterVal = ""
afterFunc = func(ctx context.Context, r *http.Response) context.Context {
afterVal = r.Header.Get(afterHeaderKey)
return ctx
}
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers <- r.Header.Get(headerKey)
w.Header().Set(afterHeaderKey, afterHeaderVal)
w.WriteHeader(http.StatusOK)
w.Write([]byte(testbody))
}))
client := httptransport.NewClient(
"GET",
mustParse(server.URL),
encode,
decode,
httptransport.ClientBefore(httptransport.SetRequestHeader(headerKey, headerVal)),
httptransport.ClientAfter(afterFunc),
)
res, err := client.Endpoint()(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
var have string
select {
case have = <-headers:
case <-time.After(time.Millisecond):
t.Fatalf("timeout waiting for %s", headerKey)
}
// Check that Request Header was successfully received
if want := headerVal; want != have {
t.Errorf("want %q, have %q", want, have)
}
// Check that Response header set from server was received in SetClientAfter
if want, have := afterVal, afterHeaderVal; want != have {
t.Errorf("want %q, have %q", want, have)
}
// Check that the response was successfully decoded
response, ok := res.(TestResponse)
if !ok {
t.Fatal("response should be TestResponse")
}
if want, have := testbody, response.String; want != have {
t.Errorf("want %q, have %q", want, have)
}
// Check that response body was closed
b := make([]byte, 1)
_, err = response.Body.Read(b)
if err == nil {
t.Fatal("wanted error, got none")
}
if doNotWant, have := io.EOF, err; doNotWant == have {
t.Errorf("do not want %q, have %q", doNotWant, have)
}
}
func TestHTTPClientBufferedStream(t *testing.T) {
// bodysize has a size big enought to make the resopnse.Body not an instant read
// so if the response is cancelled it wount be all readed and the test would fail
// The 6000 has not a particular meaning, it big enough to fulfill the usecase.
const bodysize = 6000
var (
testbody = string(make([]byte, bodysize))
encode = func(context.Context, *http.Request, interface{}) error { return nil }
decode = func(_ context.Context, r *http.Response) (interface{}, error) {
return TestResponse{r.Body, ""}, nil
}
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(testbody))
}))
client := httptransport.NewClient(
"GET",
mustParse(server.URL),
encode,
decode,
httptransport.BufferedStream(true),
)
res, err := client.Endpoint()(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
// Check that the response was successfully decoded
response, ok := res.(TestResponse)
if !ok {
t.Fatal("response should be TestResponse")
}
defer response.Body.Close()
// Faking work
time.Sleep(time.Second * 1)
// Check that response body was NOT closed
b := make([]byte, len(testbody))
_, err = response.Body.Read(b)
if want, have := io.EOF, err; have != want {
t.Fatalf("want %q, have %q", want, have)
}
if want, have := testbody, string(b); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestClientFinalizer(t *testing.T) {
var (
headerKey = "X-Henlo-Lizer"
headerVal = "Helllo you stinky lizard"
responseBody = "go eat a fly ugly\n"
done = make(chan struct{})
encode = func(context.Context, *http.Request, interface{}) error { return nil }
decode = func(_ context.Context, r *http.Response) (interface{}, error) {
return TestResponse{r.Body, ""}, nil
}
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(headerKey, headerVal)
w.Write([]byte(responseBody))
}))
defer server.Close()
client := httptransport.NewClient(
"GET",
mustParse(server.URL),
encode,
decode,
httptransport.ClientFinalizer(func(ctx context.Context, err error) {
responseHeader := ctx.Value(httptransport.ContextKeyResponseHeaders).(http.Header)
if want, have := headerVal, responseHeader.Get(headerKey); want != have {
t.Errorf("%s: want %q, have %q", headerKey, want, have)
}
responseSize := ctx.Value(httptransport.ContextKeyResponseSize).(int64)
if want, have := int64(len(responseBody)), responseSize; want != have {
t.Errorf("response size: want %d, have %d", want, have)
}
close(done)
}),
)
_, err := client.Endpoint()(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
func TestEncodeJSONRequest(t *testing.T) {
var header http.Header
var body string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil && err != io.EOF {
t.Fatal(err)
}
header = r.Header
body = string(b)
}))
defer server.Close()
serverURL, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
client := httptransport.NewClient(
"POST",
serverURL,
httptransport.EncodeJSONRequest,
func(context.Context, *http.Response) (interface{}, error) { return nil, nil },
).Endpoint()
for _, test := range []struct {
value interface{}
body string
}{
{nil, "null\n"},
{12, "12\n"},
{1.2, "1.2\n"},
{true, "true\n"},
{"test", "\"test\"\n"},
{enhancedRequest{Foo: "foo"}, "{\"foo\":\"foo\"}\n"},
} {
if _, err := client(context.Background(), test.value); err != nil {
t.Error(err)
continue
}
if body != test.body {
t.Errorf("%v: actual %#v, expected %#v", test.value, body, test.body)
}
}
if _, err := client(context.Background(), enhancedRequest{Foo: "foo"}); err != nil {
t.Fatal(err)
}
if _, ok := header["X-Edward"]; !ok {
t.Fatalf("X-Edward value: actual %v, expected %v", nil, []string{"Snowden"})
}
if v := header.Get("X-Edward"); v != "Snowden" {
t.Errorf("X-Edward string: actual %v, expected %v", v, "Snowden")
}
}
func TestSetClient(t *testing.T) {
var (
encode = func(context.Context, *http.Request, interface{}) error { return nil }
decode = func(_ context.Context, r *http.Response) (interface{}, error) {
t, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
return string(t), nil
}
)
testHttpClient := httpClientFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Request: req,
Body: ioutil.NopCloser(bytes.NewBufferString("hello, world!")),
}, nil
})
client := httptransport.NewClient(
"GET",
&url.URL{},
encode,
decode,
httptransport.SetClient(testHttpClient),
).Endpoint()
resp, err := client(context.Background(), nil)
if err != nil {
t.Fatal(err)
}
if r, ok := resp.(string); !ok || r != "hello, world!" {
t.Fatal("Expected response to be 'hello, world!' string")
}
}
func TestNewExplicitClient(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%d", r.ContentLength)
}))
defer srv.Close()
req := func(ctx context.Context, request interface{}) (*http.Request, error) {
req, _ := http.NewRequest("POST", srv.URL, strings.NewReader(request.(string)))
return req, nil
}
dec := func(_ context.Context, resp *http.Response) (response interface{}, err error) {
buf, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return string(buf), err
}
client := httptransport.NewExplicitClient(req, dec)
request := "hello world"
response, err := client.Endpoint()(context.Background(), request)
if err != nil {
t.Fatal(err)
}
if want, have := "11", response.(string); want != have {
t.Fatalf("want %q, have %q", want, have)
}
}
func mustParse(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}
type enhancedRequest struct {
Foo string `json:"foo"`
}
func (e enhancedRequest) Headers() http.Header { return http.Header{"X-Edward": []string{"Snowden"}} }
type httpClientFunc func(req *http.Request) (*http.Response, error)
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) {
return f(req)
}
================================================
FILE: transport/http/doc.go
================================================
// Package http provides a general purpose HTTP binding for endpoints.
package http
================================================
FILE: transport/http/encode_decode.go
================================================
package http
import (
"context"
"net/http"
)
// DecodeRequestFunc extracts a user-domain request object from an HTTP
// request object. It's designed to be used in HTTP servers, for server-side
// endpoints. One straightforward DecodeRequestFunc could be something that
// JSON decodes from the request body to the concrete request type.
type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error)
// EncodeRequestFunc encodes the passed request object into the HTTP request
// object. It's designed to be used in HTTP clients, for client-side
// endpoints. One straightforward EncodeRequestFunc could be something that JSON
// encodes the object directly to the request body.
type EncodeRequestFunc func(context.Context, *http.Request, interface{}) error
// CreateRequestFunc creates an outgoing HTTP request based on the passed
// request object. It's designed to be used in HTTP clients, for client-side
// endpoints. It's a more powerful version of EncodeRequestFunc, and can be used
// if more fine-grained control of the HTTP request is required.
type CreateRequestFunc func(context.Context, interface{}) (*http.Request, error)
// EncodeResponseFunc encodes the passed response object to the HTTP response
// writer. It's designed to be used in HTTP servers, for server-side
// endpoints. One straightforward EncodeResponseFunc could be something that
// JSON encodes the object directly to the response body.
type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error
// DecodeResponseFunc extracts a user-domain response object from an HTTP
// response object. It's designed to be used in HTTP clients, for client-side
// endpoints. One straightforward DecodeResponseFunc could be something that
// JSON decodes from the response body to the concrete response type.
type DecodeResponseFunc func(context.Context, *http.Response) (response interface{}, err error)
================================================
FILE: transport/http/example_test.go
================================================
package http
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
)
func ExamplePopulateRequestContext() {
handler := NewServer(
func(ctx context.Context, request interface{}) (response interface{}, err error) {
fmt.Println("Method", ctx.Value(ContextKeyRequestMethod).(string))
fmt.Println("RequestPath", ctx.Value(ContextKeyRequestPath).(string))
fmt.Println("RequestURI", ctx.Value(ContextKeyRequestURI).(string))
fmt.Println("X-Request-ID", ctx.Value(ContextKeyRequestXRequestID).(string))
return struct{}{}, nil
},
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
ServerBefore(PopulateRequestContext),
)
server := httptest.NewServer(handler)
defer server.Close()
req, _ := http.NewRequest("PATCH", fmt.Sprintf("%s/search?q=sympatico", server.URL), nil)
req.Header.Set("X-Request-Id", "a1b2c3d4e5")
http.DefaultClient.Do(req)
// Output:
// Method PATCH
// RequestPath /search
// RequestURI /search?q=sympatico
// X-Request-ID a1b2c3d4e5
}
================================================
FILE: transport/http/intercepting_writer.go
================================================
package http
import (
"io"
"net/http"
)
type interceptingWriter struct {
http.ResponseWriter
code int
written int64
}
// WriteHeader may not be explicitly called, so care must be taken to
// initialize w.code to its default value of http.StatusOK.
func (w *interceptingWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}
func (w *interceptingWriter) Write(p []byte) (int, error) {
n, err := w.ResponseWriter.Write(p)
w.written += int64(n)
return n, err
}
// reimplementInterfaces returns a wrapped version of the embedded ResponseWriter
// and selectively implements the same combination of additional interfaces as
// the wrapped one. The interfaces it may implement are: http.Hijacker,
// http.CloseNotifier, http.Pusher, http.Flusher and io.ReaderFrom. The standard
// library is known to assert the existence of these interfaces and behaves
// differently. This implementation is derived from
// https://github.com/felixge/httpsnoop.
func (w *interceptingWriter) reimplementInterfaces() http.ResponseWriter {
var (
hj, i0 = w.ResponseWriter.(http.Hijacker)
cn, i1 = w.ResponseWriter.(http.CloseNotifier)
pu, i2 = w.ResponseWriter.(http.Pusher)
fl, i3 = w.ResponseWriter.(http.Flusher)
rf, i4 = w.ResponseWriter.(io.ReaderFrom)
)
switch {
case !i0 && !i1 && !i2 && !i3 && !i4:
return struct {
http.ResponseWriter
}{w}
case !i0 && !i1 && !i2 && !i3 && i4:
return struct {
http.ResponseWriter
io.ReaderFrom
}{w, rf}
case !i0 && !i1 && !i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.Flusher
}{w, fl}
case !i0 && !i1 && !i2 && i3 && i4:
return struct {
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{w, fl, rf}
case !i0 && !i1 && i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.Pusher
}{w, pu}
case !i0 && !i1 && i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.Pusher
io.ReaderFrom
}{w, pu, rf}
case !i0 && !i1 && i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.Pusher
http.Flusher
}{w, pu, fl}
case !i0 && !i1 && i2 && i3 && i4:
return struct {
http.ResponseWriter
http.Pusher
http.Flusher
io.ReaderFrom
}{w, pu, fl, rf}
case !i0 && i1 && !i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.CloseNotifier
}{w, cn}
case !i0 && i1 && !i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{w, cn, rf}
case !i0 && i1 && !i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.CloseNotifier
http.Flusher
}{w, cn, fl}
case !i0 && i1 && !i2 && i3 && i4:
return struct {
http.ResponseWriter
http.CloseNotifier
http.Flusher
io.ReaderFrom
}{w, cn, fl, rf}
case !i0 && i1 && i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.CloseNotifier
http.Pusher
}{w, cn, pu}
case !i0 && i1 && i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.CloseNotifier
http.Pusher
io.ReaderFrom
}{w, cn, pu, rf}
case !i0 && i1 && i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.CloseNotifier
http.Pusher
http.Flusher
}{w, cn, pu, fl}
case !i0 && i1 && i2 && i3 && i4:
return struct {
http.ResponseWriter
http.CloseNotifier
http.Pusher
http.Flusher
io.ReaderFrom
}{w, cn, pu, fl, rf}
case i0 && !i1 && !i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
}{w, hj}
case i0 && !i1 && !i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{w, hj, rf}
case i0 && !i1 && !i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.Flusher
}{w, hj, fl}
case i0 && !i1 && !i2 && i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.Flusher
io.ReaderFrom
}{w, hj, fl, rf}
case i0 && !i1 && i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.Pusher
}{w, hj, pu}
case i0 && !i1 && i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.Pusher
io.ReaderFrom
}{w, hj, pu, rf}
case i0 && !i1 && i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.Pusher
http.Flusher
}{w, hj, pu, fl}
case i0 && !i1 && i2 && i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.Pusher
http.Flusher
io.ReaderFrom
}{w, hj, pu, fl, rf}
case i0 && i1 && !i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
}{w, hj, cn}
case i0 && i1 && !i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
io.ReaderFrom
}{w, hj, cn, rf}
case i0 && i1 && !i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
http.Flusher
}{w, hj, cn, fl}
case i0 && i1 && !i2 && i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
http.Flusher
io.ReaderFrom
}{w, hj, cn, fl, rf}
case i0 && i1 && i2 && !i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
http.Pusher
}{w, hj, cn, pu}
case i0 && i1 && i2 && !i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
http.Pusher
io.ReaderFrom
}{w, hj, cn, pu, rf}
case i0 && i1 && i2 && i3 && !i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
http.Pusher
http.Flusher
}{w, hj, cn, pu, fl}
case i0 && i1 && i2 && i3 && i4:
return struct {
http.ResponseWriter
http.Hijacker
http.CloseNotifier
http.Pusher
http.Flusher
io.ReaderFrom
}{w, hj, cn, pu, fl, rf}
default:
return struct {
http.ResponseWriter
}{w}
}
}
================================================
FILE: transport/http/intercepting_writer_test.go
================================================
package http
import (
"bufio"
"io"
"net"
"net/http"
"testing"
)
type versatileWriter struct {
http.ResponseWriter
closeNotifyCalled bool
hijackCalled bool
readFromCalled bool
pushCalled bool
flushCalled bool
}
func (v *versatileWriter) Flush() { v.flushCalled = true }
func (v *versatileWriter) Push(target string, opts *http.PushOptions) error {
v.pushCalled = true
return nil
}
func (v *versatileWriter) ReadFrom(r io.Reader) (n int64, err error) {
v.readFromCalled = true
return 0, nil
}
func (v *versatileWriter) CloseNotify() <-chan bool { v.closeNotifyCalled = true; return nil }
func (v *versatileWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
v.hijackCalled = true
return nil, nil, nil
}
func TestInterceptingWriter_passthroughs(t *testing.T) {
w := &versatileWriter{}
iw := (&interceptingWriter{ResponseWriter: w}).reimplementInterfaces()
iw.(http.Flusher).Flush()
iw.(http.Pusher).Push("", nil)
iw.(http.CloseNotifier).CloseNotify()
iw.(http.Hijacker).Hijack()
iw.(io.ReaderFrom).ReadFrom(nil)
if !w.flushCalled {
t.Error("Flush not called")
}
if !w.pushCalled {
t.Error("Push not called")
}
if !w.closeNotifyCalled {
t.Error("CloseNotify not called")
}
if !w.hijackCalled {
t.Error("Hijack not called")
}
if !w.readFromCalled {
t.Error("ReadFrom not called")
}
}
// TestInterceptingWriter_reimplementInterfaces is also derived from
// https://github.com/felixge/httpsnoop, like interceptingWriter.
func TestInterceptingWriter_reimplementInterfaces(t *testing.T) {
// combination 1/32
{
t.Log("http.ResponseWriter")
inner := struct {
http.ResponseWriter
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 2/32
{
t.Log("http.ResponseWriter, http.Pusher")
inner := struct {
http.ResponseWriter
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 3/32
{
t.Log("http.ResponseWriter, io.ReaderFrom")
inner := struct {
http.ResponseWriter
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 4/32
{
t.Log("http.ResponseWriter, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 5/32
{
t.Log("http.ResponseWriter, http.Hijacker")
inner := struct {
http.ResponseWriter
http.Hijacker
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 6/32
{
t.Log("http.ResponseWriter, http.Hijacker, http.Pusher")
inner := struct {
http.ResponseWriter
http.Hijacker
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 7/32
{
t.Log("http.ResponseWriter, http.Hijacker, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 8/32
{
t.Log("http.ResponseWriter, http.Hijacker, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.Hijacker
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 9/32
{
t.Log("http.ResponseWriter, http.CloseNotifier")
inner := struct {
http.ResponseWriter
http.CloseNotifier
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 10/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, http.Pusher")
inner := struct {
http.ResponseWriter
http.CloseNotifier
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 11/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 12/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 13/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, http.Hijacker")
inner := struct {
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 14/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, http.Hijacker, http.Pusher")
inner := struct {
http.ResponseWriter
http.CloseNotifier
http.Hijacker
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 15/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, http.Hijacker, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 16/32
{
t.Log("http.ResponseWriter, http.CloseNotifier, http.Hijacker, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 17/32
{
t.Log("http.ResponseWriter, http.Flusher")
inner := struct {
http.ResponseWriter
http.Flusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 18/32
{
t.Log("http.ResponseWriter, http.Flusher, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 19/32
{
t.Log("http.ResponseWriter, http.Flusher, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 20/32
{
t.Log("http.ResponseWriter, http.Flusher, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 21/32
{
t.Log("http.ResponseWriter, http.Flusher, http.Hijacker")
inner := struct {
http.ResponseWriter
http.Flusher
http.Hijacker
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 22/32
{
t.Log("http.ResponseWriter, http.Flusher, http.Hijacker, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.Hijacker
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 23/32
{
t.Log("http.ResponseWriter, http.Flusher, http.Hijacker, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 24/32
{
t.Log("http.ResponseWriter, http.Flusher, http.Hijacker, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 25/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 26/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 27/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 28/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 29/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 30/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != false {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
// combination 31/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker, io.ReaderFrom")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != false {
t.Error("unexpected interface")
}
}
// combination 32/32
{
t.Log("http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker, io.ReaderFrom, http.Pusher")
inner := struct {
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{}
w := (&interceptingWriter{ResponseWriter: inner}).reimplementInterfaces()
if _, ok := w.(http.ResponseWriter); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Flusher); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.CloseNotifier); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Hijacker); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(io.ReaderFrom); ok != true {
t.Error("unexpected interface")
}
if _, ok := w.(http.Pusher); ok != true {
t.Error("unexpected interface")
}
}
}
================================================
FILE: transport/http/jsonrpc/README.md
================================================
# JSON RPC
[JSON RPC](http://www.jsonrpc.org) is "A light weight remote procedure call protocol". It allows for the creation of simple RPC-style APIs with human-readable messages that are front-end friendly.
## Using JSON RPC with Go-Kit
Using JSON RPC and go-kit together is quite simple.
A JSON RPC _server_ acts as an [HTTP Handler](https://godoc.org/net/http#Handler), receiving all requests to the JSON RPC's URL. The server looks at the `method` property of the [Request Object](http://www.jsonrpc.org/specification#request_object), and routes it to the corresponding code.
Each JSON RPC _method_ is implemented as an `EndpointCodec`, a go-kit [Endpoint](https://godoc.org/github.com/go-kit/kit/endpoint#Endpoint), sandwiched between a decoder and encoder. The decoder picks apart the JSON RPC request params, which can be passed to your endpoint. The encoder receives the output from the endpoint and encodes a JSON-RPC result.
## Example — Add Service
Let's say we want a service that adds two ints together. We'll serve this at `http://localhost/rpc`. So a request to our `sum` method will be a POST to `http://localhost/rpc` with a request body of:
{
"id": 123,
"jsonrpc": "2.0",
"method": "sum",
"params": {
"A": 2,
"B": 2
}
}
### `EndpointCodecMap`
The routing table for incoming JSON RPC requests is the `EndpointCodecMap`. The key of the map is the JSON RPC method name. Here, we're routing the `sum` method to an `EndpointCodec` wrapped around `sumEndpoint`.
jsonrpc.EndpointCodecMap{
"sum": jsonrpc.EndpointCodec{
Endpoint: sumEndpoint,
Decode: decodeSumRequest,
Encode: encodeSumResponse,
},
}
### Decoder
type DecodeRequestFunc func(context.Context, json.RawMessage) (request interface{}, err error)
A `DecodeRequestFunc` is given the raw JSON from the `params` property of the Request object, _not_ the whole request object. It returns an object that will be the input to the Endpoint. For our purposes, the output should be a SumRequest, like this:
type SumRequest struct {
A, B int
}
So here's our decoder:
func decodeSumRequest(ctx context.Context, msg json.RawMessage) (interface{}, error) {
var req SumRequest
err := json.Unmarshal(msg, &req)
if err != nil {
return nil, err
}
return req, nil
}
So our `SumRequest` will now be passed to the endpoint. Once the endpoint has done its work, we hand over to the…
### Encoder
The encoder takes the output of the endpoint, and builds the raw JSON message that will form the `result` field of a [Response Object](http://www.jsonrpc.org/specification#response_object). Our result is going to be a plain int. Here's our encoder:
func encodeSumResponse(ctx context.Context, result interface{}) (json.RawMessage, error) {
sum, ok := result.(int)
if !ok {
return nil, errors.New("result is not an int")
}
b, err := json.Marshal(sum)
if err != nil {
return nil, err
}
return b, nil
}
### Server
Now that we have an EndpointCodec with decoder, endpoint, and encoder, we can wire up the server:
handler := jsonrpc.NewServer(jsonrpc.EndpointCodecMap{
"sum": jsonrpc.EndpointCodec{
Endpoint: sumEndpoint,
Decode: decodeSumRequest,
Encode: encodeSumResponse,
},
})
http.Handle("/rpc", handler)
http.ListenAndServe(":80", nil)
With all of this done, our example request above should result in a response like this:
{
"jsonrpc": "2.0",
"result": 4
}
================================================
FILE: transport/http/jsonrpc/client.go
================================================
package jsonrpc
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"sync/atomic"
"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
)
// Client wraps a JSON RPC method and provides a method that implements endpoint.Endpoint.
type Client struct {
client httptransport.HTTPClient
// JSON RPC endpoint URL
tgt *url.URL
// JSON RPC method name.
method string
enc EncodeRequestFunc
dec DecodeResponseFunc
before []httptransport.RequestFunc
after []httptransport.ClientResponseFunc
finalizer httptransport.ClientFinalizerFunc
requestID RequestIDGenerator
bufferedStream bool
}
type clientRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}
// NewClient constructs a usable Client for a single remote method.
func NewClient(
tgt *url.URL,
method string,
options ...ClientOption,
) *Client {
c := &Client{
client: http.DefaultClient,
method: method,
tgt: tgt,
enc: DefaultRequestEncoder,
dec: DefaultResponseDecoder,
before: []httptransport.RequestFunc{},
after: []httptransport.ClientResponseFunc{},
requestID: NewAutoIncrementID(0),
bufferedStream: false,
}
for _, option := range options {
option(c)
}
return c
}
// DefaultRequestEncoder marshals the given request to JSON.
func DefaultRequestEncoder(_ context.Context, req interface{}) (json.RawMessage, error) {
return json.Marshal(req)
}
// DefaultResponseDecoder unmarshals the result to interface{}, or returns an
// error, if found.
func DefaultResponseDecoder(_ context.Context, res Response) (interface{}, error) {
if res.Error != nil {
return nil, *res.Error
}
var result interface{}
err := json.Unmarshal(res.Result, &result)
if err != nil {
return nil, err
}
return result, nil
}
// ClientOption sets an optional parameter for clients.
type ClientOption func(*Client)
// SetClient sets the underlying HTTP client used for requests.
// By default, http.DefaultClient is used.
func SetClient(client httptransport.HTTPClient) ClientOption {
return func(c *Client) { c.client = client }
}
// ClientBefore sets the RequestFuncs that are applied to the outgoing HTTP
// request before it's invoked.
func ClientBefore(before ...httptransport.RequestFunc) ClientOption {
return func(c *Client) { c.before = append(c.before, before...) }
}
// ClientAfter sets the ClientResponseFuncs applied to the server's HTTP
// response prior to it being decoded. This is useful for obtaining anything
// from the response and adding onto the context prior to decoding.
func ClientAfter(after ...httptransport.ClientResponseFunc) ClientOption {
return func(c *Client) { c.after = append(c.after, after...) }
}
// ClientFinalizer is executed at the end of every HTTP request.
// By default, no finalizer is registered.
func ClientFinalizer(f httptransport.ClientFinalizerFunc) ClientOption {
return func(c *Client) { c.finalizer = f }
}
// ClientRequestEncoder sets the func used to encode the request params to JSON.
// If not set, DefaultRequestEncoder is used.
func ClientRequestEncoder(enc EncodeRequestFunc) ClientOption {
return func(c *Client) { c.enc = enc }
}
// ClientResponseDecoder sets the func used to decode the response params from
// JSON. If not set, DefaultResponseDecoder is used.
func ClientResponseDecoder(dec DecodeResponseFunc) ClientOption {
return func(c *Client) { c.dec = dec }
}
// RequestIDGenerator returns an ID for the request.
type RequestIDGenerator interface {
Generate() interface{}
}
// ClientRequestIDGenerator is executed before each request to generate an ID
// for the request.
// By default, AutoIncrementRequestID is used.
func ClientRequestIDGenerator(g RequestIDGenerator) ClientOption {
return func(c *Client) { c.requestID = g }
}
// BufferedStream sets whether the Response.Body is left open, allowing it
// to be read from later. Useful for transporting a file as a buffered stream.
func BufferedStream(buffered bool) ClientOption {
return func(c *Client) { c.bufferedStream = buffered }
}
// Endpoint returns a usable endpoint that invokes the remote endpoint.
func (c Client) Endpoint() endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var (
resp *http.Response
err error
)
if c.finalizer != nil {
defer func() {
if resp != nil {
ctx = context.WithValue(ctx, httptransport.ContextKeyResponseHeaders, resp.Header)
ctx = context.WithValue(ctx, httptransport.ContextKeyResponseSize, resp.ContentLength)
}
c.finalizer(ctx, err)
}()
}
ctx = context.WithValue(ctx, ContextKeyRequestMethod, c.method)
var params json.RawMessage
if params, err = c.enc(ctx, request); err != nil {
return nil, err
}
rpcReq := clientRequest{
JSONRPC: Version,
Method: c.method,
Params: params,
ID: c.requestID.Generate(),
}
req, err := http.NewRequest("POST", c.tgt.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
var b bytes.Buffer
req.Body = ioutil.NopCloser(&b)
err = json.NewEncoder(&b).Encode(rpcReq)
if err != nil {
return nil, err
}
for _, f := range c.before {
ctx = f(ctx, req)
}
resp, err = c.client.Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
if !c.bufferedStream {
defer resp.Body.Close()
}
for _, f := range c.after {
ctx = f(ctx, resp)
}
// Decode the body into an object
var rpcRes Response
err = json.NewDecoder(resp.Body).Decode(&rpcRes)
if err != nil {
return nil, err
}
response, err := c.dec(ctx, rpcRes)
if err != nil {
return nil, err
}
return response, nil
}
}
// ClientFinalizerFunc can be used to perform work at the end of a client HTTP
// request, after the response is returned. The principal
// intended use is for error logging. Additional response parameters are
// provided in the context under keys with the ContextKeyResponse prefix.
// Note: err may be nil. There maybe also no additional response parameters
// depending on when an error occurs.
type ClientFinalizerFunc func(ctx context.Context, err error)
// autoIncrementID is a RequestIDGenerator that generates
// auto-incrementing integer IDs.
type autoIncrementID struct {
v *uint64
}
// NewAutoIncrementID returns an auto-incrementing request ID generator,
// initialised with the given value.
func NewAutoIncrementID(init uint64) RequestIDGenerator {
// Offset by one so that the first generated value = init.
v := init - 1
return &autoIncrementID{v: &v}
}
// Generate satisfies RequestIDGenerator
func (i *autoIncrementID) Generate() interface{} {
id := atomic.AddUint64(i.v, 1)
return id
}
================================================
FILE: transport/http/jsonrpc/client_test.go
================================================
package jsonrpc_test
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/go-kit/kit/transport/http/jsonrpc"
)
type TestResponse struct {
Body io.ReadCloser
String string
}
type testServerResponseOptions struct {
Body string
Status int
}
func httptestServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var testReq jsonrpc.Request
if err := json.NewDecoder(r.Body).Decode(&testReq); err != nil {
t.Fatal(err)
}
var options testServerResponseOptions
if err := json.Unmarshal(testReq.Params, &options); err != nil {
t.Fatal(err)
}
if options.Status == 0 {
options.Status = http.StatusOK
}
w.WriteHeader(options.Status)
w.Write([]byte(options.Body))
}))
}
func TestBeforeAfterFuncs(t *testing.T) {
t.Parallel()
var tests = []struct {
name string
status int
body string
}{
{
name: "empty body",
body: "",
},
{
name: "empty body 500",
body: "",
status: 500,
},
{
name: "empty json body",
body: "{}",
},
{
name: "error",
body: `{"jsonrpc":"2.0","error":{"code":32603,"message":"Bad thing happened."}}`,
},
}
server := httptestServer(t)
defer server.Close()
testUrl, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
beforeCalled := false
afterCalled := false
finalizerCalled := false
sut := jsonrpc.NewClient(
testUrl,
"dummy",
jsonrpc.ClientBefore(func(ctx context.Context, req *http.Request) context.Context {
beforeCalled = true
return ctx
}),
jsonrpc.ClientAfter(func(ctx context.Context, resp *http.Response) context.Context {
afterCalled = true
return ctx
}),
jsonrpc.ClientFinalizer(func(ctx context.Context, err error) {
finalizerCalled = true
}),
)
sut.Endpoint()(context.TODO(), testServerResponseOptions{Body: tt.body, Status: tt.status})
if !beforeCalled {
t.Fatal("Expected client before func to be called. Wasn't.")
}
if !afterCalled {
t.Fatal("Expected client after func to be called. Wasn't.")
}
if !finalizerCalled {
t.Fatal("Expected client finalizer func to be called. Wasn't.")
}
})
}
}
type staticIDGenerator int
func (g staticIDGenerator) Generate() interface{} { return g }
func TestClientHappyPath(t *testing.T) {
t.Parallel()
var (
afterCalledKey = "AC"
beforeHeaderKey = "BF"
beforeHeaderValue = "beforeFuncWozEre"
testbody = `{"jsonrpc":"2.0", "result":5}`
requestBody []byte
beforeFunc = func(ctx context.Context, r *http.Request) context.Context {
r.Header.Add(beforeHeaderKey, beforeHeaderValue)
return ctx
}
encode = func(ctx context.Context, req interface{}) (json.RawMessage, error) {
return json.Marshal(req)
}
afterFunc = func(ctx context.Context, r *http.Response) context.Context {
return context.WithValue(ctx, afterCalledKey, true)
}
finalizerCalled = false
fin = func(ctx context.Context, err error) {
finalizerCalled = true
}
decode = func(ctx context.Context, res jsonrpc.Response) (interface{}, error) {
if ac := ctx.Value(afterCalledKey); ac == nil {
t.Fatal("after not called")
}
var result int
err := json.Unmarshal(res.Result, &result)
if err != nil {
return nil, err
}
return result, nil
}
wantID = 666
gen = staticIDGenerator(wantID)
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(beforeHeaderKey) != beforeHeaderValue {
t.Fatal("Header not set by before func.")
}
b, err := ioutil.ReadAll(r.Body)
if err != nil && err != io.EOF {
t.Fatal(err)
}
requestBody = b
w.WriteHeader(http.StatusOK)
w.Write([]byte(testbody))
}))
defer server.Close()
sut := jsonrpc.NewClient(
mustParse(server.URL),
"add",
jsonrpc.ClientRequestEncoder(encode),
jsonrpc.ClientResponseDecoder(decode),
jsonrpc.ClientBefore(beforeFunc),
jsonrpc.ClientAfter(afterFunc),
jsonrpc.ClientRequestIDGenerator(gen),
jsonrpc.ClientFinalizer(fin),
jsonrpc.SetClient(http.DefaultClient),
jsonrpc.BufferedStream(false),
)
type addRequest struct {
A int
B int
}
in := addRequest{2, 2}
result, err := sut.Endpoint()(context.Background(), in)
if err != nil {
t.Fatal(err)
}
ri, ok := result.(int)
if !ok {
t.Fatalf("result is not int: (%T)%+v", result, result)
}
if ri != 5 {
t.Fatalf("want=5, got=%d", ri)
}
var requestAtServer jsonrpc.Request
err = json.Unmarshal(requestBody, &requestAtServer)
if err != nil {
t.Fatal(err)
}
if id, _ := requestAtServer.ID.Int(); id != wantID {
t.Fatalf("Request ID at server: want=%d, got=%d", wantID, id)
}
if requestAtServer.JSONRPC != jsonrpc.Version {
t.Fatalf("JSON-RPC version at server: want=%s, got=%s", jsonrpc.Version, requestAtServer.JSONRPC)
}
var paramsAtServer addRequest
err = json.Unmarshal(requestAtServer.Params, ¶msAtServer)
if err != nil {
t.Fatal(err)
}
if paramsAtServer != in {
t.Fatalf("want=%+v, got=%+v", in, paramsAtServer)
}
if !finalizerCalled {
t.Fatal("Expected finalizer to be called. Wasn't.")
}
}
func TestCanUseDefaults(t *testing.T) {
t.Parallel()
var (
testbody = `{"jsonrpc":"2.0", "result":"boogaloo"}`
requestBody []byte
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil && err != io.EOF {
t.Fatal(err)
}
requestBody = b
w.WriteHeader(http.StatusOK)
w.Write([]byte(testbody))
}))
defer server.Close()
sut := jsonrpc.NewClient(
mustParse(server.URL),
"add",
)
type addRequest struct {
A int
B int
}
in := addRequest{2, 2}
result, err := sut.Endpoint()(context.Background(), in)
if err != nil {
t.Fatal(err)
}
rs, ok := result.(string)
if !ok {
t.Fatalf("result is not string: (%T)%+v", result, result)
}
if rs != "boogaloo" {
t.Fatalf("want=boogaloo, got=%s", rs)
}
var requestAtServer jsonrpc.Request
err = json.Unmarshal(requestBody, &requestAtServer)
if err != nil {
t.Fatal(err)
}
var paramsAtServer addRequest
err = json.Unmarshal(requestAtServer.Params, ¶msAtServer)
if err != nil {
t.Fatal(err)
}
if paramsAtServer != in {
t.Fatalf("want=%+v, got=%+v", in, paramsAtServer)
}
}
func TestClientCanHandleJSONRPCError(t *testing.T) {
t.Parallel()
var testbody = `{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Bad thing happened."
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(testbody))
}))
defer server.Close()
sut := jsonrpc.NewClient(mustParse(server.URL), "add")
_, err := sut.Endpoint()(context.Background(), 5)
if err == nil {
t.Fatal("Expected error, got none.")
}
{
want := "Bad thing happened."
got := err.Error()
if got != want {
t.Fatalf("error message: want=%s, got=%s", want, got)
}
}
type errorCoder interface {
ErrorCode() int
}
ec, ok := err.(errorCoder)
if !ok {
t.Fatal("Error is not errorCoder")
}
{
want := -32603
got := ec.ErrorCode()
if got != want {
t.Fatalf("error code: want=%d, got=%d", want, got)
}
}
}
func TestDefaultAutoIncrementer(t *testing.T) {
t.Parallel()
sut := jsonrpc.NewAutoIncrementID(0)
var want uint64
for ; want < 100; want++ {
got := sut.Generate()
if got != want {
t.Fatalf("want=%d, got=%d", want, got)
}
}
}
func mustParse(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}
================================================
FILE: transport/http/jsonrpc/doc.go
================================================
// Package jsonrpc provides a JSON RPC (v2.0) binding for endpoints.
// See http://www.jsonrpc.org/specification
package jsonrpc
================================================
FILE: transport/http/jsonrpc/encode_decode.go
================================================
package jsonrpc
import (
"encoding/json"
"github.com/go-kit/kit/endpoint"
"context"
)
// Server-Side Codec
// EndpointCodec defines a server Endpoint and its associated codecs
type EndpointCodec struct {
Endpoint endpoint.Endpoint
Decode DecodeRequestFunc
Encode EncodeResponseFunc
}
// EndpointCodecMap maps the Request.Method to the proper EndpointCodec
type EndpointCodecMap map[string]EndpointCodec
// DecodeRequestFunc extracts a user-domain request object from raw JSON
// It's designed to be used in JSON RPC servers, for server-side endpoints.
// One straightforward DecodeRequestFunc could be something that unmarshals
// JSON from the request body to the concrete request type.
type DecodeRequestFunc func(context.Context, json.RawMessage) (request interface{}, err error)
// EncodeResponseFunc encodes the passed response object to a JSON RPC result.
// It's designed to be used in HTTP servers, for server-side endpoints.
// One straightforward EncodeResponseFunc could be something that JSON encodes
// the object directly.
type EncodeResponseFunc func(context.Context, interface{}) (response json.RawMessage, err error)
// Client-Side Codec
// EncodeRequestFunc encodes the given request object to raw JSON.
// It's designed to be used in JSON RPC clients, for client-side
// endpoints. One straightforward EncodeResponseFunc could be something that
// JSON encodes the object directly.
type EncodeRequestFunc func(context.Context, interface{}) (request json.RawMessage, err error)
// DecodeResponseFunc extracts a user-domain response object from an JSON RPC
// response object. It's designed to be used in JSON RPC clients, for
// client-side endpoints. It is the responsibility of this function to decide
// whether any error present in the JSON RPC response should be surfaced to the
// client endpoint.
type DecodeResponseFunc func(context.Context, Response) (response interface{}, err error)
================================================
FILE: transport/http/jsonrpc/error.go
================================================
package jsonrpc
// Error defines a JSON RPC error that can be returned
// in a Response from the spec
// http://www.jsonrpc.org/specification#error_object
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Error implements error.
func (e Error) Error() string {
if e.Message != "" {
return e.Message
}
return errorMessage[e.Code]
}
// ErrorCode returns the JSON RPC error code associated with the error.
func (e Error) ErrorCode() int {
return e.Code
}
const (
// ParseError defines invalid JSON was received by the server.
// An error occurred on the server while parsing the JSON text.
ParseError int = -32700
// InvalidRequestError defines the JSON sent is not a valid Request object.
InvalidRequestError int = -32600
// MethodNotFoundError defines the method does not exist / is not available.
MethodNotFoundError int = -32601
// InvalidParamsError defines invalid method parameter(s).
InvalidParamsError int = -32602
// InternalError defines a server error
InternalError int = -32603
)
var errorMessage = map[int]string{
ParseError: "An error occurred on the server while parsing the JSON text.",
InvalidRequestError: "The JSON sent is not a valid Request object.",
MethodNotFoundError: "The method does not exist / is not available.",
InvalidParamsError: "Invalid method parameter(s).",
InternalError: "Internal JSON-RPC error.",
}
// ErrorMessage returns a message for the JSON RPC error code. It returns the empty
// string if the code is unknown.
func ErrorMessage(code int) string {
return errorMessage[code]
}
type parseError string
func (e parseError) Error() string {
return string(e)
}
func (e parseError) ErrorCode() int {
return ParseError
}
type invalidRequestError string
func (e invalidRequestError) Error() string {
return string(e)
}
func (e invalidRequestError) ErrorCode() int {
return InvalidRequestError
}
type methodNotFoundError string
func (e methodNotFoundError) Error() string {
return string(e)
}
func (e methodNotFoundError) ErrorCode() int {
return MethodNotFoundError
}
type invalidParamsError string
func (e invalidParamsError) Error() string {
return string(e)
}
func (e invalidParamsError) ErrorCode() int {
return InvalidParamsError
}
type internalError string
func (e internalError) Error() string {
return string(e)
}
func (e internalError) ErrorCode() int {
return InternalError
}
================================================
FILE: transport/http/jsonrpc/error_test.go
================================================
package jsonrpc
import "testing"
func TestError(t *testing.T) {
wantCode := ParseError
sut := Error{
Code: wantCode,
}
gotCode := sut.ErrorCode()
if gotCode != wantCode {
t.Fatalf("want=%d, got=%d", gotCode, wantCode)
}
if sut.Error() == "" {
t.Fatal("Empty error string.")
}
want := "override"
sut.Message = want
got := sut.Error()
if sut.Error() != want {
t.Fatalf("overridden error message: want=%s, got=%s", want, got)
}
}
func TestErrorsSatisfyError(t *testing.T) {
errs := []interface{}{
parseError("parseError"),
invalidRequestError("invalidRequestError"),
methodNotFoundError("methodNotFoundError"),
invalidParamsError("invalidParamsError"),
internalError("internalError"),
}
for _, e := range errs {
err, ok := e.(error)
if !ok {
t.Fatalf("Couldn't assert %s as error.", e)
}
errString := err.Error()
if errString == "" {
t.Fatal("Empty error string")
}
ec, ok := e.(ErrorCoder)
if !ok {
t.Fatalf("Couldn't assert %s as ErrorCoder.", e)
}
if ErrorMessage(ec.ErrorCode()) == "" {
t.Fatalf("Error type %s returned code of %d, which does not map to error string", e, ec.ErrorCode())
}
}
}
================================================
FILE: transport/http/jsonrpc/request_response_types.go
================================================
package jsonrpc
import (
"context"
"encoding/json"
"net/http"
)
// Request defines a JSON RPC request from the spec
// http://www.jsonrpc.org/specification#request_object
type Request struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID *RequestID `json:"id"`
}
// RequestID defines a request ID that can be string, number, or null.
// An identifier established by the Client that MUST contain a String,
// Number, or NULL value if included.
// If it is not included it is assumed to be a notification.
// The value SHOULD normally not be Null and
// Numbers SHOULD NOT contain fractional parts.
type RequestID struct {
intValue int
intError error
floatValue float32
floatError error
stringValue string
stringError error
}
// RequestFunc may take information from decoded json body and place in
// request context. In Servers, RequestFuncs are executed after json is parsed
// but prior to invoking the codec
type RequestFunc func(context.Context, *http.Request, Request) context.Context
// UnmarshalJSON satisfies json.Unmarshaler
func (id *RequestID) UnmarshalJSON(b []byte) error {
id.intError = json.Unmarshal(b, &id.intValue)
id.floatError = json.Unmarshal(b, &id.floatValue)
id.stringError = json.Unmarshal(b, &id.stringValue)
return nil
}
func (id *RequestID) MarshalJSON() ([]byte, error) {
if id.intError == nil {
return json.Marshal(id.intValue)
} else if id.floatError == nil {
return json.Marshal(id.floatValue)
} else {
return json.Marshal(id.stringValue)
}
}
// Int returns the ID as an integer value.
// An error is returned if the ID can't be treated as an int.
func (id *RequestID) Int() (int, error) {
return id.intValue, id.intError
}
// Float32 returns the ID as a float value.
// An error is returned if the ID can't be treated as an float.
func (id *RequestID) Float32() (float32, error) {
return id.floatValue, id.floatError
}
// String returns the ID as a string value.
// An error is returned if the ID can't be treated as an string.
func (id *RequestID) String() (string, error) {
return id.stringValue, id.stringError
}
// Response defines a JSON RPC response from the spec
// http://www.jsonrpc.org/specification#response_object
type Response struct {
JSONRPC string `json:"jsonrpc"`
Result json.RawMessage `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
ID *RequestID `json:"id"`
}
const (
// Version defines the version of the JSON RPC implementation
Version string = "2.0"
// ContentType defines the content type to be served.
ContentType string = "application/json; charset=utf-8"
)
type contextKey int
const (
ContextKeyRequestMethod contextKey = iota
)
================================================
FILE: transport/http/jsonrpc/request_response_types_test.go
================================================
package jsonrpc_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/go-kit/kit/transport/http/jsonrpc"
)
func TestCanUnMarshalID(t *testing.T) {
cases := []struct {
JSON string
expType string
expValue interface{}
}{
{`12345`, "int", 12345},
{`12345.6`, "float", 12345.6},
{`"stringaling"`, "string", "stringaling"},
}
for _, c := range cases {
r := jsonrpc.Request{}
JSON := fmt.Sprintf(`{"id":%s}`, c.JSON)
var foo interface{}
_ = json.Unmarshal([]byte(JSON), &foo)
err := json.Unmarshal([]byte(JSON), &r)
if err != nil {
t.Fatalf("Unexpected error unmarshaling JSON into request: %s\n", err)
}
id := r.ID
switch c.expType {
case "int":
want := c.expValue.(int)
got, err := id.Int()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("'%s' Int(): want %d, got %d.", c.JSON, want, got)
}
// Allow an int ID to be interpreted as a float.
wantf := float32(c.expValue.(int))
gotf, err := id.Float32()
if err != nil {
t.Fatal(err)
}
if gotf != wantf {
t.Fatalf("'%s' Int value as Float32(): want %f, got %f.", c.JSON, wantf, gotf)
}
_, err = id.String()
if err == nil {
t.Fatal("Expected String() to error for int value. Didn't.")
}
case "string":
want := c.expValue.(string)
got, err := id.String()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("'%s' String(): want %s, got %s.", c.JSON, want, got)
}
_, err = id.Int()
if err == nil {
t.Fatal("Expected Int() to error for string value. Didn't.")
}
_, err = id.Float32()
if err == nil {
t.Fatal("Expected Float32() to error for string value. Didn't.")
}
case "float32":
want := c.expValue.(float32)
got, err := id.Float32()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("'%s' Float32(): want %f, got %f.", c.JSON, want, got)
}
_, err = id.String()
if err == nil {
t.Fatal("Expected String() to error for float value. Didn't.")
}
_, err = id.Int()
if err == nil {
t.Fatal("Expected Int() to error for float value. Didn't.")
}
}
}
}
func TestCanUnmarshalNullID(t *testing.T) {
r := jsonrpc.Request{}
JSON := `{"id":null}`
err := json.Unmarshal([]byte(JSON), &r)
if err != nil {
t.Fatalf("Unexpected error unmarshaling JSON into request: %s\n", err)
}
if r.ID != nil {
t.Fatalf("Expected ID to be nil, got %+v.\n", r.ID)
}
}
func TestCanMarshalID(t *testing.T) {
cases := []struct {
JSON string
expType string
expValue interface{}
}{
{`12345`, "int", 12345},
{`12345.6`, "float", 12345.6},
{`"stringaling"`, "string", "stringaling"},
{`null`, "null", nil},
}
for _, c := range cases {
req := jsonrpc.Request{}
JSON := fmt.Sprintf(`{"jsonrpc":"2.0","id":%s}`, c.JSON)
json.Unmarshal([]byte(JSON), &req)
resp := jsonrpc.Response{ID: req.ID, JSONRPC: req.JSONRPC}
want := JSON
bol, _ := json.Marshal(resp)
got := string(bol)
if got != want {
t.Fatalf("'%s': want %s, got %s.", c.expType, want, got)
}
}
}
================================================
FILE: transport/http/jsonrpc/server.go
================================================
package jsonrpc
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/go-kit/log"
)
type requestIDKeyType struct{}
var requestIDKey requestIDKeyType
// Server wraps an endpoint and implements http.Handler.
type Server struct {
ecm EndpointCodecMap
before []httptransport.RequestFunc
beforeCodec []RequestFunc
after []httptransport.ServerResponseFunc
errorEncoder httptransport.ErrorEncoder
finalizer httptransport.ServerFinalizerFunc
logger log.Logger
}
// NewServer constructs a new server, which implements http.Server.
func NewServer(
ecm EndpointCodecMap,
options ...ServerOption,
) *Server {
s := &Server{
ecm: ecm,
errorEncoder: DefaultErrorEncoder,
logger: log.NewNopLogger(),
}
for _, option := range options {
option(s)
}
return s
}
// ServerOption sets an optional parameter for servers.
type ServerOption func(*Server)
// ServerBefore functions are executed on the HTTP request object before the
// request is decoded.
func ServerBefore(before ...httptransport.RequestFunc) ServerOption {
return func(s *Server) { s.before = append(s.before, before...) }
}
// ServerBeforeCodec functions are executed after the JSON request body has been
// decoded, but before the method's decoder is called. This provides an opportunity
// for middleware to inspect the contents of the rpc request before being passed
// to the codec.
func ServerBeforeCodec(beforeCodec ...RequestFunc) ServerOption {
return func(s *Server) { s.beforeCodec = append(s.beforeCodec, beforeCodec...) }
}
// ServerAfter functions are executed on the HTTP response writer after the
// endpoint is invoked, but before anything is written to the client.
func ServerAfter(after ...httptransport.ServerResponseFunc) ServerOption {
return func(s *Server) { s.after = append(s.after, after...) }
}
// ServerErrorEncoder is used to encode errors to the http.ResponseWriter
// whenever they're encountered in the processing of a request. Clients can
// use this to provide custom error formatting and response codes. By default,
// errors will be written with the DefaultErrorEncoder.
func ServerErrorEncoder(ee httptransport.ErrorEncoder) ServerOption {
return func(s *Server) { s.errorEncoder = ee }
}
// ServerErrorLogger is used to log non-terminal errors. By default, no errors
// are logged. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom ServerErrorEncoder or ServerFinalizer, both of which have access to
// the context.
func ServerErrorLogger(logger log.Logger) ServerOption {
return func(s *Server) { s.logger = logger }
}
// ServerFinalizer is executed at the end of every HTTP request.
// By default, no finalizer is registered.
func ServerFinalizer(f httptransport.ServerFinalizerFunc) ServerOption {
return func(s *Server) { s.finalizer = f }
}
// ServeHTTP implements http.Handler.
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = io.WriteString(w, "405 must POST\n")
return
}
ctx := r.Context()
if s.finalizer != nil {
iw := &interceptingWriter{w, http.StatusOK}
defer func() { s.finalizer(ctx, iw.code, r) }()
w = iw
}
for _, f := range s.before {
ctx = f(ctx, r)
}
// Decode the body into an object
var req Request
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
rpcerr := parseError("JSON could not be decoded: " + err.Error())
s.logger.Log("err", rpcerr)
s.errorEncoder(ctx, rpcerr, w)
return
}
ctx = context.WithValue(ctx, requestIDKey, req.ID)
ctx = context.WithValue(ctx, ContextKeyRequestMethod, req.Method)
for _, f := range s.beforeCodec {
ctx = f(ctx, r, req)
}
// Get the endpoint and codecs from the map using the method
// defined in the JSON object
ecm, ok := s.ecm[req.Method]
if !ok {
err := methodNotFoundError(fmt.Sprintf("Method %s was not found.", req.Method))
s.logger.Log("err", err)
s.errorEncoder(ctx, err, w)
return
}
// Decode the JSON "params"
reqParams, err := ecm.Decode(ctx, req.Params)
if err != nil {
s.logger.Log("err", err)
s.errorEncoder(ctx, err, w)
return
}
// Call the Endpoint with the params
response, err := ecm.Endpoint(ctx, reqParams)
if err != nil {
s.logger.Log("err", err)
s.errorEncoder(ctx, err, w)
return
}
for _, f := range s.after {
ctx = f(ctx, w)
}
res := Response{
ID: req.ID,
JSONRPC: Version,
}
// Encode the response from the Endpoint
resParams, err := ecm.Encode(ctx, response)
if err != nil {
s.logger.Log("err", err)
s.errorEncoder(ctx, err, w)
return
}
res.Result = resParams
w.Header().Set("Content-Type", ContentType)
_ = json.NewEncoder(w).Encode(res)
}
// DefaultErrorEncoder writes the error to the ResponseWriter,
// as a json-rpc error response, with an InternalError status code.
// The Error() string of the error will be used as the response error message.
// If the error implements ErrorCoder, the provided code will be set on the
// response error.
// If the error implements Headerer, the given headers will be set.
func DefaultErrorEncoder(ctx context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", ContentType)
if headerer, ok := err.(httptransport.Headerer); ok {
for k := range headerer.Headers() {
w.Header().Set(k, headerer.Headers().Get(k))
}
}
e := Error{
Code: InternalError,
Message: err.Error(),
}
if sc, ok := err.(ErrorCoder); ok {
e.Code = sc.ErrorCode()
}
w.WriteHeader(http.StatusOK)
var requestID *RequestID
if v := ctx.Value(requestIDKey); v != nil {
requestID = v.(*RequestID)
}
_ = json.NewEncoder(w).Encode(Response{
ID: requestID,
JSONRPC: Version,
Error: &e,
})
}
// ErrorCoder is checked by DefaultErrorEncoder. If an error value implements
// ErrorCoder, the integer result of ErrorCode() will be used as the JSONRPC
// error code when encoding the error.
//
// By default, InternalError (-32603) is used.
type ErrorCoder interface {
ErrorCode() int
}
// interceptingWriter intercepts calls to WriteHeader, so that a finalizer
// can be given the correct status code.
type interceptingWriter struct {
http.ResponseWriter
code int
}
// WriteHeader may not be explicitly called, so care must be taken to
// initialize w.code to its default value of http.StatusOK.
func (w *interceptingWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}
================================================
FILE: transport/http/jsonrpc/server_test.go
================================================
package jsonrpc_test
import (
"context"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport/http/jsonrpc"
)
func addBody() io.Reader {
return body(`{"jsonrpc": "2.0", "method": "add", "params": [3, 2], "id": 1}`)
}
func body(in string) io.Reader {
return strings.NewReader(in)
}
func unmarshalResponse(body []byte) (resp jsonrpc.Response, err error) {
err = json.Unmarshal(body, &resp)
return
}
func expectErrorCode(t *testing.T, want int, body []byte) {
t.Helper()
r, err := unmarshalResponse(body)
if err != nil {
t.Fatalf("Can't decode response: %v (%s)", err, body)
}
if r.Error == nil {
t.Fatalf("Expected error on response. Got none: %s", body)
}
if have := r.Error.Code; want != have {
t.Fatalf("Unexpected error code. Want %d, have %d: %s", want, have, body)
}
}
func expectValidRequestID(t *testing.T, want int, body []byte) {
t.Helper()
r, err := unmarshalResponse(body)
if err != nil {
t.Fatalf("Can't decode response: %v (%s)", err, body)
}
have, err := r.ID.Int()
if err != nil {
t.Fatalf("Can't get requestID in response. err=%s, body=%s", err, body)
}
if want != have {
t.Fatalf("Request ID: want %d, have %d (%s)", want, have, body)
}
}
func expectNilRequestID(t *testing.T, body []byte) {
t.Helper()
r, err := unmarshalResponse(body)
if err != nil {
t.Fatalf("Can't decode response: %v (%s)", err, body)
}
if r.ID != nil {
t.Fatalf("Request ID: want nil, have %v", r.ID)
}
}
func nopDecoder(context.Context, json.RawMessage) (interface{}, error) { return struct{}{}, nil }
func nopEncoder(context.Context, interface{}) (json.RawMessage, error) { return []byte("[]"), nil }
type mockLogger struct {
Called bool
LastArgs []interface{}
}
func (l *mockLogger) Log(keyvals ...interface{}) error {
l.Called = true
l.LastArgs = append(l.LastArgs, keyvals)
return nil
}
func TestServerBadDecode(t *testing.T) {
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: func(context.Context, json.RawMessage) (interface{}, error) { return struct{}{}, errors.New("oof") },
Encode: nopEncoder,
},
}
logger := mockLogger{}
handler := jsonrpc.NewServer(ecm, jsonrpc.ServerErrorLogger(&logger))
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Post(server.URL, "application/json", addBody())
buf, _ := ioutil.ReadAll(resp.Body)
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d: %s", want, have, buf)
}
expectErrorCode(t, jsonrpc.InternalError, buf)
if !logger.Called {
t.Fatal("Expected logger to be called with error. Wasn't.")
}
}
func TestServerBadEndpoint(t *testing.T) {
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: func(context.Context, interface{}) (interface{}, error) { return struct{}{}, errors.New("oof") },
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler := jsonrpc.NewServer(ecm)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Post(server.URL, "application/json", addBody())
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
expectErrorCode(t, jsonrpc.InternalError, buf)
expectValidRequestID(t, 1, buf)
}
func TestServerBadEncode(t *testing.T) {
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: nopDecoder,
Encode: func(context.Context, interface{}) (json.RawMessage, error) { return []byte{}, errors.New("oof") },
},
}
handler := jsonrpc.NewServer(ecm)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Post(server.URL, "application/json", addBody())
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
expectErrorCode(t, jsonrpc.InternalError, buf)
expectValidRequestID(t, 1, buf)
}
func TestServerErrorEncoder(t *testing.T) {
errTeapot := errors.New("teapot")
code := func(err error) int {
if errors.Is(err, errTeapot) {
return http.StatusTeapot
}
return http.StatusInternalServerError
}
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: func(context.Context, interface{}) (interface{}, error) { return struct{}{}, errTeapot },
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler := jsonrpc.NewServer(
ecm,
jsonrpc.ServerErrorEncoder(func(_ context.Context, err error, w http.ResponseWriter) { w.WriteHeader(code(err)) }),
)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Post(server.URL, "application/json", addBody())
if want, have := http.StatusTeapot, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestCanRejectNonPostRequest(t *testing.T) {
ecm := jsonrpc.EndpointCodecMap{}
handler := jsonrpc.NewServer(ecm)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Get(server.URL)
if want, have := http.StatusMethodNotAllowed, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestCanRejectInvalidJSON(t *testing.T) {
ecm := jsonrpc.EndpointCodecMap{}
handler := jsonrpc.NewServer(ecm)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Post(server.URL, "application/json", body("clearlynotjson"))
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
expectErrorCode(t, jsonrpc.ParseError, buf)
expectNilRequestID(t, buf)
}
func TestServerUnregisteredMethod(t *testing.T) {
ecm := jsonrpc.EndpointCodecMap{}
handler := jsonrpc.NewServer(ecm)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Post(server.URL, "application/json", addBody())
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
expectErrorCode(t, jsonrpc.MethodNotFoundError, buf)
}
func TestServerHappyPath(t *testing.T) {
step, response := testServer(t)
step()
resp := <-response
defer resp.Body.Close() // nolint
buf, _ := ioutil.ReadAll(resp.Body)
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d (%s)", want, have, buf)
}
r, err := unmarshalResponse(buf)
if err != nil {
t.Fatalf("Can't decode response. err=%s, body=%s", err, buf)
}
if r.JSONRPC != jsonrpc.Version {
t.Fatalf("JSONRPC Version: want=%s, got=%s", jsonrpc.Version, r.JSONRPC)
}
if r.Error != nil {
t.Fatalf("Unxpected error on response: %s", buf)
}
}
func TestMultipleServerBeforeCodec(t *testing.T) {
var done = make(chan struct{})
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler := jsonrpc.NewServer(
ecm,
jsonrpc.ServerBeforeCodec(func(ctx context.Context, r *http.Request, req jsonrpc.Request) context.Context {
ctx = context.WithValue(ctx, "one", 1)
return ctx
}),
jsonrpc.ServerBeforeCodec(func(ctx context.Context, r *http.Request, req jsonrpc.Request) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerBeforeCodecs are used")
}
close(done)
return ctx
}),
)
server := httptest.NewServer(handler)
defer server.Close()
http.Post(server.URL, "application/json", addBody()) // nolint
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
func TestMultipleServerBefore(t *testing.T) {
var done = make(chan struct{})
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler := jsonrpc.NewServer(
ecm,
jsonrpc.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
ctx = context.WithValue(ctx, "one", 1)
return ctx
}),
jsonrpc.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerBefores are used")
}
close(done)
return ctx
}),
)
server := httptest.NewServer(handler)
defer server.Close()
http.Post(server.URL, "application/json", addBody()) // nolint
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
func TestMultipleServerAfter(t *testing.T) {
var done = make(chan struct{})
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler := jsonrpc.NewServer(
ecm,
jsonrpc.ServerAfter(func(ctx context.Context, w http.ResponseWriter) context.Context {
ctx = context.WithValue(ctx, "one", 1)
return ctx
}),
jsonrpc.ServerAfter(func(ctx context.Context, w http.ResponseWriter) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerAfters are used")
}
close(done)
return ctx
}),
)
server := httptest.NewServer(handler)
defer server.Close()
http.Post(server.URL, "application/json", addBody()) // nolint
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
func TestCanFinalize(t *testing.T) {
var done = make(chan struct{})
var finalizerCalled bool
ecm := jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint.Nop,
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler := jsonrpc.NewServer(
ecm,
jsonrpc.ServerFinalizer(func(ctx context.Context, code int, req *http.Request) {
finalizerCalled = true
close(done)
}),
)
server := httptest.NewServer(handler)
defer server.Close()
http.Post(server.URL, "application/json", addBody()) // nolint
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
if !finalizerCalled {
t.Fatal("Finalizer was not called.")
}
}
func testServer(t *testing.T) (step func(), resp <-chan *http.Response) {
var (
stepch = make(chan bool)
endpoint = func(ctx context.Context, request interface{}) (response interface{}, err error) {
<-stepch
return struct{}{}, nil
}
response = make(chan *http.Response)
ecm = jsonrpc.EndpointCodecMap{
"add": jsonrpc.EndpointCodec{
Endpoint: endpoint,
Decode: nopDecoder,
Encode: nopEncoder,
},
}
handler = jsonrpc.NewServer(ecm)
)
go func() {
server := httptest.NewServer(handler)
defer server.Close()
rb := strings.NewReader(`{"jsonrpc": "2.0", "method": "add", "params": [3, 2], "id": 1}`)
resp, err := http.Post(server.URL, "application/json", rb)
if err != nil {
t.Error(err)
return
}
response <- resp
}()
return func() { stepch <- true }, response
}
================================================
FILE: transport/http/proto/client.go
================================================
package proto
import (
"bytes"
"context"
"errors"
"io/ioutil"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"google.golang.org/protobuf/proto"
)
// EncodeProtoRequest is an EncodeRequestFunc that serializes the request as Protobuf.
// If the request implements Headerer, the provided headers will be applied
// to the request. If the given request does not implement proto.Message, an error will
// be returned.
func EncodeProtoRequest(_ context.Context, r *http.Request, preq interface{}) error {
r.Header.Set("Content-Type", "application/x-protobuf")
if headerer, ok := preq.(httptransport.Headerer); ok {
for k := range headerer.Headers() {
r.Header.Set(k, headerer.Headers().Get(k))
}
}
req, ok := preq.(proto.Message)
if !ok {
return errors.New("response does not implement proto.Message")
}
b, err := proto.Marshal(req)
if err != nil {
return err
}
r.ContentLength = int64(len(b))
r.Body = ioutil.NopCloser(bytes.NewReader(b))
return nil
}
================================================
FILE: transport/http/proto/generate.go
================================================
package proto
//go:generate protoc proto_test.proto --go_out=. --go_opt=Mproto_test.proto=github.com/go-kit/kit/transport/http/proto --go_opt=paths=source_relative
//go:generate mv proto_test.pb.go proto_pb_test.go
================================================
FILE: transport/http/proto/proto_pb_test.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.16.0
// source: proto_test.proto
package proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Cat struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Age int32 `protobuf:"varint,1,opt,name=Age,proto3" json:"Age,omitempty"`
Breed string `protobuf:"bytes,2,opt,name=Breed,proto3" json:"Breed,omitempty"`
Name string `protobuf:"bytes,3,opt,name=Name,proto3" json:"Name,omitempty"`
}
func (x *Cat) Reset() {
*x = Cat{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_test_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Cat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Cat) ProtoMessage() {}
func (x *Cat) ProtoReflect() protoreflect.Message {
mi := &file_proto_test_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Cat.ProtoReflect.Descriptor instead.
func (*Cat) Descriptor() ([]byte, []int) {
return file_proto_test_proto_rawDescGZIP(), []int{0}
}
func (x *Cat) GetAge() int32 {
if x != nil {
return x.Age
}
return 0
}
func (x *Cat) GetBreed() string {
if x != nil {
return x.Breed
}
return ""
}
func (x *Cat) GetName() string {
if x != nil {
return x.Name
}
return ""
}
var File_proto_test_proto protoreflect.FileDescriptor
var file_proto_test_proto_rawDesc = []byte{
0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x22, 0x41, 0x0a, 0x03, 0x43, 0x61, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x41, 0x67, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x41, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x42,
0x72, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x42, 0x72, 0x65, 0x65,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x4e, 0x61, 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_test_proto_rawDescOnce sync.Once
file_proto_test_proto_rawDescData = file_proto_test_proto_rawDesc
)
func file_proto_test_proto_rawDescGZIP() []byte {
file_proto_test_proto_rawDescOnce.Do(func() {
file_proto_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_test_proto_rawDescData)
})
return file_proto_test_proto_rawDescData
}
var file_proto_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proto_test_proto_goTypes = []interface{}{
(*Cat)(nil), // 0: Cat
}
var file_proto_test_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_proto_test_proto_init() }
func file_proto_test_proto_init() {
if File_proto_test_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Cat); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_test_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proto_test_proto_goTypes,
DependencyIndexes: file_proto_test_proto_depIdxs,
MessageInfos: file_proto_test_proto_msgTypes,
}.Build()
File_proto_test_proto = out.File
file_proto_test_proto_rawDesc = nil
file_proto_test_proto_goTypes = nil
file_proto_test_proto_depIdxs = nil
}
================================================
FILE: transport/http/proto/proto_test.go
================================================
package proto
import (
"context"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"google.golang.org/protobuf/proto"
)
func TestEncodeProtoRequest(t *testing.T) {
cat := &Cat{Name: "Ziggy", Age: 13, Breed: "Lumpy"}
r := httptest.NewRequest(http.MethodGet, "/cat", nil)
err := EncodeProtoRequest(context.TODO(), r, cat)
if err != nil {
t.Errorf("expected no encoding errors but got: %s", err)
return
}
const xproto = "application/x-protobuf"
if typ := r.Header.Get("Content-Type"); typ != xproto {
t.Errorf("expected content type of %q, got %q", xproto, typ)
return
}
bod, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("expected no read errors but got: %s", err)
return
}
defer r.Body.Close()
var got Cat
err = proto.Unmarshal(bod, &got)
if err != nil {
t.Errorf("expected no proto errors but got: %s", err)
return
}
if !proto.Equal(&got, cat) {
t.Errorf("expected cats to be equal but got:\n\n%#v\n\nwant:\n\n%#v", got, cat)
return
}
}
func TestEncodeProtoResponse(t *testing.T) {
cat := &Cat{Name: "Ziggy", Age: 13, Breed: "Lumpy"}
wr := httptest.NewRecorder()
err := EncodeProtoResponse(context.TODO(), wr, cat)
if err != nil {
t.Errorf("expected no encoding errors but got: %s", err)
return
}
w := wr.Result()
const xproto = "application/x-protobuf"
if typ := w.Header.Get("Content-Type"); typ != xproto {
t.Errorf("expected content type of %q, got %q", xproto, typ)
return
}
if w.StatusCode != http.StatusTeapot {
t.Errorf("expected status code of %d, got %d", http.StatusTeapot, w.StatusCode)
return
}
bod, err := ioutil.ReadAll(w.Body)
if err != nil {
t.Errorf("expected no read errors but got: %s", err)
return
}
defer w.Body.Close()
var got Cat
err = proto.Unmarshal(bod, &got)
if err != nil {
t.Errorf("expected no proto errors but got: %s", err)
return
}
if !proto.Equal(&got, cat) {
t.Errorf("expected cats to be equal but got:\n\n%#v\n\nwant:\n\n%#v", got, cat)
return
}
}
func (c *Cat) StatusCode() int {
return http.StatusTeapot
}
================================================
FILE: transport/http/proto/proto_test.proto
================================================
syntax = "proto3";
message Cat {
int32 Age = 1;
string Breed = 2;
string Name = 3;
}
================================================
FILE: transport/http/proto/server.go
================================================
package proto
import (
"context"
"errors"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"google.golang.org/protobuf/proto"
)
// EncodeProtoResponse is an EncodeResponseFunc that serializes the response as Protobuf.
// Many Proto-over-HTTP services can use it as a sensible default. If the response
// implements Headerer, the provided headers will be applied to the response. If the
// response implements StatusCoder, the provided StatusCode will be used instead of 200.
func EncodeProtoResponse(ctx context.Context, w http.ResponseWriter, pres interface{}) error {
res, ok := pres.(proto.Message)
if !ok {
return errors.New("response does not implement proto.Message")
}
w.Header().Set("Content-Type", "application/x-protobuf")
if headerer, ok := w.(httptransport.Headerer); ok {
for k := range headerer.Headers() {
w.Header().Set(k, headerer.Headers().Get(k))
}
}
code := http.StatusOK
if sc, ok := pres.(httptransport.StatusCoder); ok {
code = sc.StatusCode()
}
w.WriteHeader(code)
if code == http.StatusNoContent {
return nil
}
if res == nil {
return nil
}
b, err := proto.Marshal(res)
if err != nil {
return err
}
_, err = w.Write(b)
if err != nil {
return err
}
return nil
}
================================================
FILE: transport/http/request_response_funcs.go
================================================
package http
import (
"context"
"net/http"
)
// RequestFunc may take information from an HTTP request and put it into a
// request context. In Servers, RequestFuncs are executed prior to invoking the
// endpoint. In Clients, RequestFuncs are executed after creating the request
// but prior to invoking the HTTP client.
type RequestFunc func(context.Context, *http.Request) context.Context
// ServerResponseFunc may take information from a request context and use it to
// manipulate a ResponseWriter. ServerResponseFuncs are only executed in
// servers, after invoking the endpoint but prior to writing a response.
type ServerResponseFunc func(context.Context, http.ResponseWriter) context.Context
// ClientResponseFunc may take information from an HTTP request and make the
// response available for consumption. ClientResponseFuncs are only executed in
// clients, after a request has been made, but prior to it being decoded.
type ClientResponseFunc func(context.Context, *http.Response) context.Context
// SetContentType returns a ServerResponseFunc that sets the Content-Type header
// to the provided value.
func SetContentType(contentType string) ServerResponseFunc {
return SetResponseHeader("Content-Type", contentType)
}
// SetResponseHeader returns a ServerResponseFunc that sets the given header.
func SetResponseHeader(key, val string) ServerResponseFunc {
return func(ctx context.Context, w http.ResponseWriter) context.Context {
w.Header().Set(key, val)
return ctx
}
}
// SetRequestHeader returns a RequestFunc that sets the given header.
func SetRequestHeader(key, val string) RequestFunc {
return func(ctx context.Context, r *http.Request) context.Context {
r.Header.Set(key, val)
return ctx
}
}
// PopulateRequestContext is a RequestFunc that populates several values into
// the context from the HTTP request. Those values may be extracted using the
// corresponding ContextKey type in this package.
func PopulateRequestContext(ctx context.Context, r *http.Request) context.Context {
for k, v := range map[contextKey]string{
ContextKeyRequestMethod: r.Method,
ContextKeyRequestURI: r.RequestURI,
ContextKeyRequestPath: r.URL.Path,
ContextKeyRequestProto: r.Proto,
ContextKeyRequestHost: r.Host,
ContextKeyRequestRemoteAddr: r.RemoteAddr,
ContextKeyRequestXForwardedFor: r.Header.Get("X-Forwarded-For"),
ContextKeyRequestXForwardedProto: r.Header.Get("X-Forwarded-Proto"),
ContextKeyRequestAuthorization: r.Header.Get("Authorization"),
ContextKeyRequestReferer: r.Header.Get("Referer"),
ContextKeyRequestUserAgent: r.Header.Get("User-Agent"),
ContextKeyRequestXRequestID: r.Header.Get("X-Request-Id"),
ContextKeyRequestAccept: r.Header.Get("Accept"),
} {
ctx = context.WithValue(ctx, k, v)
}
return ctx
}
type contextKey int
const (
// ContextKeyRequestMethod is populated in the context by
// PopulateRequestContext. Its value is r.Method.
ContextKeyRequestMethod contextKey = iota
// ContextKeyRequestURI is populated in the context by
// PopulateRequestContext. Its value is r.RequestURI.
ContextKeyRequestURI
// ContextKeyRequestPath is populated in the context by
// PopulateRequestContext. Its value is r.URL.Path.
ContextKeyRequestPath
// ContextKeyRequestProto is populated in the context by
// PopulateRequestContext. Its value is r.Proto.
ContextKeyRequestProto
// ContextKeyRequestHost is populated in the context by
// PopulateRequestContext. Its value is r.Host.
ContextKeyRequestHost
// ContextKeyRequestRemoteAddr is populated in the context by
// PopulateRequestContext. Its value is r.RemoteAddr.
ContextKeyRequestRemoteAddr
// ContextKeyRequestXForwardedFor is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("X-Forwarded-For").
ContextKeyRequestXForwardedFor
// ContextKeyRequestXForwardedProto is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("X-Forwarded-Proto").
ContextKeyRequestXForwardedProto
// ContextKeyRequestAuthorization is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("Authorization").
ContextKeyRequestAuthorization
// ContextKeyRequestReferer is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("Referer").
ContextKeyRequestReferer
// ContextKeyRequestUserAgent is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("User-Agent").
ContextKeyRequestUserAgent
// ContextKeyRequestXRequestID is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("X-Request-Id").
ContextKeyRequestXRequestID
// ContextKeyRequestAccept is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("Accept").
ContextKeyRequestAccept
// ContextKeyResponseHeaders is populated in the context whenever a
// ServerFinalizerFunc is specified. Its value is of type http.Header, and
// is captured only once the entire response has been written.
ContextKeyResponseHeaders
// ContextKeyResponseSize is populated in the context whenever a
// ServerFinalizerFunc is specified. Its value is of type int64.
ContextKeyResponseSize
)
================================================
FILE: transport/http/request_response_funcs_test.go
================================================
package http_test
import (
"context"
"net/http/httptest"
"testing"
httptransport "github.com/go-kit/kit/transport/http"
)
func TestSetHeader(t *testing.T) {
const (
key = "X-Foo"
val = "12345"
)
r := httptest.NewRecorder()
httptransport.SetResponseHeader(key, val)(context.Background(), r)
if want, have := val, r.Header().Get(key); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestSetContentType(t *testing.T) {
const contentType = "application/json"
r := httptest.NewRecorder()
httptransport.SetContentType(contentType)(context.Background(), r)
if want, have := contentType, r.Header().Get("Content-Type"); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
================================================
FILE: transport/http/server.go
================================================
package http
import (
"context"
"encoding/json"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
)
// Server wraps an endpoint and implements http.Handler.
type Server struct {
e endpoint.Endpoint
dec DecodeRequestFunc
enc EncodeResponseFunc
before []RequestFunc
after []ServerResponseFunc
errorEncoder ErrorEncoder
finalizer []ServerFinalizerFunc
errorHandler transport.ErrorHandler
}
// NewServer constructs a new server, which implements http.Handler and wraps
// the provided endpoint.
func NewServer(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...ServerOption,
) *Server {
s := &Server{
e: e,
dec: dec,
enc: enc,
errorEncoder: DefaultErrorEncoder,
errorHandler: transport.NewLogErrorHandler(log.NewNopLogger()),
}
for _, option := range options {
option(s)
}
return s
}
// ServerOption sets an optional parameter for servers.
type ServerOption func(*Server)
// ServerBefore functions are executed on the HTTP request object before the
// request is decoded.
func ServerBefore(before ...RequestFunc) ServerOption {
return func(s *Server) { s.before = append(s.before, before...) }
}
// ServerAfter functions are executed on the HTTP response writer after the
// endpoint is invoked, but before anything is written to the client.
func ServerAfter(after ...ServerResponseFunc) ServerOption {
return func(s *Server) { s.after = append(s.after, after...) }
}
// ServerErrorEncoder is used to encode errors to the http.ResponseWriter
// whenever they're encountered in the processing of a request. Clients can
// use this to provide custom error formatting and response codes. By default,
// errors will be written with the DefaultErrorEncoder.
func ServerErrorEncoder(ee ErrorEncoder) ServerOption {
return func(s *Server) { s.errorEncoder = ee }
}
// ServerErrorLogger is used to log non-terminal errors. By default, no errors
// are logged. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom ServerErrorEncoder or ServerFinalizer, both of which have access to
// the context.
// Deprecated: Use ServerErrorHandler instead.
func ServerErrorLogger(logger log.Logger) ServerOption {
return func(s *Server) { s.errorHandler = transport.NewLogErrorHandler(logger) }
}
// ServerErrorHandler is used to handle non-terminal errors. By default, non-terminal errors
// are ignored. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom ServerErrorEncoder or ServerFinalizer, both of which have access to
// the context.
func ServerErrorHandler(errorHandler transport.ErrorHandler) ServerOption {
return func(s *Server) { s.errorHandler = errorHandler }
}
// ServerFinalizer is executed at the end of every HTTP request.
// By default, no finalizer is registered.
func ServerFinalizer(f ...ServerFinalizerFunc) ServerOption {
return func(s *Server) { s.finalizer = append(s.finalizer, f...) }
}
// ServeHTTP implements http.Handler.
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if len(s.finalizer) > 0 {
iw := &interceptingWriter{w, http.StatusOK, 0}
defer func() {
ctx = context.WithValue(ctx, ContextKeyResponseHeaders, iw.Header())
ctx = context.WithValue(ctx, ContextKeyResponseSize, iw.written)
for _, f := range s.finalizer {
f(ctx, iw.code, r)
}
}()
w = iw.reimplementInterfaces()
}
for _, f := range s.before {
ctx = f(ctx, r)
}
request, err := s.dec(ctx, r)
if err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, w)
return
}
response, err := s.e(ctx, request)
if err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, w)
return
}
for _, f := range s.after {
ctx = f(ctx, w)
}
if err := s.enc(ctx, w, response); err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, w)
return
}
}
// ErrorEncoder is responsible for encoding an error to the ResponseWriter.
// Users are encouraged to use custom ErrorEncoders to encode HTTP errors to
// their clients, and will likely want to pass and check for their own error
// types. See the example shipping/handling service.
type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)
// ServerFinalizerFunc can be used to perform work at the end of an HTTP
// request, after the response has been written to the client. The principal
// intended use is for request logging. In addition to the response code
// provided in the function signature, additional response parameters are
// provided in the context under keys with the ContextKeyResponse prefix.
type ServerFinalizerFunc func(ctx context.Context, code int, r *http.Request)
// NopRequestDecoder is a DecodeRequestFunc that can be used for requests that do not
// need to be decoded, and simply returns nil, nil.
func NopRequestDecoder(ctx context.Context, r *http.Request) (interface{}, error) {
return nil, nil
}
// EncodeJSONResponse is a EncodeResponseFunc that serializes the response as a
// JSON object to the ResponseWriter. Many JSON-over-HTTP services can use it as
// a sensible default. If the response implements Headerer, the provided headers
// will be applied to the response. If the response implements StatusCoder, the
// provided StatusCode will be used instead of 200.
func EncodeJSONResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if headerer, ok := response.(Headerer); ok {
for k, values := range headerer.Headers() {
for _, v := range values {
w.Header().Add(k, v)
}
}
}
code := http.StatusOK
if sc, ok := response.(StatusCoder); ok {
code = sc.StatusCode()
}
w.WriteHeader(code)
if code == http.StatusNoContent {
return nil
}
return json.NewEncoder(w).Encode(response)
}
// DefaultErrorEncoder writes the error to the ResponseWriter, by default a
// content type of text/plain, a body of the plain text of the error, and a
// status code of 500. If the error implements Headerer, the provided headers
// will be applied to the response. If the error implements json.Marshaler, and
// the marshaling succeeds, a content type of application/json and the JSON
// encoded form of the error will be used. If the error implements StatusCoder,
// the provided StatusCode will be used instead of 500.
func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) {
contentType, body := "text/plain; charset=utf-8", []byte(err.Error())
if marshaler, ok := err.(json.Marshaler); ok {
if jsonBody, marshalErr := marshaler.MarshalJSON(); marshalErr == nil {
contentType, body = "application/json; charset=utf-8", jsonBody
}
}
w.Header().Set("Content-Type", contentType)
if headerer, ok := err.(Headerer); ok {
for k, values := range headerer.Headers() {
for _, v := range values {
w.Header().Add(k, v)
}
}
}
code := http.StatusInternalServerError
if sc, ok := err.(StatusCoder); ok {
code = sc.StatusCode()
}
w.WriteHeader(code)
w.Write(body)
}
// StatusCoder is checked by DefaultErrorEncoder. If an error value implements
// StatusCoder, the StatusCode will be used when encoding the error. By default,
// StatusInternalServerError (500) is used.
type StatusCoder interface {
StatusCode() int
}
// Headerer is checked by DefaultErrorEncoder. If an error value implements
// Headerer, the provided headers will be applied to the response writer, after
// the Content-Type is set.
type Headerer interface {
Headers() http.Header
}
================================================
FILE: transport/http/server_test.go
================================================
package http_test
import (
"context"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
)
func TestServerBadDecode(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, errors.New("dang") },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Get(server.URL)
if want, have := http.StatusInternalServerError, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestServerBadEndpoint(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, errors.New("dang") },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Get(server.URL)
if want, have := http.StatusInternalServerError, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestServerBadEncode(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return errors.New("dang") },
)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Get(server.URL)
if want, have := http.StatusInternalServerError, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestServerErrorEncoder(t *testing.T) {
errTeapot := errors.New("teapot")
code := func(err error) int {
if errors.Is(err, errTeapot) {
return http.StatusTeapot
}
return http.StatusInternalServerError
}
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, errTeapot },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
httptransport.ServerErrorEncoder(func(_ context.Context, err error, w http.ResponseWriter) { w.WriteHeader(code(err)) }),
)
server := httptest.NewServer(handler)
defer server.Close()
resp, _ := http.Get(server.URL)
if want, have := http.StatusTeapot, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestServerHappyPath(t *testing.T) {
step, response := testServer(t)
step()
resp := <-response
defer resp.Body.Close()
buf, _ := ioutil.ReadAll(resp.Body)
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d (%s)", want, have, buf)
}
}
func TestMultipleServerBefore(t *testing.T) {
var (
headerKey = "X-Henlo-Lizer"
headerVal = "Helllo you stinky lizard"
statusCode = http.StatusTeapot
responseBody = "go eat a fly ugly\n"
done = make(chan struct{})
)
handler := httptransport.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) {
return struct{}{}, nil
},
func(_ context.Context, w http.ResponseWriter, _ interface{}) error {
w.Header().Set(headerKey, headerVal)
w.WriteHeader(statusCode)
w.Write([]byte(responseBody))
return nil
},
httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
ctx = context.WithValue(ctx, "one", 1)
return ctx
}),
httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerBefores are used")
}
close(done)
return ctx
}),
)
server := httptest.NewServer(handler)
defer server.Close()
go http.Get(server.URL)
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
func TestMultipleServerAfter(t *testing.T) {
var (
headerKey = "X-Henlo-Lizer"
headerVal = "Helllo you stinky lizard"
statusCode = http.StatusTeapot
responseBody = "go eat a fly ugly\n"
done = make(chan struct{})
)
handler := httptransport.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) {
return struct{}{}, nil
},
func(_ context.Context, w http.ResponseWriter, _ interface{}) error {
w.Header().Set(headerKey, headerVal)
w.WriteHeader(statusCode)
w.Write([]byte(responseBody))
return nil
},
httptransport.ServerAfter(func(ctx context.Context, w http.ResponseWriter) context.Context {
ctx = context.WithValue(ctx, "one", 1)
return ctx
}),
httptransport.ServerAfter(func(ctx context.Context, w http.ResponseWriter) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerAfters are used")
}
close(done)
return ctx
}),
)
server := httptest.NewServer(handler)
defer server.Close()
go http.Get(server.URL)
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
func TestServerFinalizer(t *testing.T) {
var (
headerKey = "X-Henlo-Lizer"
headerVal = "Helllo you stinky lizard"
statusCode = http.StatusTeapot
responseBody = "go eat a fly ugly\n"
done = make(chan struct{})
)
handler := httptransport.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) {
return struct{}{}, nil
},
func(_ context.Context, w http.ResponseWriter, _ interface{}) error {
w.Header().Set(headerKey, headerVal)
w.WriteHeader(statusCode)
w.Write([]byte(responseBody))
return nil
},
httptransport.ServerFinalizer(func(ctx context.Context, code int, _ *http.Request) {
if want, have := statusCode, code; want != have {
t.Errorf("StatusCode: want %d, have %d", want, have)
}
responseHeader := ctx.Value(httptransport.ContextKeyResponseHeaders).(http.Header)
if want, have := headerVal, responseHeader.Get(headerKey); want != have {
t.Errorf("%s: want %q, have %q", headerKey, want, have)
}
responseSize := ctx.Value(httptransport.ContextKeyResponseSize).(int64)
if want, have := int64(len(responseBody)), responseSize; want != have {
t.Errorf("response size: want %d, have %d", want, have)
}
close(done)
}),
)
server := httptest.NewServer(handler)
defer server.Close()
go http.Get(server.URL)
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
}
type enhancedResponse struct {
Foo string `json:"foo"`
}
func (e enhancedResponse) StatusCode() int { return http.StatusPaymentRequired }
func (e enhancedResponse) Headers() http.Header { return http.Header{"X-Edward": []string{"Snowden"}} }
func TestEncodeJSONResponse(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return enhancedResponse{Foo: "bar"}, nil },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
httptransport.EncodeJSONResponse,
)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
if want, have := http.StatusPaymentRequired, resp.StatusCode; want != have {
t.Errorf("StatusCode: want %d, have %d", want, have)
}
if want, have := "Snowden", resp.Header.Get("X-Edward"); want != have {
t.Errorf("X-Edward: want %q, have %q", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
if want, have := `{"foo":"bar"}`, strings.TrimSpace(string(buf)); want != have {
t.Errorf("Body: want %s, have %s", want, have)
}
}
type multiHeaderResponse struct{}
func (_ multiHeaderResponse) Headers() http.Header {
return http.Header{"Vary": []string{"Origin", "User-Agent"}}
}
func TestAddMultipleHeaders(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return multiHeaderResponse{}, nil },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
httptransport.EncodeJSONResponse,
)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
expect := map[string]map[string]struct{}{"Vary": map[string]struct{}{"Origin": struct{}{}, "User-Agent": struct{}{}}}
for k, vls := range resp.Header {
for _, v := range vls {
delete((expect[k]), v)
}
if len(expect[k]) != 0 {
t.Errorf("Header: unexpected header %s: %v", k, expect[k])
}
}
}
type multiHeaderResponseError struct {
multiHeaderResponse
msg string
}
func (m multiHeaderResponseError) Error() string {
return m.msg
}
func TestAddMultipleHeadersErrorEncoder(t *testing.T) {
errStr := "oh no"
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) {
return nil, multiHeaderResponseError{msg: errStr}
},
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
httptransport.EncodeJSONResponse,
)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
expect := map[string]map[string]struct{}{"Vary": map[string]struct{}{"Origin": struct{}{}, "User-Agent": struct{}{}}}
for k, vls := range resp.Header {
for _, v := range vls {
delete((expect[k]), v)
}
if len(expect[k]) != 0 {
t.Errorf("Header: unexpected header %s: %v", k, expect[k])
}
}
if b, _ := ioutil.ReadAll(resp.Body); errStr != string(b) {
t.Errorf("ErrorEncoder: got: %q, expected: %q", b, errStr)
}
}
type noContentResponse struct{}
func (e noContentResponse) StatusCode() int { return http.StatusNoContent }
func TestEncodeNoContent(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return noContentResponse{}, nil },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
httptransport.EncodeJSONResponse,
)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
if want, have := http.StatusNoContent, resp.StatusCode; want != have {
t.Errorf("StatusCode: want %d, have %d", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
if want, have := 0, len(buf); want != have {
t.Errorf("Body: want no content, have %d bytes", have)
}
}
type enhancedError struct{}
func (e enhancedError) Error() string { return "enhanced error" }
func (e enhancedError) StatusCode() int { return http.StatusTeapot }
func (e enhancedError) MarshalJSON() ([]byte, error) { return []byte(`{"err":"enhanced"}`), nil }
func (e enhancedError) Headers() http.Header { return http.Header{"X-Enhanced": []string{"1"}} }
func TestEnhancedError(t *testing.T) {
handler := httptransport.NewServer(
func(context.Context, interface{}) (interface{}, error) { return nil, enhancedError{} },
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(_ context.Context, w http.ResponseWriter, _ interface{}) error { return nil },
)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if want, have := http.StatusTeapot, resp.StatusCode; want != have {
t.Errorf("StatusCode: want %d, have %d", want, have)
}
if want, have := "1", resp.Header.Get("X-Enhanced"); want != have {
t.Errorf("X-Enhanced: want %q, have %q", want, have)
}
buf, _ := ioutil.ReadAll(resp.Body)
if want, have := `{"err":"enhanced"}`, strings.TrimSpace(string(buf)); want != have {
t.Errorf("Body: want %s, have %s", want, have)
}
}
func TestNoOpRequestDecoder(t *testing.T) {
resw := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Error("Failed to create request")
}
handler := httptransport.NewServer(
func(ctx context.Context, request interface{}) (interface{}, error) {
if request != nil {
t.Error("Expected nil request in endpoint when using NopRequestDecoder")
}
return nil, nil
},
httptransport.NopRequestDecoder,
httptransport.EncodeJSONResponse,
)
handler.ServeHTTP(resw, req)
if resw.Code != http.StatusOK {
t.Errorf("Expected status code %d but got %d", http.StatusOK, resw.Code)
}
}
func testServer(t *testing.T) (step func(), resp <-chan *http.Response) {
var (
stepch = make(chan bool)
endpoint = func(context.Context, interface{}) (interface{}, error) { <-stepch; return struct{}{}, nil }
response = make(chan *http.Response)
handler = httptransport.NewServer(
endpoint,
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context { return ctx }),
httptransport.ServerAfter(func(ctx context.Context, w http.ResponseWriter) context.Context { return ctx }),
)
)
go func() {
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Error(err)
return
}
response <- resp
}()
return func() { stepch <- true }, response
}
================================================
FILE: transport/httprp/doc.go
================================================
// Package httprp provides an HTTP reverse-proxy transport. HTTP handlers that
// need to proxy requests to another HTTP service can do so with this package by
// specifying the URL to forward the request to.
package httprp
================================================
FILE: transport/httprp/server.go
================================================
package httprp
import (
"context"
"net/http"
"net/http/httputil"
"net/url"
)
// RequestFunc may take information from an HTTP request and put it into a
// request context. BeforeFuncs are executed prior to invoking the
// endpoint.
type RequestFunc func(context.Context, *http.Request) context.Context
// Server is a proxying request handler.
type Server struct {
proxy http.Handler
before []RequestFunc
errorEncoder func(w http.ResponseWriter, err error)
}
// NewServer constructs a new server that implements http.Server and will proxy
// requests to the given base URL using its scheme, host, and base path.
// If the target's path is "/base" and the incoming request was for "/dir",
// the target request will be for /base/dir.
func NewServer(
baseURL *url.URL,
options ...ServerOption,
) *Server {
s := &Server{
proxy: httputil.NewSingleHostReverseProxy(baseURL),
}
for _, option := range options {
option(s)
}
return s
}
// ServerOption sets an optional parameter for servers.
type ServerOption func(*Server)
// ServerBefore functions are executed on the HTTP request object before the
// request is decoded.
func ServerBefore(before ...RequestFunc) ServerOption {
return func(s *Server) { s.before = append(s.before, before...) }
}
// ServeHTTP implements http.Handler.
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
for _, f := range s.before {
ctx = f(ctx, r)
}
s.proxy.ServeHTTP(w, r)
}
================================================
FILE: transport/httprp/server_test.go
================================================
package httprp_test
import (
"context"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
httptransport "github.com/go-kit/kit/transport/httprp"
)
func TestServerHappyPathSingleServer(t *testing.T) {
originServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("hey"))
}))
defer originServer.Close()
originURL, _ := url.Parse(originServer.URL)
handler := httptransport.NewServer(
originURL,
)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()
resp, _ := http.Get(proxyServer.URL)
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
responseBody, _ := ioutil.ReadAll(resp.Body)
if want, have := "hey", string(responseBody); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestServerHappyPathSingleServerWithServerOptions(t *testing.T) {
const (
headerKey = "X-TEST-HEADER"
headerVal = "go-kit-proxy"
)
originServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if want, have := headerVal, r.Header.Get(headerKey); want != have {
t.Errorf("want %q, have %q", want, have)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("hey"))
}))
defer originServer.Close()
originURL, _ := url.Parse(originServer.URL)
handler := httptransport.NewServer(
originURL,
httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
r.Header.Add(headerKey, headerVal)
return ctx
}),
)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()
resp, _ := http.Get(proxyServer.URL)
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
responseBody, _ := ioutil.ReadAll(resp.Body)
if want, have := "hey", string(responseBody); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestServerOriginServerNotFoundResponse(t *testing.T) {
originServer := httptest.NewServer(http.NotFoundHandler())
defer originServer.Close()
originURL, _ := url.Parse(originServer.URL)
handler := httptransport.NewServer(
originURL,
)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()
resp, _ := http.Get(proxyServer.URL)
if want, have := http.StatusNotFound, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestServerOriginServerUnreachable(t *testing.T) {
// create a server, then promptly shut it down
originServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
originURL, _ := url.Parse(originServer.URL)
originServer.Close()
handler := httptransport.NewServer(
originURL,
)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()
resp, _ := http.Get(proxyServer.URL)
switch resp.StatusCode {
case http.StatusBadGateway: // go1.7 and beyond
break
case http.StatusInternalServerError: // to go1.7
break
default:
t.Errorf("want %d or %d, have %d", http.StatusBadGateway, http.StatusInternalServerError, resp.StatusCode)
}
}
func TestMultipleServerBefore(t *testing.T) {
const (
headerKey = "X-TEST-HEADER"
headerVal = "go-kit-proxy"
)
originServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if want, have := headerVal, r.Header.Get(headerKey); want != have {
t.Errorf("want %q, have %q", want, have)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("hey"))
}))
defer originServer.Close()
originURL, _ := url.Parse(originServer.URL)
handler := httptransport.NewServer(
originURL,
httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
r.Header.Add(headerKey, headerVal)
return ctx
}),
httptransport.ServerBefore(func(ctx context.Context, r *http.Request) context.Context {
return ctx
}),
)
proxyServer := httptest.NewServer(handler)
defer proxyServer.Close()
resp, _ := http.Get(proxyServer.URL)
if want, have := http.StatusOK, resp.StatusCode; want != have {
t.Errorf("want %d, have %d", want, have)
}
responseBody, _ := ioutil.ReadAll(resp.Body)
if want, have := "hey", string(responseBody); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
================================================
FILE: transport/nats/doc.go
================================================
// Package nats provides a NATS transport.
package nats
================================================
FILE: transport/nats/encode_decode.go
================================================
package nats
import (
"context"
"github.com/nats-io/nats.go"
)
// DecodeRequestFunc extracts a user-domain request object from a publisher
// request object. It's designed to be used in NATS subscribers, for subscriber-side
// endpoints. One straightforward DecodeRequestFunc could be something that
// JSON decodes from the request body to the concrete response type.
type DecodeRequestFunc func(context.Context, *nats.Msg) (request interface{}, err error)
// EncodeRequestFunc encodes the passed request object into the NATS request
// object. It's designed to be used in NATS publishers, for publisher-side
// endpoints. One straightforward EncodeRequestFunc could something that JSON
// encodes the object directly to the request payload.
type EncodeRequestFunc func(context.Context, *nats.Msg, interface{}) error
// EncodeResponseFunc encodes the passed response object to the subscriber reply.
// It's designed to be used in NATS subscribers, for subscriber-side
// endpoints. One straightforward EncodeResponseFunc could be something that
// JSON encodes the object directly to the response body.
type EncodeResponseFunc func(context.Context, string, *nats.Conn, interface{}) error
// DecodeResponseFunc extracts a user-domain response object from an NATS
// response object. It's designed to be used in NATS publisher, for publisher-side
// endpoints. One straightforward DecodeResponseFunc could be something that
// JSON decodes from the response payload to the concrete response type.
type DecodeResponseFunc func(context.Context, *nats.Msg) (response interface{}, err error)
================================================
FILE: transport/nats/publisher.go
================================================
package nats
import (
"context"
"encoding/json"
"github.com/go-kit/kit/endpoint"
"github.com/nats-io/nats.go"
"time"
)
// Publisher wraps a URL and provides a method that implements endpoint.Endpoint.
type Publisher struct {
publisher *nats.Conn
subject string
enc EncodeRequestFunc
dec DecodeResponseFunc
before []RequestFunc
after []PublisherResponseFunc
timeout time.Duration
}
// NewPublisher constructs a usable Publisher for a single remote method.
func NewPublisher(
publisher *nats.Conn,
subject string,
enc EncodeRequestFunc,
dec DecodeResponseFunc,
options ...PublisherOption,
) *Publisher {
p := &Publisher{
publisher: publisher,
subject: subject,
enc: enc,
dec: dec,
timeout: 10 * time.Second,
}
for _, option := range options {
option(p)
}
return p
}
// PublisherOption sets an optional parameter for clients.
type PublisherOption func(*Publisher)
// PublisherBefore sets the RequestFuncs that are applied to the outgoing NATS
// request before it's invoked.
func PublisherBefore(before ...RequestFunc) PublisherOption {
return func(p *Publisher) { p.before = append(p.before, before...) }
}
// PublisherAfter sets the ClientResponseFuncs applied to the incoming NATS
// request prior to it being decoded. This is useful for obtaining anything off
// of the response and adding onto the context prior to decoding.
func PublisherAfter(after ...PublisherResponseFunc) PublisherOption {
return func(p *Publisher) { p.after = append(p.after, after...) }
}
// PublisherTimeout sets the available timeout for NATS request.
func PublisherTimeout(timeout time.Duration) PublisherOption {
return func(p *Publisher) { p.timeout = timeout }
}
// Endpoint returns a usable endpoint that invokes the remote endpoint.
func (p Publisher) Endpoint() endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()
msg := nats.Msg{Subject: p.subject}
if err := p.enc(ctx, &msg, request); err != nil {
return nil, err
}
for _, f := range p.before {
ctx = f(ctx, &msg)
}
resp, err := p.publisher.RequestWithContext(ctx, msg.Subject, msg.Data)
if err != nil {
return nil, err
}
for _, f := range p.after {
ctx = f(ctx, resp)
}
response, err := p.dec(ctx, resp)
if err != nil {
return nil, err
}
return response, nil
}
}
// EncodeJSONRequest is an EncodeRequestFunc that serializes the request as a
// JSON object to the Data of the Msg. Many JSON-over-NATS services can use it as
// a sensible default.
func EncodeJSONRequest(_ context.Context, msg *nats.Msg, request interface{}) error {
b, err := json.Marshal(request)
if err != nil {
return err
}
msg.Data = b
return nil
}
================================================
FILE: transport/nats/publisher_test.go
================================================
package nats_test
import (
"context"
"strings"
"testing"
"time"
natstransport "github.com/go-kit/kit/transport/nats"
"github.com/nats-io/nats.go"
)
func TestPublisher(t *testing.T) {
var (
testdata = "testdata"
encode = func(context.Context, *nats.Msg, interface{}) error { return nil }
decode = func(_ context.Context, msg *nats.Msg) (interface{}, error) {
return TestResponse{string(msg.Data), ""}, nil
}
)
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", func(msg *nats.Msg) {
if err := c.Publish(msg.Reply, []byte(testdata)); err != nil {
t.Fatal(err)
}
})
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
publisher := natstransport.NewPublisher(
c,
"natstransport.test",
encode,
decode,
)
res, err := publisher.Endpoint()(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
response, ok := res.(TestResponse)
if !ok {
t.Fatal("response should be TestResponse")
}
if want, have := testdata, response.String; want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestPublisherBefore(t *testing.T) {
var (
testdata = "testdata"
encode = func(context.Context, *nats.Msg, interface{}) error { return nil }
decode = func(_ context.Context, msg *nats.Msg) (interface{}, error) {
return TestResponse{string(msg.Data), ""}, nil
}
)
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", func(msg *nats.Msg) {
if err := c.Publish(msg.Reply, msg.Data); err != nil {
t.Fatal(err)
}
})
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
publisher := natstransport.NewPublisher(
c,
"natstransport.test",
encode,
decode,
natstransport.PublisherBefore(func(ctx context.Context, msg *nats.Msg) context.Context {
msg.Data = []byte(strings.ToUpper(string(testdata)))
return ctx
}),
)
res, err := publisher.Endpoint()(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
response, ok := res.(TestResponse)
if !ok {
t.Fatal("response should be TestResponse")
}
if want, have := strings.ToUpper(testdata), response.String; want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestPublisherAfter(t *testing.T) {
var (
testdata = "testdata"
encode = func(context.Context, *nats.Msg, interface{}) error { return nil }
decode = func(_ context.Context, msg *nats.Msg) (interface{}, error) {
return TestResponse{string(msg.Data), ""}, nil
}
)
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", func(msg *nats.Msg) {
if err := c.Publish(msg.Reply, []byte(testdata)); err != nil {
t.Fatal(err)
}
})
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
publisher := natstransport.NewPublisher(
c,
"natstransport.test",
encode,
decode,
natstransport.PublisherAfter(func(ctx context.Context, msg *nats.Msg) context.Context {
msg.Data = []byte(strings.ToUpper(string(msg.Data)))
return ctx
}),
)
res, err := publisher.Endpoint()(context.Background(), struct{}{})
if err != nil {
t.Fatal(err)
}
response, ok := res.(TestResponse)
if !ok {
t.Fatal("response should be TestResponse")
}
if want, have := strings.ToUpper(testdata), response.String; want != have {
t.Errorf("want %q, have %q", want, have)
}
}
func TestPublisherTimeout(t *testing.T) {
var (
encode = func(context.Context, *nats.Msg, interface{}) error { return nil }
decode = func(_ context.Context, msg *nats.Msg) (interface{}, error) {
return TestResponse{string(msg.Data), ""}, nil
}
)
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
ch := make(chan struct{})
defer close(ch)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", func(msg *nats.Msg) {
<-ch
})
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
publisher := natstransport.NewPublisher(
c,
"natstransport.test",
encode,
decode,
natstransport.PublisherTimeout(time.Second),
)
_, err = publisher.Endpoint()(context.Background(), struct{}{})
if err != context.DeadlineExceeded {
t.Errorf("want %s, have %s", context.DeadlineExceeded, err)
}
}
func TestPublisherCancellation(t *testing.T) {
var (
testdata = "testdata"
encode = func(context.Context, *nats.Msg, interface{}) error { return nil }
decode = func(_ context.Context, msg *nats.Msg) (interface{}, error) {
return TestResponse{string(msg.Data), ""}, nil
}
)
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", func(msg *nats.Msg) {
if err := c.Publish(msg.Reply, []byte(testdata)); err != nil {
t.Fatal(err)
}
})
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
publisher := natstransport.NewPublisher(
c,
"natstransport.test",
encode,
decode,
)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = publisher.Endpoint()(ctx, struct{}{})
if err != context.Canceled {
t.Errorf("want %s, have %s", context.Canceled, err)
}
}
func TestEncodeJSONRequest(t *testing.T) {
var data string
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", func(msg *nats.Msg) {
data = string(msg.Data)
if err := c.Publish(msg.Reply, []byte("")); err != nil {
t.Fatal(err)
}
})
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
publisher := natstransport.NewPublisher(
c,
"natstransport.test",
natstransport.EncodeJSONRequest,
func(context.Context, *nats.Msg) (interface{}, error) { return nil, nil },
).Endpoint()
for _, test := range []struct {
value interface{}
body string
}{
{nil, "null"},
{12, "12"},
{1.2, "1.2"},
{true, "true"},
{"test", "\"test\""},
{struct {
Foo string `json:"foo"`
}{"foo"}, "{\"foo\":\"foo\"}"},
} {
if _, err := publisher(context.Background(), test.value); err != nil {
t.Fatal(err)
continue
}
if data != test.body {
t.Errorf("%v: actual %#v, expected %#v", test.value, data, test.body)
}
}
}
================================================
FILE: transport/nats/request_response_funcs.go
================================================
package nats
import (
"context"
"github.com/nats-io/nats.go"
)
// RequestFunc may take information from a publisher request and put it into a
// request context. In Subscribers, RequestFuncs are executed prior to invoking the
// endpoint.
type RequestFunc func(context.Context, *nats.Msg) context.Context
// SubscriberResponseFunc may take information from a request context and use it to
// manipulate a Publisher. SubscriberResponseFuncs are only executed in
// subscribers, after invoking the endpoint but prior to publishing a reply.
type SubscriberResponseFunc func(context.Context, *nats.Conn) context.Context
// PublisherResponseFunc may take information from an NATS request and make the
// response available for consumption. ClientResponseFuncs are only executed in
// clients, after a request has been made, but prior to it being decoded.
type PublisherResponseFunc func(context.Context, *nats.Msg) context.Context
================================================
FILE: transport/nats/subscriber.go
================================================
package nats
import (
"context"
"encoding/json"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport"
"github.com/go-kit/log"
"github.com/nats-io/nats.go"
)
// Subscriber wraps an endpoint and provides nats.MsgHandler.
type Subscriber struct {
e endpoint.Endpoint
dec DecodeRequestFunc
enc EncodeResponseFunc
before []RequestFunc
after []SubscriberResponseFunc
errorEncoder ErrorEncoder
finalizer []SubscriberFinalizerFunc
errorHandler transport.ErrorHandler
}
// NewSubscriber constructs a new subscriber, which provides nats.MsgHandler and wraps
// the provided endpoint.
func NewSubscriber(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...SubscriberOption,
) *Subscriber {
s := &Subscriber{
e: e,
dec: dec,
enc: enc,
errorEncoder: DefaultErrorEncoder,
errorHandler: transport.NewLogErrorHandler(log.NewNopLogger()),
}
for _, option := range options {
option(s)
}
return s
}
// SubscriberOption sets an optional parameter for subscribers.
type SubscriberOption func(*Subscriber)
// SubscriberBefore functions are executed on the publisher request object before the
// request is decoded.
func SubscriberBefore(before ...RequestFunc) SubscriberOption {
return func(s *Subscriber) { s.before = append(s.before, before...) }
}
// SubscriberAfter functions are executed on the subscriber reply after the
// endpoint is invoked, but before anything is published to the reply.
func SubscriberAfter(after ...SubscriberResponseFunc) SubscriberOption {
return func(s *Subscriber) { s.after = append(s.after, after...) }
}
// SubscriberErrorEncoder is used to encode errors to the subscriber reply
// whenever they're encountered in the processing of a request. Clients can
// use this to provide custom error formatting. By default,
// errors will be published with the DefaultErrorEncoder.
func SubscriberErrorEncoder(ee ErrorEncoder) SubscriberOption {
return func(s *Subscriber) { s.errorEncoder = ee }
}
// SubscriberErrorLogger is used to log non-terminal errors. By default, no errors
// are logged. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom SubscriberErrorEncoder which has access to the context.
// Deprecated: Use SubscriberErrorHandler instead.
func SubscriberErrorLogger(logger log.Logger) SubscriberOption {
return func(s *Subscriber) { s.errorHandler = transport.NewLogErrorHandler(logger) }
}
// SubscriberErrorHandler is used to handle non-terminal errors. By default, non-terminal errors
// are ignored. This is intended as a diagnostic measure. Finer-grained control
// of error handling, including logging in more detail, should be performed in a
// custom SubscriberErrorEncoder which has access to the context.
func SubscriberErrorHandler(errorHandler transport.ErrorHandler) SubscriberOption {
return func(s *Subscriber) { s.errorHandler = errorHandler }
}
// SubscriberFinalizer is executed at the end of every request from a publisher through NATS.
// By default, no finalizer is registered.
func SubscriberFinalizer(f ...SubscriberFinalizerFunc) SubscriberOption {
return func(s *Subscriber) { s.finalizer = f }
}
// ServeMsg provides nats.MsgHandler.
func (s Subscriber) ServeMsg(nc *nats.Conn) func(msg *nats.Msg) {
return func(msg *nats.Msg) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if len(s.finalizer) > 0 {
defer func() {
for _, f := range s.finalizer {
f(ctx, msg)
}
}()
}
for _, f := range s.before {
ctx = f(ctx, msg)
}
request, err := s.dec(ctx, msg)
if err != nil {
s.errorHandler.Handle(ctx, err)
if msg.Reply == "" {
return
}
s.errorEncoder(ctx, err, msg.Reply, nc)
return
}
response, err := s.e(ctx, request)
if err != nil {
s.errorHandler.Handle(ctx, err)
if msg.Reply == "" {
return
}
s.errorEncoder(ctx, err, msg.Reply, nc)
return
}
for _, f := range s.after {
ctx = f(ctx, nc)
}
if msg.Reply == "" {
return
}
if err := s.enc(ctx, msg.Reply, nc, response); err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, msg.Reply, nc)
return
}
}
}
// ErrorEncoder is responsible for encoding an error to the subscriber reply.
// Users are encouraged to use custom ErrorEncoders to encode errors to
// their replies, and will likely want to pass and check for their own error
// types.
type ErrorEncoder func(ctx context.Context, err error, reply string, nc *nats.Conn)
// SubscriberFinalizerFunc can be used to perform work at the end of an request
// from a publisher, after the response has been written to the publisher. The principal
// intended use is for request logging.
type SubscriberFinalizerFunc func(ctx context.Context, msg *nats.Msg)
// NopRequestDecoder is a DecodeRequestFunc that can be used for requests that do not
// need to be decoded, and simply returns nil, nil.
func NopRequestDecoder(_ context.Context, _ *nats.Msg) (interface{}, error) {
return nil, nil
}
// EncodeJSONResponse is a EncodeResponseFunc that serializes the response as a
// JSON object to the subscriber reply. Many JSON-over services can use it as
// a sensible default.
func EncodeJSONResponse(_ context.Context, reply string, nc *nats.Conn, response interface{}) error {
b, err := json.Marshal(response)
if err != nil {
return err
}
return nc.Publish(reply, b)
}
// DefaultErrorEncoder writes the error to the subscriber reply.
func DefaultErrorEncoder(_ context.Context, err error, reply string, nc *nats.Conn) {
logger := log.NewNopLogger()
type Response struct {
Error string `json:"err"`
}
var response Response
response.Error = err.Error()
b, err := json.Marshal(response)
if err != nil {
logger.Log("err", err)
return
}
if err := nc.Publish(reply, b); err != nil {
logger.Log("err", err)
}
}
================================================
FILE: transport/nats/subscriber_test.go
================================================
package nats_test
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/go-kit/kit/endpoint"
natstransport "github.com/go-kit/kit/transport/nats"
)
type TestResponse struct {
String string `json:"str"`
Error string `json:"err"`
}
func newNATSConn(t *testing.T) (*server.Server, *nats.Conn) {
s, err := server.NewServer(&server.Options{
Host: "localhost",
Port: 0,
})
if err != nil {
t.Fatal(err)
}
go s.Start()
for i := 0; i < 5 && !s.Running(); i++ {
t.Logf("Running %v", s.Running())
time.Sleep(time.Second)
}
if !s.Running() {
s.Shutdown()
s.WaitForShutdown()
t.Fatal("not yet running")
}
if ok := s.ReadyForConnections(5 * time.Second); !ok {
t.Fatal("not ready for connections")
}
c, err := nats.Connect("nats://"+s.Addr().String(), nats.Name(t.Name()))
if err != nil {
t.Fatalf("failed to connect to NATS server: %s", err)
}
return s, c
}
func TestSubscriberBadDecode(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, errors.New("dang") },
func(context.Context, string, *nats.Conn, interface{}) error { return nil },
)
resp := testRequest(t, c, handler)
if want, have := "dang", resp.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
func TestSubscriberBadEndpoint(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, errors.New("dang") },
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
func(context.Context, string, *nats.Conn, interface{}) error { return nil },
)
resp := testRequest(t, c, handler)
if want, have := "dang", resp.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
func TestSubscriberBadEncode(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil },
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
func(context.Context, string, *nats.Conn, interface{}) error { return errors.New("dang") },
)
resp := testRequest(t, c, handler)
if want, have := "dang", resp.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
func TestSubscriberErrorEncoder(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
errTeapot := errors.New("teapot")
code := func(err error) error {
if errors.Is(err, errTeapot) {
return err
}
return errors.New("dang")
}
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return struct{}{}, errTeapot },
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
func(context.Context, string, *nats.Conn, interface{}) error { return nil },
natstransport.SubscriberErrorEncoder(func(_ context.Context, err error, reply string, nc *nats.Conn) {
var r TestResponse
r.Error = code(err).Error()
b, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
if err := c.Publish(reply, b); err != nil {
t.Fatal(err)
}
}),
)
resp := testRequest(t, c, handler)
if want, have := errTeapot.Error(), resp.Error; want != have {
t.Errorf("want %s, have %s", want, have)
}
}
func TestSubscriberHappySubject(t *testing.T) {
step, response := testSubscriber(t)
step()
r := <-response
var resp TestResponse
err := json.Unmarshal(r.Data, &resp)
if err != nil {
t.Fatal(err)
}
if want, have := "", resp.Error; want != have {
t.Errorf("want %s, have %s (%s)", want, have, r.Data)
}
}
func TestMultipleSubscriberBefore(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
var (
response = struct{ Body string }{"go eat a fly ugly\n"}
wg sync.WaitGroup
done = make(chan struct{})
)
handler := natstransport.NewSubscriber(
endpoint.Nop,
func(context.Context, *nats.Msg) (interface{}, error) {
return struct{}{}, nil
},
func(_ context.Context, reply string, nc *nats.Conn, _ interface{}) error {
b, err := json.Marshal(response)
if err != nil {
return err
}
return c.Publish(reply, b)
},
natstransport.SubscriberBefore(func(ctx context.Context, _ *nats.Msg) context.Context {
ctx = context.WithValue(ctx, "one", 1)
return ctx
}),
natstransport.SubscriberBefore(func(ctx context.Context, _ *nats.Msg) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerBefores are used")
}
close(done)
return ctx
}),
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
wg.Add(1)
go func() {
defer wg.Done()
_, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
wg.Wait()
}
func TestMultipleSubscriberAfter(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
var (
response = struct{ Body string }{"go eat a fly ugly\n"}
wg sync.WaitGroup
done = make(chan struct{})
)
handler := natstransport.NewSubscriber(
endpoint.Nop,
func(context.Context, *nats.Msg) (interface{}, error) {
return struct{}{}, nil
},
func(_ context.Context, reply string, nc *nats.Conn, _ interface{}) error {
b, err := json.Marshal(response)
if err != nil {
return err
}
return c.Publish(reply, b)
},
natstransport.SubscriberAfter(func(ctx context.Context, nc *nats.Conn) context.Context {
return context.WithValue(ctx, "one", 1)
}),
natstransport.SubscriberAfter(func(ctx context.Context, nc *nats.Conn) context.Context {
if _, ok := ctx.Value("one").(int); !ok {
t.Error("Value was not set properly when multiple ServerAfters are used")
}
close(done)
return ctx
}),
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
wg.Add(1)
go func() {
defer wg.Done()
_, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
wg.Wait()
}
func TestSubscriberFinalizerFunc(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
var (
response = struct{ Body string }{"go eat a fly ugly\n"}
wg sync.WaitGroup
done = make(chan struct{})
)
handler := natstransport.NewSubscriber(
endpoint.Nop,
func(context.Context, *nats.Msg) (interface{}, error) {
return struct{}{}, nil
},
func(_ context.Context, reply string, nc *nats.Conn, _ interface{}) error {
b, err := json.Marshal(response)
if err != nil {
return err
}
return c.Publish(reply, b)
},
natstransport.SubscriberFinalizer(func(ctx context.Context, _ *nats.Msg) {
close(done)
}),
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
wg.Add(1)
go func() {
defer wg.Done()
_, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for finalizer")
}
wg.Wait()
}
func TestEncodeJSONResponse(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) {
return struct {
Foo string `json:"foo"`
}{"bar"}, nil
},
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
natstransport.EncodeJSONResponse,
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
r, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
if want, have := `{"foo":"bar"}`, strings.TrimSpace(string(r.Data)); want != have {
t.Errorf("Body: want %s, have %s", want, have)
}
}
type responseError struct {
msg string
}
func (m responseError) Error() string {
return m.msg
}
func TestErrorEncoder(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
errResp := struct {
Error string `json:"err"`
}{"oh no"}
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) {
return nil, responseError{msg: errResp.Error}
},
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
natstransport.EncodeJSONResponse,
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
r, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
b, err := json.Marshal(errResp)
if err != nil {
t.Fatal(err)
}
if string(b) != string(r.Data) {
t.Errorf("ErrorEncoder: got: %q, expected: %q", r.Data, b)
}
}
type noContentResponse struct{}
func TestEncodeNoContent(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
handler := natstransport.NewSubscriber(
func(context.Context, interface{}) (interface{}, error) { return noContentResponse{}, nil },
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
natstransport.EncodeJSONResponse,
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
r, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
if want, have := `{}`, strings.TrimSpace(string(r.Data)); want != have {
t.Errorf("Body: want %s, have %s", want, have)
}
}
func TestNoOpRequestDecoder(t *testing.T) {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
handler := natstransport.NewSubscriber(
func(ctx context.Context, request interface{}) (interface{}, error) {
if request != nil {
t.Error("Expected nil request in endpoint when using NopRequestDecoder")
}
return nil, nil
},
natstransport.NopRequestDecoder,
natstransport.EncodeJSONResponse,
)
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
r, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
if want, have := `null`, strings.TrimSpace(string(r.Data)); want != have {
t.Errorf("Body: want %s, have %s", want, have)
}
}
func testSubscriber(t *testing.T) (step func(), resp <-chan *nats.Msg) {
var (
stepch = make(chan bool)
endpoint = func(context.Context, interface{}) (interface{}, error) {
<-stepch
return struct{}{}, nil
}
response = make(chan *nats.Msg)
handler = natstransport.NewSubscriber(
endpoint,
func(context.Context, *nats.Msg) (interface{}, error) { return struct{}{}, nil },
natstransport.EncodeJSONResponse,
natstransport.SubscriberBefore(func(ctx context.Context, msg *nats.Msg) context.Context { return ctx }),
natstransport.SubscriberAfter(func(ctx context.Context, nc *nats.Conn) context.Context { return ctx }),
)
)
go func() {
s, c := newNATSConn(t)
defer func() { s.Shutdown(); s.WaitForShutdown() }()
defer c.Close()
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
r, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
response <- r
}()
return func() { stepch <- true }, response
}
func testRequest(t *testing.T, c *nats.Conn, handler *natstransport.Subscriber) TestResponse {
sub, err := c.QueueSubscribe("natstransport.test", "natstransport", handler.ServeMsg(c))
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
r, err := c.Request("natstransport.test", []byte("test data"), 2*time.Second)
if err != nil {
t.Fatal(err)
}
var resp TestResponse
err = json.Unmarshal(r.Data, &resp)
if err != nil {
t.Fatal(err)
}
return resp
}
================================================
FILE: transport/netrpc/README.md
================================================
# net/rpc
[net/rpc](https://golang.org/pkg/net/rpc) is an RPC transport that's part of the Go standard library.
It's a simple and fast transport that's appropriate when all of your services are written in Go.
Using net/rpc with Go kit is very simple.
Just write a simple binding from your service definition to the net/rpc definition.
See [netrpc_binding.go](https://github.com/go-kit/kit/blob/ec8b02591ee873433565a1ae9d317353412d1d27/examples/addsvc/netrpc_binding.go) for an example.
That's it!
The net/rpc binding can be registered to a name, and bound to an HTTP handler, the same as any other net/rpc endpoint.
And within your service, you can use standard Go kit components and idioms.
See [addsvc](https://github.com/go-kit/examples/tree/master/addsvc) for a complete working example with net/rpc support.
And remember: Go kit services can support multiple transports simultaneously.
================================================
FILE: transport/thrift/README.md
================================================
# Thrift
[Thrift](https://thrift.apache.org/) is a large IDL and transport package from Apache, popularized by Facebook.
Thrift is well-supported in Go kit, for organizations that already have significant Thrift investment.
And using Thrift with Go kit is very simple.
First, define your service in the Thrift IDL.
The [Thrift IDL documentation](https://thrift.apache.org/docs/idl) provides more details.
See [addsvc.thrift](https://github.com/go-kit/examples/blob/master/addsvc/thrift/addsvc.thrift) for an example.
Make sure the Thrift definition matches your service's Go kit (interface) definition.
Next, [download Thrift](https://thrift.apache.org/download) and [install the compiler](https://thrift.apache.org/docs/install/).
On a Mac, you may be able to `brew install thrift`.
Then, compile your service definition, from .thrift to .go.
You'll probably want to specify the package_prefix option to the --gen go flag.
See [THRIFT-3021](https://issues.apache.org/jira/browse/THRIFT-3021) for more details.
```
thrift -r --gen go:package_prefix=github.com/my-org/my-repo/thrift/gen-go/ add.thrift
```
Finally, write a tiny binding from your service definition to the Thrift definition.
It's a straightforward conversion from one domain to the other.
See [thrift.go](https://github.com/go-kit/examples/blob/master/addsvc/pkg/addtransport/thrift.go) for an example.
That's it!
The Thrift binding can be bound to a listener and serve normal Thrift requests.
And within your service, you can use standard Go kit components and idioms.
Unfortunately, setting up a Thrift listener is rather laborious and nonidiomatic in Go.
Fortunately, [addsvc](https://github.com/go-kit/examples/tree/master/addsvc) is a complete working example with Thrift support.
And remember: Go kit services can support multiple transports simultaneously.
================================================
FILE: util/README.md
================================================
# util
This directory holds packages of general utility to multiple consumers within Go kit,
and potentially other consumers in the wider Go ecosystem.
There is no `package util` and will never be.
================================================
FILE: util/conn/doc.go
================================================
// Package conn provides utilities related to connections.
package conn
================================================
FILE: util/conn/manager.go
================================================
package conn
import (
"errors"
"math/rand"
"net"
"time"
"github.com/go-kit/log"
)
// Dialer imitates net.Dial. Dialer is assumed to yield connections that are
// safe for use by multiple concurrent goroutines.
type Dialer func(network, address string) (net.Conn, error)
// AfterFunc imitates time.After.
type AfterFunc func(time.Duration) <-chan time.Time
// Manager manages a net.Conn.
//
// Clients provide a way to create the connection with a Dialer, network, and
// address. Clients should Take the connection when they want to use it, and Put
// back whatever error they receive from its use. When a non-nil error is Put,
// the connection is invalidated, and a new connection is established.
// Connection failures are retried after an exponential backoff.
type Manager struct {
dialer Dialer
network string
address string
after AfterFunc
logger log.Logger
takec chan net.Conn
putc chan error
}
// NewManager returns a connection manager using the passed Dialer, network, and
// address. The AfterFunc is used to control exponential backoff and retries.
// The logger is used to log errors; pass a log.NopLogger if you don't care to
// receive them. For normal use, prefer NewDefaultManager.
func NewManager(d Dialer, network, address string, after AfterFunc, logger log.Logger) *Manager {
m := &Manager{
dialer: d,
network: network,
address: address,
after: after,
logger: logger,
takec: make(chan net.Conn),
putc: make(chan error),
}
go m.loop()
return m
}
// NewDefaultManager is a helper constructor, suitable for most normal use in
// real (non-test) code. It uses the real net.Dial and time.After functions.
func NewDefaultManager(network, address string, logger log.Logger) *Manager {
return NewManager(net.Dial, network, address, time.After, logger)
}
// Take yields the current connection. It may be nil.
func (m *Manager) Take() net.Conn {
return <-m.takec
}
// Put accepts an error that came from a previously yielded connection. If the
// error is non-nil, the manager will invalidate the current connection and try
// to reconnect, with exponential backoff. Putting a nil error is a no-op.
func (m *Manager) Put(err error) {
m.putc <- err
}
// Write writes the passed data to the connection in a single Take/Put cycle.
func (m *Manager) Write(b []byte) (int, error) {
conn := m.Take()
if conn == nil {
return 0, ErrConnectionUnavailable
}
n, err := conn.Write(b)
defer m.Put(err)
return n, err
}
func (m *Manager) loop() {
var (
conn = dial(m.dialer, m.network, m.address, m.logger) // may block slightly
connc = make(chan net.Conn, 1)
reconnectc <-chan time.Time // initially nil
backoff = time.Second
)
// If the initial dial fails, we need to trigger a reconnect via the loop
// body, below. If we did this in a goroutine, we would race on the conn
// variable. So we use a buffered chan instead.
connc <- conn
for {
select {
case <-reconnectc:
reconnectc = nil // one-shot
go func() { connc <- dial(m.dialer, m.network, m.address, m.logger) }()
case conn = <-connc:
if conn == nil {
// didn't work
backoff = Exponential(backoff) // wait longer
reconnectc = m.after(backoff) // try again
} else {
// worked!
backoff = time.Second // reset wait time
reconnectc = nil // no retry necessary
}
case m.takec <- conn:
case err := <-m.putc:
if err != nil && conn != nil {
m.logger.Log("err", err)
conn.Close()
conn = nil // connection is bad
reconnectc = m.after(time.Nanosecond) // trigger immediately
}
}
}
}
func dial(d Dialer, network, address string, logger log.Logger) net.Conn {
conn, err := d(network, address)
if err != nil {
logger.Log("err", err)
conn = nil // just to be sure
}
return conn
}
// Exponential takes a duration and returns another one that is twice as long, +/- 50%. It is
// used to provide backoff for operations that may fail and should avoid thundering herds.
// See https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ for rationale
func Exponential(d time.Duration) time.Duration {
d *= 2
jitter := rand.Float64() + 0.5
d = time.Duration(int64(float64(d.Nanoseconds()) * jitter))
if d > time.Minute {
d = time.Minute
}
return d
}
// ErrConnectionUnavailable is returned by the Manager's Write method when the
// manager cannot yield a good connection.
var ErrConnectionUnavailable = errors.New("connection unavailable")
================================================
FILE: util/conn/manager_test.go
================================================
package conn
import (
"errors"
"net"
"sync/atomic"
"testing"
"time"
"github.com/go-kit/log"
)
func TestManager(t *testing.T) {
var (
tickc = make(chan time.Time)
after = func(time.Duration) <-chan time.Time { return tickc }
dialconn = &mockConn{}
dialerr = error(nil)
dialer = func(string, string) (net.Conn, error) { return dialconn, dialerr }
mgr = NewManager(dialer, "netw", "addr", after, log.NewNopLogger())
)
// First conn should be fine.
conn := mgr.Take()
if conn == nil {
t.Fatal("nil conn")
}
// Write and check it went through.
if _, err := conn.Write([]byte{1, 2, 3}); err != nil {
t.Fatal(err)
}
if want, have := uint64(3), atomic.LoadUint64(&dialconn.wr); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Put an error to kill the conn.
mgr.Put(errors.New("should kill the connection"))
// First takes should fail.
for i := 0; i < 10; i++ {
if conn = mgr.Take(); conn != nil {
t.Fatalf("iteration %d: want nil conn, got real conn", i)
}
}
// Trigger the reconnect.
tickc <- time.Now()
// The dial should eventually succeed and yield a good conn.
if !within(100*time.Millisecond, func() bool {
conn = mgr.Take()
return conn != nil
}) {
t.Fatal("conn remained nil")
}
// Write and check it went through.
if _, err := conn.Write([]byte{4, 5}); err != nil {
t.Fatal(err)
}
if want, have := uint64(5), atomic.LoadUint64(&dialconn.wr); want != have {
t.Errorf("want %d, have %d", want, have)
}
// Dial starts failing.
dialconn, dialerr = nil, errors.New("oh noes")
mgr.Put(errors.New("trigger that reconnect y'all"))
if conn = mgr.Take(); conn != nil {
t.Fatalf("want nil conn, got real conn")
}
// As many reconnects as they want.
go func() {
done := time.After(100 * time.Millisecond)
for {
select {
case tickc <- time.Now():
case <-done:
return
}
}
}()
// The dial should never succeed.
if within(100*time.Millisecond, func() bool {
conn = mgr.Take()
return conn != nil
}) {
t.Fatal("eventually got a good conn, despite failing dialer")
}
}
func TestIssue292(t *testing.T) {
// The util/conn.Manager won't attempt to reconnect to the provided endpoint
// if the endpoint is initially unavailable (e.g. dial tcp :8080:
// getsockopt: connection refused). If the endpoint is up when
// conn.NewManager is called and then goes down/up, it reconnects just fine.
var (
tickc = make(chan time.Time)
after = func(time.Duration) <-chan time.Time { return tickc }
dialconn = net.Conn(nil)
dialerr = errors.New("fail")
dialer = func(string, string) (net.Conn, error) { return dialconn, dialerr }
mgr = NewManager(dialer, "netw", "addr", after, log.NewNopLogger())
)
if conn := mgr.Take(); conn != nil {
t.Fatal("first Take should have yielded nil conn, but didn't")
}
dialconn, dialerr = &mockConn{}, nil
select {
case tickc <- time.Now():
case <-time.After(time.Second):
t.Fatal("manager isn't listening for a tick, despite a failed dial")
}
if !within(time.Second, func() bool {
return mgr.Take() != nil
}) {
t.Fatal("second Take should have yielded good conn, but didn't")
}
}
type mockConn struct {
rd, wr uint64
}
func (c *mockConn) Read(b []byte) (n int, err error) {
atomic.AddUint64(&c.rd, uint64(len(b)))
return len(b), nil
}
func (c *mockConn) Write(b []byte) (n int, err error) {
atomic.AddUint64(&c.wr, uint64(len(b)))
return len(b), nil
}
func (c *mockConn) Close() error { return nil }
func (c *mockConn) LocalAddr() net.Addr { return nil }
func (c *mockConn) RemoteAddr() net.Addr { return nil }
func (c *mockConn) SetDeadline(t time.Time) error { return nil }
func (c *mockConn) SetReadDeadline(t time.Time) error { return nil }
func (c *mockConn) SetWriteDeadline(t time.Time) error { return nil }
func within(d time.Duration, f func() bool) bool {
deadline := time.Now().Add(d)
for {
if time.Now().After(deadline) {
return false
}
if f() {
return true
}
time.Sleep(d / 10)
}
}