Update weaveworks/common to latest version

```
$ gvt delete github.com/weaveworks/common
$ gvt fetch --revision 4d96fd8dcf2c7b417912c6219b310112cb4a4626 github.com/weaveworks/common
2018/07/23 15:31:11 Fetching: github.com/weaveworks/common
2018/07/23 15:31:14 · Skipping (existing): github.com/golang/protobuf/ptypes/any
2018/07/23 15:31:14 · Fetching recursive dependency: github.com/pkg/errors
2018/07/23 15:31:16 · Skipping (existing): github.com/aws/aws-sdk-go/aws
2018/07/23 15:31:16 · Fetching recursive dependency: github.com/sirupsen/logrus
2018/07/23 15:31:18 ·· Skipping (existing): golang.org/x/sys/unix
2018/07/23 15:31:18 ·· Skipping (existing): golang.org/x/crypto/ssh/terminal
2018/07/23 15:31:18 · Skipping (existing): google.golang.org/grpc/status
2018/07/23 15:31:18 · Skipping (existing): github.com/gorilla/mux
2018/07/23 15:31:18 · Fetching recursive dependency: github.com/opentracing-contrib/go-stdlib/nethttp
2018/07/23 15:31:20 ·· Skipping (existing): github.com/opentracing/opentracing-go/ext
2018/07/23 15:31:20 ·· Skipping (existing): github.com/opentracing/opentracing-go/log
2018/07/23 15:31:20 ·· Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:31:20 · Skipping (existing): github.com/prometheus/client_golang/prometheus
2018/07/23 15:31:20 · Skipping (existing): google.golang.org/grpc
2018/07/23 15:31:20 · Skipping (existing): github.com/pmezard/go-difflib/difflib
2018/07/23 15:31:20 · Fetching recursive dependency: github.com/go-kit/kit/log
2018/07/23 15:31:23 ·· Fetching recursive dependency: github.com/go-logfmt/logfmt
2018/07/23 15:31:25 ··· Fetching recursive dependency: github.com/kr/logfmt
2018/07/23 15:31:27 ·· Fetching recursive dependency: github.com/go-stack/stack
2018/07/23 15:31:29 · Fetching recursive dependency: google.golang.org/genproto/googleapis/rpc/status
2018/07/23 15:31:37 ·· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:31:37 ·· Skipping (existing): github.com/golang/protobuf/ptypes/any
2018/07/23 15:31:37 · Skipping (existing): github.com/opentracing/opentracing-go/log
2018/07/23 15:31:37 · Fetching recursive dependency: github.com/sercand/kuberesolver
2018/07/23 15:31:39 ·· Skipping (existing): google.golang.org/grpc/grpclog
2018/07/23 15:31:39 ·· Skipping (existing): google.golang.org/grpc/resolver
2018/07/23 15:31:39 ·· Skipping (existing): golang.org/x/net/context
2018/07/23 15:31:39 · Skipping (existing): google.golang.org/grpc/metadata
2018/07/23 15:31:39 · Skipping (existing): github.com/opentracing/opentracing-go/ext
2018/07/23 15:31:39 · Skipping (existing): github.com/armon/go-socks5
2018/07/23 15:31:39 · Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:31:39 · Skipping (existing): github.com/davecgh/go-spew/spew
2018/07/23 15:31:39 · Skipping (existing): github.com/golang/protobuf/ptypes
2018/07/23 15:31:39 · Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:31:39 · Fetching recursive dependency: github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc
2018/07/23 15:31:41 ·· Skipping (existing): github.com/opentracing/opentracing-go/log
2018/07/23 15:31:41 ·· Skipping (existing): golang.org/x/net/context
2018/07/23 15:31:41 ·· Skipping (existing): google.golang.org/grpc/codes
2018/07/23 15:31:41 ·· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:31:41 ·· Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:31:41 ·· Skipping (existing): github.com/opentracing/opentracing-go/ext
2018/07/23 15:31:41 ·· Skipping (existing): google.golang.org/grpc
2018/07/23 15:31:41 ·· Skipping (existing): google.golang.org/grpc/metadata
2018/07/23 15:31:41 ·· Skipping (existing): google.golang.org/grpc/status
2018/07/23 15:31:41 · Fetching recursive dependency: github.com/uber/jaeger-client-go/config
2018/07/23 15:31:44 ·· Fetching recursive dependency: github.com/uber/jaeger-client-go/internal/throttler/remote
2018/07/23 15:31:44 ··· Fetching recursive dependency: github.com/uber/jaeger-client-go/utils
2018/07/23 15:31:44 ···· Fetching recursive dependency: github.com/uber/jaeger-client-go/thrift
2018/07/23 15:31:44 ···· Fetching recursive dependency: github.com/uber/jaeger-client-go/thrift-gen/agent
2018/07/23 15:31:44 ····· Fetching recursive dependency: github.com/uber/jaeger-client-go/thrift-gen/jaeger
2018/07/23 15:31:44 ····· Fetching recursive dependency: github.com/uber/jaeger-client-go/thrift-gen/zipkincore
2018/07/23 15:31:44 ··· Fetching recursive dependency: github.com/uber/jaeger-client-go
2018/07/23 15:31:44 ···· Fetching recursive dependency: github.com/crossdock/crossdock-go
2018/07/23 15:31:46 ····· Skipping (existing): github.com/davecgh/go-spew/spew
2018/07/23 15:31:46 ····· Skipping (existing): golang.org/x/net/context/ctxhttp
2018/07/23 15:31:46 ····· Skipping (existing): golang.org/x/net/context
2018/07/23 15:31:46 ····· Skipping (existing): github.com/pmezard/go-difflib/difflib
2018/07/23 15:31:46 ···· Skipping (existing): github.com/opentracing/opentracing-go/log
2018/07/23 15:31:46 ···· Fetching recursive dependency: go.uber.org/zap/zapcore
2018/07/23 15:31:49 ····· Fetching recursive dependency: go.uber.org/atomic
2018/07/23 15:31:51 ····· Fetching recursive dependency: go.uber.org/zap/internal/bufferpool
2018/07/23 15:31:51 ······ Fetching recursive dependency: go.uber.org/zap/buffer
2018/07/23 15:31:51 ····· Fetching recursive dependency: go.uber.org/multierr
2018/07/23 15:31:54 ····· Fetching recursive dependency: go.uber.org/zap/internal/exit
2018/07/23 15:31:54 ····· Fetching recursive dependency: go.uber.org/zap/internal/color
2018/07/23 15:31:54 ···· Fetching recursive dependency: go.uber.org/zap
2018/07/23 15:31:54 ···· Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:31:54 ···· Skipping (existing): github.com/opentracing/opentracing-go/ext
2018/07/23 15:31:54 ···· Fetching recursive dependency: github.com/uber/jaeger-lib/metrics
2018/07/23 15:31:56 ····· Fetching recursive dependency: github.com/uber-go/tally
2018/07/23 15:31:58 ······ Fetching recursive dependency: github.com/m3db/prometheus_client_golang/prometheus/promhttp
2018/07/23 15:32:00 ······· Skipping (existing): github.com/prometheus/client_golang/prometheus
2018/07/23 15:32:00 ······· Skipping (existing): github.com/prometheus/common/expfmt
2018/07/23 15:32:00 ······· Skipping (existing): github.com/prometheus/client_model/go
2018/07/23 15:32:00 ······ Fetching recursive dependency: gopkg.in/validator.v2
2018/07/23 15:32:06 ······ Fetching recursive dependency: github.com/cactus/go-statsd-client/statsd
2018/07/23 15:32:08 ······ Skipping (existing): gopkg.in/yaml.v2
2018/07/23 15:32:08 ······ Fetching recursive dependency: github.com/m3db/prometheus_client_golang/prometheus
2018/07/23 15:32:08 ······· Skipping (existing): github.com/prometheus/procfs
2018/07/23 15:32:08 ······· Skipping (existing): github.com/prometheus/client_model/go
2018/07/23 15:32:08 ······· Skipping (existing): github.com/prometheus/common/expfmt
2018/07/23 15:32:08 ······· Skipping (existing): golang.org/x/net/context
2018/07/23 15:32:08 ······· Skipping (existing): github.com/beorn7/perks/quantile
2018/07/23 15:32:08 ······· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:32:08 ······· Skipping (existing): github.com/prometheus/common/model
2018/07/23 15:32:08 ······· Skipping (existing): github.com/prometheus/client_golang/prometheus
2018/07/23 15:32:08 ······ Fetching recursive dependency: github.com/apache/thrift/lib/go/thrift
2018/07/23 15:32:13 ····· Skipping (existing): github.com/stretchr/testify/assert
2018/07/23 15:32:13 ····· Fetching recursive dependency: github.com/go-kit/kit/metrics/influx
2018/07/23 15:32:13 ······ Fetching recursive dependency: github.com/influxdata/influxdb/client/v2
2018/07/23 15:32:17 ······· Fetching recursive dependency: github.com/influxdata/influxdb/models
2018/07/23 15:32:17 ········ Fetching recursive dependency: github.com/influxdata/influxdb/pkg/escape
2018/07/23 15:32:17 ······ Fetching recursive dependency: github.com/go-kit/kit/metrics
2018/07/23 15:32:17 ······· Fetching recursive dependency: github.com/performancecopilot/speed
2018/07/23 15:32:19 ······· Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/aws
2018/07/23 15:32:28 ········ Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/internal/sdk
2018/07/23 15:32:28 ········ Skipping (existing): github.com/go-ini/ini
2018/07/23 15:32:28 ········ Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/service/sts
2018/07/23 15:32:28 ········· Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/private/protocol/query
2018/07/23 15:32:28 ·········· Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/private/protocol
2018/07/23 15:32:28 ········· Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/internal/awsutil
2018/07/23 15:32:28 ·········· Skipping (existing): github.com/jmespath/go-jmespath
2018/07/23 15:32:28 ······· Fetching recursive dependency: github.com/aws/aws-sdk-go-v2/service/cloudwatch
2018/07/23 15:32:29 ······· Skipping (existing): github.com/aws/aws-sdk-go/aws
2018/07/23 15:32:29 ······· Skipping (existing): github.com/prometheus/client_golang/prometheus
2018/07/23 15:32:29 ······· Skipping (existing): github.com/aws/aws-sdk-go/service/cloudwatch
2018/07/23 15:32:29 ······· Skipping (existing): github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface
2018/07/23 15:32:29 ······· Fetching recursive dependency: golang.org/x/sync/errgroup
2018/07/23 15:32:31 ········ Skipping (existing): golang.org/x/net/context
2018/07/23 15:32:31 ······· Fetching recursive dependency: github.com/go-kit/kit/util/conn
2018/07/23 15:32:31 ······· Fetching recursive dependency: github.com/VividCortex/gohistogram
2018/07/23 15:32:33 ····· Skipping (existing): github.com/prometheus/client_golang/prometheus
2018/07/23 15:32:33 ····· Fetching recursive dependency: github.com/codahale/hdrhistogram
2018/07/23 15:32:35 ·· Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:32:35 · Fetching recursive dependency: github.com/mwitkow/go-grpc-middleware
2018/07/23 15:32:37 ·· Fetching recursive dependency: github.com/grpc-ecosystem/go-grpc-middleware/logging
2018/07/23 15:32:39 ··· Fetching recursive dependency: github.com/grpc-ecosystem/go-grpc-middleware
2018/07/23 15:32:39 ···· Fetching recursive dependency: github.com/golang/protobuf/jsonpb
2018/07/23 15:32:42 ····· Skipping (existing): github.com/golang/protobuf/ptypes/timestamp
2018/07/23 15:32:42 ····· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:32:42 ····· Skipping (existing): github.com/golang/protobuf/ptypes/duration
2018/07/23 15:32:42 ····· Skipping (existing): github.com/golang/protobuf/ptypes/any
2018/07/23 15:32:42 ····· Skipping (existing): github.com/golang/protobuf/ptypes/struct
2018/07/23 15:32:42 ····· Skipping (existing): github.com/golang/protobuf/ptypes/wrappers
2018/07/23 15:32:42 ···· Skipping (existing): google.golang.org/grpc/metadata
2018/07/23 15:32:42 ···· Fetching recursive dependency: github.com/stretchr/testify/suite
2018/07/23 15:32:45 ····· Skipping (existing): github.com/stretchr/testify/assert
2018/07/23 15:32:45 ····· Fetching recursive dependency: github.com/stretchr/testify/require
2018/07/23 15:32:45 ······ Skipping (existing): github.com/stretchr/testify/assert
2018/07/23 15:32:45 ···· Skipping (existing): google.golang.org/grpc/peer
2018/07/23 15:32:45 ···· Skipping (existing): golang.org/x/net/context
2018/07/23 15:32:45 ···· Skipping (existing): golang.org/x/net/trace
2018/07/23 15:32:45 ···· Fetching recursive dependency: github.com/gogo/protobuf/gogoproto
2018/07/23 15:32:48 ····· Fetching recursive dependency: github.com/gogo/protobuf/protoc-gen-gogo/descriptor
2018/07/23 15:32:48 ······ Skipping (existing): github.com/gogo/protobuf/proto
2018/07/23 15:32:48 ····· Skipping (existing): github.com/gogo/protobuf/proto
2018/07/23 15:32:48 ···· Skipping (existing): google.golang.org/grpc/credentials
2018/07/23 15:32:48 ···· Skipping (existing): google.golang.org/grpc
2018/07/23 15:32:48 ···· Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:32:48 ···· Skipping (existing): google.golang.org/grpc/codes
2018/07/23 15:32:48 ···· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:32:48 ···· Skipping (existing): google.golang.org/grpc/grpclog
2018/07/23 15:32:48 ···· Skipping (existing): github.com/opentracing/opentracing-go/ext
2018/07/23 15:32:48 ···· Skipping (existing): github.com/opentracing/opentracing-go/log
2018/07/23 15:32:48 ··· Skipping (existing): golang.org/x/net/context
2018/07/23 15:32:48 ··· Skipping (existing): google.golang.org/grpc
2018/07/23 15:32:48 ··· Skipping (existing): google.golang.org/grpc/grpclog
2018/07/23 15:32:48 ··· Skipping (existing): google.golang.org/grpc/codes
2018/07/23 15:32:48 ··· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:32:48 ·· Skipping (existing): github.com/opentracing/opentracing-go
2018/07/23 15:32:48 ·· Skipping (existing): google.golang.org/grpc
2018/07/23 15:32:48 ·· Skipping (existing): golang.org/x/net/context
2018/07/23 15:32:48 ·· Skipping (existing): google.golang.org/grpc/codes
2018/07/23 15:32:48 ·· Skipping (existing): google.golang.org/grpc/grpclog
2018/07/23 15:32:48 ·· Skipping (existing): github.com/opentracing/opentracing-go/log
2018/07/23 15:32:48 ·· Skipping (existing): google.golang.org/grpc/metadata
2018/07/23 15:32:48 ·· Skipping (existing): google.golang.org/grpc/peer
2018/07/23 15:32:48 ·· Skipping (existing): google.golang.org/grpc/credentials
2018/07/23 15:32:48 ·· Skipping (existing): github.com/golang/protobuf/proto
2018/07/23 15:32:48 ·· Skipping (existing): golang.org/x/net/trace
2018/07/23 15:32:48 ·· Skipping (existing): github.com/opentracing/opentracing-go/ext
2018/07/23 15:32:48 · Fetching recursive dependency: github.com/weaveworks/promrus
2018/07/23 15:32:53 ·· Skipping (existing): gopkg.in/yaml.v2
2018/07/23 15:32:53 ·· Skipping (existing): golang.org/x/net/context/ctxhttp
2018/07/23 15:32:53 ·· Fetching recursive dependency: github.com/stretchr/objx
2018/07/23 15:32:55 ·· Fetching recursive dependency: gopkg.in/alecthomas/kingpin.v2
2018/07/23 15:32:58 ··· Fetching recursive dependency: github.com/alecthomas/units
2018/07/23 15:33:00 ··· Fetching recursive dependency: github.com/alecthomas/template
2018/07/23 15:33:02 ·· Fetching recursive dependency: github.com/julienschmidt/httprouter
2018/07/23 15:33:05 ·· Skipping (existing): golang.org/x/net/context
2018/07/23 15:33:05 · Skipping (existing): github.com/aws/aws-sdk-go/aws/credentials
2018/07/23 15:33:05 · Skipping (existing): github.com/golang/protobuf/ptypes/empty
2018/07/23 15:33:05 · Skipping (existing): golang.org/x/net/context
2018/07/23 15:33:05 · Skipping (existing): golang.org/x/tools/cover
2018/07/23 15:33:05 · Skipping (existing): github.com/mgutz/ansi
```
This commit is contained in:
Marc Carré
2018-07-23 15:33:58 +02:00
parent 0153f016d3
commit 652cc90f98
1466 changed files with 394515 additions and 359 deletions

201
vendor/github.com/mwitkow/go-grpc-middleware/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,67 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_auth
import (
"github.com/grpc-ecosystem/go-grpc-middleware"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// AuthFunc is the pluggable function that performs authentication.
//
// The passed in `Context` will contain the gRPC metadata.MD object (for header-based authentication) and
// the peer.Peer information that can contain transport-based credentials (e.g. `credentials.AuthInfo`).
//
// The returned context will be propagated to handlers, allowing user changes to `Context`. However,
// please make sure that the `Context` returned is a child `Context` of the one passed in.
//
// If error is returned, its `grpc.Code()` will be returned to the user as well as the verbatim message.
// Please make sure you use `codes.Unauthenticated` (lacking auth) and `codes.PermissionDenied`
// (authed, but lacking perms) appropriately.
type AuthFunc func(ctx context.Context) (context.Context, error)
// ServiceAuthFuncOverride allows a given gRPC service implementation to override the global `AuthFunc`.
//
// If a service implements the AuthFuncOverride method, it takes precedence over the `AuthFunc` method,
// and will be called instead of AuthFunc for all method invocations within that service.
type ServiceAuthFuncOverride interface {
AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error)
}
// UnaryServerInterceptor returns a new unary server interceptors that performs per-request auth.
func UnaryServerInterceptor(authFunc AuthFunc) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var newCtx context.Context
var err error
if overrideSrv, ok := info.Server.(ServiceAuthFuncOverride); ok {
newCtx, err = overrideSrv.AuthFuncOverride(ctx, info.FullMethod)
} else {
newCtx, err = authFunc(ctx)
}
if err != nil {
return nil, err
}
return handler(newCtx, req)
}
}
// StreamServerInterceptor returns a new unary server interceptors that performs per-request auth.
func StreamServerInterceptor(authFunc AuthFunc) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
var newCtx context.Context
var err error
if overrideSrv, ok := srv.(ServiceAuthFuncOverride); ok {
newCtx, err = overrideSrv.AuthFuncOverride(stream.Context(), info.FullMethod)
} else {
newCtx, err = authFunc(stream.Context())
}
if err != nil {
return err
}
wrapped := grpc_middleware.WrapServerStream(stream)
wrapped.WrappedContext = newCtx
return handler(srv, wrapped)
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_auth` a generic server-side auth middleware for gRPC.
Server Side Auth Middleware
It allows for easy assertion of `:authorization` headers in gRPC calls, be it HTTP Basic auth, or
OAuth2 Bearer tokens.
The middleware takes a user-customizable `AuthFunc`, which can be customized to verify and extract
auth information from the request. The extracted information can be put in the `context.Context` of
handlers downstream for retrieval.
It also allows for per-service implementation overrides of `AuthFunc`. See `ServiceAuthFuncOverride`.
Please see examples for simple examples of use.
*/
package grpc_auth

View File

@@ -0,0 +1,38 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_auth
import (
"strings"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
var (
headerAuthorize = "authorization"
)
// AuthFromMD is a helper function for extracting the :authorization header from the gRPC metadata of the request.
//
// It expects the `:authorization` header to be of a certain scheme (e.g. `basic`, `bearer`), in a
// case-insensitive format (see rfc2617, sec 1.2). If no such authorization is found, or the token
// is of wrong scheme, an error with gRPC status `Unauthenticated` is returned.
func AuthFromMD(ctx context.Context, expectedScheme string) (string, error) {
val := metautils.ExtractIncoming(ctx).Get(headerAuthorize)
if val == "" {
return "", grpc.Errorf(codes.Unauthenticated, "Request unauthenticated with "+expectedScheme)
}
splits := strings.SplitN(val, " ", 2)
if len(splits) < 2 {
return "", grpc.Errorf(codes.Unauthenticated, "Bad authorization string")
}
if strings.ToLower(splits[0]) != strings.ToLower(expectedScheme) {
return "", grpc.Errorf(codes.Unauthenticated, "Request unauthenticated with "+expectedScheme)
}
return splits[1], nil
}

183
vendor/github.com/mwitkow/go-grpc-middleware/chain.go generated vendored Normal file
View File

@@ -0,0 +1,183 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
// gRPC Server Interceptor chaining middleware.
package grpc_middleware
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// ChainUnaryServer creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryServer(one, two, three) will execute one before two before three, and three
// will see context changes of one and two.
func ChainUnaryServer(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
n := len(interceptors)
if n > 1 {
lastI := n - 1
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var (
chainHandler grpc.UnaryHandler
curI int
)
chainHandler = func(currentCtx context.Context, currentReq interface{}) (interface{}, error) {
if curI == lastI {
return handler(currentCtx, currentReq)
}
curI++
resp, err := interceptors[curI](currentCtx, currentReq, info, chainHandler)
curI--
return resp, err
}
return interceptors[0](ctx, req, info, chainHandler)
}
}
if n == 1 {
return interceptors[0]
}
// n == 0; Dummy interceptor maintained for backward compatibility to avoid returning nil.
return func(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return handler(ctx, req)
}
}
// ChainStreamServer creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryServer(one, two, three) will execute one before two before three.
// If you want to pass context between interceptors, use WrapServerStream.
func ChainStreamServer(interceptors ...grpc.StreamServerInterceptor) grpc.StreamServerInterceptor {
n := len(interceptors)
if n > 1 {
lastI := n - 1
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
var (
chainHandler grpc.StreamHandler
curI int
)
chainHandler = func(currentSrv interface{}, currentStream grpc.ServerStream) error {
if curI == lastI {
return handler(currentSrv, currentStream)
}
curI++
err := interceptors[curI](currentSrv, currentStream, info, chainHandler)
curI--
return err
}
return interceptors[0](srv, stream, info, chainHandler)
}
}
if n == 1 {
return interceptors[0]
}
// n == 0; Dummy interceptor maintained for backward compatibility to avoid returning nil.
return func(srv interface{}, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return handler(srv, stream)
}
}
// ChainUnaryClient creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainUnaryClient(one, two, three) will execute one before two before three.
func ChainUnaryClient(interceptors ...grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor {
n := len(interceptors)
if n > 1 {
lastI := n - 1
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var (
chainHandler grpc.UnaryInvoker
curI int
)
chainHandler = func(currentCtx context.Context, currentMethod string, currentReq, currentRepl interface{}, currentConn *grpc.ClientConn, currentOpts ...grpc.CallOption) error {
if curI == lastI {
return invoker(currentCtx, currentMethod, currentReq, currentRepl, currentConn, currentOpts...)
}
curI++
err := interceptors[curI](currentCtx, currentMethod, currentReq, currentRepl, currentConn, chainHandler, currentOpts...)
curI--
return err
}
return interceptors[0](ctx, method, req, reply, cc, chainHandler, opts...)
}
}
if n == 1 {
return interceptors[0]
}
// n == 0; Dummy interceptor maintained for backward compatibility to avoid returning nil.
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return invoker(ctx, method, req, reply, cc, opts...)
}
}
// ChainStreamClient creates a single interceptor out of a chain of many interceptors.
//
// Execution is done in left-to-right order, including passing of context.
// For example ChainStreamClient(one, two, three) will execute one before two before three.
func ChainStreamClient(interceptors ...grpc.StreamClientInterceptor) grpc.StreamClientInterceptor {
n := len(interceptors)
if n > 1 {
lastI := n - 1
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
var (
chainHandler grpc.Streamer
curI int
)
chainHandler = func(currentCtx context.Context, currentDesc *grpc.StreamDesc, currentConn *grpc.ClientConn, currentMethod string, currentOpts ...grpc.CallOption) (grpc.ClientStream, error) {
if curI == lastI {
return streamer(currentCtx, currentDesc, currentConn, currentMethod, currentOpts...)
}
curI++
stream, err := interceptors[curI](currentCtx, currentDesc, currentConn, currentMethod, chainHandler, currentOpts...)
curI--
return stream, err
}
return interceptors[0](ctx, desc, cc, method, chainHandler, opts...)
}
}
if n == 1 {
return interceptors[0]
}
// n == 0; Dummy interceptor maintained for backward compatibility to avoid returning nil.
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return streamer(ctx, desc, cc, method, opts...)
}
}
// Chain creates a single interceptor out of a chain of many interceptors.
//
// WithUnaryServerChain is a grpc.Server config option that accepts multiple unary interceptors.
// Basically syntactic sugar.
func WithUnaryServerChain(interceptors ...grpc.UnaryServerInterceptor) grpc.ServerOption {
return grpc.UnaryInterceptor(ChainUnaryServer(interceptors...))
}
// WithStreamServerChain is a grpc.Server config option that accepts multiple stream interceptors.
// Basically syntactic sugar.
func WithStreamServerChain(interceptors ...grpc.StreamServerInterceptor) grpc.ServerOption {
return grpc.StreamInterceptor(ChainStreamServer(interceptors...))
}

69
vendor/github.com/mwitkow/go-grpc-middleware/doc.go generated vendored Normal file
View File

@@ -0,0 +1,69 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_middleware` is a collection of gRPC middleware packages: interceptors, helpers and tools.
Middleware
gRPC is a fantastic RPC middleware, which sees a lot of adoption in the Golang world. However, the
upstream gRPC codebase is relatively bare bones.
This package, and most of its child packages provides commonly needed middleware for gRPC:
client-side interceptors for retires, server-side interceptors for input validation and auth,
functions for chaining said interceptors, metadata convenience methods and more.
Chaining
By default, gRPC doesn't allow one to have more than one interceptor either on the client nor on
the server side. `grpc_middleware` provides convenient chaining methods
Simple way of turning a multiple interceptors into a single interceptor. Here's an example for
server chaining:
myServer := grpc.NewServer(
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(loggingStream, monitoringStream, authStream)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(loggingUnary, monitoringUnary, authUnary),
)
These interceptors will be executed from left to right: logging, monitoring and auth.
Here's an example for client side chaining:
clientConn, err = grpc.Dial(
address,
grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(monitoringClientUnary, retryUnary)),
grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient(monitoringClientStream, retryStream)),
)
client = pb_testproto.NewTestServiceClient(clientConn)
resp, err := client.PingEmpty(s.ctx, &myservice.Request{Msg: "hello"})
These interceptors will be executed from left to right: monitoring and then retry logic.
The retry interceptor will call every interceptor that follows it whenever when a retry happens.
Writing Your Own
Implementing your own interceptor is pretty trivial: there are interfaces for that. But the interesting
bit exposing common data to handlers (and other middleware), similarly to HTTP Middleware design.
For example, you may want to pass the identity of the caller from the auth interceptor all the way
to the handling function.
For example, a client side interceptor example for auth looks like:
func FakeAuthUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
newCtx := context.WithValue(ctx, "user_id", "john@example.com")
return handler(newCtx, req)
}
Unfortunately, it's not as easy for streaming RPCs. These have the `context.Context` embedded within
the `grpc.ServerStream` object. To pass values through context, a wrapper (`WrappedServerStream`) is
needed. For example:
func FakeAuthStreamingInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
newStream := grpc_middleware.WrapServerStream(stream)
newStream.WrappedContext = context.WithValue(ctx, "user_id", "john@example.com")
return handler(srv, stream)
}
*/
package grpc_middleware

View File

@@ -0,0 +1,35 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_logging
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// ErrorToCode function determines the error code of an error
// This makes using custom errors with grpc middleware easier
type ErrorToCode func(err error) codes.Code
func DefaultErrorToCode(err error) codes.Code {
return grpc.Code(err)
}
// Decider function defines rules for suppressing any interceptor logs
type Decider func(fullMethodName string, err error) bool
// DefaultDeciderMethod is the default implementation of decider to see if you should log the call
// by default this if always true so all calls are logged
func DefaultDeciderMethod(fullMethodName string, err error) bool {
return true
}
// ServerPayloadLoggingDecider is a user-provided function for deciding whether to log the server-side
// request/response payloads
type ServerPayloadLoggingDecider func(ctx context.Context, fullMethodName string, servingObject interface{}) bool
// ClientPayloadLoggingDecider is a user-provided function for deciding whether to log the client-side
// request/response payloads
type ClientPayloadLoggingDecider func(ctx context.Context, fullMethodName string) bool

View File

@@ -0,0 +1,35 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
//
/*
grpc_logging is a "parent" package for gRPC logging middlewares.
General functionality of all middleware
The gRPC logging middleware populates request-scoped data to `grpc_ctxtags.Tags` that relate to the current gRPC call
(e.g. service and method names).
Once the gRPC logging middleware has added the gRPC specific Tags to the ctx they will then be written with the logs
that are made using the `ctx_logrus` or `ctx_zap` loggers.
All logging middleware will emit a final log statement. It is based on the error returned by the handler function,
the gRPC status code, an error (if any) and it will emit at a level controlled via `WithLevels`.
This parent package
This particular package is intended for use by other middleware, logging or otherwise. It contains interfaces that other
logging middlewares *could* share . This allows code to be shared between different implementations.
Field names
All field names of loggers follow the OpenTracing semantics definitions, with `grpc.` prefix if needed:
https://github.com/opentracing/specification/blob/master/semantic_conventions.md
Implementations
There are two implementations at the moment: logrus and zap
See relevant packages below.
*/
package grpc_logging

View File

@@ -0,0 +1,65 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_logrus
import (
"path"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// UnaryClientInterceptor returns a new unary client interceptor that optionally logs the execution of external gRPC calls.
func UnaryClientInterceptor(entry *logrus.Entry, opts ...Option) grpc.UnaryClientInterceptor {
o := evaluateClientOpt(opts)
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
fields := newClientLoggerFields(ctx, method)
startTime := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
logFinalClientLine(o, entry.WithFields(fields), startTime, err, "finished client unary call")
return err
}
}
// StreamServerInterceptor returns a new streaming client interceptor that optionally logs the execution of external gRPC calls.
func StreamClientInterceptor(entry *logrus.Entry, opts ...Option) grpc.StreamClientInterceptor {
o := evaluateClientOpt(opts)
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
fields := newClientLoggerFields(ctx, method)
startTime := time.Now()
clientStream, err := streamer(ctx, desc, cc, method, opts...)
logFinalClientLine(o, entry.WithFields(fields), startTime, err, "finished client streaming call")
return clientStream, err
}
}
func logFinalClientLine(o *options, entry *logrus.Entry, startTime time.Time, err error, msg string) {
code := o.codeFunc(err)
level := o.levelFunc(code)
durField, durVal := o.durationFunc(time.Now().Sub(startTime))
fields := logrus.Fields{
"grpc.code": code.String(),
durField: durVal,
}
if err != nil {
fields[logrus.ErrorKey] = err
}
levelLogf(
entry.WithFields(fields),
level,
msg)
}
func newClientLoggerFields(ctx context.Context, fullMethodString string) logrus.Fields {
service := path.Dir(fullMethodString)[1:]
method := path.Base(fullMethodString)
return logrus.Fields{
SystemField: "grpc",
KindField: "client",
"grpc.service": service,
"grpc.method": method,
}
}

View File

@@ -0,0 +1,19 @@
package grpc_logrus
import (
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
// AddFields adds logrus fields to the logger.
// Deprecated: should use the ctxlogrus.Extract instead
func AddFields(ctx context.Context, fields logrus.Fields) {
ctxlogrus.AddFields(ctx, fields)
}
// Extract takes the call-scoped logrus.Entry from grpc_logrus middleware.
// Deprecated: should use the ctxlogrus.Extract instead
func Extract(ctx context.Context) *logrus.Entry {
return ctxlogrus.Extract(ctx)
}

View File

@@ -0,0 +1,65 @@
package ctxlogrus
import (
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
type ctxLoggerMarker struct{}
type ctxLogger struct {
logger *logrus.Entry
fields logrus.Fields
}
var (
ctxLoggerKey = &ctxLoggerMarker{}
)
// AddFields adds logrus fields to the logger.
func AddFields(ctx context.Context, fields logrus.Fields) {
l, ok := ctx.Value(ctxLoggerKey).(*ctxLogger)
if !ok || l == nil {
return
}
for k, v := range fields {
l.fields[k] = v
}
}
// Extract takes the call-scoped logrus.Entry from ctx_logrus middleware.
//
// If the ctx_logrus middleware wasn't used, a no-op `logrus.Entry` is returned. This makes it safe to
// use regardless.
func Extract(ctx context.Context) *logrus.Entry {
l, ok := ctx.Value(ctxLoggerKey).(*ctxLogger)
if !ok || l == nil {
return logrus.NewEntry(nullLogger)
}
fields := logrus.Fields{}
// Add grpc_ctxtags tags metadata until now.
tags := grpc_ctxtags.Extract(ctx)
for k, v := range tags.Values() {
fields[k] = v
}
// Add logrus fields added until now.
for k, v := range l.fields {
fields[k] = v
}
return l.logger.WithFields(fields)
}
// ToContext adds the logrus.Entry to the context for extraction later.
// Returning the new context that has been created.
func ToContext(ctx context.Context, entry *logrus.Entry) context.Context {
l := &ctxLogger{
logger: entry,
fields: logrus.Fields{},
}
return context.WithValue(ctx, ctxLoggerKey, l)
}

View File

@@ -0,0 +1,14 @@
/*
`ctxlogrus` is a ctxlogger that is backed by logrus
It accepts a user-configured `logrus.Logger` that will be used for logging. The same `logrus.Logger` will
be populated into the `context.Context` passed into gRPC handler code.
You can use `ctx_logrus.Extract` to log into a request-scoped `logrus.Logger` instance in your handler code.
As `ctx_logrus.Extract` will iterate all tags on from `grpc_ctxtags` it is therefore expensive so it is advised that you
extract once at the start of the function from the context and reuse it for the remainder of the function (see examples).
Please see examples and tests for examples of use.
*/
package ctxlogrus

View File

@@ -0,0 +1,16 @@
package ctxlogrus
import (
"io/ioutil"
"github.com/sirupsen/logrus"
)
var (
nullLogger = &logrus.Logger{
Out: ioutil.Discard,
Formatter: new(logrus.TextFormatter),
Hooks: make(logrus.LevelHooks),
Level: logrus.PanicLevel,
}
)

View File

@@ -0,0 +1,67 @@
/*
`grpc_logrus` is a gRPC logging middleware backed by Logrus loggers
It accepts a user-configured `logrus.Entry` that will be used for logging completed gRPC calls. The same
`logrus.Entry` will be used for logging completed gRPC calls, and be populated into the `context.Context` passed into gRPC handler code.
On calling `StreamServerInterceptor` or `UnaryServerInterceptor` this logging middleware will add gRPC call information
to the ctx so that it will be present on subsequent use of the `ctxlogrus` logger.
This package also implements request and response *payload* logging, both for server-side and client-side. These will be
logged as structured `jsonpb` fields for every message received/sent (both unary and streaming). For that please use
`Payload*Interceptor` functions for that. Please note that the user-provided function that determines whetether to log
the full request/response payload needs to be written with care, this can significantly slow down gRPC.
If a deadline is present on the gRPC request the grpc.request.deadline tag is populated when the request begins. grpc.request.deadline
is a string representing the time (RFC3339) when the current call will expire.
Logrus can also be made as a backend for gRPC library internals. For that use `ReplaceGrpcLogger`.
*Server Interceptor*
Below is a JSON formatted example of a log that would be logged by the server interceptor:
{
"level": "info", // string logrus log levels
"msg": "finished unary call", // string log message
"grpc.code": "OK", // string grpc status code
"grpc.method": "Ping", // string method name
"grpc.service": "mwitkow.testproto.TestService", // string full name of the called service
"grpc.start_time": "2006-01-02T15:04:05Z07:00", // string RFC3339 representation of the start time
"grpc.request.deadline": "2006-01-02T15:04:05Z07:00", // string RFC3339 deadline of the current request if supplied
"grpc.request.value": "something", // string value on the request
"grpc.time_ms": 1.234, // float32 run time of the call in ms
"peer.address": {
"IP": "127.0.0.1", // string IP address of calling party
"Port": 60216, // int port call is coming in on
"Zone": "" // string peer zone for caller
},
"span.kind": "server", // string client | server
"system": "grpc" // string
"custom_field": "custom_value", // string user defined field
"custom_tags.int": 1337, // int user defined tag on the ctx
"custom_tags.string": "something", // string user defined tag on the ctx
}
*Payload Interceptor*
Below is a JSON formatted example of a log that would be logged by the payload interceptor:
{
"level": "info", // string logrus log levels
"msg": "client request payload logged as grpc.request.content", // string log message
"grpc.request.content": { // object content of RPC request
"value": "something", // string defined by caller
"sleepTimeMs": 9999 // int defined by caller
},
"grpc.method": "Ping", // string method being called
"grpc.service": "mwitkow.testproto.TestService", // string service being called
"span.kind": "client", // string client | server
"system": "grpc" // string
}
Note - due to implementation ZAP differs from Logrus in the "grpc.request.content" object by having an inner "msg" object.
Please see examples and tests for examples of use.
*/
package grpc_logrus

View File

@@ -0,0 +1,15 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_logrus
import (
"github.com/sirupsen/logrus"
"google.golang.org/grpc/grpclog"
)
// ReplaceGrpcLogger sets the given logrus.Logger as a gRPC-level logger.
// This should be called *before* any other initialization, preferably from init() functions.
func ReplaceGrpcLogger(logger *logrus.Entry) {
grpclog.SetLogger(logger.WithField("system", SystemField))
}

View File

@@ -0,0 +1,185 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_logrus
import (
"time"
"github.com/grpc-ecosystem/go-grpc-middleware/logging"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
)
var (
defaultOptions = &options{
levelFunc: nil,
shouldLog: grpc_logging.DefaultDeciderMethod,
codeFunc: grpc_logging.DefaultErrorToCode,
durationFunc: DefaultDurationToField,
}
)
type options struct {
levelFunc CodeToLevel
shouldLog grpc_logging.Decider
codeFunc grpc_logging.ErrorToCode
durationFunc DurationToField
}
func evaluateServerOpt(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
optCopy.levelFunc = DefaultCodeToLevel
for _, o := range opts {
o(optCopy)
}
return optCopy
}
func evaluateClientOpt(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
optCopy.levelFunc = DefaultClientCodeToLevel
for _, o := range opts {
o(optCopy)
}
return optCopy
}
type Option func(*options)
// CodeToLevel function defines the mapping between gRPC return codes and interceptor log level.
type CodeToLevel func(code codes.Code) logrus.Level
// DurationToField function defines how to produce duration fields for logging
type DurationToField func(duration time.Duration) (key string, value interface{})
// WithDecider customizes the function for deciding if the gRPC interceptor logs should log.
func WithDecider(f grpc_logging.Decider) Option {
return func(o *options) {
o.shouldLog = f
}
}
// WithLevels customizes the function for mapping gRPC return codes and interceptor log level statements.
func WithLevels(f CodeToLevel) Option {
return func(o *options) {
o.levelFunc = f
}
}
// WithCodes customizes the function for mapping errors to error codes.
func WithCodes(f grpc_logging.ErrorToCode) Option {
return func(o *options) {
o.codeFunc = f
}
}
// WithDurationField customizes the function for mapping request durations to log fields.
func WithDurationField(f DurationToField) Option {
return func(o *options) {
o.durationFunc = f
}
}
// DefaultCodeToLevel is the default implementation of gRPC return codes to log levels for server side.
func DefaultCodeToLevel(code codes.Code) logrus.Level {
switch code {
case codes.OK:
return logrus.InfoLevel
case codes.Canceled:
return logrus.InfoLevel
case codes.Unknown:
return logrus.ErrorLevel
case codes.InvalidArgument:
return logrus.InfoLevel
case codes.DeadlineExceeded:
return logrus.WarnLevel
case codes.NotFound:
return logrus.InfoLevel
case codes.AlreadyExists:
return logrus.InfoLevel
case codes.PermissionDenied:
return logrus.WarnLevel
case codes.Unauthenticated:
return logrus.InfoLevel // unauthenticated requests can happen
case codes.ResourceExhausted:
return logrus.WarnLevel
case codes.FailedPrecondition:
return logrus.WarnLevel
case codes.Aborted:
return logrus.WarnLevel
case codes.OutOfRange:
return logrus.WarnLevel
case codes.Unimplemented:
return logrus.ErrorLevel
case codes.Internal:
return logrus.ErrorLevel
case codes.Unavailable:
return logrus.WarnLevel
case codes.DataLoss:
return logrus.ErrorLevel
default:
return logrus.ErrorLevel
}
}
// DefaultClientCodeToLevel is the default implementation of gRPC return codes to log levels for client side.
func DefaultClientCodeToLevel(code codes.Code) logrus.Level {
switch code {
case codes.OK:
return logrus.DebugLevel
case codes.Canceled:
return logrus.DebugLevel
case codes.Unknown:
return logrus.InfoLevel
case codes.InvalidArgument:
return logrus.DebugLevel
case codes.DeadlineExceeded:
return logrus.InfoLevel
case codes.NotFound:
return logrus.DebugLevel
case codes.AlreadyExists:
return logrus.DebugLevel
case codes.PermissionDenied:
return logrus.InfoLevel
case codes.Unauthenticated:
return logrus.InfoLevel // unauthenticated requests can happen
case codes.ResourceExhausted:
return logrus.DebugLevel
case codes.FailedPrecondition:
return logrus.DebugLevel
case codes.Aborted:
return logrus.DebugLevel
case codes.OutOfRange:
return logrus.DebugLevel
case codes.Unimplemented:
return logrus.WarnLevel
case codes.Internal:
return logrus.WarnLevel
case codes.Unavailable:
return logrus.WarnLevel
case codes.DataLoss:
return logrus.WarnLevel
default:
return logrus.InfoLevel
}
}
// DefaultDurationToField is the default implementation of converting request duration to a log field (key and value).
var DefaultDurationToField = DurationToTimeMillisField
// DurationToTimeMillisField converts the duration to milliseconds and uses the key `grpc.time_ms`.
func DurationToTimeMillisField(duration time.Duration) (key string, value interface{}) {
return "grpc.time_ms", durationToMilliseconds(duration)
}
// DurationToDurationField uses the duration value to log the request duration.
func DurationToDurationField(duration time.Duration) (key string, value interface{}) {
return "grpc.duration", duration
}
func durationToMilliseconds(duration time.Duration) float32 {
return float32(duration.Nanoseconds()/1000) / 1000
}

View File

@@ -0,0 +1,144 @@
package grpc_logrus
import (
"bytes"
"fmt"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/go-grpc-middleware/logging"
"github.com/grpc-ecosystem/go-grpc-middleware/tags/logrus"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var (
// JsonPbMarshaller is the marshaller used for serializing protobuf messages.
JsonPbMarshaller = &jsonpb.Marshaler{}
)
// PayloadUnaryServerInterceptor returns a new unary server interceptors that logs the payloads of requests.
//
// This *only* works when placed *after* the `grpc_logrus.UnaryServerInterceptor`. However, the logging can be done to a
// separate instance of the logger.
func PayloadUnaryServerInterceptor(entry *logrus.Entry, decider grpc_logging.ServerPayloadLoggingDecider) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !decider(ctx, info.FullMethod, info.Server) {
return handler(ctx, req)
}
// Use the provided logrus.Entry for logging but use the fields from context.
logEntry := entry.WithFields(ctx_logrus.Extract(ctx).Data)
logProtoMessageAsJson(logEntry, req, "grpc.request.content", "server request payload logged as grpc.request.content field")
resp, err := handler(ctx, req)
if err == nil {
logProtoMessageAsJson(logEntry, resp, "grpc.response.content", "server response payload logged as grpc.request.content field")
}
return resp, err
}
}
// PayloadStreamServerInterceptor returns a new server server interceptors that logs the payloads of requests.
//
// This *only* works when placed *after* the `grpc_logrus.StreamServerInterceptor`. However, the logging can be done to a
// separate instance of the logger.
func PayloadStreamServerInterceptor(entry *logrus.Entry, decider grpc_logging.ServerPayloadLoggingDecider) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if !decider(stream.Context(), info.FullMethod, srv) {
return handler(srv, stream)
}
// Use the provided logrus.Entry for logging but use the fields from context.
logEntry := entry.WithFields(Extract(stream.Context()).Data)
newStream := &loggingServerStream{ServerStream: stream, entry: logEntry}
return handler(srv, newStream)
}
}
// PayloadUnaryClientInterceptor returns a new unary client interceptor that logs the paylods of requests and responses.
func PayloadUnaryClientInterceptor(entry *logrus.Entry, decider grpc_logging.ClientPayloadLoggingDecider) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if !decider(ctx, method) {
return invoker(ctx, method, req, reply, cc, opts...)
}
logEntry := entry.WithFields(newClientLoggerFields(ctx, method))
logProtoMessageAsJson(logEntry, req, "grpc.request.content", "client request payload logged as grpc.request.content")
err := invoker(ctx, method, req, reply, cc, opts...)
if err == nil {
logProtoMessageAsJson(logEntry, reply, "grpc.response.content", "client response payload logged as grpc.response.content")
}
return err
}
}
// PayloadStreamClientInterceptor returns a new streaming client interceptor that logs the paylods of requests and responses.
func PayloadStreamClientInterceptor(entry *logrus.Entry, decider grpc_logging.ClientPayloadLoggingDecider) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if !decider(ctx, method) {
return streamer(ctx, desc, cc, method, opts...)
}
logEntry := entry.WithFields(newClientLoggerFields(ctx, method))
clientStream, err := streamer(ctx, desc, cc, method, opts...)
newStream := &loggingClientStream{ClientStream: clientStream, entry: logEntry}
return newStream, err
}
}
type loggingClientStream struct {
grpc.ClientStream
entry *logrus.Entry
}
func (l *loggingClientStream) SendMsg(m interface{}) error {
err := l.ClientStream.SendMsg(m)
if err == nil {
logProtoMessageAsJson(l.entry, m, "grpc.request.content", "server request payload logged as grpc.request.content field")
}
return err
}
func (l *loggingClientStream) RecvMsg(m interface{}) error {
err := l.ClientStream.RecvMsg(m)
if err == nil {
logProtoMessageAsJson(l.entry, m, "grpc.response.content", "server response payload logged as grpc.response.content field")
}
return err
}
type loggingServerStream struct {
grpc.ServerStream
entry *logrus.Entry
}
func (l *loggingServerStream) SendMsg(m interface{}) error {
err := l.ServerStream.SendMsg(m)
if err == nil {
logProtoMessageAsJson(l.entry, m, "grpc.response.content", "server response payload logged as grpc.response.content field")
}
return err
}
func (l *loggingServerStream) RecvMsg(m interface{}) error {
err := l.ServerStream.RecvMsg(m)
if err == nil {
logProtoMessageAsJson(l.entry, m, "grpc.request.content", "server request payload logged as grpc.request.content field")
}
return err
}
func logProtoMessageAsJson(entry *logrus.Entry, pbMsg interface{}, key string, msg string) {
if p, ok := pbMsg.(proto.Message); ok {
entry.WithField(key, &jsonpbMarshalleble{p}).Info(msg)
}
}
type jsonpbMarshalleble struct {
proto.Message
}
func (j *jsonpbMarshalleble) MarshalJSON() ([]byte, error) {
b := &bytes.Buffer{}
if err := JsonPbMarshaller.Marshal(b, j.Message); err != nil {
return nil, fmt.Errorf("jsonpb serializer failed: %v", err)
}
return b.Bytes(), nil
}

View File

@@ -0,0 +1,129 @@
// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
package grpc_logrus
import (
"path"
"time"
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
"github.com/grpc-ecosystem/go-grpc-middleware/tags/logrus"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var (
// SystemField is used in every log statement made through grpc_logrus. Can be overwritten before any initialization code.
SystemField = "system"
// KindField describes the log gield used to incicate whether this is a server or a client log statment.
KindField = "span.kind"
)
// UnaryServerInterceptor returns a new unary server interceptors that adds logrus.Entry to the context.
func UnaryServerInterceptor(entry *logrus.Entry, opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateServerOpt(opts)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
startTime := time.Now()
newCtx := newLoggerForCall(ctx, entry, info.FullMethod, startTime)
resp, err := handler(newCtx, req)
if !o.shouldLog(info.FullMethod, err) {
return resp, err
}
code := o.codeFunc(err)
level := o.levelFunc(code)
durField, durVal := o.durationFunc(time.Since(startTime))
fields := logrus.Fields{
"grpc.code": code.String(),
durField: durVal,
}
if err != nil {
fields[logrus.ErrorKey] = err
}
levelLogf(
ctx_logrus.Extract(newCtx).WithFields(fields), // re-extract logger from newCtx, as it may have extra fields that changed in the holder.
level,
"finished unary call with code "+code.String())
return resp, err
}
}
// StreamServerInterceptor returns a new streaming server interceptor that adds logrus.Entry to the context.
func StreamServerInterceptor(entry *logrus.Entry, opts ...Option) grpc.StreamServerInterceptor {
o := evaluateServerOpt(opts)
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
startTime := time.Now()
newCtx := newLoggerForCall(stream.Context(), entry, info.FullMethod, startTime)
wrapped := grpc_middleware.WrapServerStream(stream)
wrapped.WrappedContext = newCtx
err := handler(srv, wrapped)
if !o.shouldLog(info.FullMethod, err) {
return err
}
code := o.codeFunc(err)
level := o.levelFunc(code)
durField, durVal := o.durationFunc(time.Since(startTime))
fields := logrus.Fields{
"grpc.code": code.String(),
durField: durVal,
}
if err != nil {
fields[logrus.ErrorKey] = err
}
levelLogf(
ctx_logrus.Extract(newCtx).WithFields(fields), // re-extract logger from newCtx, as it may have extra fields that changed in the holder.
level,
"finished streaming call with code "+code.String())
return err
}
}
func levelLogf(entry *logrus.Entry, level logrus.Level, format string, args ...interface{}) {
switch level {
case logrus.DebugLevel:
entry.Debugf(format, args...)
case logrus.InfoLevel:
entry.Infof(format, args...)
case logrus.WarnLevel:
entry.Warningf(format, args...)
case logrus.ErrorLevel:
entry.Errorf(format, args...)
case logrus.FatalLevel:
entry.Fatalf(format, args...)
case logrus.PanicLevel:
entry.Panicf(format, args...)
}
}
func newLoggerForCall(ctx context.Context, entry *logrus.Entry, fullMethodString string, start time.Time) context.Context {
service := path.Dir(fullMethodString)[1:]
method := path.Base(fullMethodString)
callLog := entry.WithFields(
logrus.Fields{
SystemField: "grpc",
KindField: "server",
"grpc.service": service,
"grpc.method": method,
"grpc.start_time": start.Format(time.RFC3339),
})
if d, ok := ctx.Deadline(); ok {
callLog = callLog.WithFields(
logrus.Fields{
"grpc.request.deadline": d.Format(time.RFC3339),
})
}
callLog = callLog.WithFields(ctx_logrus.Extract(ctx).Data)
return ctxlogrus.ToContext(ctx, callLog)
}

View File

@@ -0,0 +1,64 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_zap
import (
"path"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var (
// ClientField is used in every client-side log statement made through grpc_zap. Can be overwritten before initialization.
ClientField = zap.String("span.kind", "client")
)
// UnaryClientInterceptor returns a new unary client interceptor that optionally logs the execution of external gRPC calls.
func UnaryClientInterceptor(logger *zap.Logger, opts ...Option) grpc.UnaryClientInterceptor {
o := evaluateClientOpt(opts)
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
fields := newClientLoggerFields(ctx, method)
startTime := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
logFinalClientLine(o, logger.With(fields...), startTime, err, "finished client unary call")
return err
}
}
// StreamClientInterceptor returns a new streaming client interceptor that optionally logs the execution of external gRPC calls.
func StreamClientInterceptor(logger *zap.Logger, opts ...Option) grpc.StreamClientInterceptor {
o := evaluateClientOpt(opts)
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
fields := newClientLoggerFields(ctx, method)
startTime := time.Now()
clientStream, err := streamer(ctx, desc, cc, method, opts...)
logFinalClientLine(o, logger.With(fields...), startTime, err, "finished client streaming call")
return clientStream, err
}
}
func logFinalClientLine(o *options, logger *zap.Logger, startTime time.Time, err error, msg string) {
code := o.codeFunc(err)
level := o.levelFunc(code)
logger.Check(level, msg).Write(
zap.Error(err),
zap.String("grpc.code", code.String()),
o.durationFunc(time.Now().Sub(startTime)),
)
}
func newClientLoggerFields(ctx context.Context, fullMethodString string) []zapcore.Field {
service := path.Dir(fullMethodString)[1:]
method := path.Base(fullMethodString)
return []zapcore.Field{
SystemField,
ClientField,
zap.String("grpc.service", service),
zap.String("grpc.method", method),
}
}

View File

@@ -0,0 +1,20 @@
package grpc_zap
import (
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/context"
)
// AddFields adds zap fields to the logger.
// Deprecated: should use the ctxzap.AddFields instead
func AddFields(ctx context.Context, fields ...zapcore.Field) {
ctxzap.AddFields(ctx, fields...)
}
// Extract takes the call-scoped Logger from grpc_zap middleware.
// Deprecated: should use the ctxzap.Extract instead
func Extract(ctx context.Context) *zap.Logger {
return ctxzap.Extract(ctx)
}

View File

@@ -0,0 +1,64 @@
package ctxzap
import (
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/context"
)
type ctxMarker struct{}
type ctxLogger struct {
logger *zap.Logger
fields []zapcore.Field
}
var (
ctxMarkerKey = &ctxMarker{}
nullLogger = zap.NewNop()
)
// AddFields adds zap fields to the logger.
func AddFields(ctx context.Context, fields ...zapcore.Field) {
l, ok := ctx.Value(ctxMarkerKey).(*ctxLogger)
if !ok || l == nil {
return
}
l.fields = append(l.fields, fields...)
}
// Extract takes the call-scoped Logger from grpc_zap middleware.
//
// It always returns a Logger that has all the grpc_ctxtags updated.
func Extract(ctx context.Context) *zap.Logger {
l, ok := ctx.Value(ctxMarkerKey).(*ctxLogger)
if !ok || l == nil {
return nullLogger
}
// Add grpc_ctxtags tags metadata until now.
fields := TagsToFields(ctx)
// Add zap fields added until now.
fields = append(fields, l.fields...)
return l.logger.With(fields...)
}
// TagsToFields transforms the Tags on the supplied context into zap fields.
func TagsToFields(ctx context.Context) []zapcore.Field {
fields := []zapcore.Field{}
tags := grpc_ctxtags.Extract(ctx)
for k, v := range tags.Values() {
fields = append(fields, zap.Any(k, v))
}
return fields
}
// ToContext adds the zap.Logger to the context for extraction later.
// Returning the new context that has been created.
func ToContext(ctx context.Context, logger *zap.Logger) context.Context {
l := &ctxLogger{
logger: logger,
}
return context.WithValue(ctx, ctxMarkerKey, l)
}

View File

@@ -0,0 +1,14 @@
/*
`ctxzap` is a ctxlogger that is backed by Zap
It accepts a user-configured `zap.Logger` that will be used for logging. The same `zap.Logger` will
be populated into the `context.Context` passed into gRPC handler code.
You can use `ctxzap.Extract` to log into a request-scoped `zap.Logger` instance in your handler code.
As `ctxzap.Extract` will iterate all tags on from `grpc_ctxtags` it is therefore expensive so it is advised that you
extract once at the start of the function from the context and reuse it for the remainder of the function (see examples).
Please see examples and tests for examples of use.
*/
package ctxzap

View File

@@ -0,0 +1,74 @@
/*
`grpc_zap` is a gRPC logging middleware backed by ZAP loggers
It accepts a user-configured `zap.Logger` that will be used for logging completed gRPC calls. The same `zap.Logger` will
be used for logging completed gRPC calls, and be populated into the `context.Context` passed into gRPC handler code.
On calling `StreamServerInterceptor` or `UnaryServerInterceptor` this logging middleware will add gRPC call information
to the ctx so that it will be present on subsequent use of the `ctx_zap` logger.
If a deadline is present on the gRPC request the grpc.request.deadline tag is populated when the request begins. grpc.request.deadline
is a string representing the time (RFC3339) when the current call will expire.
This package also implements request and response *payload* logging, both for server-side and client-side. These will be
logged as structured `jsonpb` fields for every message received/sent (both unary and streaming). For that please use
`Payload*Interceptor` functions for that. Please note that the user-provided function that determines whetether to log
the full request/response payload needs to be written with care, this can significantly slow down gRPC.
ZAP can also be made as a backend for gRPC library internals. For that use `ReplaceGrpcLogger`.
*Server Interceptor*
Below is a JSON formatted example of a log that would be logged by the server interceptor:
{
"level": "info", // string zap log levels
"msg": "finished unary call", // string log message
"grpc.code": "OK", // string grpc status code
"grpc.method": "Ping", // string method name
"grpc.service": "mwitkow.testproto.TestService", // string full name of the called service
"grpc.start_time": "2006-01-02T15:04:05Z07:00", // string RFC3339 representation of the start time
"grpc.request.deadline": "2006-01-02T15:04:05Z07:00", // string RFC3339 deadline of the current request if supplied
"grpc.request.value": "something", // string value on the request
"grpc.time_ms": 1.345, // float32 run time of the call in ms
"peer.address": {
"IP": "127.0.0.1", // string IP address of calling party
"Port": 60216, // int port call is coming in on
"Zone": "" // string peer zone for caller
},
"span.kind": "server", // string client | server
"system": "grpc" // string
"custom_field": "custom_value", // string user defined field
"custom_tags.int": 1337, // int user defined tag on the ctx
"custom_tags.string": "something", // string user defined tag on the ctx
}
*Payload Interceptor*
Below is a JSON formatted example of a log that would be logged by the payload interceptor:
{
"level": "info", // string zap log levels
"msg": "client request payload logged as grpc.request.content", // string log message
"grpc.request.content": { // object content of RPC request
"msg" : { // object ZAP specific inner object
"value": "something", // string defined by caller
"sleepTimeMs": 9999 // int defined by caller
}
},
"grpc.method": "Ping", // string method being called
"grpc.service": "mwitkow.testproto.TestService", // string service being called
"span.kind": "client", // string client | server
"system": "grpc" // string
}
Note - due to implementation ZAP differs from Logrus in the "grpc.request.content" object by having an inner "msg" object.
Please see examples and tests for examples of use.
*/
package grpc_zap

View File

@@ -0,0 +1,46 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_zap
import (
"fmt"
"go.uber.org/zap"
"google.golang.org/grpc/grpclog"
)
// ReplaceGrpcLogger sets the given zap.Logger as a gRPC-level logger.
// This should be called *before* any other initialization, preferably from init() functions.
func ReplaceGrpcLogger(logger *zap.Logger) {
zgl := &zapGrpcLogger{logger.With(SystemField, zap.Bool("grpc_log", true))}
grpclog.SetLogger(zgl)
}
type zapGrpcLogger struct {
logger *zap.Logger
}
func (l *zapGrpcLogger) Fatal(args ...interface{}) {
l.logger.Fatal(fmt.Sprint(args...))
}
func (l *zapGrpcLogger) Fatalf(format string, args ...interface{}) {
l.logger.Fatal(fmt.Sprintf(format, args...))
}
func (l *zapGrpcLogger) Fatalln(args ...interface{}) {
l.logger.Fatal(fmt.Sprint(args...))
}
func (l *zapGrpcLogger) Print(args ...interface{}) {
l.logger.Info(fmt.Sprint(args...))
}
func (l *zapGrpcLogger) Printf(format string, args ...interface{}) {
l.logger.Info(fmt.Sprintf(format, args...))
}
func (l *zapGrpcLogger) Println(args ...interface{}) {
l.logger.Info(fmt.Sprint(args...))
}

View File

@@ -0,0 +1,184 @@
package grpc_zap
import (
"time"
"github.com/grpc-ecosystem/go-grpc-middleware/logging"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"google.golang.org/grpc/codes"
)
var (
defaultOptions = &options{
levelFunc: DefaultCodeToLevel,
shouldLog: grpc_logging.DefaultDeciderMethod,
codeFunc: grpc_logging.DefaultErrorToCode,
durationFunc: DefaultDurationToField,
}
)
type options struct {
levelFunc CodeToLevel
shouldLog grpc_logging.Decider
codeFunc grpc_logging.ErrorToCode
durationFunc DurationToField
}
func evaluateServerOpt(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
optCopy.levelFunc = DefaultCodeToLevel
for _, o := range opts {
o(optCopy)
}
return optCopy
}
func evaluateClientOpt(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
optCopy.levelFunc = DefaultClientCodeToLevel
for _, o := range opts {
o(optCopy)
}
return optCopy
}
type Option func(*options)
// CodeToLevel function defines the mapping between gRPC return codes and interceptor log level.
type CodeToLevel func(code codes.Code) zapcore.Level
// DurationToField function defines how to produce duration fields for logging
type DurationToField func(duration time.Duration) zapcore.Field
// WithDecider customizes the function for deciding if the gRPC interceptor logs should log.
func WithDecider(f grpc_logging.Decider) Option {
return func(o *options) {
o.shouldLog = f
}
}
// WithLevels customizes the function for mapping gRPC return codes and interceptor log level statements.
func WithLevels(f CodeToLevel) Option {
return func(o *options) {
o.levelFunc = f
}
}
// WithCodes customizes the function for mapping errors to error codes.
func WithCodes(f grpc_logging.ErrorToCode) Option {
return func(o *options) {
o.codeFunc = f
}
}
// WithDurationField customizes the function for mapping request durations to Zap fields.
func WithDurationField(f DurationToField) Option {
return func(o *options) {
o.durationFunc = f
}
}
// DefaultCodeToLevel is the default implementation of gRPC return codes and interceptor log level for server side.
func DefaultCodeToLevel(code codes.Code) zapcore.Level {
switch code {
case codes.OK:
return zap.InfoLevel
case codes.Canceled:
return zap.InfoLevel
case codes.Unknown:
return zap.ErrorLevel
case codes.InvalidArgument:
return zap.InfoLevel
case codes.DeadlineExceeded:
return zap.WarnLevel
case codes.NotFound:
return zap.InfoLevel
case codes.AlreadyExists:
return zap.InfoLevel
case codes.PermissionDenied:
return zap.WarnLevel
case codes.Unauthenticated:
return zap.InfoLevel // unauthenticated requests can happen
case codes.ResourceExhausted:
return zap.WarnLevel
case codes.FailedPrecondition:
return zap.WarnLevel
case codes.Aborted:
return zap.WarnLevel
case codes.OutOfRange:
return zap.WarnLevel
case codes.Unimplemented:
return zap.ErrorLevel
case codes.Internal:
return zap.ErrorLevel
case codes.Unavailable:
return zap.WarnLevel
case codes.DataLoss:
return zap.ErrorLevel
default:
return zap.ErrorLevel
}
}
// DefaultClientCodeToLevel is the default implementation of gRPC return codes to log levels for client side.
func DefaultClientCodeToLevel(code codes.Code) zapcore.Level {
switch code {
case codes.OK:
return zap.DebugLevel
case codes.Canceled:
return zap.DebugLevel
case codes.Unknown:
return zap.InfoLevel
case codes.InvalidArgument:
return zap.DebugLevel
case codes.DeadlineExceeded:
return zap.InfoLevel
case codes.NotFound:
return zap.DebugLevel
case codes.AlreadyExists:
return zap.DebugLevel
case codes.PermissionDenied:
return zap.InfoLevel
case codes.Unauthenticated:
return zap.InfoLevel // unauthenticated requests can happen
case codes.ResourceExhausted:
return zap.DebugLevel
case codes.FailedPrecondition:
return zap.DebugLevel
case codes.Aborted:
return zap.DebugLevel
case codes.OutOfRange:
return zap.DebugLevel
case codes.Unimplemented:
return zap.WarnLevel
case codes.Internal:
return zap.WarnLevel
case codes.Unavailable:
return zap.WarnLevel
case codes.DataLoss:
return zap.WarnLevel
default:
return zap.InfoLevel
}
}
// DefaultDurationToField is the default implementation of converting request duration to a Zap field.
var DefaultDurationToField = DurationToTimeMillisField
// DurationToTimeMillisField converts the duration to milliseconds and uses the key `grpc.time_ms`.
func DurationToTimeMillisField(duration time.Duration) zapcore.Field {
return zap.Float32("grpc.time_ms", durationToMilliseconds(duration))
}
// DurationToDurationField uses a Duration field to log the request duration
// and leaves it up to Zap's encoder settings to determine how that is output.
func DurationToDurationField(duration time.Duration) zapcore.Field {
return zap.Duration("grpc.duration", duration)
}
func durationToMilliseconds(duration time.Duration) float32 {
return float32(duration.Nanoseconds()/1000) / 1000
}

View File

@@ -0,0 +1,149 @@
package grpc_zap
import (
"bytes"
"fmt"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/go-grpc-middleware/logging"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var (
// JsonPbMarshaller is the marshaller used for serializing protobuf messages.
JsonPbMarshaller = &jsonpb.Marshaler{}
)
// PayloadUnaryServerInterceptor returns a new unary server interceptors that logs the payloads of requests.
//
// This *only* works when placed *after* the `grpc_zap.UnaryServerInterceptor`. However, the logging can be done to a
// separate instance of the logger.
func PayloadUnaryServerInterceptor(logger *zap.Logger, decider grpc_logging.ServerPayloadLoggingDecider) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !decider(ctx, info.FullMethod, info.Server) {
return handler(ctx, req)
}
// Use the provided zap.Logger for logging but use the fields from context.
logEntry := logger.With(append(serverCallFields(info.FullMethod), ctxzap.TagsToFields(ctx)...)...)
logProtoMessageAsJson(logEntry, req, "grpc.request.content", "server request payload logged as grpc.request.content field")
resp, err := handler(ctx, req)
if err == nil {
logProtoMessageAsJson(logEntry, resp, "grpc.response.content", "server response payload logged as grpc.request.content field")
}
return resp, err
}
}
// PayloadStreamServerInterceptor returns a new server server interceptors that logs the payloads of requests.
//
// This *only* works when placed *after* the `grpc_zap.StreamServerInterceptor`. However, the logging can be done to a
// separate instance of the logger.
func PayloadStreamServerInterceptor(logger *zap.Logger, decider grpc_logging.ServerPayloadLoggingDecider) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if !decider(stream.Context(), info.FullMethod, srv) {
return handler(srv, stream)
}
logEntry := logger.With(append(serverCallFields(info.FullMethod), ctxzap.TagsToFields(stream.Context())...)...)
newStream := &loggingServerStream{ServerStream: stream, logger: logEntry}
return handler(srv, newStream)
}
}
// PayloadUnaryClientInterceptor returns a new unary client interceptor that logs the paylods of requests and responses.
func PayloadUnaryClientInterceptor(logger *zap.Logger, decider grpc_logging.ClientPayloadLoggingDecider) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if !decider(ctx, method) {
return invoker(ctx, method, req, reply, cc, opts...)
}
logEntry := logger.With(newClientLoggerFields(ctx, method)...)
logProtoMessageAsJson(logEntry, req, "grpc.request.content", "client request payload logged as grpc.request.content")
err := invoker(ctx, method, req, reply, cc, opts...)
if err == nil {
logProtoMessageAsJson(logEntry, reply, "grpc.response.content", "client response payload logged as grpc.response.content")
}
return err
}
}
// PayloadStreamClientInterceptor returns a new streaming client interceptor that logs the paylods of requests and responses.
func PayloadStreamClientInterceptor(logger *zap.Logger, decider grpc_logging.ClientPayloadLoggingDecider) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if !decider(ctx, method) {
return streamer(ctx, desc, cc, method, opts...)
}
logEntry := logger.With(newClientLoggerFields(ctx, method)...)
clientStream, err := streamer(ctx, desc, cc, method, opts...)
newStream := &loggingClientStream{ClientStream: clientStream, logger: logEntry}
return newStream, err
}
}
type loggingClientStream struct {
grpc.ClientStream
logger *zap.Logger
}
func (l *loggingClientStream) SendMsg(m interface{}) error {
err := l.ClientStream.SendMsg(m)
if err == nil {
logProtoMessageAsJson(l.logger, m, "grpc.request.content", "server request payload logged as grpc.request.content field")
}
return err
}
func (l *loggingClientStream) RecvMsg(m interface{}) error {
err := l.ClientStream.RecvMsg(m)
if err == nil {
logProtoMessageAsJson(l.logger, m, "grpc.response.content", "server response payload logged as grpc.response.content field")
}
return err
}
type loggingServerStream struct {
grpc.ServerStream
logger *zap.Logger
}
func (l *loggingServerStream) SendMsg(m interface{}) error {
err := l.ServerStream.SendMsg(m)
if err == nil {
logProtoMessageAsJson(l.logger, m, "grpc.response.content", "server response payload logged as grpc.response.content field")
}
return err
}
func (l *loggingServerStream) RecvMsg(m interface{}) error {
err := l.ServerStream.RecvMsg(m)
if err == nil {
logProtoMessageAsJson(l.logger, m, "grpc.request.content", "server request payload logged as grpc.request.content field")
}
return err
}
func logProtoMessageAsJson(logger *zap.Logger, pbMsg interface{}, key string, msg string) {
if p, ok := pbMsg.(proto.Message); ok {
logger.Check(zapcore.InfoLevel, msg).Write(zap.Object(key, &jsonpbObjectMarshaler{pb: p}))
}
}
type jsonpbObjectMarshaler struct {
pb proto.Message
}
func (j *jsonpbObjectMarshaler) MarshalLogObject(e zapcore.ObjectEncoder) error {
// ZAP jsonEncoder deals with AddReflect by using json.MarshalObject. The same thing applies for consoleEncoder.
return e.AddReflected("msg", j)
}
func (j *jsonpbObjectMarshaler) MarshalJSON() ([]byte, error) {
b := &bytes.Buffer{}
if err := JsonPbMarshaller.Marshal(b, j.pb); err != nil {
return nil, fmt.Errorf("jsonpb serializer failed: %v", err)
}
return b.Bytes(), nil
}

View File

@@ -0,0 +1,96 @@
package grpc_zap
import (
"path"
"time"
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/grpc-ecosystem/go-grpc-middleware/tags/zap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var (
// SystemField is used in every log statement made through grpc_zap. Can be overwritten before any initialization code.
SystemField = zap.String("system", "grpc")
// ServerField is used in every server-side log statement made through grpc_zap.Can be overwritten before initialization.
ServerField = zap.String("span.kind", "server")
)
// UnaryServerInterceptor returns a new unary server interceptors that adds zap.Logger to the context.
func UnaryServerInterceptor(logger *zap.Logger, opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateServerOpt(opts)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
startTime := time.Now()
newCtx := newLoggerForCall(ctx, logger, info.FullMethod, startTime)
resp, err := handler(newCtx, req)
if !o.shouldLog(info.FullMethod, err) {
return resp, err
}
code := o.codeFunc(err)
level := o.levelFunc(code)
// re-extract logger from newCtx, as it may have extra fields that changed in the holder.
ctx_zap.Extract(newCtx).Check(level, "finished unary call with code "+code.String()).Write(
zap.Error(err),
zap.String("grpc.code", code.String()),
o.durationFunc(time.Since(startTime)),
)
return resp, err
}
}
// StreamServerInterceptor returns a new streaming server interceptor that adds zap.Logger to the context.
func StreamServerInterceptor(logger *zap.Logger, opts ...Option) grpc.StreamServerInterceptor {
o := evaluateServerOpt(opts)
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
startTime := time.Now()
newCtx := newLoggerForCall(stream.Context(), logger, info.FullMethod, startTime)
wrapped := grpc_middleware.WrapServerStream(stream)
wrapped.WrappedContext = newCtx
err := handler(srv, wrapped)
if !o.shouldLog(info.FullMethod, err) {
return err
}
code := o.codeFunc(err)
level := o.levelFunc(code)
// re-extract logger from newCtx, as it may have extra fields that changed in the holder.
ctx_zap.Extract(newCtx).Check(level, "finished streaming call with code "+code.String()).Write(
zap.Error(err),
zap.String("grpc.code", code.String()),
o.durationFunc(time.Since(startTime)),
)
return err
}
}
func serverCallFields(fullMethodString string) []zapcore.Field {
service := path.Dir(fullMethodString)[1:]
method := path.Base(fullMethodString)
return []zapcore.Field{
SystemField,
ServerField,
zap.String("grpc.service", service),
zap.String("grpc.method", method),
}
}
func newLoggerForCall(ctx context.Context, logger *zap.Logger, fullMethodString string, start time.Time) context.Context {
f := ctxzap.TagsToFields(ctx)
f = append(f, zap.String("grpc.start_time", start.Format(time.RFC3339)))
if d, ok := ctx.Deadline(); ok {
f = append(f, zap.String("grpc.request.deadline", d.Format(time.RFC3339)))
}
callLog := logger.With(append(f, serverCallFields(fullMethodString)...)...)
return ctxzap.ToContext(ctx, callLog)
}

View File

@@ -0,0 +1,15 @@
// Copyright 2017 David Ackroyd. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_recovery` are intereceptors that recover from gRPC handler panics.
Server Side Recovery Middleware
By default a panic will be converted into a gRPC error with `code.Internal`.
Handling can be customised by providing an alternate recovery function.
Please see examples for simple examples of use.
*/
package grpc_recovery

View File

@@ -0,0 +1,48 @@
// Copyright 2017 David Ackroyd. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_recovery
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// RecoveryHandlerFunc is a function that recovers from the panic `p` by returning an `error`.
type RecoveryHandlerFunc func(p interface{}) (err error)
// UnaryServerInterceptor returns a new unary server interceptor for panic recovery.
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateOptions(opts)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = recoverFrom(r, o.recoveryHandlerFunc)
}
}()
return handler(ctx, req)
}
}
// StreamServerInterceptor returns a new streaming server interceptor for panic recovery.
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
o := evaluateOptions(opts)
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) {
defer func() {
if r := recover(); r != nil {
err = recoverFrom(r, o.recoveryHandlerFunc)
}
}()
return handler(srv, stream)
}
}
func recoverFrom(p interface{}, r RecoveryHandlerFunc) error {
if r == nil {
return grpc.Errorf(codes.Internal, "%s", p)
}
return r(p)
}

View File

@@ -0,0 +1,32 @@
// Copyright 2017 David Ackroyd. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_recovery
var (
defaultOptions = &options{
recoveryHandlerFunc: nil,
}
)
type options struct {
recoveryHandlerFunc RecoveryHandlerFunc
}
func evaluateOptions(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
for _, o := range opts {
o(optCopy)
}
return optCopy
}
type Option func(*options)
// WithRecoveryHandler customizes the function for recovering from a panic.
func WithRecoveryHandler(f RecoveryHandlerFunc) Option {
return func(o *options) {
o.recoveryHandlerFunc = f
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_retry
import (
"time"
"github.com/grpc-ecosystem/go-grpc-middleware/util/backoffutils"
)
// BackoffLinear is very simple: it waits for a fixed period of time between calls.
func BackoffLinear(waitBetween time.Duration) BackoffFunc {
return func(attempt uint) time.Duration {
return waitBetween
}
}
// BackoffLinearWithJitter waits a set period of time, allowing for jitter (fractional adjustment).
//
// For example waitBetween=1s and jitter=0.10 can generate waits between 900ms and 1100ms.
func BackoffLinearWithJitter(waitBetween time.Duration, jitterFraction float64) BackoffFunc {
return func(attempt uint) time.Duration {
return backoffutils.JitterUp(waitBetween, jitterFraction)
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_retry` provides client-side request retry logic for gRPC.
Client-Side Request Retry Interceptor
It allows for automatic retry, inside the generated gRPC code of requests based on the gRPC status
of the reply. It supports unary (1:1), and server stream (1:n) requests.
By default the interceptors *are disabled*, preventing accidental use of retries. You can easily
override the number of retries (setting them to more than 0) with a `grpc.ClientOption`, e.g.:
myclient.Ping(ctx, goodPing, grpc_retry.WithMax(5))
Other default options are: retry on `ResourceExhausted` and `Unavailable` gRPC codes, use a 50ms
linear backoff with 10% jitter.
For chained interceptors, the retry interceptor will call every interceptor that follows it
whenever when a retry happens.
Please see examples for more advanced use.
*/
package grpc_retry

View File

@@ -0,0 +1,120 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_retry
import (
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
var (
// DefaultRetriableCodes is a set of well known types gRPC codes that should be retri-able.
//
// `ResourceExhausted` means that the user quota, e.g. per-RPC limits, have been reached.
// `Unavailable` means that system is currently unavailable and the client should retry again.
DefaultRetriableCodes = []codes.Code{codes.ResourceExhausted, codes.Unavailable}
defaultOptions = &options{
max: 0, // disabed
perCallTimeout: 0, // disabled
includeHeader: true,
codes: DefaultRetriableCodes,
backoffFunc: BackoffLinearWithJitter(50*time.Millisecond /*jitter*/, 0.10),
}
)
// BackoffFunc denotes a family of functions that control the backoff duration between call retries.
//
// They are called with an identifier of the attempt, and should return a time the system client should
// hold off for. If the time returned is longer than the `context.Context.Deadline` of the request
// the deadline of the request takes precedence and the wait will be interrupted before proceeding
// with the next iteration.
type BackoffFunc func(attempt uint) time.Duration
// Disable disables the retry behaviour on this call, or this interceptor.
//
// Its semantically the same to `WithMax`
func Disable() CallOption {
return WithMax(0)
}
// WithMax sets the maximum number of retries on this call, or this interceptor.
func WithMax(maxRetries uint) CallOption {
return CallOption{applyFunc: func(o *options) {
o.max = maxRetries
}}
}
// WithBackoff sets the `BackoffFunc `used to control time between retries.
func WithBackoff(bf BackoffFunc) CallOption {
return CallOption{applyFunc: func(o *options) {
o.backoffFunc = bf
}}
}
// WithCodes sets which codes should be retried.
//
// Please *use with care*, as you may be retrying non-idempotend calls.
//
// You cannot automatically retry on Cancelled and Deadline, please use `WithPerRetryTimeout` for these.
func WithCodes(retryCodes ...codes.Code) CallOption {
return CallOption{applyFunc: func(o *options) {
o.codes = retryCodes
}}
}
// WithPerRetryTimeout sets the RPC timeout per call (including initial call) on this call, or this interceptor.
//
// The context.Deadline of the call takes precedence and sets the maximum time the whole invocation
// will take, but WithPerRetryTimeout can be used to limit the RPC time per each call.
//
// For example, with context.Deadline = now + 10s, and WithPerRetryTimeout(3 * time.Seconds), each
// of the retry calls (including the initial one) will have a deadline of now + 3s.
//
// A value of 0 disables the timeout overrides completely and returns to each retry call using the
// parent `context.Deadline`.
func WithPerRetryTimeout(timeout time.Duration) CallOption {
return CallOption{applyFunc: func(o *options) {
o.perCallTimeout = timeout
}}
}
type options struct {
max uint
perCallTimeout time.Duration
includeHeader bool
codes []codes.Code
backoffFunc BackoffFunc
}
// CallOption is a grpc.CallOption that is local to grpc_retry.
type CallOption struct {
grpc.EmptyCallOption // make sure we implement private after() and before() fields so we don't panic.
applyFunc func(opt *options)
}
func reuseOrNewWithCallOptions(opt *options, callOptions []CallOption) *options {
if len(callOptions) == 0 {
return opt
}
optCopy := &options{}
*optCopy = *opt
for _, f := range callOptions {
f.applyFunc(optCopy)
}
return optCopy
}
func filterCallOptions(callOptions []grpc.CallOption) (grpcOptions []grpc.CallOption, retryOptions []CallOption) {
for _, opt := range callOptions {
if co, ok := opt.(CallOption); ok {
retryOptions = append(retryOptions, co)
} else {
grpcOptions = append(grpcOptions, opt)
}
}
return grpcOptions, retryOptions
}

View File

@@ -0,0 +1,296 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_retry
import (
"fmt"
"io"
"sync"
"time"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"golang.org/x/net/context"
"golang.org/x/net/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
)
const (
AttemptMetadataKey = "x-retry-attempty"
)
// UnaryClientInterceptor returns a new retrying unary client interceptor.
//
// The default configuration of the interceptor is to not retry *at all*. This behaviour can be
// changed through options (e.g. WithMax) on creation of the interceptor or on call (through grpc.CallOptions).
func UnaryClientInterceptor(optFuncs ...CallOption) grpc.UnaryClientInterceptor {
intOpts := reuseOrNewWithCallOptions(defaultOptions, optFuncs)
return func(parentCtx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
grpcOpts, retryOpts := filterCallOptions(opts)
callOpts := reuseOrNewWithCallOptions(intOpts, retryOpts)
// short circuit for simplicity, and avoiding allocations.
if callOpts.max == 0 {
return invoker(parentCtx, method, req, reply, cc, grpcOpts...)
}
var lastErr error
for attempt := uint(0); attempt < callOpts.max; attempt++ {
if err := waitRetryBackoff(attempt, parentCtx, callOpts); err != nil {
return err
}
callCtx := perCallContext(parentCtx, callOpts, attempt)
lastErr = invoker(callCtx, method, req, reply, cc, grpcOpts...)
// TODO(mwitkow): Maybe dial and transport errors should be retriable?
if lastErr == nil {
return nil
}
logTrace(parentCtx, "grpc_retry attempt: %d, got err: %v", attempt, lastErr)
if isContextError(lastErr) {
if parentCtx.Err() != nil {
logTrace(parentCtx, "grpc_retry attempt: %d, parent context error: %v", attempt, parentCtx.Err())
// its the parent context deadline or cancellation.
return lastErr
} else {
logTrace(parentCtx, "grpc_retry attempt: %d, context error from retry call", attempt)
// its the callCtx deadline or cancellation, in which case try again.
continue
}
}
if !isRetriable(lastErr, callOpts) {
return lastErr
}
}
return lastErr
}
}
// StreamClientInterceptor returns a new retrying stream client interceptor for server side streaming calls.
//
// The default configuration of the interceptor is to not retry *at all*. This behaviour can be
// changed through options (e.g. WithMax) on creation of the interceptor or on call (through grpc.CallOptions).
//
// Retry logic is available *only for ServerStreams*, i.e. 1:n streams, as the internal logic needs
// to buffer the messages sent by the client. If retry is enabled on any other streams (ClientStreams,
// BidiStreams), the retry interceptor will fail the call.
func StreamClientInterceptor(optFuncs ...CallOption) grpc.StreamClientInterceptor {
intOpts := reuseOrNewWithCallOptions(defaultOptions, optFuncs)
return func(parentCtx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
grpcOpts, retryOpts := filterCallOptions(opts)
callOpts := reuseOrNewWithCallOptions(intOpts, retryOpts)
// short circuit for simplicity, and avoiding allocations.
if callOpts.max == 0 {
return streamer(parentCtx, desc, cc, method, grpcOpts...)
}
if desc.ClientStreams {
return nil, grpc.Errorf(codes.Unimplemented, "grpc_retry: cannot retry on ClientStreams, set grpc_retry.Disable()")
}
logTrace(parentCtx, "grpc_retry attempt: %d, no backoff for this call", 0)
callCtx := perCallContext(parentCtx, callOpts, 0)
newStreamer, err := streamer(callCtx, desc, cc, method, grpcOpts...)
if err != nil {
// TODO(mwitkow): Maybe dial and transport errors should be retriable?
return nil, err
}
retryingStreamer := &serverStreamingRetryingStream{
ClientStream: newStreamer,
callOpts: callOpts,
parentCtx: parentCtx,
streamerCall: func(ctx context.Context) (grpc.ClientStream, error) {
return streamer(ctx, desc, cc, method, grpcOpts...)
},
}
return retryingStreamer, nil
}
}
// type serverStreamingRetryingStream is the implementation of grpc.ClientStream that acts as a
// proxy to the underlying call. If any of the RecvMsg() calls fail, it will try to reestablish
// a new ClientStream according to the retry policy.
type serverStreamingRetryingStream struct {
grpc.ClientStream
bufferedSends []interface{} // single messsage that the client can sen
receivedGood bool // indicates whether any prior receives were successful
wasClosedSend bool // indicates that CloseSend was closed
parentCtx context.Context
callOpts *options
streamerCall func(ctx context.Context) (grpc.ClientStream, error)
mu sync.RWMutex
}
func (s *serverStreamingRetryingStream) setStream(clientStream grpc.ClientStream) {
s.mu.Lock()
s.ClientStream = clientStream
s.mu.Unlock()
}
func (s *serverStreamingRetryingStream) getStream() grpc.ClientStream {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ClientStream
}
func (s *serverStreamingRetryingStream) SendMsg(m interface{}) error {
s.mu.Lock()
s.bufferedSends = append(s.bufferedSends, m)
s.mu.Unlock()
return s.getStream().SendMsg(m)
}
func (s *serverStreamingRetryingStream) CloseSend() error {
s.mu.Lock()
s.wasClosedSend = true
s.mu.Unlock()
return s.getStream().CloseSend()
}
func (s *serverStreamingRetryingStream) Header() (metadata.MD, error) {
return s.getStream().Header()
}
func (s *serverStreamingRetryingStream) Trailer() metadata.MD {
return s.getStream().Trailer()
}
func (s *serverStreamingRetryingStream) RecvMsg(m interface{}) error {
attemptRetry, lastErr := s.receiveMsgAndIndicateRetry(m)
if !attemptRetry {
return lastErr // success or hard failure
}
// We start off from attempt 1, because zeroth was already made on normal SendMsg().
for attempt := uint(1); attempt < s.callOpts.max; attempt++ {
if err := waitRetryBackoff(attempt, s.parentCtx, s.callOpts); err != nil {
return err
}
callCtx := perCallContext(s.parentCtx, s.callOpts, attempt)
newStream, err := s.reestablishStreamAndResendBuffer(callCtx)
if err != nil {
// TODO(mwitkow): Maybe dial and transport errors should be retriable?
return err
}
s.setStream(newStream)
attemptRetry, lastErr = s.receiveMsgAndIndicateRetry(m)
//fmt.Printf("Received message and indicate: %v %v\n", attemptRetry, lastErr)
if !attemptRetry {
return lastErr
}
}
return lastErr
}
func (s *serverStreamingRetryingStream) receiveMsgAndIndicateRetry(m interface{}) (bool, error) {
s.mu.RLock()
wasGood := s.receivedGood
s.mu.RUnlock()
err := s.getStream().RecvMsg(m)
if err == nil || err == io.EOF {
s.mu.Lock()
s.receivedGood = true
s.mu.Unlock()
return false, err
} else if wasGood {
// previous RecvMsg in the stream succeeded, no retry logic should interfere
return false, err
}
if isContextError(err) {
if s.parentCtx.Err() != nil {
logTrace(s.parentCtx, "grpc_retry parent context error: %v", s.parentCtx.Err())
return false, err
} else {
logTrace(s.parentCtx, "grpc_retry context error from retry call")
// its the callCtx deadline or cancellation, in which case try again.
return true, err
}
}
return isRetriable(err, s.callOpts), err
}
func (s *serverStreamingRetryingStream) reestablishStreamAndResendBuffer(callCtx context.Context) (grpc.ClientStream, error) {
s.mu.RLock()
bufferedSends := s.bufferedSends
s.mu.RUnlock()
newStream, err := s.streamerCall(callCtx)
if err != nil {
logTrace(callCtx, "grpc_retry failed redialing new stream: %v", err)
return nil, err
}
for _, msg := range bufferedSends {
if err := newStream.SendMsg(msg); err != nil {
logTrace(callCtx, "grpc_retry failed resending message: %v", err)
return nil, err
}
}
if err := newStream.CloseSend(); err != nil {
logTrace(callCtx, "grpc_retry failed CloseSend on new stream %v", err)
return nil, err
}
return newStream, nil
}
func waitRetryBackoff(attempt uint, parentCtx context.Context, callOpts *options) error {
var waitTime time.Duration = 0
if attempt > 0 {
waitTime = callOpts.backoffFunc(attempt)
}
if waitTime > 0 {
logTrace(parentCtx, "grpc_retry attempt: %d, backoff for %v", attempt, waitTime)
timer := time.NewTimer(waitTime)
select {
case <-parentCtx.Done():
timer.Stop()
return contextErrToGrpcErr(parentCtx.Err())
case <-timer.C:
}
}
return nil
}
func isRetriable(err error, callOpts *options) bool {
errCode := grpc.Code(err)
if isContextError(err) {
// context errors are not retriable based on user settings.
return false
}
for _, code := range callOpts.codes {
if code == errCode {
return true
}
}
return false
}
func isContextError(err error) bool {
return grpc.Code(err) == codes.DeadlineExceeded || grpc.Code(err) == codes.Canceled
}
func perCallContext(parentCtx context.Context, callOpts *options, attempt uint) context.Context {
ctx := parentCtx
if callOpts.perCallTimeout != 0 {
ctx, _ = context.WithTimeout(ctx, callOpts.perCallTimeout)
}
if attempt > 0 && callOpts.includeHeader {
mdClone := metautils.ExtractOutgoing(ctx).Clone().Set(AttemptMetadataKey, fmt.Sprintf("%d", attempt))
ctx = mdClone.ToOutgoing(ctx)
}
return ctx
}
func contextErrToGrpcErr(err error) error {
switch err {
case context.DeadlineExceeded:
return grpc.Errorf(codes.DeadlineExceeded, err.Error())
case context.Canceled:
return grpc.Errorf(codes.Canceled, err.Error())
default:
return grpc.Errorf(codes.Unknown, err.Error())
}
}
func logTrace(ctx context.Context, format string, a ...interface{}) {
tr, ok := trace.FromContext(ctx)
if !ok {
return
}
tr.LazyPrintf(format, a...)
}

View File

@@ -0,0 +1,163 @@
// Code generated by protoc-gen-gogo.
// source: fields.proto
// DO NOT EDIT!
/*
Package mwitkow_testproto is a generated protocol buffer package.
This file is used for testing discovery of log fields from requests using reflection and gogo proto more tags.
It is generated from these files:
fields.proto
It has these top-level messages:
Metadata
PingId
Ping
PingRequest
Pong
PongRequest
*/
package mwitkow_testproto
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Metadata struct {
Tags []string `protobuf:"bytes,1,rep,name=tags" json:"tags,omitempty" log_field:"meta_tags"`
}
func (m *Metadata) Reset() { *m = Metadata{} }
func (m *Metadata) String() string { return proto.CompactTextString(m) }
func (*Metadata) ProtoMessage() {}
func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptorFields, []int{0} }
type PingId struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" log_field:"ping_id"`
}
func (m *PingId) Reset() { *m = PingId{} }
func (m *PingId) String() string { return proto.CompactTextString(m) }
func (*PingId) ProtoMessage() {}
func (*PingId) Descriptor() ([]byte, []int) { return fileDescriptorFields, []int{1} }
type Ping struct {
Id *PingId `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
}
func (m *Ping) Reset() { *m = Ping{} }
func (m *Ping) String() string { return proto.CompactTextString(m) }
func (*Ping) ProtoMessage() {}
func (*Ping) Descriptor() ([]byte, []int) { return fileDescriptorFields, []int{2} }
func (m *Ping) GetId() *PingId {
if m != nil {
return m.Id
}
return nil
}
type PingRequest struct {
Ping *Ping `protobuf:"bytes,1,opt,name=ping" json:"ping,omitempty"`
Meta *Metadata `protobuf:"bytes,2,opt,name=meta" json:"meta,omitempty"`
}
func (m *PingRequest) Reset() { *m = PingRequest{} }
func (m *PingRequest) String() string { return proto.CompactTextString(m) }
func (*PingRequest) ProtoMessage() {}
func (*PingRequest) Descriptor() ([]byte, []int) { return fileDescriptorFields, []int{3} }
func (m *PingRequest) GetPing() *Ping {
if m != nil {
return m.Ping
}
return nil
}
func (m *PingRequest) GetMeta() *Metadata {
if m != nil {
return m.Meta
}
return nil
}
type Pong struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" log_field:"pong_id"`
}
func (m *Pong) Reset() { *m = Pong{} }
func (m *Pong) String() string { return proto.CompactTextString(m) }
func (*Pong) ProtoMessage() {}
func (*Pong) Descriptor() ([]byte, []int) { return fileDescriptorFields, []int{4} }
type PongRequest struct {
Pong *Pong `protobuf:"bytes,1,opt,name=pong" json:"pong,omitempty"`
Meta *Metadata `protobuf:"bytes,2,opt,name=meta" json:"meta,omitempty"`
}
func (m *PongRequest) Reset() { *m = PongRequest{} }
func (m *PongRequest) String() string { return proto.CompactTextString(m) }
func (*PongRequest) ProtoMessage() {}
func (*PongRequest) Descriptor() ([]byte, []int) { return fileDescriptorFields, []int{5} }
func (m *PongRequest) GetPong() *Pong {
if m != nil {
return m.Pong
}
return nil
}
func (m *PongRequest) GetMeta() *Metadata {
if m != nil {
return m.Meta
}
return nil
}
func init() {
proto.RegisterType((*Metadata)(nil), "mwitkow.testproto.Metadata")
proto.RegisterType((*PingId)(nil), "mwitkow.testproto.PingId")
proto.RegisterType((*Ping)(nil), "mwitkow.testproto.Ping")
proto.RegisterType((*PingRequest)(nil), "mwitkow.testproto.PingRequest")
proto.RegisterType((*Pong)(nil), "mwitkow.testproto.Pong")
proto.RegisterType((*PongRequest)(nil), "mwitkow.testproto.PongRequest")
}
func init() { proto.RegisterFile("fields.proto", fileDescriptorFields) }
var fileDescriptorFields = []byte{
// 304 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x50, 0x41, 0x4e, 0xeb, 0x30,
0x14, 0xfc, 0xe9, 0x77, 0x2b, 0xea, 0xb2, 0xc1, 0x80, 0xda, 0x82, 0x44, 0x2b, 0x6f, 0x28, 0x42,
0x4d, 0x44, 0x59, 0xc1, 0xb2, 0x1b, 0xc4, 0x02, 0xa9, 0xf2, 0x05, 0x22, 0x07, 0xbb, 0xc6, 0x6a,
0x92, 0x57, 0x88, 0x43, 0xaf, 0xc2, 0xe9, 0xba, 0xe1, 0x06, 0x3d, 0x01, 0xf2, 0x0b, 0xa0, 0xa0,
0x02, 0x0b, 0x76, 0x9e, 0xf7, 0x66, 0xe6, 0xcd, 0x98, 0xee, 0xce, 0xad, 0x4e, 0x55, 0x11, 0x2e,
0x9f, 0xc0, 0x01, 0xdb, 0xcb, 0x56, 0xd6, 0x2d, 0x60, 0x15, 0x3a, 0x5d, 0x38, 0x1c, 0x1d, 0x8d,
0x8d, 0x75, 0x0f, 0x65, 0x12, 0xde, 0x43, 0x16, 0x19, 0x30, 0x10, 0xe1, 0x38, 0x29, 0xe7, 0x88,
0x10, 0xe0, 0xab, 0x72, 0xe0, 0x57, 0x74, 0xe7, 0x4e, 0x3b, 0xa9, 0xa4, 0x93, 0x6c, 0x4c, 0x89,
0x93, 0xa6, 0xe8, 0x05, 0xc3, 0xff, 0xa3, 0xf6, 0xb4, 0xbf, 0x59, 0x0f, 0x0e, 0x53, 0x30, 0x31,
0x5e, 0xbc, 0xe6, 0x99, 0x76, 0x32, 0xf6, 0x7b, 0x2e, 0x90, 0xc6, 0x2f, 0x68, 0x6b, 0x66, 0x73,
0x73, 0xab, 0xd8, 0x29, 0x6d, 0x58, 0xd5, 0x0b, 0x86, 0xc1, 0xa8, 0x39, 0xed, 0x6e, 0xd6, 0x83,
0xfd, 0x9a, 0x6c, 0x69, 0x73, 0x13, 0x5b, 0xc5, 0x45, 0xc3, 0x2a, 0x7e, 0x43, 0x89, 0x97, 0xb0,
0xb3, 0x4f, 0x41, 0x67, 0xd2, 0x0f, 0xb7, 0x4a, 0x84, 0x95, 0xaf, 0x97, 0xb0, 0x03, 0xda, 0x7c,
0x96, 0x69, 0xa9, 0x7b, 0x8d, 0x61, 0x30, 0x6a, 0x8b, 0x0a, 0xf0, 0x05, 0xed, 0x78, 0x8e, 0xd0,
0x8f, 0xa5, 0x2e, 0x1c, 0x3b, 0xa7, 0xc4, 0xdf, 0x79, 0x77, 0xec, 0xfe, 0xe0, 0x28, 0x90, 0xc4,
0x22, 0x4a, 0x7c, 0x17, 0x34, 0xec, 0x4c, 0x8e, 0xbf, 0x21, 0x7f, 0xfc, 0x88, 0x40, 0x22, 0x8f,
0x28, 0x99, 0x41, 0x6e, 0x6a, 0x35, 0xdb, 0xdb, 0x35, 0xa1, 0x56, 0xd3, 0xa7, 0x83, 0xaf, 0xe9,
0xe0, 0xf7, 0x74, 0x80, 0xe9, 0xe0, 0x0f, 0xe9, 0xa6, 0xe4, 0xe5, 0xf5, 0xe4, 0x5f, 0xd2, 0xc2,
0xdd, 0xe5, 0x5b, 0x00, 0x00, 0x00, 0xff, 0xff, 0x1d, 0x67, 0x44, 0x3b, 0x20, 0x02, 0x00, 0x00,
}

View File

@@ -0,0 +1,111 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_testing
import (
"net"
"time"
"flag"
"path"
"runtime"
pb_testproto "github.com/grpc-ecosystem/go-grpc-middleware/testing/testproto"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
flagTls = flag.Bool("use_tls", true, "whether all gRPC middleware tests should use tls")
)
func getTestingCertsPath() string {
_, callerPath, _, _ := runtime.Caller(0)
return path.Join(path.Dir(callerPath), "certs")
}
// InterceptorTestSuite is a testify/Suite that starts a gRPC PingService server and a client.
type InterceptorTestSuite struct {
suite.Suite
TestService pb_testproto.TestServiceServer
ServerOpts []grpc.ServerOption
ClientOpts []grpc.DialOption
ServerListener net.Listener
Server *grpc.Server
clientConn *grpc.ClientConn
Client pb_testproto.TestServiceClient
}
func (s *InterceptorTestSuite) SetupSuite() {
var err error
s.ServerListener, err = net.Listen("tcp", "127.0.0.1:0")
require.NoError(s.T(), err, "must be able to allocate a port for serverListener")
if *flagTls {
creds, err := credentials.NewServerTLSFromFile(
path.Join(getTestingCertsPath(), "localhost.crt"),
path.Join(getTestingCertsPath(), "localhost.key"),
)
require.NoError(s.T(), err, "failed reading server credentials for localhost.crt")
s.ServerOpts = append(s.ServerOpts, grpc.Creds(creds))
}
// This is the point where we hook up the interceptor
s.Server = grpc.NewServer(s.ServerOpts...)
// Crete a service of the instantiator hasn't provided one.
if s.TestService == nil {
s.TestService = &TestPingService{T: s.T()}
}
pb_testproto.RegisterTestServiceServer(s.Server, s.TestService)
go func() {
s.Server.Serve(s.ServerListener)
}()
s.Client = s.NewClient(s.ClientOpts...)
}
func (s *InterceptorTestSuite) NewClient(dialOpts ...grpc.DialOption) pb_testproto.TestServiceClient {
newDialOpts := append(dialOpts, grpc.WithBlock(), grpc.WithTimeout(2*time.Second))
if *flagTls {
creds, err := credentials.NewClientTLSFromFile(
path.Join(getTestingCertsPath(), "localhost.crt"), "localhost")
require.NoError(s.T(), err, "failed reading client credentials for localhost.crt")
newDialOpts = append(newDialOpts, grpc.WithTransportCredentials(creds))
} else {
newDialOpts = append(newDialOpts, grpc.WithInsecure())
}
clientConn, err := grpc.Dial(s.ServerAddr(), newDialOpts...)
require.NoError(s.T(), err, "must not error on client Dial")
return pb_testproto.NewTestServiceClient(clientConn)
}
func (s *InterceptorTestSuite) ServerAddr() string {
return s.ServerListener.Addr().String()
}
func (s *InterceptorTestSuite) SimpleCtx() context.Context {
ctx, _ := context.WithTimeout(context.TODO(), 2*time.Second)
return ctx
}
func (s *InterceptorTestSuite) DeadlineCtx(deadline time.Time) context.Context {
ctx, _ := context.WithDeadline(context.TODO(), deadline)
return ctx
}
func (s *InterceptorTestSuite) TearDownSuite() {
time.Sleep(10 * time.Millisecond)
if s.ServerListener != nil {
s.Server.GracefulStop()
s.T().Logf("stopped grpc.Server at: %v", s.ServerAddr())
s.ServerListener.Close()
}
if s.clientConn != nil {
s.clientConn.Close()
}
}

View File

@@ -0,0 +1,31 @@
package grpc_testing
import (
"io"
"sync"
)
// MutexReadWriter is a io.ReadWriter that can be read and worked on from multiple go routines.
type MutexReadWriter struct {
sync.Mutex
rw io.ReadWriter
}
// NewMutexReadWriter creates a new thread-safe io.ReadWriter.
func NewMutexReadWriter(rw io.ReadWriter) *MutexReadWriter {
return &MutexReadWriter{rw: rw}
}
// Write implements the io.Writer interface.
func (m *MutexReadWriter) Write(p []byte) (int, error) {
m.Lock()
defer m.Unlock()
return m.rw.Write(p)
}
// Read implements the io.Reader interface.
func (m *MutexReadWriter) Read(p []byte) (int, error) {
m.Lock()
defer m.Unlock()
return m.rw.Read(p)
}

View File

@@ -0,0 +1,71 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
Package `grpc_testing` provides helper functions for testing validators in this package.
*/
package grpc_testing
import (
"io"
"testing"
pb_testproto "github.com/grpc-ecosystem/go-grpc-middleware/testing/testproto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
const (
// DefaultPongValue is the default value used.
DefaultResponseValue = "default_response_value"
// ListResponseCount is the expeted number of responses to PingList
ListResponseCount = 100
)
type TestPingService struct {
T *testing.T
}
func (s *TestPingService) PingEmpty(ctx context.Context, _ *pb_testproto.Empty) (*pb_testproto.PingResponse, error) {
return &pb_testproto.PingResponse{Value: DefaultResponseValue, Counter: 42}, nil
}
func (s *TestPingService) Ping(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.PingResponse, error) {
// Send user trailers and headers.
return &pb_testproto.PingResponse{Value: ping.Value, Counter: 42}, nil
}
func (s *TestPingService) PingError(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.Empty, error) {
code := codes.Code(ping.ErrorCodeReturned)
return nil, grpc.Errorf(code, "Userspace error.")
}
func (s *TestPingService) PingList(ping *pb_testproto.PingRequest, stream pb_testproto.TestService_PingListServer) error {
if ping.ErrorCodeReturned != 0 {
return grpc.Errorf(codes.Code(ping.ErrorCodeReturned), "foobar")
}
// Send user trailers and headers.
for i := 0; i < ListResponseCount; i++ {
stream.Send(&pb_testproto.PingResponse{Value: ping.Value, Counter: int32(i)})
}
return nil
}
func (s *TestPingService) PingStream(stream pb_testproto.TestService_PingStreamServer) error {
count := 0
for true {
ping, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
stream.Send(&pb_testproto.PingResponse{Value: ping.Value, Counter: int32(count)})
count += 1
}
return nil
}

View File

@@ -0,0 +1,8 @@
// Manual code for logging field extraction tests.
package mwitkow_testproto
// This is implementing grpc_logging.requestLogFieldsExtractor
func (m *PingRequest) ExtractRequestFields(appendToMap map[string]interface{}) {
appendToMap["value"] = m.Value
}

View File

@@ -0,0 +1,12 @@
// Manual code for validation tests.
package mwitkow_testproto
import "errors"
func (p *PingRequest) Validate() error {
if p.SleepTimeMs > 10000 {
return errors.New("cannot sleep for more than 10s")
}
return nil
}

View File

@@ -0,0 +1,394 @@
// Code generated by protoc-gen-go.
// source: test.proto
// DO NOT EDIT!
/*
Package mwitkow_testproto is a generated protocol buffer package.
It is generated from these files:
test.proto
It has these top-level messages:
Empty
PingRequest
PingResponse
*/
package mwitkow_testproto
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Empty struct {
}
func (m *Empty) Reset() { *m = Empty{} }
func (m *Empty) String() string { return proto.CompactTextString(m) }
func (*Empty) ProtoMessage() {}
func (*Empty) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
type PingRequest struct {
Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
SleepTimeMs int32 `protobuf:"varint,2,opt,name=sleep_time_ms,json=sleepTimeMs" json:"sleep_time_ms,omitempty"`
ErrorCodeReturned uint32 `protobuf:"varint,3,opt,name=error_code_returned,json=errorCodeReturned" json:"error_code_returned,omitempty"`
}
func (m *PingRequest) Reset() { *m = PingRequest{} }
func (m *PingRequest) String() string { return proto.CompactTextString(m) }
func (*PingRequest) ProtoMessage() {}
func (*PingRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *PingRequest) GetValue() string {
if m != nil {
return m.Value
}
return ""
}
func (m *PingRequest) GetSleepTimeMs() int32 {
if m != nil {
return m.SleepTimeMs
}
return 0
}
func (m *PingRequest) GetErrorCodeReturned() uint32 {
if m != nil {
return m.ErrorCodeReturned
}
return 0
}
type PingResponse struct {
Value string `protobuf:"bytes,1,opt,name=Value,json=value" json:"Value,omitempty"`
Counter int32 `protobuf:"varint,2,opt,name=counter" json:"counter,omitempty"`
}
func (m *PingResponse) Reset() { *m = PingResponse{} }
func (m *PingResponse) String() string { return proto.CompactTextString(m) }
func (*PingResponse) ProtoMessage() {}
func (*PingResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *PingResponse) GetValue() string {
if m != nil {
return m.Value
}
return ""
}
func (m *PingResponse) GetCounter() int32 {
if m != nil {
return m.Counter
}
return 0
}
func init() {
proto.RegisterType((*Empty)(nil), "mwitkow.testproto.Empty")
proto.RegisterType((*PingRequest)(nil), "mwitkow.testproto.PingRequest")
proto.RegisterType((*PingResponse)(nil), "mwitkow.testproto.PingResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for TestService service
type TestServiceClient interface {
PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PingResponse, error)
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*Empty, error)
PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (TestService_PingListClient, error)
PingStream(ctx context.Context, opts ...grpc.CallOption) (TestService_PingStreamClient, error)
}
type testServiceClient struct {
cc *grpc.ClientConn
}
func NewTestServiceClient(cc *grpc.ClientConn) TestServiceClient {
return &testServiceClient{cc}
}
func (c *testServiceClient) PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PingResponse, error) {
out := new(PingResponse)
err := grpc.Invoke(ctx, "/mwitkow.testproto.TestService/PingEmpty", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *testServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) {
out := new(PingResponse)
err := grpc.Invoke(ctx, "/mwitkow.testproto.TestService/Ping", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *testServiceClient) PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*Empty, error) {
out := new(Empty)
err := grpc.Invoke(ctx, "/mwitkow.testproto.TestService/PingError", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *testServiceClient) PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (TestService_PingListClient, error) {
stream, err := grpc.NewClientStream(ctx, &_TestService_serviceDesc.Streams[0], c.cc, "/mwitkow.testproto.TestService/PingList", opts...)
if err != nil {
return nil, err
}
x := &testServicePingListClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type TestService_PingListClient interface {
Recv() (*PingResponse, error)
grpc.ClientStream
}
type testServicePingListClient struct {
grpc.ClientStream
}
func (x *testServicePingListClient) Recv() (*PingResponse, error) {
m := new(PingResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *testServiceClient) PingStream(ctx context.Context, opts ...grpc.CallOption) (TestService_PingStreamClient, error) {
stream, err := grpc.NewClientStream(ctx, &_TestService_serviceDesc.Streams[1], c.cc, "/mwitkow.testproto.TestService/PingStream", opts...)
if err != nil {
return nil, err
}
x := &testServicePingStreamClient{stream}
return x, nil
}
type TestService_PingStreamClient interface {
Send(*PingRequest) error
Recv() (*PingResponse, error)
grpc.ClientStream
}
type testServicePingStreamClient struct {
grpc.ClientStream
}
func (x *testServicePingStreamClient) Send(m *PingRequest) error {
return x.ClientStream.SendMsg(m)
}
func (x *testServicePingStreamClient) Recv() (*PingResponse, error) {
m := new(PingResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// Server API for TestService service
type TestServiceServer interface {
PingEmpty(context.Context, *Empty) (*PingResponse, error)
Ping(context.Context, *PingRequest) (*PingResponse, error)
PingError(context.Context, *PingRequest) (*Empty, error)
PingList(*PingRequest, TestService_PingListServer) error
PingStream(TestService_PingStreamServer) error
}
func RegisterTestServiceServer(s *grpc.Server, srv TestServiceServer) {
s.RegisterService(&_TestService_serviceDesc, srv)
}
func _TestService_PingEmpty_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestServiceServer).PingEmpty(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mwitkow.testproto.TestService/PingEmpty",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestServiceServer).PingEmpty(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
func _TestService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestServiceServer).Ping(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mwitkow.testproto.TestService/Ping",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestServiceServer).Ping(ctx, req.(*PingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TestService_PingError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestServiceServer).PingError(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mwitkow.testproto.TestService/PingError",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestServiceServer).PingError(ctx, req.(*PingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TestService_PingList_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(PingRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(TestServiceServer).PingList(m, &testServicePingListServer{stream})
}
type TestService_PingListServer interface {
Send(*PingResponse) error
grpc.ServerStream
}
type testServicePingListServer struct {
grpc.ServerStream
}
func (x *testServicePingListServer) Send(m *PingResponse) error {
return x.ServerStream.SendMsg(m)
}
func _TestService_PingStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(TestServiceServer).PingStream(&testServicePingStreamServer{stream})
}
type TestService_PingStreamServer interface {
Send(*PingResponse) error
Recv() (*PingRequest, error)
grpc.ServerStream
}
type testServicePingStreamServer struct {
grpc.ServerStream
}
func (x *testServicePingStreamServer) Send(m *PingResponse) error {
return x.ServerStream.SendMsg(m)
}
func (x *testServicePingStreamServer) Recv() (*PingRequest, error) {
m := new(PingRequest)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
var _TestService_serviceDesc = grpc.ServiceDesc{
ServiceName: "mwitkow.testproto.TestService",
HandlerType: (*TestServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "PingEmpty",
Handler: _TestService_PingEmpty_Handler,
},
{
MethodName: "Ping",
Handler: _TestService_Ping_Handler,
},
{
MethodName: "PingError",
Handler: _TestService_PingError_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "PingList",
Handler: _TestService_PingList_Handler,
ServerStreams: true,
},
{
StreamName: "PingStream",
Handler: _TestService_PingStream_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "test.proto",
}
func init() { proto.RegisterFile("test.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 288 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x90, 0x4f, 0x4b, 0xfb, 0x30,
0x18, 0xc7, 0x97, 0xdf, 0x7e, 0x75, 0xee, 0xa9, 0x3b, 0x2c, 0x7a, 0x28, 0x1e, 0xb4, 0xe4, 0xd4,
0x53, 0x19, 0x7a, 0xf7, 0x22, 0xa2, 0x82, 0xa2, 0xb4, 0xc3, 0x6b, 0x99, 0xed, 0x83, 0x04, 0x97,
0xa6, 0x26, 0x4f, 0x57, 0x7c, 0x19, 0xbe, 0x63, 0x49, 0x56, 0x41, 0x98, 0x43, 0x0f, 0x3b, 0xe6,
0xfb, 0x79, 0xf8, 0xfe, 0x09, 0x00, 0xa1, 0xa5, 0xb4, 0x31, 0x9a, 0x34, 0x9f, 0xaa, 0x4e, 0xd2,
0xab, 0xee, 0x52, 0xa7, 0x79, 0x49, 0x8c, 0x20, 0xb8, 0x52, 0x0d, 0xbd, 0x8b, 0x0e, 0xc2, 0x47,
0x59, 0xbf, 0x64, 0xf8, 0xd6, 0xa2, 0x25, 0x7e, 0x04, 0xc1, 0x6a, 0xb1, 0x6c, 0x31, 0x62, 0x31,
0x4b, 0xc6, 0xd9, 0xfa, 0xc1, 0x05, 0x4c, 0xec, 0x12, 0xb1, 0x29, 0x48, 0x2a, 0x2c, 0x94, 0x8d,
0xfe, 0xc5, 0x2c, 0x09, 0xb2, 0xd0, 0x8b, 0x73, 0xa9, 0xf0, 0xde, 0xf2, 0x14, 0x0e, 0xd1, 0x18,
0x6d, 0x8a, 0x52, 0x57, 0x58, 0x18, 0xa4, 0xd6, 0xd4, 0x58, 0x45, 0xc3, 0x98, 0x25, 0x93, 0x6c,
0xea, 0xd1, 0xa5, 0xae, 0x30, 0xeb, 0x81, 0xb8, 0x80, 0x83, 0x75, 0xb0, 0x6d, 0x74, 0x6d, 0xd1,
0x25, 0x3f, 0x6d, 0x26, 0x47, 0x30, 0x2a, 0x75, 0x5b, 0x13, 0x9a, 0x3e, 0xf3, 0xeb, 0x79, 0xf6,
0x31, 0x84, 0x70, 0x8e, 0x96, 0x72, 0x34, 0x2b, 0x59, 0x22, 0xbf, 0x81, 0xb1, 0xf3, 0xf3, 0xab,
0x78, 0x94, 0x6e, 0x4c, 0x4e, 0x3d, 0x39, 0x3e, 0xfd, 0x81, 0x7c, 0xef, 0x21, 0x06, 0xfc, 0x16,
0xfe, 0x3b, 0x85, 0x9f, 0x6c, 0x3d, 0xf5, 0x7f, 0xf5, 0x17, 0xab, 0xeb, 0xbe, 0x94, 0x5b, 0xff,
0xab, 0xdf, 0xd6, 0xd2, 0x62, 0xc0, 0x1f, 0x60, 0xdf, 0x9d, 0xde, 0x49, 0x4b, 0x3b, 0xe8, 0x35,
0x63, 0x3c, 0x07, 0x70, 0x5a, 0x4e, 0x06, 0x17, 0x6a, 0x07, 0x96, 0x09, 0x9b, 0xb1, 0xe7, 0x3d,
0x4f, 0xce, 0x3f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x3f, 0x2a, 0x8a, 0x7b, 0x7d, 0x02, 0x00, 0x00,
}

View File

@@ -0,0 +1,142 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"io"
"sync"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
)
// UnaryClientInterceptor returns a new unary client interceptor for OpenTracing.
func UnaryClientInterceptor(opts ...Option) grpc.UnaryClientInterceptor {
o := evaluateOptions(opts)
return func(parentCtx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if o.filterOutFunc != nil && !o.filterOutFunc(parentCtx, method) {
return invoker(parentCtx, method, req, reply, cc, opts...)
}
newCtx, clientSpan := newClientSpanFromContext(parentCtx, o.tracer, method)
err := invoker(newCtx, method, req, reply, cc, opts...)
finishClientSpan(clientSpan, err)
return err
}
}
// StreamClientInterceptor returns a new streaming client interceptor for OpenTracing.
func StreamClientInterceptor(opts ...Option) grpc.StreamClientInterceptor {
o := evaluateOptions(opts)
return func(parentCtx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if o.filterOutFunc != nil && !o.filterOutFunc(parentCtx, method) {
return streamer(parentCtx, desc, cc, method, opts...)
}
newCtx, clientSpan := newClientSpanFromContext(parentCtx, o.tracer, method)
clientStream, err := streamer(newCtx, desc, cc, method, opts...)
if err != nil {
finishClientSpan(clientSpan, err)
return nil, err
}
return &tracedClientStream{ClientStream: clientStream, clientSpan: clientSpan}, nil
}
}
// type serverStreamingRetryingStream is the implementation of grpc.ClientStream that acts as a
// proxy to the underlying call. If any of the RecvMsg() calls fail, it will try to reestablish
// a new ClientStream according to the retry policy.
type tracedClientStream struct {
grpc.ClientStream
mu sync.Mutex
alreadyFinished bool
clientSpan opentracing.Span
}
func (s *tracedClientStream) Header() (metadata.MD, error) {
h, err := s.ClientStream.Header()
if err != nil {
s.finishClientSpan(err)
}
return h, err
}
func (s *tracedClientStream) SendMsg(m interface{}) error {
err := s.ClientStream.SendMsg(m)
if err != nil {
s.finishClientSpan(err)
}
return err
}
func (s *tracedClientStream) CloseSend() error {
err := s.ClientStream.CloseSend()
if err != nil {
s.finishClientSpan(err)
}
return err
}
func (s *tracedClientStream) RecvMsg(m interface{}) error {
err := s.ClientStream.RecvMsg(m)
if err != nil {
s.finishClientSpan(err)
}
return err
}
func (s *tracedClientStream) finishClientSpan(err error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.alreadyFinished {
finishClientSpan(s.clientSpan, err)
s.alreadyFinished = true
}
}
// ClientAddContextTags returns a context with specified opentracing tags, which
// are used by UnaryClientInterceptor/StreamClientInterceptor when creating a
// new span.
func ClientAddContextTags(ctx context.Context, tags opentracing.Tags) context.Context {
return context.WithValue(ctx, clientSpanTagKey{}, tags)
}
type clientSpanTagKey struct{}
func newClientSpanFromContext(ctx context.Context, tracer opentracing.Tracer, fullMethodName string) (context.Context, opentracing.Span) {
var parentSpanCtx opentracing.SpanContext
if parent := opentracing.SpanFromContext(ctx); parent != nil {
parentSpanCtx = parent.Context()
}
opts := []opentracing.StartSpanOption{
opentracing.ChildOf(parentSpanCtx),
ext.SpanKindRPCClient,
grpcTag,
}
if tagx := ctx.Value(clientSpanTagKey{}); tagx != nil {
if opt, ok := tagx.(opentracing.StartSpanOption); ok {
opts = append(opts, opt)
}
}
clientSpan := tracer.StartSpan(fullMethodName, opts...)
// Make sure we add this to the metadata of the call, so it gets propagated:
md := metautils.ExtractOutgoing(ctx).Clone()
if err := tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, metadataTextMap(md)); err != nil {
grpclog.Printf("grpc_opentracing: failed serializing trace information: %v", err)
}
ctxWithMetadata := md.ToOutgoing(ctx)
return opentracing.ContextWithSpan(ctxWithMetadata, clientSpan), clientSpan
}
func finishClientSpan(clientSpan opentracing.Span, err error) {
if err != nil && err != io.EOF {
ext.Error.Set(clientSpan, true)
clientSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
}
clientSpan.Finish()
}

View File

@@ -0,0 +1,22 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_opentracing` adds OpenTracing
OpenTracing Interceptors
These are both client-side and server-side interceptors for OpenTracing. They are a provider-agnostic, with backends
such as Zipkin, or Google Stackdriver Trace.
For a service that sends out requests and receives requests, you *need* to use both, otherwise downstream requests will
not have the appropriate requests propagated.
All server-side spans are tagged with grpc_ctxtags information.
For more information see:
http://opentracing.io/documentation/
https://github.com/opentracing/specification/blob/master/semantic_conventions.md
*/
package grpc_opentracing

View File

@@ -0,0 +1,50 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"strings"
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/opentracing/opentracing-go"
"google.golang.org/grpc/grpclog"
)
const (
TagTraceId = "trace.traceid"
TagSpanId = "trace.spanid"
)
// hackyInjectOpentracingIdsToTags writes the given context to the ctxtags.
// This is done in an incredibly hacky way, because the public-facing interface of opentracing doesn't give access to
// the TraceId and SpanId of the SpanContext. Only the Tracer's Inject/Extract methods know what these are.
// Most tracers have them encoded as keys with 'traceid' and 'spanid':
// https://github.com/openzipkin/zipkin-go-opentracing/blob/594640b9ef7e5c994e8d9499359d693c032d738c/propagation_ot.go#L29
// https://github.com/opentracing/basictracer-go/blob/1b32af207119a14b1b231d451df3ed04a72efebf/propagation_ot.go#L26
// Jaeger from Uber use one-key schema with next format '{trace-id}:{span-id}:{parent-span-id}:{flags}'
// https://www.jaegertracing.io/docs/client-libraries/#trace-span-identity
func hackyInjectOpentracingIdsToTags(span opentracing.Span, tags grpc_ctxtags.Tags) {
if err := span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, &hackyTagsCarrier{tags}); err != nil {
grpclog.Printf("grpc_opentracing: failed extracting trace info into ctx %v", err)
}
}
// hackyTagsCarrier is a really hacky way of
type hackyTagsCarrier struct {
grpc_ctxtags.Tags
}
func (t *hackyTagsCarrier) Set(key, val string) {
if strings.Contains(key, "traceid") || strings.Contains(strings.ToLower(key), "traceid") {
t.Tags.Set(TagTraceId, val) // this will most likely be base-16 (hex) encoded
} else if (strings.Contains(key, "spanid") && !strings.Contains(key, "parent")) || (strings.Contains(strings.ToLower(key), "spanid") && !strings.Contains(strings.ToLower(key), "parent")) {
t.Tags.Set(TagSpanId, val) // this will most likely be base-16 (hex) encoded
} else if key == "uber-trace-id" {
parts := strings.Split(val, ":")
if len(parts) >= 2 {
t.Tags.Set(TagTraceId, parts[0])
t.Tags.Set(TagSpanId, parts[1])
}
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"encoding/base64"
"strings"
"fmt"
"google.golang.org/grpc/metadata"
)
const (
binHdrSuffix = "-bin"
)
// metadataTextMap extends a metadata.MD to be an opentracing textmap
type metadataTextMap metadata.MD
// Set is a opentracing.TextMapReader interface that extracts values.
func (m metadataTextMap) Set(key, val string) {
// gRPC allows for complex binary values to be written.
encodedKey, encodedVal := encodeKeyValue(key, val)
// The metadata object is a multimap, and previous values may exist, but for opentracing headers, we do not append
// we just override.
m[encodedKey] = []string{encodedVal}
}
// ForeachKey is a opentracing.TextMapReader interface that extracts values.
func (m metadataTextMap) ForeachKey(callback func(key, val string) error) error {
for k, vv := range m {
for _, v := range vv {
if decodedKey, decodedVal, err := metadata.DecodeKeyValue(k, v); err == nil {
if err = callback(decodedKey, decodedVal); err != nil {
return err
}
} else {
return fmt.Errorf("failed decoding opentracing from gRPC metadata: %v", err)
}
}
}
return nil
}
// encodeKeyValue encodes key and value qualified for transmission via gRPC.
// note: copy pasted from private values of grpc.metadata
func encodeKeyValue(k, v string) (string, string) {
k = strings.ToLower(k)
if strings.HasSuffix(k, binHdrSuffix) {
val := base64.StdEncoding.EncodeToString([]byte(v))
v = string(val)
}
return k, v
}

View File

@@ -0,0 +1,55 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"context"
"github.com/opentracing/opentracing-go"
)
var (
defaultOptions = &options{
filterOutFunc: nil,
tracer: nil,
}
)
// FilterFunc allows users to provide a function that filters out certain methods from being traced.
//
// If it returns false, the given request will not be traced.
type FilterFunc func(ctx context.Context, fullMethodName string) bool
type options struct {
filterOutFunc FilterFunc
tracer opentracing.Tracer
}
func evaluateOptions(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
for _, o := range opts {
o(optCopy)
}
if optCopy.tracer == nil {
optCopy.tracer = opentracing.GlobalTracer()
}
return optCopy
}
type Option func(*options)
// WithFilterFunc customizes the function used for deciding whether a given call is traced or not.
func WithFilterFunc(f FilterFunc) Option {
return func(o *options) {
o.filterOutFunc = f
}
}
// WithTracer sets a custom tracer to be used for this middleware, otherwise the opentracing.GlobalTracer is used.
func WithTracer(tracer opentracing.Tracer) Option {
return func(o *options) {
o.tracer = tracer
}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2017 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_opentracing
import (
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
var (
grpcTag = opentracing.Tag{Key: string(ext.Component), Value: "gRPC"}
)
// UnaryServerInterceptor returns a new unary server interceptor for OpenTracing.
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateOptions(opts)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if o.filterOutFunc != nil && !o.filterOutFunc(ctx, info.FullMethod) {
return handler(ctx, req)
}
newCtx, serverSpan := newServerSpanFromInbound(ctx, o.tracer, info.FullMethod)
resp, err := handler(newCtx, req)
finishServerSpan(ctx, serverSpan, err)
return resp, err
}
}
// StreamServerInterceptor returns a new streaming server interceptor for OpenTracing.
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
o := evaluateOptions(opts)
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if o.filterOutFunc != nil && !o.filterOutFunc(stream.Context(), info.FullMethod) {
return handler(srv, stream)
}
newCtx, serverSpan := newServerSpanFromInbound(stream.Context(), o.tracer, info.FullMethod)
wrappedStream := grpc_middleware.WrapServerStream(stream)
wrappedStream.WrappedContext = newCtx
err := handler(srv, wrappedStream)
finishServerSpan(newCtx, serverSpan, err)
return err
}
}
func newServerSpanFromInbound(ctx context.Context, tracer opentracing.Tracer, fullMethodName string) (context.Context, opentracing.Span) {
md := metautils.ExtractIncoming(ctx)
parentSpanContext, err := tracer.Extract(opentracing.HTTPHeaders, metadataTextMap(md))
if err != nil && err != opentracing.ErrSpanContextNotFound {
grpclog.Printf("grpc_opentracing: failed parsing trace information: %v", err)
}
serverSpan := tracer.StartSpan(
fullMethodName,
// this is magical, it attaches the new span to the parent parentSpanContext, and creates an unparented one if empty.
ext.RPCServerOption(parentSpanContext),
grpcTag,
)
hackyInjectOpentracingIdsToTags(serverSpan, grpc_ctxtags.Extract(ctx))
return opentracing.ContextWithSpan(ctx, serverSpan), serverSpan
}
func finishServerSpan(ctx context.Context, serverSpan opentracing.Span, err error) {
// Log context information
tags := grpc_ctxtags.Extract(ctx)
for k, v := range tags.Values() {
// Don't tag errors, log them instead.
if vErr, ok := v.(error); ok {
serverSpan.LogKV(k, vErr.Error())
} else {
serverSpan.SetTag(k, v)
}
}
if err != nil {
ext.Error.Set(serverSpan, true)
serverSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
}
serverSpan.Finish()
}

View File

@@ -0,0 +1,23 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
Backoff Helper Utilities
Implements common backoff features.
*/
package backoffutils
import (
"math/rand"
"time"
)
// JitterUp adds random jitter to the duration.
//
// This adds or substracts time from the duration within a given jitter fraction.
// For example for 10s and jitter 0.1, it will returna time within [9s, 11s])
func JitterUp(duration time.Duration, jitter float64) time.Duration {
multiplier := jitter * (rand.Float64()*2 - 1)
return time.Duration(float64(duration) * (1 + multiplier))
}

View File

@@ -0,0 +1,19 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
Package `metautils` provides convenience functions for dealing with gRPC metadata.MD objects inside
Context handlers.
While the upstream grpc-go package contains decent functionality (see https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-metadata.md)
they are hard to use.
The majority of functions center around the NiceMD, which is a convenience wrapper around metadata.MD. For example
the following code allows you to easily extract incoming metadata (server handler) and put it into a new client context
metadata.
nmd := metautils.ExtractIncoming(serverCtx).Clone(":authorization", ":custom")
clientCtx := nmd.Set("x-client-header", "2").Set("x-another", "3").ToOutgoing(ctx)
*/
package metautils

View File

@@ -0,0 +1,126 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package metautils
import (
"strings"
"golang.org/x/net/context"
"google.golang.org/grpc/metadata"
)
// NiceMD is a convenience wrapper definiting extra functions on the metadata.
type NiceMD metadata.MD
// ExtractIncoming extracts an inbound metadata from the server-side context.
//
// This function always returns a NiceMD wrapper of the metadata.MD, in case the context doesn't have metadata it returns
// a new empty NiceMD.
func ExtractIncoming(ctx context.Context) NiceMD {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return NiceMD(metadata.Pairs())
}
return NiceMD(md)
}
// ExtractOutgoing extracts an outbound metadata from the client-side context.
//
// This function always returns a NiceMD wrapper of the metadata.MD, in case the context doesn't have metadata it returns
// a new empty NiceMD.
func ExtractOutgoing(ctx context.Context) NiceMD {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
return NiceMD(metadata.Pairs())
}
return NiceMD(md)
}
// Clone performs a *deep* copy of the metadata.MD.
//
// You can specify the lower-case copiedKeys to only copy certain whitelisted keys. If no keys are explicitly whitelisted
// all keys get copied.
func (m NiceMD) Clone(copiedKeys ...string) NiceMD {
newMd := NiceMD(metadata.Pairs())
for k, vv := range m {
found := false
if len(copiedKeys) == 0 {
found = true
} else {
for _, allowedKey := range copiedKeys {
if strings.ToLower(allowedKey) == strings.ToLower(k) {
found = true
break
}
}
}
if !found {
continue
}
newMd[k] = make([]string, len(vv))
copy(newMd[k], vv)
}
return NiceMD(newMd)
}
// ToOutgoing sets the given NiceMD as a client-side context for dispatching.
func (m NiceMD) ToOutgoing(ctx context.Context) context.Context {
return metadata.NewOutgoingContext(ctx, metadata.MD(m))
}
// ToIncoming sets the given NiceMD as a server-side context for dispatching.
//
// This is mostly useful in ServerInterceptors..
func (m NiceMD) ToIncoming(ctx context.Context) context.Context {
return metadata.NewIncomingContext(ctx, metadata.MD(m))
}
// Get retrieves a single value from the metadata.
//
// It works analogously to http.Header.Get, returning the first value if there are many set. If the value is not set,
// an empty string is returned.
//
// The function is binary-key safe.
func (m NiceMD) Get(key string) string {
k, _ := encodeKeyValue(key, "")
vv, ok := m[k]
if !ok {
return ""
}
return vv[0]
}
// Del retrieves a single value from the metadata.
//
// It works analogously to http.Header.Del, deleting all values if they exist.
//
// The function is binary-key safe.
func (m NiceMD) Del(key string) NiceMD {
k, _ := encodeKeyValue(key, "")
delete(m, k)
return m
}
// Set sets the given value in a metadata.
//
// It works analogously to http.Header.Set, overwriting all previous metadata values.
//
// The function is binary-key safe.
func (m NiceMD) Set(key string, value string) NiceMD {
k, v := encodeKeyValue(key, value)
m[k] = []string{v}
return m
}
// Add retrieves a single value from the metadata.
//
// It works analogously to http.Header.Add, as it appends to any existing values associated with key.
//
// The function is binary-key safe.
func (m NiceMD) Add(key string, value string) NiceMD {
k, v := encodeKeyValue(key, value)
m[k] = append(m[k], v)
return m
}

View File

@@ -0,0 +1,22 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package metautils
import (
"encoding/base64"
"strings"
)
const (
binHdrSuffix = "-bin"
)
func encodeKeyValue(k, v string) (string, string) {
k = strings.ToLower(k)
if strings.HasSuffix(k, binHdrSuffix) {
val := base64.StdEncoding.EncodeToString([]byte(v))
v = string(val)
}
return k, v
}

View File

@@ -0,0 +1,45 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
/*
`grpc_validator` is a generic request contents validator server-side middleware for gRPC.
Request Validator Middleware
Validating input is important, and hard. It also causes a lot of boilerplate code. This middleware
checks for the existence of a `Validate` method on each of the messages of a gRPC request. This
includes the single request of the `Unary` calls, as well as each message of the inbound Stream calls.
In case of a validation failure, an `InvalidArgument` gRPC status is returned, along with a
description of the validation failure.
While it is generic, it was intended to be used with https://github.com/mwitkow/go-proto-validators,
a Go protocol buffers codegen plugin that creates the `Validate` methods (including nested messages)
based on declarative options in the `.proto` files themselves. For example:
syntax = "proto3";
package validator.examples;
import "github.com/mwitkow/go-proto-validators/validator.proto";
message InnerMessage {
// some_integer can only be in range (1, 100).
int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}];
// some_float can only be in range (0;1).
double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}];
}
message OuterMessage {
// important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).
string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}];
// proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.
InnerMessage inner = 2 [(validator.field) = {msg_exists : true}];
}
The `OuterMessage.Validate` would include validation of regexes, existence of the InnerMessage and
the range values within it. The `grpc_validator` middleware would then automatically use that to
check all messages processed by the server.
Please consult https://github.com/mwitkow/go-proto-validators for details on `protoc` invocation and
other parameters of customization.
*/
package grpc_validator

View File

@@ -0,0 +1,57 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_validator
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
type validator interface {
Validate() error
}
// UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages.
//
// Invalid messages will be rejected with `InvalidArgument` before reaching any userspace handlers.
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if v, ok := req.(validator); ok {
if err := v.Validate(); err != nil {
return nil, grpc.Errorf(codes.InvalidArgument, err.Error())
}
}
return handler(ctx, req)
}
}
// StreamServerInterceptor returns a new streaming server interceptor that validates incoming messages.
//
// The stage at which invalid messages will be rejected with `InvalidArgument` varies based on the
// type of the RPC. For `ServerStream` (1:m) requests, it will happen before reaching any userspace
// handlers. For `ClientStream` (n:1) or `BidiStream` (n:m) RPCs, the messages will be rejected on
// calls to `stream.Recv()`.
func StreamServerInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
wrapper := &recvWrapper{stream}
return handler(srv, wrapper)
}
}
type recvWrapper struct {
grpc.ServerStream
}
func (s *recvWrapper) RecvMsg(m interface{}) error {
if err := s.ServerStream.RecvMsg(m); err != nil {
return err
}
if v, ok := m.(validator); ok {
if err := v.Validate(); err != nil {
return grpc.Errorf(codes.InvalidArgument, err.Error())
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.
package grpc_middleware
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// WrappedServerStream is a thin wrapper around grpc.ServerStream that allows modifying context.
type WrappedServerStream struct {
grpc.ServerStream
// WrappedContext is the wrapper's own Context. You can assign it.
WrappedContext context.Context
}
// Context returns the wrapper's WrappedContext, overwriting the nested grpc.ServerStream.Context()
func (w *WrappedServerStream) Context() context.Context {
return w.WrappedContext
}
// WrapServerStream returns a ServerStream that has the ability to overwrite context.
func WrapServerStream(stream grpc.ServerStream) *WrappedServerStream {
if existing, ok := stream.(*WrappedServerStream); ok {
return existing
}
return &WrappedServerStream{ServerStream: stream, WrappedContext: stream.Context()}
}